From a929f988bc95fd3bd1c3f84aa7ba95235ba84de2 Mon Sep 17 00:00:00 2001 From: Tyera Date: Wed, 20 Sep 2023 00:00:51 -0600 Subject: [PATCH 1/2] Make active stake consistent in split (#33295) * Add feature gate * Add helper fn * Require split destination to be rent-exempt if it is active * Update cli to prefund split accounts * cli: require rent param with sign-only * Update tokens to prefund split accounts * Update split tests with sysvar accounts * Fix test_split_to_account_with_rent_exempt_reserve * Fix test_staked_split_destination_minimum_balance * Fix test_split_more_than_staked * Fix test_split_minimum_stake_delegation and remove misleading StakeState::Initialized case * Fix test_split_from_larger_sized_account * Add test for pre-/post-activation behavior splitting some or all of stake account * Assert active stake * Fix runtime test * Ignore stake-pool downstream * Review comments * Feature gate sysvar reads (cherry picked from commit bca41edf204e322df03c6d529da32ae0ab256d23) # Conflicts: # cli/tests/stake.rs # programs/stake/src/stake_instruction.rs # programs/stake/src/stake_state.rs # runtime/tests/stake.rs # sdk/src/feature_set.rs --- .github/workflows/downstream-project-spl.yml | 2 +- cli/src/cli.rs | 4 + cli/src/stake.rs | 78 +- cli/tests/stake.rs | 18 +- programs/stake/src/stake_instruction.rs | 748 +++++++++++++++++-- programs/stake/src/stake_state.rs | 55 ++ runtime/tests/stake.rs | 16 +- sdk/src/feature_set.rs | 31 + tokens/src/arg_parser.rs | 1 + tokens/src/args.rs | 1 + tokens/src/commands.rs | 30 +- tokens/src/lib.rs | 1 + tokens/src/main.rs | 3 +- tokens/src/stake.rs | 15 + 14 files changed, 924 insertions(+), 79 deletions(-) create mode 100644 tokens/src/stake.rs diff --git a/.github/workflows/downstream-project-spl.yml b/.github/workflows/downstream-project-spl.yml index 534a3190ffe738..8f5eacaeb7cd00 100644 --- a/.github/workflows/downstream-project-spl.yml +++ b/.github/workflows/downstream-project-spl.yml @@ -130,7 +130,7 @@ jobs: - [governance/addin-mock/program, governance/program] - [memo/program] - [name-service/program] - - [stake-pool/program] + # - [stake-pool/program] - [single-pool/program] steps: diff --git a/cli/src/cli.rs b/cli/src/cli.rs index a8728cbb1a1da9..a3f851b6df77bd 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -235,6 +235,7 @@ pub enum CliCommand { lamports: u64, fee_payer: SignerIndex, compute_unit_price: Option, + rent_exempt_reserve: Option, }, MergeStake { stake_account_pubkey: Pubkey, @@ -1215,6 +1216,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { lamports, fee_payer, compute_unit_price, + rent_exempt_reserve, } => process_split_stake( &rpc_client, config, @@ -1231,6 +1233,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { *lamports, *fee_payer, compute_unit_price.as_ref(), + rent_exempt_reserve.as_ref(), ), CliCommand::MergeStake { stake_account_pubkey, @@ -2232,6 +2235,7 @@ mod tests { lamports: 30, fee_payer: 0, compute_unit_price: None, + rent_exempt_reserve: None, }; config.signers = vec![&keypair, &split_stake_account]; let result = process_command(&config); diff --git a/cli/src/stake.rs b/cli/src/stake.rs index f5f90fadd11213..81fa3929413325 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -53,7 +53,7 @@ use { tools::{acceptable_reference_epoch_credits, eligible_for_deactivate_delinquent}, }, stake_history::{Epoch, StakeHistory}, - system_instruction::SystemError, + system_instruction::{self, SystemError}, sysvar::{clock, stake_history}, transaction::Transaction, }, @@ -119,6 +119,13 @@ pub struct StakeAuthorizationIndexed { pub new_authority_signer: Option, } +struct SignOnlySplitNeedsRent {} +impl ArgsConfig for SignOnlySplitNeedsRent { + fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { + arg.requires("rent_exempt_reserve_sol") + } +} + pub trait StakeSubCommands { fn stake_subcommands(self) -> Self; } @@ -491,11 +498,21 @@ impl StakeSubCommands for App<'_, '_> { will be at a derived address of SPLIT_STAKE_ACCOUNT") ) .arg(stake_authority_arg()) - .offline_args() + .offline_args_config(&SignOnlySplitNeedsRent{}) .nonce_args(false) .arg(fee_payer_arg()) .arg(memo_arg()) .arg(compute_unit_price_arg()) + .arg( + Arg::with_name("rent_exempt_reserve_sol") + .long("rent-exempt-reserve-sol") + .value_name("AMOUNT") + .takes_value(true) + .validator(is_amount) + .requires("sign_only") + .help("Offline signing only: the rent-exempt amount to move into the new \ + stake account, in SOL") + ) ) .subcommand( SubCommand::with_name("merge-stake") @@ -1025,6 +1042,7 @@ pub fn parse_split_stake( let signer_info = default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?; let compute_unit_price = value_of(matches, COMPUTE_UNIT_PRICE_ARG.name); + let rent_exempt_reserve = lamports_of_sol(matches, "rent_exempt_reserve_sol"); Ok(CliCommandInfo { command: CliCommand::SplitStake { @@ -1041,6 +1059,7 @@ pub fn parse_split_stake( lamports, fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(), compute_unit_price, + rent_exempt_reserve, }, signers: signer_info.signers, }) @@ -1850,6 +1869,7 @@ pub fn process_split_stake( lamports: u64, fee_payer: SignerIndex, compute_unit_price: Option<&u64>, + rent_exempt_reserve: Option<&u64>, ) -> ProcessResult { let split_stake_account = config.signers[split_stake_account]; let fee_payer = config.signers[fee_payer]; @@ -1883,7 +1903,7 @@ pub fn process_split_stake( split_stake_account.pubkey() }; - if !sign_only { + let rent_exempt_reserve = if !sign_only { if let Ok(stake_account) = rpc_client.get_account(&split_stake_account_address) { let err_msg = if stake_account.owner == stake::program::id() { format!("Stake account {split_stake_account_address} already exists") @@ -1904,30 +1924,44 @@ pub fn process_split_stake( )) .into()); } - } + minimum_balance + } else { + rent_exempt_reserve + .cloned() + .expect("rent_exempt_reserve_sol is required with sign_only") + }; let recent_blockhash = blockhash_query.get_blockhash(rpc_client, config.commitment)?; - let ixs = if let Some(seed) = split_stake_account_seed { - stake_instruction::split_with_seed( - stake_account_pubkey, - &stake_authority.pubkey(), - lamports, - &split_stake_account_address, - &split_stake_account.pubkey(), - seed, + let mut ixs = vec![system_instruction::transfer( + &fee_payer.pubkey(), + &split_stake_account_address, + rent_exempt_reserve, + )]; + if let Some(seed) = split_stake_account_seed { + ixs.append( + &mut stake_instruction::split_with_seed( + stake_account_pubkey, + &stake_authority.pubkey(), + lamports, + &split_stake_account_address, + &split_stake_account.pubkey(), + seed, + ) + .with_memo(memo) + .with_compute_unit_price(compute_unit_price), ) - .with_memo(memo) - .with_compute_unit_price(compute_unit_price) } else { - stake_instruction::split( - stake_account_pubkey, - &stake_authority.pubkey(), - lamports, - &split_stake_account_address, + ixs.append( + &mut stake_instruction::split( + stake_account_pubkey, + &stake_authority.pubkey(), + lamports, + &split_stake_account_address, + ) + .with_memo(memo) + .with_compute_unit_price(compute_unit_price), ) - .with_memo(memo) - .with_compute_unit_price(compute_unit_price) }; let nonce_authority = config.signers[nonce_authority]; @@ -4845,6 +4879,7 @@ mod tests { lamports: 50_000_000_000, fee_payer: 0, compute_unit_price: None, + rent_exempt_reserve: None, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -4912,6 +4947,7 @@ mod tests { lamports: 50_000_000_000, fee_payer: 1, compute_unit_price: None, + rent_exempt_reserve: None, }, signers: vec![ Presigner::new(&stake_auth_pubkey, &stake_sig).into(), diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 1ec23d141af3ea..1196989572ce46 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -1467,6 +1467,10 @@ fn test_stake_split() { config.json_rpc_url = test_validator.rpc_url(); config.signers = vec![&default_signer]; + let minimum_balance = rpc_client + .get_minimum_balance_for_rent_exemption(StakeStateV2::size_of()) + .unwrap(); + let mut config_offline = CliConfig::recent_for_tests(); config_offline.json_rpc_url = String::default(); config_offline.signers = vec![&offline_signer]; @@ -1494,10 +1498,14 @@ fn test_stake_split() { check_balance!(1_000_000_000_000, &rpc_client, &offline_pubkey); // Create stake account, identity is authority +<<<<<<< HEAD let stake_balance = rpc_client .get_minimum_balance_for_rent_exemption(StakeState::size_of()) .unwrap() + 10_000_000_000; +======= + let stake_balance = minimum_balance + 10_000_000_000; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let stake_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); let stake_account_pubkey = stake_keypair.pubkey(); config.signers.push(&stake_keypair); @@ -1567,6 +1575,7 @@ fn test_stake_split() { lamports: 2 * stake_balance, fee_payer: 0, compute_unit_price: None, + rent_exempt_reserve: Some(minimum_balance), }; config_offline.output_format = OutputFormat::JsonCompact; let sig_response = process_command(&config_offline).unwrap(); @@ -1591,10 +1600,15 @@ fn test_stake_split() { lamports: 2 * stake_balance, fee_payer: 0, compute_unit_price: None, + rent_exempt_reserve: None, }; process_command(&config).unwrap(); - check_balance!(8 * stake_balance, &rpc_client, &stake_account_pubkey,); - check_balance!(2 * stake_balance, &rpc_client, &split_account.pubkey(),); + check_balance!(8 * stake_balance, &rpc_client, &stake_account_pubkey); + check_balance!( + 2 * stake_balance + minimum_balance, + &rpc_client, + &split_account.pubkey() + ); } #[test] diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 124e079aee920c..d1b8b3d23b6133 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -561,6 +561,7 @@ mod tests { feature_set } +<<<<<<< HEAD /// The "old old" behavior is both before the stake minimum delegation was raised *and* before /// undelegated stake accounts could have zero lamports beyond rent fn feature_set_old_old_behavior() -> Arc { @@ -569,6 +570,12 @@ mod tests { .unwrap() .deactivate(&feature_set::stake_allow_zero_undelegated_amount::id()); feature_set +======= + fn feature_set_without_require_rent_exempt_split_destination() -> Arc { + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&feature_set::require_rent_exempt_split_destination::id()); + Arc::new(feature_set) +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) } fn create_default_account() -> AccountSharedData { @@ -681,6 +688,25 @@ mod tests { ) } + fn get_active_stake_for_tests( + stake_accounts: &[AccountSharedData], + clock: &Clock, + stake_history: &StakeHistory, + ) -> u64 { + let mut active_stake = 0; + for account in stake_accounts { + if let StakeStateV2::Stake(_meta, stake, _stake_flags) = account.state().unwrap() { + let stake_status = stake.delegation.stake_activating_and_deactivating( + clock.epoch, + Some(stake_history), + None, + ); + active_stake += stake_status.effective; + } + } + active_stake + } + #[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")] #[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")] #[test_case(feature_set_all_enabled(); "all_enabled")] @@ -2747,6 +2773,12 @@ mod tests { #[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")] #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split(feature_set: Arc) { + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let clock = Clock { + epoch: current_epoch, + ..Clock::default() + }; let stake_address = solana_sdk::pubkey::new_rand(); let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = minimum_delegation * 2; @@ -2760,7 +2792,7 @@ mod tests { .unwrap(); let mut transaction_accounts = vec![ (stake_address, AccountSharedData::default()), - (split_to_address, split_to_account), + (split_to_address, split_to_account.clone()), ( rent::id(), create_account_shared_data_for_test(&Rent { @@ -2768,6 +2800,15 @@ mod tests { ..Rent::default() }), ), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), ]; let instruction_accounts = vec![ AccountMeta { @@ -2795,6 +2836,11 @@ mod tests { &id(), ) .unwrap(); + let expected_active_stake = get_active_stake_for_tests( + &[stake_account.clone(), split_to_account.clone()], + &clock, + &stake_history, + ); transaction_accounts[0] = (stake_address, stake_account); // should fail, split more than available @@ -2820,6 +2866,12 @@ mod tests { stake_lamports ); + // no deactivated stake + assert_eq!( + expected_active_stake, + get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + ); + assert_eq!(from(&accounts[0]).unwrap(), from(&accounts[1]).unwrap()); match state { StakeState::Initialized(_meta) => { @@ -4088,7 +4140,17 @@ mod tests { fn test_split_minimum_stake_delegation(feature_set: Arc) { let minimum_delegation = crate::get_minimum_delegation(&feature_set); let rent = Rent::default(); +<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); +======= + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let clock = Clock { + epoch: current_epoch, + ..Clock::default() + }; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let source_address = Pubkey::new_unique(); let source_meta = Meta { rent_exempt_reserve, @@ -4096,9 +4158,15 @@ mod tests { }; let dest_address = Pubkey::new_unique(); let dest_account = AccountSharedData::new_data_with_space( +<<<<<<< HEAD 0, &StakeState::Uninitialized, StakeState::size_of(), +======= + rent_exempt_reserve, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) &id(), ) .unwrap(); @@ -4114,24 +4182,25 @@ mod tests { is_writable: true, }, ]; - for (source_reserve, dest_reserve, expected_result) in [ - (rent_exempt_reserve, rent_exempt_reserve, Ok(())), + for (source_delegation, split_amount, expected_result) in [ + (minimum_delegation * 2, minimum_delegation, Ok(())), ( - rent_exempt_reserve, - rent_exempt_reserve - 1, + minimum_delegation * 2, + minimum_delegation - 1, Err(InstructionError::InsufficientFunds), ), ( - rent_exempt_reserve - 1, - rent_exempt_reserve, + (minimum_delegation * 2) - 1, + minimum_delegation, Err(InstructionError::InsufficientFunds), ), ( - rent_exempt_reserve - 1, - rent_exempt_reserve - 1, + (minimum_delegation - 1) * 2, + minimum_delegation - 1, Err(InstructionError::InsufficientFunds), ), ] { +<<<<<<< HEAD // The source account's starting balance is equal to *both* the source and dest // accounts' *final* balance let mut source_starting_balance = source_reserve + dest_reserve; @@ -4165,6 +4234,44 @@ mod tests { expected_result.clone(), ); } +======= + let source_account = AccountSharedData::new_data_with_space( + source_delegation + rent_exempt_reserve, + &just_stake(source_meta, source_delegation), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let expected_active_stake = get_active_stake_for_tests( + &[source_account.clone(), dest_account.clone()], + &clock, + &stake_history, + ); + let accounts = process_instruction( + Arc::clone(&feature_set), + &serialize(&StakeInstruction::Split(split_amount)).unwrap(), + vec![ + (source_address, source_account), + (dest_address, dest_account.clone()), + (rent::id(), create_account_shared_data_for_test(&rent)), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ], + instruction_accounts.clone(), + expected_result.clone(), + ); + assert_eq!( + expected_active_stake, + get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + ); +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) } } @@ -4181,7 +4288,17 @@ mod tests { fn test_split_full_amount_minimum_stake_delegation(feature_set: Arc) { let minimum_delegation = crate::get_minimum_delegation(&feature_set); let rent = Rent::default(); +<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); +======= + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let clock = Clock { + epoch: current_epoch, + ..Clock::default() + }; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let source_address = Pubkey::new_unique(); let source_meta = Meta { rent_exempt_reserve, @@ -4228,17 +4345,35 @@ mod tests { &id(), ) .unwrap(); - process_instruction( + let expected_active_stake = get_active_stake_for_tests( + &[source_account.clone(), dest_account.clone()], + &clock, + &stake_history, + ); + let accounts = process_instruction( Arc::clone(&feature_set), &serialize(&StakeInstruction::Split(source_account.lamports())).unwrap(), vec![ (source_address, source_account), (dest_address, dest_account.clone()), (rent::id(), create_account_shared_data_for_test(&rent)), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), ], instruction_accounts.clone(), expected_result.clone(), ); + assert_eq!( + expected_active_stake, + get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + ); } } } @@ -4350,7 +4485,17 @@ mod tests { ) { let minimum_delegation = crate::get_minimum_delegation(&feature_set); let rent = Rent::default(); +<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); +======= + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let clock = Clock { + epoch: current_epoch, + ..Clock::default() + }; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let source_address = Pubkey::new_unique(); let destination_address = Pubkey::new_unique(); let instruction_accounts = vec![ @@ -4402,17 +4547,26 @@ mod tests { minimum_delegation.saturating_sub(1), // when minimum is 0, this blows up! Err(InstructionError::InsufficientFunds), ), - // destination is not rent exempt, so split enough for rent and minimum delegation - (rent_exempt_reserve - 1, minimum_delegation + 1, Ok(())), + // destination is not rent exempt, so any split amount fails, including enough for rent + // and minimum delegation + ( + rent_exempt_reserve - 1, + minimum_delegation + 1, + Err(InstructionError::InsufficientFunds), + ), // destination is not rent exempt, but split amount only for minimum delegation ( rent_exempt_reserve - 1, minimum_delegation, Err(InstructionError::InsufficientFunds), ), - // destination has smallest non-zero balance, so can split the minimum balance - // requirements minus what destination already has - (1, rent_exempt_reserve + minimum_delegation - 1, Ok(())), + // destination is not rent exempt, so any split amount fails, including case where + // destination has smallest non-zero balance + ( + 1, + rent_exempt_reserve + minimum_delegation - 1, + Err(InstructionError::InsufficientFunds), + ), // destination has smallest non-zero balance, but cannot split less than the minimum // balance requirements minus what destination already has ( @@ -4420,9 +4574,13 @@ mod tests { rent_exempt_reserve + minimum_delegation - 2, Err(InstructionError::InsufficientFunds), ), - // destination has zero lamports, so split must be at least rent exempt reserve plus - // minimum delegation - (0, rent_exempt_reserve + minimum_delegation, Ok(())), + // destination has zero lamports, so any split amount fails, including at least rent + // exempt reserve plus minimum delegation + ( + 0, + rent_exempt_reserve + minimum_delegation, + Err(InstructionError::InsufficientFunds), + ), // destination has zero lamports, but split amount is less than rent exempt reserve // plus minimum delegation ( @@ -4453,6 +4611,11 @@ mod tests { &id(), ) .unwrap(); + let expected_active_stake = get_active_stake_for_tests( + &[source_account.clone(), destination_account.clone()], + &clock, + &stake_history, + ); let accounts = process_instruction( Arc::clone(&feature_set), &serialize(&StakeInstruction::Split(split_amount)).unwrap(), @@ -4460,12 +4623,30 @@ mod tests { (source_address, source_account.clone()), (destination_address, destination_account), (rent::id(), create_account_shared_data_for_test(&rent)), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), ], instruction_accounts.clone(), expected_result.clone(), ); +<<<<<<< HEAD // For the expected OK cases, when the source's StakeState is Stake, then the // destination's StakeState *must* also end up as Stake as well. Additionally, +======= + assert_eq!( + expected_active_stake, + get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + ); + // For the expected OK cases, when the source's StakeStateV2 is Stake, then the + // destination's StakeStateV2 *must* also end up as Stake as well. Additionally, +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) // check to ensure the destination's delegation amount is correct. If the // destination is already rent exempt, then the destination's stake delegation // *must* equal the split amount. Otherwise, the split amount must first be used to @@ -4944,7 +5125,13 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_more_than_staked(feature_set: Arc) { let rent = Rent::default(); +<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); +======= + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; let stake_address = solana_sdk::pubkey::new_rand(); @@ -4963,9 +5150,15 @@ mod tests { .unwrap(); let split_to_address = solana_sdk::pubkey::new_rand(); let split_to_account = AccountSharedData::new_data_with_space( +<<<<<<< HEAD 0, &StakeState::Uninitialized, StakeState::size_of(), +======= + rent_exempt_reserve, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) &id(), ) .unwrap(); @@ -4973,6 +5166,21 @@ mod tests { (stake_address, stake_account), (split_to_address, split_to_account), (rent::id(), create_account_shared_data_for_test(&rent)), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + ( + clock::id(), + create_account_shared_data_for_test(&Clock { + epoch: current_epoch, + ..Clock::default() + }), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), ]; let instruction_accounts = vec![ AccountMeta { @@ -5001,7 +5209,17 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_with_rent(feature_set: Arc) { let rent = Rent::default(); +<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); +======= + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let clock = Clock { + epoch: current_epoch, + ..Clock::default() + }; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_address = solana_sdk::pubkey::new_rand(); let split_to_address = solana_sdk::pubkey::new_rand(); @@ -5046,10 +5264,24 @@ mod tests { &id(), ) .unwrap(); + let expected_active_stake = get_active_stake_for_tests( + &[stake_account.clone(), split_to_account.clone()], + &clock, + &stake_history, + ); let mut transaction_accounts = vec![ (stake_address, stake_account), (split_to_address, split_to_account.clone()), (rent::id(), create_account_shared_data_for_test(&rent)), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), ]; // not enough to make a non-zero stake account @@ -5073,7 +5305,7 @@ mod tests { Err(InstructionError::InsufficientFunds), ); - // split account already has way enough lamports + // split account already has enough lamports transaction_accounts[1].1.set_lamports(*minimum_balance); let accounts = process_instruction( Arc::clone(&feature_set), @@ -5082,6 +5314,10 @@ mod tests { instruction_accounts.clone(), Ok(()), ); + assert_eq!( + expected_active_stake, + get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + ); // verify no stake leakage in the case of a stake if let StakeState::Stake(meta, stake) = state { @@ -5109,7 +5345,17 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_to_account_with_rent_exempt_reserve(feature_set: Arc) { let rent = Rent::default(); +<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); +======= + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let clock = Clock { + epoch: current_epoch, + ..Clock::default() + }; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; let stake_address = solana_sdk::pubkey::new_rand(); @@ -5140,17 +5386,7 @@ mod tests { }, ]; - // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly - // rent_exempt_reserve, and more than rent_exempt_reserve. The empty case is not covered in - // test_split, since that test uses a Meta with rent_exempt_reserve = 0 - let split_lamport_balances = vec![ - 0, - rent_exempt_reserve - 1, - rent_exempt_reserve, - rent_exempt_reserve + minimum_delegation - 1, - rent_exempt_reserve + minimum_delegation, - ]; - for initial_balance in split_lamport_balances { + let transaction_accounts = |initial_balance: u64| -> Vec<(Pubkey, AccountSharedData)> { let split_to_account = AccountSharedData::new_data_with_space( initial_balance, &StakeState::Uninitialized, @@ -5158,11 +5394,63 @@ mod tests { &id(), ) .unwrap(); - let transaction_accounts = vec![ + vec![ (stake_address, stake_account.clone()), (split_to_address, split_to_account), (rent::id(), create_account_shared_data_for_test(&rent)), - ]; + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ] + }; + + // Test insufficient account prefunding, including empty and less than rent_exempt_reserve. + // The empty case is not covered in test_split, since that test uses a Meta with + // rent_exempt_reserve = 0 + let split_lamport_balances = vec![0, rent_exempt_reserve - 1]; + for initial_balance in split_lamport_balances { + let transaction_accounts = transaction_accounts(initial_balance); + // split more than available fails + process_instruction( + Arc::clone(&feature_set), + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + // split to insufficiently funded dest fails + process_instruction( + Arc::clone(&feature_set), + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + } + + // Test various account prefunding, including exactly rent_exempt_reserve, and more than + // rent_exempt_reserve + let split_lamport_balances = vec![ + rent_exempt_reserve, + rent_exempt_reserve + minimum_delegation - 1, + rent_exempt_reserve + minimum_delegation, + ]; + for initial_balance in split_lamport_balances { + let transaction_accounts = transaction_accounts(initial_balance); + let expected_active_stake = get_active_stake_for_tests( + &[ + transaction_accounts[0].1.clone(), + transaction_accounts[1].1.clone(), + ], + &clock, + &stake_history, + ); // split more than available fails process_instruction( @@ -5186,6 +5474,11 @@ mod tests { accounts[0].lamports() + accounts[1].lamports(), stake_lamports + initial_balance, ); + // no deactivated stake + assert_eq!( + expected_active_stake, + get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + ); if let StakeState::Stake(meta, stake) = state { let expected_stake = @@ -5232,8 +5525,19 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_from_larger_sized_account(feature_set: Arc) { let rent = Rent::default(); +<<<<<<< HEAD let source_larger_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of() + 100); let split_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); +======= + let source_larger_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of() + 100); + let split_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let clock = Clock { + epoch: current_epoch, + ..Clock::default() + }; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = (source_larger_rent_exempt_reserve + minimum_delegation) * 2; let stake_address = solana_sdk::pubkey::new_rand(); @@ -5264,17 +5568,7 @@ mod tests { }, ]; - // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly - // rent_exempt_reserve, and more than rent_exempt_reserve. The empty case is not covered in - // test_split, since that test uses a Meta with rent_exempt_reserve = 0 - let split_lamport_balances = vec![ - 0, - split_rent_exempt_reserve - 1, - split_rent_exempt_reserve, - split_rent_exempt_reserve + minimum_delegation - 1, - split_rent_exempt_reserve + minimum_delegation, - ]; - for initial_balance in split_lamport_balances { + let transaction_accounts = |initial_balance: u64| -> Vec<(Pubkey, AccountSharedData)> { let split_to_account = AccountSharedData::new_data_with_space( initial_balance, &StakeState::Uninitialized, @@ -5282,11 +5576,52 @@ mod tests { &id(), ) .unwrap(); - let transaction_accounts = vec![ + vec![ (stake_address, stake_account.clone()), (split_to_address, split_to_account), (rent::id(), create_account_shared_data_for_test(&rent)), - ]; + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ] + }; + + // Test insufficient account prefunding, including empty and less than rent_exempt_reserve + let split_lamport_balances = vec![0, split_rent_exempt_reserve - 1]; + for initial_balance in split_lamport_balances { + process_instruction( + Arc::clone(&feature_set), + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts(initial_balance), + instruction_accounts.clone(), + Err(InstructionError::InsufficientFunds), + ); + } + + // Test various account prefunding, including exactly rent_exempt_reserve, and more than + // rent_exempt_reserve. The empty case is not covered in test_split, since that test uses a + // Meta with rent_exempt_reserve = 0 + let split_lamport_balances = vec![ + split_rent_exempt_reserve, + split_rent_exempt_reserve + minimum_delegation - 1, + split_rent_exempt_reserve + minimum_delegation, + ]; + for initial_balance in split_lamport_balances { + let transaction_accounts = transaction_accounts(initial_balance); + let expected_active_stake = get_active_stake_for_tests( + &[ + transaction_accounts[0].1.clone(), + transaction_accounts[1].1.clone(), + ], + &clock, + &stake_history, + ); // split more than available fails process_instruction( @@ -5310,6 +5645,11 @@ mod tests { accounts[0].lamports() + accounts[1].lamports(), stake_lamports + initial_balance ); + // no deactivated stake + assert_eq!( + expected_active_stake, + get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + ); if let StakeState::Stake(meta, stake) = state { let expected_split_meta = Meta { @@ -5361,8 +5701,15 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_from_smaller_sized_account(feature_set: Arc) { let rent = Rent::default(); +<<<<<<< HEAD let source_smaller_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); let split_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of() + 100); +======= + let source_smaller_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let split_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of() + 100); + let stake_history = StakeHistory::default(); + let current_epoch = 100; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let stake_lamports = split_rent_exempt_reserve + 1; let stake_address = solana_sdk::pubkey::new_rand(); let meta = Meta { @@ -5411,6 +5758,21 @@ mod tests { (stake_address, stake_account.clone()), (split_to_address, split_to_account), (rent::id(), create_account_shared_data_for_test(&rent)), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + ( + clock::id(), + create_account_shared_data_for_test(&Clock { + epoch: current_epoch, + ..Clock::default() + }), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), ]; // should always return error when splitting to larger account @@ -5438,7 +5800,17 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_100_percent_of_source(feature_set: Arc) { let rent = Rent::default(); +<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); +======= + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let clock = Clock { + epoch: current_epoch, + ..Clock::default() + }; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = rent_exempt_reserve + minimum_delegation; let stake_address = solana_sdk::pubkey::new_rand(); @@ -5480,10 +5852,24 @@ mod tests { &id(), ) .unwrap(); + let expected_active_stake = get_active_stake_for_tests( + &[stake_account.clone(), split_to_account.clone()], + &clock, + &stake_history, + ); let transaction_accounts = vec![ (stake_address, stake_account), (split_to_address, split_to_account.clone()), (rent::id(), create_account_shared_data_for_test(&rent)), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), ]; // split 100% over to dest @@ -5500,6 +5886,11 @@ mod tests { accounts[0].lamports() + accounts[1].lamports(), stake_lamports ); + // no deactivated stake + assert_eq!( + expected_active_stake, + get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + ); match state { StakeState::Initialized(_) => { @@ -5532,7 +5923,17 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_100_percent_of_source_to_account_with_lamports(feature_set: Arc) { let rent = Rent::default(); +<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); +======= + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let clock = Clock { + epoch: current_epoch, + ..Clock::default() + }; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = rent_exempt_reserve + minimum_delegation; let stake_address = solana_sdk::pubkey::new_rand(); @@ -5581,10 +5982,24 @@ mod tests { &id(), ) .unwrap(); + let expected_active_stake = get_active_stake_for_tests( + &[stake_account.clone(), split_to_account.clone()], + &clock, + &stake_history, + ); let transaction_accounts = vec![ (stake_address, stake_account.clone()), (split_to_address, split_to_account), (rent::id(), create_account_shared_data_for_test(&rent)), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), ]; // split 100% over to dest @@ -5601,6 +6016,11 @@ mod tests { accounts[0].lamports() + accounts[1].lamports(), stake_lamports + initial_balance ); + // no deactivated stake + assert_eq!( + expected_active_stake, + get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + ); if let StakeState::Stake(meta, stake) = state { assert_eq!( @@ -5626,8 +6046,19 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_rent_exemptness(feature_set: Arc) { let rent = Rent::default(); +<<<<<<< HEAD let source_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of() + 100); let split_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); +======= + let source_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of() + 100); + let split_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let clock = Clock { + epoch: current_epoch, + ..Clock::default() + }; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = source_rent_exempt_reserve + minimum_delegation; let stake_address = solana_sdk::pubkey::new_rand(); @@ -5673,6 +6104,15 @@ mod tests { (stake_address, stake_account), (split_to_address, split_to_account), (rent::id(), create_account_shared_data_for_test(&rent)), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), ]; process_instruction( Arc::clone(&feature_set), @@ -5698,10 +6138,30 @@ mod tests { &id(), ) .unwrap(); + let expected_active_stake = get_active_stake_for_tests( + &[stake_account.clone(), split_to_account.clone()], + &clock, + &stake_history, + ); let transaction_accounts = vec![ (stake_address, stake_account), (split_to_address, split_to_account), (rent::id(), create_account_shared_data_for_test(&rent)), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + ( + clock::id(), + create_account_shared_data_for_test(&Clock { + epoch: current_epoch, + ..Clock::default() + }), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), ]; let accounts = process_instruction( Arc::clone(&feature_set), @@ -5711,6 +6171,10 @@ mod tests { Ok(()), ); assert_eq!(accounts[1].lamports(), stake_lamports); + assert_eq!( + expected_active_stake, + get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + ); let expected_split_meta = Meta { authorized: Authorized::auto(&stake_address), @@ -5754,6 +6218,200 @@ mod tests { } } + #[test_case(feature_set_without_require_rent_exempt_split_destination(), Ok(()); "without_require_rent_exempt_split_destination")] + #[test_case(feature_set_all_enabled(), Err(InstructionError::InsufficientFunds); "all_enabled")] + fn test_split_require_rent_exempt_destination( + feature_set: Arc, + expected_result: Result<(), InstructionError>, + ) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let clock = Clock { + epoch: current_epoch, + ..Clock::default() + }; + let minimum_delegation = crate::get_minimum_delegation(&feature_set); + let delegation_amount = 3 * minimum_delegation; + let source_lamports = rent_exempt_reserve + delegation_amount; + let source_address = Pubkey::new_unique(); + let destination_address = Pubkey::new_unique(); + let meta = Meta { + authorized: Authorized::auto(&source_address), + rent_exempt_reserve, + ..Meta::default() + }; + let instruction_accounts = vec![ + AccountMeta { + pubkey: source_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: destination_address, + is_signer: false, + is_writable: true, + }, + ]; + + for (split_amount, expected_result) in [ + (2 * minimum_delegation, expected_result), + (source_lamports, Ok(())), + ] { + for (state, expected_result) in &[ + (StakeStateV2::Initialized(meta), Ok(())), + (just_stake(meta, delegation_amount), expected_result), + ] { + let source_account = AccountSharedData::new_data_with_space( + source_lamports, + &state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + + let transaction_accounts = + |initial_balance: u64| -> Vec<(Pubkey, AccountSharedData)> { + let destination_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + vec![ + (source_address, source_account.clone()), + (destination_address, destination_account), + (rent::id(), create_account_shared_data_for_test(&rent)), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ] + }; + + // Test insufficient recipient prefunding; should error once feature is activated + let split_lamport_balances = vec![0, rent_exempt_reserve - 1]; + for initial_balance in split_lamport_balances { + let transaction_accounts = transaction_accounts(initial_balance); + let expected_active_stake = get_active_stake_for_tests( + &[source_account.clone(), transaction_accounts[1].1.clone()], + &clock, + &stake_history, + ); + let result_accounts = process_instruction( + Arc::clone(&feature_set), + &serialize(&StakeInstruction::Split(split_amount)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + expected_result.clone(), + ); + let result_active_stake = + get_active_stake_for_tests(&result_accounts[0..2], &clock, &stake_history); + if expected_active_stake > 0 // starting stake was delegated + // partial split + && result_accounts[0].lamports() > 0 + // successful split to deficient recipient + && expected_result.is_ok() + { + assert_ne!(expected_active_stake, result_active_stake); + } else { + assert_eq!(expected_active_stake, result_active_stake); + } + } + + // Test recipient prefunding, including exactly rent_exempt_reserve, and more than + // rent_exempt_reserve. + let split_lamport_balances = vec![rent_exempt_reserve, rent_exempt_reserve + 1]; + for initial_balance in split_lamport_balances { + let transaction_accounts = transaction_accounts(initial_balance); + let expected_active_stake = get_active_stake_for_tests( + &[source_account.clone(), transaction_accounts[1].1.clone()], + &clock, + &stake_history, + ); + let accounts = process_instruction( + Arc::clone(&feature_set), + &serialize(&StakeInstruction::Split(split_amount)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + + // no lamport leakage + assert_eq!( + accounts[0].lamports() + accounts[1].lamports(), + source_lamports + initial_balance + ); + + // no deactivated stake + assert_eq!( + expected_active_stake, + get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + ); + + if let StakeStateV2::Stake(meta, stake, stake_flags) = state { + // split entire source account, including rent-exempt reserve + if accounts[0].lamports() == 0 { + assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state()); + assert_eq!( + Ok(StakeStateV2::Stake( + *meta, + Stake { + delegation: Delegation { + // delegated amount should not include source + // rent-exempt reserve + stake: delegation_amount, + ..stake.delegation + }, + ..*stake + }, + *stake_flags, + )), + accounts[1].state() + ); + } else { + assert_eq!( + Ok(StakeStateV2::Stake( + *meta, + Stake { + delegation: Delegation { + stake: minimum_delegation, + ..stake.delegation + }, + ..*stake + }, + *stake_flags, + )), + accounts[0].state() + ); + assert_eq!( + Ok(StakeStateV2::Stake( + *meta, + Stake { + delegation: Delegation { + stake: split_amount, + ..stake.delegation + }, + ..*stake + }, + *stake_flags, + )), + accounts[1].state() + ); + } + } + } + } + } + } + #[test_case(feature_set_old_warmup_cooldown_no_minimum_delegation(); "old_warmup_cooldown_no_min_delegation")] #[test_case(feature_set_old_warmup_cooldown(); "old_warmup_cooldown")] #[test_case(feature_set_all_enabled(); "all_enabled")] diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index a6ee6463a5b8e5..bfdba2e198455b 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -105,6 +105,19 @@ pub(crate) fn new_warmup_cooldown_rate_epoch(invoke_context: &InvokeContext) -> .new_warmup_cooldown_rate_epoch(epoch_schedule.as_ref()) } +fn get_stake_status( + invoke_context: &InvokeContext, + stake: &Stake, + clock: &Clock, +) -> Result { + let stake_history = invoke_context.get_sysvar_cache().get_stake_history()?; + Ok(stake.delegation.stake_activating_and_deactivating( + clock.epoch, + Some(&stake_history), + new_warmup_cooldown_rate_epoch(invoke_context), + )) +} + fn redelegate_stake( invoke_context: &InvokeContext, stake: &mut Stake, @@ -709,6 +722,16 @@ pub fn split( StakeState::Stake(meta, mut stake) => { meta.authorized.check(signers, StakeAuthorize::Staker)?; let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set); + let is_active = if invoke_context + .feature_set + .is_active(&feature_set::require_rent_exempt_split_destination::id()) + { + let clock = invoke_context.get_sysvar_cache().get_clock()?; + let status = get_stake_status(invoke_context, &stake, &clock)?; + status.effective > 0 + } else { + false + }; let validated_split_info = validate_split_amount( invoke_context, transaction_context, @@ -719,6 +742,7 @@ pub fn split( &meta, Some(&stake), minimum_delegation, + is_active, )?; // split the stake, subtract rent_exempt_balance unless @@ -800,8 +824,13 @@ pub fn split( split_index, lamports, &meta, +<<<<<<< HEAD None, additional_required_lamports, +======= + 0, // additional_required_lamports + false, +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) )?; let mut split_meta = meta; split_meta.rent_exempt_reserve = validated_split_info.destination_rent_exempt_reserve; @@ -963,6 +992,7 @@ pub fn redelegate( let vote_state = vote_account.get_state::()?; let (stake_meta, effective_stake) = +<<<<<<< HEAD if let StakeState::Stake(meta, stake) = stake_account.get_state()? { let stake_history = invoke_context.get_sysvar_cache().get_stake_history()?; let status = stake.delegation.stake_activating_and_deactivating( @@ -970,6 +1000,10 @@ pub fn redelegate( Some(&stake_history), new_warmup_cooldown_rate_epoch(invoke_context), ); +======= + if let StakeStateV2::Stake(meta, stake, _stake_flags) = stake_account.get_state()? { + let status = get_stake_status(invoke_context, &stake, &clock)?; +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) if status.effective == 0 || status.activating != 0 || status.deactivating != 0 { ic_msg!(invoke_context, "stake is not active"); return Err(StakeError::RedelegateTransientOrInactiveStake.into()); @@ -1244,6 +1278,7 @@ fn validate_split_amount( source_meta: &Meta, source_stake: Option<&Stake>, additional_required_lamports: u64, + source_is_active: bool, ) -> Result { let source_account = instruction_context .try_borrow_instruction_account(transaction_context, source_account_index)?; @@ -1285,10 +1320,28 @@ fn validate_split_amount( // nothing to do here } + let rent = invoke_context.get_sysvar_cache().get_rent()?; + let destination_rent_exempt_reserve = rent.minimum_balance(destination_data_len); + + // As of feature `require_rent_exempt_split_destination`, if the source is active stake, one of + // these criteria must be met: + // 1. the destination account must be prefunded with at least the rent-exempt reserve, or + // 2. the split must consume 100% of the source + if invoke_context + .feature_set + .is_active(&feature_set::require_rent_exempt_split_destination::id()) + && source_is_active + && source_remaining_balance != 0 + && destination_lamports < destination_rent_exempt_reserve + { + return Err(InstructionError::InsufficientFunds); + } + // Verify the destination account meets the minimum balance requirements // This must handle: // 1. The destination account having a different rent exempt reserve due to data size changes // 2. The destination account being prefunded, which would lower the minimum split amount +<<<<<<< HEAD let destination_rent_exempt_reserve = if invoke_context .feature_set .is_active(&stake_split_uses_rent_sysvar::ID) @@ -1302,6 +1355,8 @@ fn validate_split_amount( destination_data_len as u64, ) }; +======= +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let destination_minimum_balance = destination_rent_exempt_reserve.saturating_add(additional_required_lamports); let destination_balance_deficit = diff --git a/runtime/tests/stake.rs b/runtime/tests/stake.rs index 64501af84bc3fb..52fb0b1955ab61 100644 --- a/runtime/tests/stake.rs +++ b/runtime/tests/stake.rs @@ -426,15 +426,25 @@ fn test_stake_account_lifetime() { let split_stake_keypair = Keypair::new(); let split_stake_pubkey = split_stake_keypair.pubkey(); +<<<<<<< HEAD let bank_client = BankClient::new_shared(&bank); +======= + bank.transfer( + stake_rent_exempt_reserve, + &mint_keypair, + &split_stake_pubkey, + ) + .unwrap(); + let bank_client = BankClient::new_shared(bank.clone()); + +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) // Test split let split_starting_delegation = stake_minimum_delegation + bonus_delegation; - let split_starting_balance = split_starting_delegation + stake_rent_exempt_reserve; let message = Message::new( &stake_instruction::split( &stake_pubkey, &stake_pubkey, - split_starting_balance, + split_starting_delegation, &split_stake_pubkey, ), Some(&mint_pubkey), @@ -449,7 +459,7 @@ fn test_stake_account_lifetime() { get_staked(&bank, &split_stake_pubkey), split_starting_delegation, ); - let stake_remaining_balance = balance - split_starting_balance; + let stake_remaining_balance = balance - split_starting_delegation; // Deactivate the split let message = Message::new( diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 5a342f77f5ef33..b014b51f6b33cc 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -674,6 +674,29 @@ pub mod revise_turbine_epoch_stakes { solana_sdk::declare_id!("BTWmtJC8U5ZLMbBUUA1k6As62sYjPEjAiNAT55xYGdJU"); } +<<<<<<< HEAD +======= +pub mod enable_poseidon_syscall { + solana_sdk::declare_id!("FL9RsQA6TVUoh5xJQ9d936RHSebA1NLQqe3Zv9sXZRpr"); +} + +pub mod timely_vote_credits { + solana_sdk::declare_id!("2oXpeh141pPZCTCFHBsvCwG2BtaHZZAtrVhwaxSy6brS"); +} + +pub mod remaining_compute_units_syscall_enabled { + solana_sdk::declare_id!("5TuppMutoyzhUSfuYdhgzD47F92GL1g89KpCZQKqedxP"); +} + +pub mod enable_program_runtime_v2_and_loader_v4 { + solana_sdk::declare_id!("8oBxsYqnCvUTGzgEpxPcnVf7MLbWWPYddE33PftFeBBd"); +} + +pub mod require_rent_exempt_split_destination { + solana_sdk::declare_id!("D2aip4BBr8NPWtU9vLrwrBvbuaQ8w1zV38zFLxx4pfBV"); +} + +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -837,6 +860,14 @@ lazy_static! { (bpf_account_data_direct_mapping::id(), "use memory regions to map account data into the rbpf vm instead of copying the data"), (reduce_stake_warmup_cooldown::id(), "reduce stake warmup cooldown from 25% to 9%"), (revise_turbine_epoch_stakes::id(), "revise turbine epoch stakes"), +<<<<<<< HEAD +======= + (enable_poseidon_syscall::id(), "Enable Poseidon syscall"), + (timely_vote_credits::id(), "use timeliness of votes in determining credits to award"), + (remaining_compute_units_syscall_enabled::id(), "enable the remaining_compute_units syscall"), + (enable_program_runtime_v2_and_loader_v4::id(), "Enable Program-Runtime-v2 and Loader-v4 #33293"), + (require_rent_exempt_split_destination::id(), "Require stake split destination account to be rent exempt"), +>>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/tokens/src/arg_parser.rs b/tokens/src/arg_parser.rs index e40b29237c344b..924c4e3e8eebb6 100644 --- a/tokens/src/arg_parser.rs +++ b/tokens/src/arg_parser.rs @@ -559,6 +559,7 @@ fn parse_distribute_stake_args( stake_authority, withdraw_authority, lockup_authority, + rent_exempt_reserve: None, }; let stake_args = StakeArgs { unlocked_sol: sol_to_lamports(value_t_or_exit!(matches, "unlocked_sol", f64)), diff --git a/tokens/src/args.rs b/tokens/src/args.rs index b1f1522e1558bf..0dd4859f51e948 100644 --- a/tokens/src/args.rs +++ b/tokens/src/args.rs @@ -5,6 +5,7 @@ pub struct SenderStakeArgs { pub stake_authority: Box, pub withdraw_authority: Box, pub lockup_authority: Option>, + pub rent_exempt_reserve: Option, } pub struct StakeArgs { diff --git a/tokens/src/commands.rs b/tokens/src/commands.rs index 5b2603814b87c5..c10ad508d61a1c 100644 --- a/tokens/src/commands.rs +++ b/tokens/src/commands.rs @@ -31,7 +31,7 @@ use { signature::{unique_signers, Signature, Signer}, stake::{ instruction::{self as stake_instruction, LockupArgs}, - state::{Authorized, Lockup, StakeAuthorize}, + state::{Authorized, Lockup, StakeAuthorize, StakeStateV2}, }, system_instruction, transaction::Transaction, @@ -234,12 +234,24 @@ fn distribution_instructions( Some(sender_stake_args) => { let stake_authority = sender_stake_args.stake_authority.pubkey(); let withdraw_authority = sender_stake_args.withdraw_authority.pubkey(); - let mut instructions = stake_instruction::split( + let rent_exempt_reserve = sender_stake_args + .rent_exempt_reserve + .expect("SenderStakeArgs.rent_exempt_reserve should be populated"); + + // Transfer some tokens to stake account to cover rent-exempt reserve. + let mut instructions = vec![system_instruction::transfer( + &sender_pubkey, + new_stake_account_address, + rent_exempt_reserve, + )]; + + // Split to stake account + instructions.append(&mut stake_instruction::split( &sender_stake_args.stake_account_address, &stake_authority, - allocation.amount - unlocked_sol, + allocation.amount - unlocked_sol - rent_exempt_reserve, new_stake_account_address, - ); + )); // Make the recipient the new stake authority instructions.push(stake_instruction::authorize( @@ -1174,11 +1186,15 @@ pub fn test_process_distribute_stake_with_client(client: &RpcClient, sender_keyp let output_file = NamedTempFile::new().unwrap(); let output_path = output_file.path().to_str().unwrap().to_string(); + let rent_exempt_reserve = client + .get_minimum_balance_for_rent_exemption(StakeStateV2::size_of()) + .unwrap(); let sender_stake_args = SenderStakeArgs { stake_account_address, stake_authority: Box::new(stake_authority), withdraw_authority: Box::new(withdraw_authority), lockup_authority: None, + rent_exempt_reserve: Some(rent_exempt_reserve), }; let stake_args = StakeArgs { unlocked_sol: sol_to_lamports(1.0), @@ -1529,14 +1545,14 @@ mod tests { )); // Same recipient, same lockups } - const SET_LOCKUP_INDEX: usize = 5; + const SET_LOCKUP_INDEX: usize = 6; #[test] fn test_set_split_stake_lockup() { let lockup_date_str = "2021-01-07T00:00:00Z"; let allocation = Allocation { recipient: Pubkey::default().to_string(), - amount: sol_to_lamports(1.0), + amount: sol_to_lamports(1.002_282_880), lockup_date: lockup_date_str.to_string(), }; let stake_account_address = solana_sdk::pubkey::new_rand(); @@ -1548,6 +1564,7 @@ mod tests { stake_authority: Box::new(Keypair::new()), withdraw_authority: Box::new(Keypair::new()), lockup_authority: Some(Box::new(lockup_authority)), + rent_exempt_reserve: Some(2_282_880), }; let stake_args = StakeArgs { lockup_authority: Some(lockup_authority_address), @@ -1821,6 +1838,7 @@ mod tests { stake_authority: Box::new(stake_authority), withdraw_authority: Box::new(withdraw_authority), lockup_authority: None, + rent_exempt_reserve: Some(2_282_880), }; StakeArgs { diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index 8df0d4f482e4d5..2fac44edae0965 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -4,4 +4,5 @@ pub mod args; pub mod commands; mod db; pub mod spl_token; +pub mod stake; pub mod token_display; diff --git a/tokens/src/main.rs b/tokens/src/main.rs index f72278a99f9cca..c97287671dace5 100644 --- a/tokens/src/main.rs +++ b/tokens/src/main.rs @@ -2,7 +2,7 @@ use { solana_clap_utils::input_validators::normalize_to_url_if_moniker, solana_cli_config::{Config, CONFIG_FILE}, solana_rpc_client::rpc_client::RpcClient, - solana_tokens::{arg_parser::parse_args, args::Command, commands, spl_token}, + solana_tokens::{arg_parser::parse_args, args::Command, commands, spl_token, stake}, std::{ env, error::Error, @@ -43,6 +43,7 @@ fn main() -> Result<(), Box> { match command_args.command { Command::DistributeTokens(mut args) => { spl_token::update_token_args(&client, &mut args.spl_token_args)?; + stake::update_stake_args(&client, &mut args.stake_args)?; commands::process_allocations(&client, &args, exit)?; } Command::Balances(mut args) => { diff --git a/tokens/src/stake.rs b/tokens/src/stake.rs new file mode 100644 index 00000000000000..3f1c35a3b4df36 --- /dev/null +++ b/tokens/src/stake.rs @@ -0,0 +1,15 @@ +use { + crate::{args::StakeArgs, commands::Error}, + solana_rpc_client::rpc_client::RpcClient, + solana_sdk::stake::state::StakeStateV2, +}; + +pub fn update_stake_args(client: &RpcClient, args: &mut Option) -> Result<(), Error> { + if let Some(stake_args) = args { + if let Some(sender_args) = &mut stake_args.sender_stake_args { + let rent = client.get_minimum_balance_for_rent_exemption(StakeStateV2::size_of())?; + sender_args.rent_exempt_reserve = Some(rent); + } + } + Ok(()) +} From 9eb709cb2caf6b2718921d46d2906aebec7d0f56 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Wed, 20 Sep 2023 00:32:01 -0600 Subject: [PATCH 2/2] Fix conflicts --- cli/tests/stake.rs | 9 +- programs/stake/src/stake_instruction.rs | 140 +++--------------------- programs/stake/src/stake_state.rs | 46 +++----- runtime/tests/stake.rs | 6 +- sdk/src/feature_set.rs | 26 ----- tokens/src/commands.rs | 4 +- tokens/src/stake.rs | 4 +- 7 files changed, 38 insertions(+), 197 deletions(-) diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 1196989572ce46..ab08867585b479 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -1468,7 +1468,7 @@ fn test_stake_split() { config.signers = vec![&default_signer]; let minimum_balance = rpc_client - .get_minimum_balance_for_rent_exemption(StakeStateV2::size_of()) + .get_minimum_balance_for_rent_exemption(StakeState::size_of()) .unwrap(); let mut config_offline = CliConfig::recent_for_tests(); @@ -1498,14 +1498,7 @@ fn test_stake_split() { check_balance!(1_000_000_000_000, &rpc_client, &offline_pubkey); // Create stake account, identity is authority -<<<<<<< HEAD - let stake_balance = rpc_client - .get_minimum_balance_for_rent_exemption(StakeState::size_of()) - .unwrap() - + 10_000_000_000; -======= let stake_balance = minimum_balance + 10_000_000_000; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let stake_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); let stake_account_pubkey = stake_keypair.pubkey(); config.signers.push(&stake_keypair); diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index d1b8b3d23b6133..78f956c4a60878 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -561,7 +561,6 @@ mod tests { feature_set } -<<<<<<< HEAD /// The "old old" behavior is both before the stake minimum delegation was raised *and* before /// undelegated stake accounts could have zero lamports beyond rent fn feature_set_old_old_behavior() -> Arc { @@ -570,12 +569,12 @@ mod tests { .unwrap() .deactivate(&feature_set::stake_allow_zero_undelegated_amount::id()); feature_set -======= + } + fn feature_set_without_require_rent_exempt_split_destination() -> Arc { let mut feature_set = FeatureSet::all_enabled(); feature_set.deactivate(&feature_set::require_rent_exempt_split_destination::id()); Arc::new(feature_set) ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) } fn create_default_account() -> AccountSharedData { @@ -695,7 +694,7 @@ mod tests { ) -> u64 { let mut active_stake = 0; for account in stake_accounts { - if let StakeStateV2::Stake(_meta, stake, _stake_flags) = account.state().unwrap() { + if let StakeState::Stake(_meta, stake) = account.state().unwrap() { let stake_status = stake.delegation.stake_activating_and_deactivating( clock.epoch, Some(stake_history), @@ -4140,17 +4139,13 @@ mod tests { fn test_split_minimum_stake_delegation(feature_set: Arc) { let minimum_delegation = crate::get_minimum_delegation(&feature_set); let rent = Rent::default(); -<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); -======= - let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); let stake_history = StakeHistory::default(); let current_epoch = 100; let clock = Clock { epoch: current_epoch, ..Clock::default() }; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let source_address = Pubkey::new_unique(); let source_meta = Meta { rent_exempt_reserve, @@ -4158,15 +4153,9 @@ mod tests { }; let dest_address = Pubkey::new_unique(); let dest_account = AccountSharedData::new_data_with_space( -<<<<<<< HEAD - 0, + rent_exempt_reserve, &StakeState::Uninitialized, StakeState::size_of(), -======= - rent_exempt_reserve, - &StakeStateV2::Uninitialized, - StakeStateV2::size_of(), ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) &id(), ) .unwrap(); @@ -4200,45 +4189,10 @@ mod tests { Err(InstructionError::InsufficientFunds), ), ] { -<<<<<<< HEAD - // The source account's starting balance is equal to *both* the source and dest - // accounts' *final* balance - let mut source_starting_balance = source_reserve + dest_reserve; - for (delegation, source_stake_state) in &[ - (0, StakeState::Initialized(source_meta)), - ( - minimum_delegation, - just_stake( - source_meta, - minimum_delegation * 2 + source_starting_balance - rent_exempt_reserve, - ), - ), - ] { - source_starting_balance += delegation * 2; - let source_account = AccountSharedData::new_data_with_space( - source_starting_balance, - source_stake_state, - StakeState::size_of(), - &id(), - ) - .unwrap(); - process_instruction( - Arc::clone(&feature_set), - &serialize(&StakeInstruction::Split(dest_reserve + delegation)).unwrap(), - vec![ - (source_address, source_account), - (dest_address, dest_account.clone()), - (rent::id(), create_account_shared_data_for_test(&rent)), - ], - instruction_accounts.clone(), - expected_result.clone(), - ); - } -======= let source_account = AccountSharedData::new_data_with_space( source_delegation + rent_exempt_reserve, &just_stake(source_meta, source_delegation), - StakeStateV2::size_of(), + StakeState::size_of(), &id(), ) .unwrap(); @@ -4271,7 +4225,6 @@ mod tests { expected_active_stake, get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) ); ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) } } @@ -4288,17 +4241,13 @@ mod tests { fn test_split_full_amount_minimum_stake_delegation(feature_set: Arc) { let minimum_delegation = crate::get_minimum_delegation(&feature_set); let rent = Rent::default(); -<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); -======= - let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); let stake_history = StakeHistory::default(); let current_epoch = 100; let clock = Clock { epoch: current_epoch, ..Clock::default() }; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let source_address = Pubkey::new_unique(); let source_meta = Meta { rent_exempt_reserve, @@ -4485,17 +4434,13 @@ mod tests { ) { let minimum_delegation = crate::get_minimum_delegation(&feature_set); let rent = Rent::default(); -<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); -======= - let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); let stake_history = StakeHistory::default(); let current_epoch = 100; let clock = Clock { epoch: current_epoch, ..Clock::default() }; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let source_address = Pubkey::new_unique(); let destination_address = Pubkey::new_unique(); let instruction_accounts = vec![ @@ -4636,17 +4581,12 @@ mod tests { instruction_accounts.clone(), expected_result.clone(), ); -<<<<<<< HEAD - // For the expected OK cases, when the source's StakeState is Stake, then the - // destination's StakeState *must* also end up as Stake as well. Additionally, -======= assert_eq!( expected_active_stake, get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) ); - // For the expected OK cases, when the source's StakeStateV2 is Stake, then the - // destination's StakeStateV2 *must* also end up as Stake as well. Additionally, ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) + // For the expected OK cases, when the source's StakeState is Stake, then the + // destination's StakeState *must* also end up as Stake as well. Additionally, // check to ensure the destination's delegation amount is correct. If the // destination is already rent exempt, then the destination's stake delegation // *must* equal the split amount. Otherwise, the split amount must first be used to @@ -5125,13 +5065,9 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_more_than_staked(feature_set: Arc) { let rent = Rent::default(); -<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); -======= - let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); let stake_history = StakeHistory::default(); let current_epoch = 100; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; let stake_address = solana_sdk::pubkey::new_rand(); @@ -5150,15 +5086,9 @@ mod tests { .unwrap(); let split_to_address = solana_sdk::pubkey::new_rand(); let split_to_account = AccountSharedData::new_data_with_space( -<<<<<<< HEAD - 0, + rent_exempt_reserve, &StakeState::Uninitialized, StakeState::size_of(), -======= - rent_exempt_reserve, - &StakeStateV2::Uninitialized, - StakeStateV2::size_of(), ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) &id(), ) .unwrap(); @@ -5209,17 +5139,13 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_with_rent(feature_set: Arc) { let rent = Rent::default(); -<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); -======= - let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); let stake_history = StakeHistory::default(); let current_epoch = 100; let clock = Clock { epoch: current_epoch, ..Clock::default() }; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_address = solana_sdk::pubkey::new_rand(); let split_to_address = solana_sdk::pubkey::new_rand(); @@ -5345,17 +5271,13 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_to_account_with_rent_exempt_reserve(feature_set: Arc) { let rent = Rent::default(); -<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); -======= - let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); let stake_history = StakeHistory::default(); let current_epoch = 100; let clock = Clock { epoch: current_epoch, ..Clock::default() }; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; let stake_address = solana_sdk::pubkey::new_rand(); @@ -5525,19 +5447,14 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_from_larger_sized_account(feature_set: Arc) { let rent = Rent::default(); -<<<<<<< HEAD let source_larger_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of() + 100); let split_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); -======= - let source_larger_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of() + 100); - let split_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); let stake_history = StakeHistory::default(); let current_epoch = 100; let clock = Clock { epoch: current_epoch, ..Clock::default() }; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = (source_larger_rent_exempt_reserve + minimum_delegation) * 2; let stake_address = solana_sdk::pubkey::new_rand(); @@ -5701,15 +5618,10 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_from_smaller_sized_account(feature_set: Arc) { let rent = Rent::default(); -<<<<<<< HEAD let source_smaller_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); let split_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of() + 100); -======= - let source_smaller_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); - let split_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of() + 100); let stake_history = StakeHistory::default(); let current_epoch = 100; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let stake_lamports = split_rent_exempt_reserve + 1; let stake_address = solana_sdk::pubkey::new_rand(); let meta = Meta { @@ -5800,17 +5712,13 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_100_percent_of_source(feature_set: Arc) { let rent = Rent::default(); -<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); -======= - let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); let stake_history = StakeHistory::default(); let current_epoch = 100; let clock = Clock { epoch: current_epoch, ..Clock::default() }; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = rent_exempt_reserve + minimum_delegation; let stake_address = solana_sdk::pubkey::new_rand(); @@ -5923,17 +5831,13 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_100_percent_of_source_to_account_with_lamports(feature_set: Arc) { let rent = Rent::default(); -<<<<<<< HEAD let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); -======= - let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); let stake_history = StakeHistory::default(); let current_epoch = 100; let clock = Clock { epoch: current_epoch, ..Clock::default() }; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = rent_exempt_reserve + minimum_delegation; let stake_address = solana_sdk::pubkey::new_rand(); @@ -6046,19 +5950,14 @@ mod tests { #[test_case(feature_set_all_enabled(); "all_enabled")] fn test_split_rent_exemptness(feature_set: Arc) { let rent = Rent::default(); -<<<<<<< HEAD let source_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of() + 100); let split_rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); -======= - let source_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of() + 100); - let split_rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); let stake_history = StakeHistory::default(); let current_epoch = 100; let clock = Clock { epoch: current_epoch, ..Clock::default() }; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let minimum_delegation = crate::get_minimum_delegation(&feature_set); let stake_lamports = source_rent_exempt_reserve + minimum_delegation; let stake_address = solana_sdk::pubkey::new_rand(); @@ -6225,7 +6124,7 @@ mod tests { expected_result: Result<(), InstructionError>, ) { let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of()); let stake_history = StakeHistory::default(); let current_epoch = 100; let clock = Clock { @@ -6260,13 +6159,13 @@ mod tests { (source_lamports, Ok(())), ] { for (state, expected_result) in &[ - (StakeStateV2::Initialized(meta), Ok(())), + (StakeState::Initialized(meta), Ok(())), (just_stake(meta, delegation_amount), expected_result), ] { let source_account = AccountSharedData::new_data_with_space( source_lamports, &state, - StakeStateV2::size_of(), + StakeState::size_of(), &id(), ) .unwrap(); @@ -6275,8 +6174,8 @@ mod tests { |initial_balance: u64| -> Vec<(Pubkey, AccountSharedData)> { let destination_account = AccountSharedData::new_data_with_space( initial_balance, - &StakeStateV2::Uninitialized, - StakeStateV2::size_of(), + &StakeState::Uninitialized, + StakeState::size_of(), &id(), ) .unwrap(); @@ -6356,12 +6255,12 @@ mod tests { get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) ); - if let StakeStateV2::Stake(meta, stake, stake_flags) = state { + if let StakeState::Stake(meta, stake) = state { // split entire source account, including rent-exempt reserve if accounts[0].lamports() == 0 { - assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state()); + assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); assert_eq!( - Ok(StakeStateV2::Stake( + Ok(StakeState::Stake( *meta, Stake { delegation: Delegation { @@ -6372,13 +6271,12 @@ mod tests { }, ..*stake }, - *stake_flags, )), accounts[1].state() ); } else { assert_eq!( - Ok(StakeStateV2::Stake( + Ok(StakeState::Stake( *meta, Stake { delegation: Delegation { @@ -6387,12 +6285,11 @@ mod tests { }, ..*stake }, - *stake_flags, )), accounts[0].state() ); assert_eq!( - Ok(StakeStateV2::Stake( + Ok(StakeState::Stake( *meta, Stake { delegation: Delegation { @@ -6401,7 +6298,6 @@ mod tests { }, ..*stake }, - *stake_flags, )), accounts[1].state() ); diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index bfdba2e198455b..757d2e60a1e97c 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -824,13 +824,9 @@ pub fn split( split_index, lamports, &meta, -<<<<<<< HEAD None, additional_required_lamports, -======= - 0, // additional_required_lamports false, ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) )?; let mut split_meta = meta; split_meta.rent_exempt_reserve = validated_split_info.destination_rent_exempt_reserve; @@ -992,18 +988,8 @@ pub fn redelegate( let vote_state = vote_account.get_state::()?; let (stake_meta, effective_stake) = -<<<<<<< HEAD if let StakeState::Stake(meta, stake) = stake_account.get_state()? { - let stake_history = invoke_context.get_sysvar_cache().get_stake_history()?; - let status = stake.delegation.stake_activating_and_deactivating( - clock.epoch, - Some(&stake_history), - new_warmup_cooldown_rate_epoch(invoke_context), - ); -======= - if let StakeStateV2::Stake(meta, stake, _stake_flags) = stake_account.get_state()? { let status = get_stake_status(invoke_context, &stake, &clock)?; ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) if status.effective == 0 || status.activating != 0 || status.deactivating != 0 { ic_msg!(invoke_context, "stake is not active"); return Err(StakeError::RedelegateTransientOrInactiveStake.into()); @@ -1268,6 +1254,7 @@ struct ValidatedSplitInfo { /// minimum balance requirements, which is the rent exempt reserve plus the minimum stake /// delegation, and that the source account has enough lamports for the request split amount. If /// not, return an error. +#[allow(clippy::too_many_arguments)] fn validate_split_amount( invoke_context: &InvokeContext, transaction_context: &TransactionContext, @@ -1320,8 +1307,19 @@ fn validate_split_amount( // nothing to do here } - let rent = invoke_context.get_sysvar_cache().get_rent()?; - let destination_rent_exempt_reserve = rent.minimum_balance(destination_data_len); + let destination_rent_exempt_reserve = if invoke_context + .feature_set + .is_active(&stake_split_uses_rent_sysvar::ID) + { + let rent = invoke_context.get_sysvar_cache().get_rent()?; + rent.minimum_balance(destination_data_len) + } else { + calculate_split_rent_exempt_reserve( + source_meta.rent_exempt_reserve, + source_data_len as u64, + destination_data_len as u64, + ) + }; // As of feature `require_rent_exempt_split_destination`, if the source is active stake, one of // these criteria must be met: @@ -1341,22 +1339,6 @@ fn validate_split_amount( // This must handle: // 1. The destination account having a different rent exempt reserve due to data size changes // 2. The destination account being prefunded, which would lower the minimum split amount -<<<<<<< HEAD - let destination_rent_exempt_reserve = if invoke_context - .feature_set - .is_active(&stake_split_uses_rent_sysvar::ID) - { - let rent = invoke_context.get_sysvar_cache().get_rent()?; - rent.minimum_balance(destination_data_len) - } else { - calculate_split_rent_exempt_reserve( - source_meta.rent_exempt_reserve, - source_data_len as u64, - destination_data_len as u64, - ) - }; -======= ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) let destination_minimum_balance = destination_rent_exempt_reserve.saturating_add(additional_required_lamports); let destination_balance_deficit = diff --git a/runtime/tests/stake.rs b/runtime/tests/stake.rs index 52fb0b1955ab61..8bafdfa186c291 100644 --- a/runtime/tests/stake.rs +++ b/runtime/tests/stake.rs @@ -426,18 +426,14 @@ fn test_stake_account_lifetime() { let split_stake_keypair = Keypair::new(); let split_stake_pubkey = split_stake_keypair.pubkey(); -<<<<<<< HEAD - let bank_client = BankClient::new_shared(&bank); -======= bank.transfer( stake_rent_exempt_reserve, &mint_keypair, &split_stake_pubkey, ) .unwrap(); - let bank_client = BankClient::new_shared(bank.clone()); + let bank_client = BankClient::new_shared(&bank); ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) // Test split let split_starting_delegation = stake_minimum_delegation + bonus_delegation; let message = Message::new( diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index b014b51f6b33cc..5b982432793f26 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -674,29 +674,10 @@ pub mod revise_turbine_epoch_stakes { solana_sdk::declare_id!("BTWmtJC8U5ZLMbBUUA1k6As62sYjPEjAiNAT55xYGdJU"); } -<<<<<<< HEAD -======= -pub mod enable_poseidon_syscall { - solana_sdk::declare_id!("FL9RsQA6TVUoh5xJQ9d936RHSebA1NLQqe3Zv9sXZRpr"); -} - -pub mod timely_vote_credits { - solana_sdk::declare_id!("2oXpeh141pPZCTCFHBsvCwG2BtaHZZAtrVhwaxSy6brS"); -} - -pub mod remaining_compute_units_syscall_enabled { - solana_sdk::declare_id!("5TuppMutoyzhUSfuYdhgzD47F92GL1g89KpCZQKqedxP"); -} - -pub mod enable_program_runtime_v2_and_loader_v4 { - solana_sdk::declare_id!("8oBxsYqnCvUTGzgEpxPcnVf7MLbWWPYddE33PftFeBBd"); -} - pub mod require_rent_exempt_split_destination { solana_sdk::declare_id!("D2aip4BBr8NPWtU9vLrwrBvbuaQ8w1zV38zFLxx4pfBV"); } ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -860,14 +841,7 @@ lazy_static! { (bpf_account_data_direct_mapping::id(), "use memory regions to map account data into the rbpf vm instead of copying the data"), (reduce_stake_warmup_cooldown::id(), "reduce stake warmup cooldown from 25% to 9%"), (revise_turbine_epoch_stakes::id(), "revise turbine epoch stakes"), -<<<<<<< HEAD -======= - (enable_poseidon_syscall::id(), "Enable Poseidon syscall"), - (timely_vote_credits::id(), "use timeliness of votes in determining credits to award"), - (remaining_compute_units_syscall_enabled::id(), "enable the remaining_compute_units syscall"), - (enable_program_runtime_v2_and_loader_v4::id(), "Enable Program-Runtime-v2 and Loader-v4 #33293"), (require_rent_exempt_split_destination::id(), "Require stake split destination account to be rent exempt"), ->>>>>>> bca41edf20 (Make active stake consistent in split (#33295)) /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/tokens/src/commands.rs b/tokens/src/commands.rs index c10ad508d61a1c..ac284393e6cd74 100644 --- a/tokens/src/commands.rs +++ b/tokens/src/commands.rs @@ -31,7 +31,7 @@ use { signature::{unique_signers, Signature, Signer}, stake::{ instruction::{self as stake_instruction, LockupArgs}, - state::{Authorized, Lockup, StakeAuthorize, StakeStateV2}, + state::{Authorized, Lockup, StakeAuthorize, StakeState}, }, system_instruction, transaction::Transaction, @@ -1187,7 +1187,7 @@ pub fn test_process_distribute_stake_with_client(client: &RpcClient, sender_keyp let output_path = output_file.path().to_str().unwrap().to_string(); let rent_exempt_reserve = client - .get_minimum_balance_for_rent_exemption(StakeStateV2::size_of()) + .get_minimum_balance_for_rent_exemption(StakeState::size_of()) .unwrap(); let sender_stake_args = SenderStakeArgs { stake_account_address, diff --git a/tokens/src/stake.rs b/tokens/src/stake.rs index 3f1c35a3b4df36..2647541b6e402f 100644 --- a/tokens/src/stake.rs +++ b/tokens/src/stake.rs @@ -1,13 +1,13 @@ use { crate::{args::StakeArgs, commands::Error}, solana_rpc_client::rpc_client::RpcClient, - solana_sdk::stake::state::StakeStateV2, + solana_sdk::stake::state::StakeState, }; pub fn update_stake_args(client: &RpcClient, args: &mut Option) -> Result<(), Error> { if let Some(stake_args) = args { if let Some(sender_args) = &mut stake_args.sender_stake_args { - let rent = client.get_minimum_balance_for_rent_exemption(StakeStateV2::size_of())?; + let rent = client.get_minimum_balance_for_rent_exemption(StakeState::size_of())?; sender_args.rent_exempt_reserve = Some(rent); } }