diff --git a/Cargo.lock b/Cargo.lock index 3d50b0763..cca1527e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6069,6 +6069,7 @@ dependencies = [ "solana-program-runtime", "solana-runtime", "solana-sdk 1.15.0", + "solana-stake-program", "solana-vote-program", "thiserror", "tokio", diff --git a/program-test/Cargo.toml b/program-test/Cargo.toml index 49b091541..0c9a2c0dd 100644 --- a/program-test/Cargo.toml +++ b/program-test/Cargo.toml @@ -27,3 +27,6 @@ solana-sdk = { path = "../sdk", version = "=1.15.0" } solana-vote-program = { path = "../programs/vote", version = "=1.15.0" } thiserror = "1.0" tokio = { version = "1", features = ["full"] } + +[dev-dependencies] +solana-stake-program = { path = "../programs/stake", version = "=1.15.0" } diff --git a/program-test/tests/warp.rs b/program-test/tests/warp.rs index 6e29fa3ad..aab7101c6 100644 --- a/program-test/tests/warp.rs +++ b/program-test/tests/warp.rs @@ -1,11 +1,13 @@ #![allow(clippy::integer_arithmetic)] use { bincode::deserialize, + log::debug, solana_banks_client::BanksClient, solana_program_test::{ processor, ProgramTest, ProgramTestBanksClientExt, ProgramTestContext, ProgramTestError, }, solana_sdk::{ + account::Account, account_info::{next_account_info, AccountInfo}, clock::Clock, entrypoint::ProgramResult, @@ -26,9 +28,10 @@ use { }, transaction::{Transaction, TransactionError}, }, + solana_stake_program::stake_state, solana_vote_program::{ vote_instruction, - vote_state::{VoteInit, VoteState}, + vote_state::{self, VoteInit, VoteState}, }, std::convert::TryInto, }; @@ -276,6 +279,122 @@ async fn stake_rewards_from_warp() { ); } +#[tokio::test] +async fn stake_rewards_filter_bench_100() { + stake_rewards_filter_bench_core(100).await; +} + +async fn stake_rewards_filter_bench_core(num_stake_accounts: u64) { + // Initialize and start the test network + let mut program_test = ProgramTest::default(); + + // create vote account + let vote_address = Pubkey::new_unique(); + let node_address = Pubkey::new_unique(); + + let vote_account = vote_state::create_account(&vote_address, &node_address, 0, 1_000_000_000); + program_test.add_account(vote_address, vote_account.clone().into()); + + // create stake accounts with 0.9 sol to test min-stake filtering + const TEST_FILTER_STAKE: u64 = 900_000_000; // 0.9 sol + let mut to_filter = vec![]; + for i in 0..num_stake_accounts { + let stake_pubkey = Pubkey::new_unique(); + let stake_account = Account::from(stake_state::create_account( + &stake_pubkey, + &vote_address, + &vote_account, + &Rent::default(), + TEST_FILTER_STAKE, + )); + program_test.add_account(stake_pubkey, stake_account); + to_filter.push(stake_pubkey); + if i % 100 == 0 { + debug!("create stake account {} {}", i, stake_pubkey); + } + } + + let mut context = program_test.start_with_context().await; + + let stake_lamports = 2_000_000_000_000; + + let user_keypair = Keypair::new(); + let stake_address = + setup_stake(&mut context, &user_keypair, &vote_address, stake_lamports).await; + + let account = context + .banks_client + .get_account(stake_address) + .await + .expect("account exists") + .unwrap(); + assert_eq!(account.lamports, stake_lamports); + + // warp one epoch forward for normal inflation, no rewards collected + let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; + context.warp_to_slot(first_normal_slot).unwrap(); + let account = context + .banks_client + .get_account(stake_address) + .await + .expect("account exists") + .unwrap(); + assert_eq!(account.lamports, stake_lamports); + + context.increment_vote_account_credits(&vote_address, 100); + + // go forward and see that rewards have been distributed + let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; + context + .warp_to_slot(first_normal_slot + slots_per_epoch) + .unwrap(); + + let account = context + .banks_client + .get_account(stake_address) + .await + .expect("account exists") + .unwrap(); + assert!(account.lamports > stake_lamports); + + // check that filtered stake accounts are excluded from receiving epoch rewards + for stake_address in to_filter { + let account = context + .banks_client + .get_account(stake_address) + .await + .expect("account exists") + .unwrap(); + assert_eq!(account.lamports, TEST_FILTER_STAKE); + } + + // check that stake is fully active + let stake_history_account = context + .banks_client + .get_account(stake_history::id()) + .await + .expect("account exists") + .unwrap(); + + let clock_account = context + .banks_client + .get_account(clock::id()) + .await + .expect("account exists") + .unwrap(); + + let stake_state: StakeState = deserialize(&account.data).unwrap(); + let stake_history: StakeHistory = deserialize(&stake_history_account.data).unwrap(); + let clock: Clock = deserialize(&clock_account.data).unwrap(); + let stake = stake_state.stake().unwrap(); + assert_eq!( + stake + .delegation + .stake_activating_and_deactivating(clock.epoch, Some(&stake_history)), + StakeActivationStatus::with_effective(stake.delegation.stake), + ); +} + async fn check_credits_observed( banks_client: &mut BanksClient, stake_address: Pubkey, diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 205594ed1..a5a783392 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -133,7 +133,7 @@ use { lamports::LamportsError, message::{AccountKeys, SanitizedMessage}, native_loader, - native_token::sol_to_lamports, + native_token::{sol_to_lamports, LAMPORTS_PER_SOL}, nonce::{self, state::DurableNonce, NONCED_TX_MARKER_IX_INDEX}, nonce_account, packet::PACKET_DATA_SIZE, @@ -2526,7 +2526,7 @@ impl Bank { let invalid_cached_vote_accounts = AtomicUsize::default(); let invalid_cached_stake_accounts_rent_epoch = AtomicUsize::default(); - let stake_delegations: Vec<_> = stakes.stake_delegations().iter().collect(); + let stake_delegations = self.filter_stake_delegations(&stakes); thread_pool.install(|| { stake_delegations .into_par_iter() @@ -2666,6 +2666,39 @@ impl Bank { } } + fn filter_stake_delegations<'a>( + &self, + stakes: &'a Stakes>, + ) -> Vec<(&'a Pubkey, &'a StakeAccount)> { + if self + .feature_set + .is_active(&feature_set::stake_minimum_delegation_for_rewards::id()) + { + let num_stake_delegations = stakes.stake_delegations().len(); + let min_stake_delegation = + solana_stake_program::get_minimum_delegation(&self.feature_set) + .max(LAMPORTS_PER_SOL); + + let (stake_delegations, filter_timer) = measure!(stakes + .stake_delegations() + .iter() + .filter(|(_stake_pubkey, cached_stake_account)| { + cached_stake_account.delegation().stake >= min_stake_delegation + }) + .collect::>()); + + datapoint_info!( + "stake_account_filter_time", + ("filter_time_us", filter_timer.as_us(), i64), + ("num_stake_delegations_before", num_stake_delegations, i64), + ("num_stake_delegations_after", stake_delegations.len(), i64) + ); + stake_delegations + } else { + stakes.stake_delegations().iter().collect() + } + } + fn load_vote_and_stake_accounts( &self, thread_pool: &ThreadPool, @@ -2675,7 +2708,8 @@ impl Bank { F: Fn(&RewardCalculationEvent) + Send + Sync, { let stakes = self.stakes_cache.stakes(); - let stake_delegations: Vec<_> = stakes.stake_delegations().iter().collect(); + let stake_delegations = self.filter_stake_delegations(&stakes); + // Obtain all unique voter pubkeys from stake delegations. fn merge(mut acc: HashSet, other: HashSet) -> HashSet { if acc.len() < other.len() { @@ -7988,7 +8022,6 @@ pub(crate) mod tests { instruction::{AccountMeta, CompiledInstruction, Instruction, InstructionError}, loader_upgradeable_instruction::UpgradeableLoaderInstruction, message::{Message, MessageHeader}, - native_token::LAMPORTS_PER_SOL, nonce, poh_config::PohConfig, program::MAX_RETURN_DATA, @@ -17520,7 +17553,7 @@ pub(crate) mod tests { let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_vote_accounts( 1_000_000_000, &validator_keypairs, - vec![10_000; 2], + vec![LAMPORTS_PER_SOL; 2], ); let bank = Arc::new(Bank::new_for_tests(&genesis_config)); let vote_and_stake_accounts = diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 69e135f20..2b88154af 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -398,6 +398,10 @@ pub mod stake_raise_minimum_delegation_to_1_sol { solana_sdk::declare_id!("4xmyBuR2VCXzy9H6qYpH9ckfgnTuMDQFPFBfTs4eBCY1"); } +pub mod stake_minimum_delegation_for_rewards { + solana_sdk::declare_id!("ELjxSXwNsyXGfAh8TqX8ih22xeT8huF6UngQirbLKYKH"); +} + pub mod add_set_compute_unit_price_ix { solana_sdk::declare_id!("98std1NSHqXi9WYvFShfVepRdCoq1qvsp8fsR2XZtG8g"); } @@ -671,6 +675,7 @@ lazy_static! { (stake_allow_zero_undelegated_amount::id(), "Allow zero-lamport undelegated amount for initialized stakes #24670"), (require_static_program_ids_in_transaction::id(), "require static program ids in versioned transactions"), (stake_raise_minimum_delegation_to_1_sol::id(), "Raise minimum stake delegation to 1.0 SOL #24357"), + (stake_minimum_delegation_for_rewards::id(), "stakes must be at least the minimum delegation to earn rewards"), (add_set_compute_unit_price_ix::id(), "add compute budget ix for setting a compute unit price"), (disable_deploy_of_alloc_free_syscall::id(), "disable new deployments of deprecated sol_alloc_free_ syscall"), (include_account_index_in_rent_error::id(), "include account index in rent tx error #25190"),