From 0af1452749eda8c986e0255cd4d3817a913f8e9a Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Wed, 23 Oct 2024 09:48:37 +0400 Subject: [PATCH 1/4] runtime: bank: use `get_current_epoch_stake` for SVM processing env --- runtime/src/bank.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 9c9879ae438417..c292c13bc6c565 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -3673,8 +3673,8 @@ impl Bank { RentCollectorWithMetrics::new(self.rent_collector.clone()); let processing_environment = TransactionProcessingEnvironment { blockhash, - epoch_total_stake: self.epoch_total_stake(self.epoch()), - epoch_vote_accounts: self.epoch_vote_accounts(self.epoch()), + epoch_total_stake: self.get_current_epoch_total_stake(), + epoch_vote_accounts: self.get_current_epoch_vote_accounts(), feature_set: Arc::clone(&self.feature_set), fee_structure: Some(&self.fee_structure), lamports_per_signature, @@ -6302,6 +6302,13 @@ impl Bank { .map(|epoch_stakes| epoch_stakes.total_stake()) } + /// Get the total epoch stake for the current Bank::epoch + pub fn get_current_epoch_total_stake(&self) -> Option { + self.epoch_stakes + .get(&self.epoch.saturating_add(1)) + .map(|epoch_stakes| epoch_stakes.total_stake()) + } + /// vote accounts for the specific epoch along with the stake /// attributed to each account pub fn epoch_vote_accounts(&self, epoch: Epoch) -> Option<&VoteAccountsHashMap> { @@ -6309,6 +6316,16 @@ impl Bank { Some(epoch_stakes.vote_accounts().as_ref()) } + /// Get the vote accounts along with the stake attributed to each account + /// for the current Bank::epoch + pub fn get_current_epoch_vote_accounts(&self) -> Option<&VoteAccountsHashMap> { + let epoch_stakes = self + .epoch_stakes + .get(&self.epoch.saturating_add(1))? + .stakes(); + Some(epoch_stakes.vote_accounts().as_ref()) + } + /// Get the fixed authorized voter for the given vote account for the /// current epoch pub fn epoch_authorized_voter(&self, vote_account: &Pubkey) -> Option<&Pubkey> { From 4d6ba2189f64277080c08773dec5846a1872ac5a Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Wed, 23 Oct 2024 10:09:09 +0400 Subject: [PATCH 2/4] runtime: bank: expand on `test_bank_epoch_stakes` --- runtime/src/bank/tests.rs | 131 ++++++++++++++++++++++++++++++++------ 1 file changed, 110 insertions(+), 21 deletions(-) diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 2d6f92e2bd4e89..598068b50728dc 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -13233,6 +13233,11 @@ fn test_bank_epoch_stakes() { let initial_epochs = bank0.epoch_stake_keys(); assert_eq!(initial_epochs, vec![0, 1]); + // Bank 0: + + // First query with bank's epoch. As noted by the above check, the epoch + // stakes cache actually contains values for the _leader schedule_ epoch + // (N + 1). Therefore, we should be able to query both. assert_eq!(bank0.epoch(), 0); assert_eq!(bank0.epoch_total_stake(0), Some(total_stake)); assert_eq!(bank0.epoch_node_id_to_stake(0, &Pubkey::new_unique()), None); @@ -13242,6 +13247,37 @@ fn test_bank_epoch_stakes() { Some(stakes[i]) ); } + + // Now query for epoch 1 on bank 0. + assert_eq!(bank0.epoch().saturating_add(1), 1); + assert_eq!(bank0.epoch_total_stake(1), Some(total_stake)); + assert_eq!(bank0.epoch_node_id_to_stake(1, &Pubkey::new_unique()), None); + for (i, keypair) in voting_keypairs.iter().enumerate() { + assert_eq!( + bank0.epoch_node_id_to_stake(1, &keypair.node_keypair.pubkey()), + Some(stakes[i]) + ); + } + + // Note using bank's `current_epoch_stake_*` methods should return the + // same values. + assert_eq!(bank0.get_current_epoch_total_stake(), Some(total_stake)); + assert_eq!( + bank0.get_current_epoch_total_stake(), + bank0.epoch_total_stake(1) + ); + assert_eq!( + bank0.get_current_epoch_vote_accounts().unwrap().len(), + voting_keypairs.len() + ); + assert_eq!( + bank0.epoch_vote_accounts(1), + bank0.get_current_epoch_vote_accounts(), + ); + + // Bank 1: + + // Run the same exercise. First query the bank's epoch. assert_eq!(bank1.epoch(), 1); assert_eq!(bank1.epoch_total_stake(1), Some(total_stake)); assert_eq!(bank1.epoch_node_id_to_stake(1, &Pubkey::new_unique()), None); @@ -13252,32 +13288,85 @@ fn test_bank_epoch_stakes() { ); } - let new_epoch_stakes = EpochStakes::new_for_tests( - voting_keypairs - .iter() - .map(|keypair| { - let node_id = keypair.node_keypair.pubkey(); - let authorized_voter = keypair.vote_keypair.pubkey(); - let vote_account = VoteAccount::try_from(create_account_with_authorized( - &node_id, - &authorized_voter, - &node_id, - 0, - 100, - )) - .unwrap(); - (authorized_voter, (100_u64, vote_account)) - }) - .collect::>(), - 1, + // Now query for epoch 2 on bank 1. + assert_eq!(bank1.epoch().saturating_add(1), 2); + assert_eq!(bank1.epoch_total_stake(2), Some(total_stake)); + assert_eq!(bank1.epoch_node_id_to_stake(2, &Pubkey::new_unique()), None); + for (i, keypair) in voting_keypairs.iter().enumerate() { + assert_eq!( + bank1.epoch_node_id_to_stake(2, &keypair.node_keypair.pubkey()), + Some(stakes[i]) + ); + } + + // Again, using bank's `current_epoch_stake_*` methods should return the + // same values. + assert_eq!(bank1.get_current_epoch_total_stake(), Some(total_stake)); + assert_eq!( + bank1.get_current_epoch_total_stake(), + bank1.epoch_total_stake(2) + ); + assert_eq!( + bank1.get_current_epoch_vote_accounts().unwrap().len(), + voting_keypairs.len() + ); + assert_eq!( + bank1.epoch_vote_accounts(2), + bank1.get_current_epoch_vote_accounts(), + ); + + // Setup new epoch stakes on Bank 1 for both leader schedule epochs. + let make_new_epoch_stakes = |stake_coefficient: u64| { + EpochStakes::new_for_tests( + voting_keypairs + .iter() + .map(|keypair| { + let node_id = keypair.node_keypair.pubkey(); + let authorized_voter = keypair.vote_keypair.pubkey(); + let vote_account = VoteAccount::try_from(create_account_with_authorized( + &node_id, + &authorized_voter, + &node_id, + 0, + 100, + )) + .unwrap(); + (authorized_voter, (stake_coefficient, vote_account)) + }) + .collect::>(), + 1, + ) + }; + let stake_coefficient_epoch_1 = 100; + let stake_coefficient_epoch_2 = 500; + bank1.set_epoch_stakes_for_test(1, make_new_epoch_stakes(stake_coefficient_epoch_1)); + bank1.set_epoch_stakes_for_test(2, make_new_epoch_stakes(stake_coefficient_epoch_2)); + + // Run the exercise again with the new stake. Now epoch 1 should have the + // stake added for epoch 1 (`stake_coefficient_epoch_1`). + assert_eq!( + bank1.epoch_total_stake(1), + Some(stake_coefficient_epoch_1 * num_of_nodes) ); - bank1.set_epoch_stakes_for_test(1, new_epoch_stakes); - assert_eq!(bank1.epoch_total_stake(1), Some(100 * num_of_nodes)); assert_eq!(bank1.epoch_node_id_to_stake(1, &Pubkey::new_unique()), None); for keypair in voting_keypairs.iter() { assert_eq!( bank1.epoch_node_id_to_stake(1, &keypair.node_keypair.pubkey()), - Some(100) + Some(stake_coefficient_epoch_1) + ); + } + + // Now query for epoch 2 on bank 1. Epoch 2 should have the stake added for + // epoch 2 (`stake_coefficient_epoch_2`). + assert_eq!( + bank1.epoch_total_stake(2), + Some(stake_coefficient_epoch_2 * num_of_nodes) + ); + assert_eq!(bank1.epoch_node_id_to_stake(2, &Pubkey::new_unique()), None); + for keypair in voting_keypairs.iter() { + assert_eq!( + bank1.epoch_node_id_to_stake(2, &keypair.node_keypair.pubkey()), + Some(stake_coefficient_epoch_2) ); } } From e284ff7218a7eb952e45751ffce9c6c112946bbf Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Wed, 23 Oct 2024 12:30:44 +0400 Subject: [PATCH 3/4] add SBF test for `sol_get_epoch_stake` --- programs/sbf/Cargo.lock | 9 ++ programs/sbf/Cargo.toml | 5 + .../rust/syscall-get-epoch-stake/Cargo.toml | 15 +++ .../rust/syscall-get-epoch-stake/src/lib.rs | 39 +++++++ programs/sbf/tests/syscall_get_epoch_stake.rs | 102 ++++++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 programs/sbf/rust/syscall-get-epoch-stake/Cargo.toml create mode 100644 programs/sbf/rust/syscall-get-epoch-stake/src/lib.rs create mode 100644 programs/sbf/tests/syscall_get_epoch_stake.rs diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index dc21b10abe671c..21dfaab9c58c53 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6229,6 +6229,8 @@ dependencies = [ "solana-timings", "solana-transaction-status", "solana-type-overrides", + "solana-vote", + "solana-vote-program", "solana_rbpf", ] @@ -6667,6 +6669,13 @@ dependencies = [ "solana-program", ] +[[package]] +name = "solana-sbf-syscall-get-epoch-stake" +version = "2.1.0" +dependencies = [ + "solana-program", +] + [[package]] name = "solana-sdk" version = "2.1.0" diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index 7a60f3a55d6e3b..bb7da1607fb002 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -64,6 +64,8 @@ solana-svm-transaction = { path = "../../svm-transaction", version = "=2.1.0" } solana-timings = { path = "../../timings", version = "=2.1.0" } solana-transaction-status = { path = "../../transaction-status", version = "=2.1.0" } solana-type-overrides = { path = "../../type-overrides", version = "=2.1.0" } +solana-vote = { path = "../../vote", version = "=2.1.0" } +solana-vote-program = { path = "../../programs/vote", version = "=2.1.0" } agave-validator = { path = "../../validator", version = "=2.1.0" } solana-zk-token-sdk = { path = "../../zk-token-sdk", version = "=2.1.0" } solana_rbpf = "=0.8.5" @@ -128,6 +130,8 @@ solana-svm-transaction = { workspace = true } solana-timings = { workspace = true } solana-transaction-status = { workspace = true } solana-type-overrides = { workspace = true } +solana-vote = { workspace = true } +solana-vote-program = { workspace = true } solana_rbpf = { workspace = true } [[bench]] @@ -186,6 +190,7 @@ members = [ "rust/simulation", "rust/spoof1", "rust/spoof1_system", + "rust/syscall-get-epoch-stake", "rust/sysvar", "rust/upgradeable", "rust/upgraded", diff --git a/programs/sbf/rust/syscall-get-epoch-stake/Cargo.toml b/programs/sbf/rust/syscall-get-epoch-stake/Cargo.toml new file mode 100644 index 00000000000000..c3fb6e3f6dd04b --- /dev/null +++ b/programs/sbf/rust/syscall-get-epoch-stake/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "solana-sbf-syscall-get-epoch-stake" +version = { workspace = true } +description = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] +solana-program = { workspace = true } + +[lib] +crate-type = ["cdylib"] diff --git a/programs/sbf/rust/syscall-get-epoch-stake/src/lib.rs b/programs/sbf/rust/syscall-get-epoch-stake/src/lib.rs new file mode 100644 index 00000000000000..986ed681409a2a --- /dev/null +++ b/programs/sbf/rust/syscall-get-epoch-stake/src/lib.rs @@ -0,0 +1,39 @@ +//! Example Rust-based SBF program that tests the `sol_get_epoch_stake` +//! syscall. + +extern crate solana_program; +use solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + epoch_stake::{get_epoch_stake_for_vote_account, get_epoch_total_stake}, + msg, + program::set_return_data, + pubkey::Pubkey, +}; + +solana_program::entrypoint_no_alloc!(process_instruction); +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { + // Total stake. + let total_stake = get_epoch_total_stake(); + assert_ne!(total_stake, 0); + msg!("Total Stake: {}", total_stake); + + // Vote accounts. + let check_vote_account_stake = |i: usize| { + let vote_address = accounts[i].key; + let vote_stake = get_epoch_stake_for_vote_account(vote_address); + assert_ne!(vote_stake, 0); + msg!("Vote Stake for account {}: {}", i, vote_stake); + }; + check_vote_account_stake(0); + check_vote_account_stake(1); + + // For good measure, set the return data to total stake. + set_return_data(&total_stake.to_le_bytes()); + + Ok(()) +} diff --git a/programs/sbf/tests/syscall_get_epoch_stake.rs b/programs/sbf/tests/syscall_get_epoch_stake.rs new file mode 100644 index 00000000000000..2de6d36bbf7664 --- /dev/null +++ b/programs/sbf/tests/syscall_get_epoch_stake.rs @@ -0,0 +1,102 @@ +#![cfg(feature = "sbf_rust")] + +use { + solana_runtime::{ + bank::Bank, + bank_client::BankClient, + epoch_stakes::EpochStakes, + genesis_utils::{ + create_genesis_config_with_vote_accounts, GenesisConfigInfo, ValidatorVoteKeypairs, + }, + loader_utils::load_upgradeable_program_and_advance_slot, + }, + solana_sdk::{ + instruction::{AccountMeta, Instruction}, + message::Message, + signature::{Keypair, Signer}, + transaction::{SanitizedTransaction, Transaction}, + }, + solana_vote::vote_account::VoteAccount, + solana_vote_program::vote_state::create_account_with_authorized, + std::collections::HashMap, +}; + +#[test] +fn test_syscall_get_epoch_stake() { + solana_logger::setup(); + + // Two vote accounts with stake. + let stakes = vec![100_000_000, 500_000_000]; + let voting_keypairs = vec![ + ValidatorVoteKeypairs::new_rand(), + ValidatorVoteKeypairs::new_rand(), + ]; + let total_stake: u64 = stakes.iter().sum(); + + let GenesisConfigInfo { + genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_vote_accounts(1_000_000_000, &voting_keypairs, stakes.clone()); + + let mut bank = Bank::new_for_tests(&genesis_config); + + // Intentionally overwrite the bank epoch with no stake, to ensure the + // syscall gets the _current_ epoch stake based on the leader schedule + // (N + 1). + let epoch_stakes_epoch_0 = EpochStakes::new_for_tests( + voting_keypairs + .iter() + .map(|keypair| { + let node_id = keypair.node_keypair.pubkey(); + let authorized_voter = keypair.vote_keypair.pubkey(); + let vote_account = VoteAccount::try_from(create_account_with_authorized( + &node_id, + &authorized_voter, + &node_id, + 0, + 100, + )) + .unwrap(); + (authorized_voter, (0, vote_account)) // No stake. + }) + .collect::>(), + 0, // Leader schedule epoch 0 + ); + bank.set_epoch_stakes_for_test(0, epoch_stakes_epoch_0); + + let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); + let mut bank_client = BankClient::new_shared(bank); + + let authority_keypair = Keypair::new(); + let (bank, program_id) = load_upgradeable_program_and_advance_slot( + &mut bank_client, + bank_forks.as_ref(), + &mint_keypair, + &authority_keypair, + "solana_sbf_syscall_get_epoch_stake", + ); + bank.freeze(); + + let instruction = Instruction::new_with_bytes( + program_id, + &[], + vec![ + AccountMeta::new_readonly(voting_keypairs[0].vote_keypair.pubkey(), false), + AccountMeta::new_readonly(voting_keypairs[1].vote_keypair.pubkey(), false), + ], + ); + + let blockhash = bank.last_blockhash(); + let message = Message::new(&[instruction], Some(&mint_keypair.pubkey())); + let transaction = Transaction::new(&[&mint_keypair], message, blockhash); + let sanitized_tx = SanitizedTransaction::from_transaction_for_tests(transaction); + + let result = bank.simulate_transaction(&sanitized_tx, false); + + assert!(result.result.is_ok()); + + let return_data_le_bytes: [u8; 8] = result.return_data.unwrap().data[0..8].try_into().unwrap(); + let total_stake_from_return_data = u64::from_le_bytes(return_data_le_bytes); + assert_eq!(total_stake_from_return_data, total_stake); +} From 251c0b6031ec7d86bb0bd926e232d2ba50c627be Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 24 Oct 2024 10:18:11 +0400 Subject: [PATCH 4/4] use existing `current_epoch_stakes` for new methods --- runtime/src/bank.rs | 21 +++++++++------------ runtime/src/bank/tests.rs | 16 ++++++++-------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index c292c13bc6c565..632055fb8bde5f 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -3673,8 +3673,8 @@ impl Bank { RentCollectorWithMetrics::new(self.rent_collector.clone()); let processing_environment = TransactionProcessingEnvironment { blockhash, - epoch_total_stake: self.get_current_epoch_total_stake(), - epoch_vote_accounts: self.get_current_epoch_vote_accounts(), + epoch_total_stake: Some(self.get_current_epoch_total_stake()), + epoch_vote_accounts: Some(self.get_current_epoch_vote_accounts()), feature_set: Arc::clone(&self.feature_set), fee_structure: Some(&self.fee_structure), lamports_per_signature, @@ -6303,10 +6303,8 @@ impl Bank { } /// Get the total epoch stake for the current Bank::epoch - pub fn get_current_epoch_total_stake(&self) -> Option { - self.epoch_stakes - .get(&self.epoch.saturating_add(1)) - .map(|epoch_stakes| epoch_stakes.total_stake()) + pub fn get_current_epoch_total_stake(&self) -> u64 { + self.current_epoch_stakes().total_stake() } /// vote accounts for the specific epoch along with the stake @@ -6318,12 +6316,11 @@ impl Bank { /// Get the vote accounts along with the stake attributed to each account /// for the current Bank::epoch - pub fn get_current_epoch_vote_accounts(&self) -> Option<&VoteAccountsHashMap> { - let epoch_stakes = self - .epoch_stakes - .get(&self.epoch.saturating_add(1))? - .stakes(); - Some(epoch_stakes.vote_accounts().as_ref()) + pub fn get_current_epoch_vote_accounts(&self) -> &VoteAccountsHashMap { + self.current_epoch_stakes() + .stakes() + .vote_accounts() + .as_ref() } /// Get the fixed authorized voter for the given vote account for the diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 598068b50728dc..15ea742b65b4eb 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -13261,17 +13261,17 @@ fn test_bank_epoch_stakes() { // Note using bank's `current_epoch_stake_*` methods should return the // same values. - assert_eq!(bank0.get_current_epoch_total_stake(), Some(total_stake)); + assert_eq!(bank0.get_current_epoch_total_stake(), total_stake); assert_eq!( bank0.get_current_epoch_total_stake(), - bank0.epoch_total_stake(1) + bank0.epoch_total_stake(1).unwrap(), ); assert_eq!( - bank0.get_current_epoch_vote_accounts().unwrap().len(), + bank0.get_current_epoch_vote_accounts().len(), voting_keypairs.len() ); assert_eq!( - bank0.epoch_vote_accounts(1), + bank0.epoch_vote_accounts(1).unwrap(), bank0.get_current_epoch_vote_accounts(), ); @@ -13301,17 +13301,17 @@ fn test_bank_epoch_stakes() { // Again, using bank's `current_epoch_stake_*` methods should return the // same values. - assert_eq!(bank1.get_current_epoch_total_stake(), Some(total_stake)); + assert_eq!(bank1.get_current_epoch_total_stake(), total_stake); assert_eq!( bank1.get_current_epoch_total_stake(), - bank1.epoch_total_stake(2) + bank1.epoch_total_stake(2).unwrap(), ); assert_eq!( - bank1.get_current_epoch_vote_accounts().unwrap().len(), + bank1.get_current_epoch_vote_accounts().len(), voting_keypairs.len() ); assert_eq!( - bank1.epoch_vote_accounts(2), + bank1.epoch_vote_accounts(2).unwrap(), bank1.get_current_epoch_vote_accounts(), );