From 977528360e5b14299823e95eef97e942719ad43f Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Wed, 27 Nov 2024 00:55:02 -0800 Subject: [PATCH 01/35] add mollusk --- Cargo.lock | 39 +++++++++++++++++++++ program/Cargo.toml | 1 + program/tests/{tests.rs => program_test.rs} | 20 +++-------- 3 files changed, 45 insertions(+), 15 deletions(-) rename program/tests/{tests.rs => program_test.rs} (99%) diff --git a/Cargo.lock b/Cargo.lock index f2c6bc0..2a58ba0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2346,6 +2346,44 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "mollusk-svm" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0af5273982e065c575c75a5b234e2d4a90863cb1bbb915650631b346861e44e5" +dependencies = [ + "bincode", + "mollusk-svm-error", + "mollusk-svm-keys", + "solana-bpf-loader-program", + "solana-compute-budget", + "solana-logger", + "solana-program-runtime", + "solana-sdk", + "solana-system-program", + "solana-timings", +] + +[[package]] +name = "mollusk-svm-error" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5f3cb33936bc3946244edc3c7cc85f086d00322f2bead230bd652ac966507c" +dependencies = [ + "solana-sdk", + "thiserror 1.0.69", +] + +[[package]] +name = "mollusk-svm-keys" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47cbc83276b881193fe6be786394cb7be3bcccefabe5134503e121fde9bc127" +dependencies = [ + "mollusk-svm-error", + "solana-sdk", +] + [[package]] name = "nix" version = "0.29.0" @@ -5075,6 +5113,7 @@ dependencies = [ "arrayref", "bincode", "borsh 1.5.3", + "mollusk-svm", "num-derive 0.4.2", "num-traits", "num_enum", diff --git a/program/Cargo.toml b/program/Cargo.toml index c8e07fb..658ea0e 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -22,6 +22,7 @@ solana-program = "2.1" thiserror = "1.0.63" [dev-dependencies] +mollusk-svm = "0.0.11" solana-program-test = "2.1" solana-sdk = "2.1" solana-vote-program = "2.1" diff --git a/program/tests/tests.rs b/program/tests/program_test.rs similarity index 99% rename from program/tests/tests.rs rename to program/tests/program_test.rs index 46ead70..8b09ce8 100644 --- a/program/tests/tests.rs +++ b/program/tests/program_test.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] -#![allow(unused_imports)] #![allow(clippy::arithmetic_side_effects)] use { @@ -7,32 +5,24 @@ use { solana_sdk::{ account::Account as SolanaAccount, entrypoint::ProgramResult, - feature_set::{move_stake_and_move_lamports_ixs, stake_raise_minimum_delegation_to_1_sol}, - hash::Hash, instruction::Instruction, - native_token::LAMPORTS_PER_SOL, program_error::ProgramError, pubkey::Pubkey, signature::{Keypair, Signer}, signers::Signers, stake::{ self, - instruction::{ - self as ixn, LockupArgs, LockupCheckedArgs, StakeError, StakeInstruction, - }, - state::{ - Authorized, Delegation, Lockup, Meta, Stake, StakeActivationStatus, StakeAuthorize, - StakeStateV2, - }, + instruction::{self as ixn, LockupArgs, StakeError}, + state::{Authorized, Delegation, Lockup, Meta, Stake, StakeAuthorize, StakeStateV2}, }, system_instruction, system_program, - sysvar::{clock::Clock, rent::Rent, stake_history::StakeHistory}, + sysvar::{clock::Clock, stake_history::StakeHistory}, transaction::{Transaction, TransactionError}, }, - solana_stake_program::{id, processor::Processor}, + solana_stake_program::id, solana_vote_program::{ self, vote_instruction, - vote_state::{self, VoteInit, VoteState, VoteStateVersions}, + vote_state::{VoteInit, VoteState, VoteStateVersions}, }, test_case::{test_case, test_matrix}, }; From aae4eed7bc8c390e7c2e0650c6bb4cc6b4ecd947 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:38:11 -0800 Subject: [PATCH 02/35] lock solana for mollusk --- Cargo.lock | 8 +------- program/Cargo.toml | 10 +++++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a58ba0..266139b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2349,8 +2349,6 @@ dependencies = [ [[package]] name = "mollusk-svm" version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0af5273982e065c575c75a5b234e2d4a90863cb1bbb915650631b346861e44e5" dependencies = [ "bincode", "mollusk-svm-error", @@ -2367,8 +2365,6 @@ dependencies = [ [[package]] name = "mollusk-svm-error" version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5f3cb33936bc3946244edc3c7cc85f086d00322f2bead230bd652ac966507c" dependencies = [ "solana-sdk", "thiserror 1.0.69", @@ -2377,8 +2373,6 @@ dependencies = [ [[package]] name = "mollusk-svm-keys" version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47cbc83276b881193fe6be786394cb7be3bcccefabe5134503e121fde9bc127" dependencies = [ "mollusk-svm-error", "solana-sdk", @@ -5117,10 +5111,10 @@ dependencies = [ "num-derive 0.4.2", "num-traits", "num_enum", + "solana-account", "solana-program", "solana-program-test", "solana-sdk", - "solana-vote-program", "test-case", "thiserror 1.0.69", ] diff --git a/program/Cargo.toml b/program/Cargo.toml index 658ea0e..2264a3c 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -18,14 +18,14 @@ borsh = { version = "1.5.1", features = ["derive", "unstable__schema"] } num-derive = "0.4" num-traits = "0.2" num_enum = "0.7.3" -solana-program = "2.1" +solana-program = "=2.1.0" thiserror = "1.0.63" [dev-dependencies] -mollusk-svm = "0.0.11" -solana-program-test = "2.1" -solana-sdk = "2.1" -solana-vote-program = "2.1" +mollusk-svm = { path = "/home/hana/work/misc/mollusk/harness" } +solana-account = { version = "=2.1.0", features = ["bincode"] } +solana-program-test = "=2.1.0" +solana-sdk = "=2.1.0" test-case = "3.3.1" [lib] From bd52ecce819723619d4181a49b13e1c23cedf844 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:38:34 -0800 Subject: [PATCH 03/35] initial test setup --- .../tests/fixtures/solana_stake_program.so | 1 + program/tests/mollusk.rs | 148 ++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 120000 program/tests/fixtures/solana_stake_program.so create mode 100644 program/tests/mollusk.rs diff --git a/program/tests/fixtures/solana_stake_program.so b/program/tests/fixtures/solana_stake_program.so new file mode 120000 index 0000000..75ba6f3 --- /dev/null +++ b/program/tests/fixtures/solana_stake_program.so @@ -0,0 +1 @@ +../../../target/deploy/solana_stake_program.so \ No newline at end of file diff --git a/program/tests/mollusk.rs b/program/tests/mollusk.rs new file mode 100644 index 0000000..8f32b8b --- /dev/null +++ b/program/tests/mollusk.rs @@ -0,0 +1,148 @@ +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(clippy::arithmetic_side_effects)] + +use { + mollusk_svm::{result::Check, Mollusk}, + solana_account::{AccountSharedData, WritableAccount}, + solana_sdk::{ + account::Account as SolanaAccount, + entrypoint::ProgramResult, + feature_set::{move_stake_and_move_lamports_ixs, stake_raise_minimum_delegation_to_1_sol}, + hash::Hash, + instruction::Instruction, + native_token::LAMPORTS_PER_SOL, + program_error::ProgramError, + pubkey::Pubkey, + signature::{Keypair, Signer}, + signers::Signers, + stake::{ + self, + instruction::{ + self as ixn, LockupArgs, LockupCheckedArgs, StakeError, StakeInstruction, + }, + state::{ + warmup_cooldown_rate, Authorized, Delegation, Lockup, Meta, Stake, + StakeActivationStatus, StakeAuthorize, StakeStateV2, + }, + }, + stake_history::StakeHistoryEntry, + system_instruction, system_program, + sysvar::{ + clock::Clock, epoch_schedule::EpochSchedule, rent::Rent, stake_history::StakeHistory, + }, + transaction::{Transaction, TransactionError}, + vote::{ + program as vote_program, + state::{VoteInit, VoteState, VoteStateVersions}, + }, + }, + solana_stake_program::{id, processor::Processor}, + std::collections::HashMap, + test_case::{test_case, test_matrix}, +}; + +// XXX ok so wow i am going to have to write a lot of shit +// we need a mechanism to create basically arbitrary stake accounts +// this means all states (uninit, init, activating, active, deactivating, deactive) +// we need to be able to make a stake history that gives us partial activation/deactivation +// actually we need to set up stake history ourselves correctly in all cases +// we need to be able to set lockup and authority arbitrarily +// we need helpers to set up with seed pubkeys +// ideally we automatically check missing signer failures +// need to create a vote account... ugh we need to get credits right for DeactivateDelinquent +// for delegate we just need owner, vote account pubkey, and credits (can be 0) + +// arbitrary, but gives us room to set up activations/deactivations serveral epochs in the past +const EXECUTION_EPOCH: u64 = 8; + +// mollusk doesnt charge transaction fees, this is just a convenient source of lamports +const PAYER: Pubkey = Pubkey::from_str_const("PAYER7y5empWisbHsxHGE7vgBWKZCemGo1gKw7NpQSK"); +const PAYER_BALANCE: u64 = 1_000_000 * LAMPORTS_PER_SOL; + +// two vote accounts with no credits, fine for all stake tests except DeactivateDelinquent +const VOTE_ACCOUNT_RED: Pubkey = + Pubkey::from_str_const("REDjn6cyjcZkXAvRHWFtAd4chwHd6MmtqT2u965cDqg"); +const VOTE_ACCOUNT_BLUE: Pubkey = + Pubkey::from_str_const("BLUE7fsMB69ti5fDRZEbZVoWdXbCoA8bwk963vXsZs7"); + +// two blank stake accounts that can be serialized into for tests +const STAKE_ACCOUNT_BLACK: Pubkey = + Pubkey::from_str_const("BLACK8oXP6Ar933gupyVZqunKYmmb8rEnrbPSqpxbFt"); +const STAKE_ACCOUNT_WHITE: Pubkey = + Pubkey::from_str_const("WH1TE3e9czGF33AtbkTBbQ4BQ3EY7BaL8utApeYfSnL"); + +// stake delegated to some imaginary vote account in all epochs +// with a warmup/cooldown rate of 9%, routine tests moving under 9sol can ignore stake history +// while also making it easy to write tests involving partial (de)activations +// if the warmup/cooldown rate changes, this number must be adjusted +const PERSISTANT_ACTIVE_STAKE: u64 = 100 * LAMPORTS_PER_SOL; +#[test] +fn assert_warmup_cooldown_rate() { + assert_eq!(warmup_cooldown_rate(0, Some(0)), 0.09); +} + +// hardcoded for convenience +const STAKE_RENT_EXEMPTION: u64 = 2_282_880; +#[test] +fn assert_stake_rent_exemption() { + assert_eq!( + Rent::default().minimum_balance(StakeStateV2::size_of()), + STAKE_RENT_EXEMPTION + ); +} + +struct Env { + run: Mollusk, + accounts: HashMap, +} + +fn setup() -> Env { + // create a test environment at the execution epoch + let mut accounts = HashMap::new(); + let mut mollusk = Mollusk::new(&id(), "solana_stake_program"); + mollusk.warp_to_slot(EXECUTION_EPOCH * mollusk.sysvars.epoch_schedule.slots_per_epoch + 1); + assert_eq!(mollusk.sysvars.clock.epoch, EXECUTION_EPOCH); + + // backfill stake history + for epoch in 0..EXECUTION_EPOCH { + mollusk.sysvars.stake_history.add( + epoch, + StakeHistoryEntry::with_effective(PERSISTANT_ACTIVE_STAKE), + ); + } + + // add a lamports source + let payer_data = + AccountSharedData::new_rent_epoch(PAYER_BALANCE, 0, &system_program::id(), u64::MAX); + accounts.insert(PAYER, payer_data); + + // create two vote accounts + let vote_rent_exemption = Rent::default().minimum_balance(VoteState::size_of()); + let vote_state = bincode::serialize(&VoteState::default()).unwrap(); + let vote_data = AccountSharedData::create( + vote_rent_exemption, + vote_state, + vote_program::id(), + false, + u64::MAX, + ); + accounts.insert(VOTE_ACCOUNT_RED, vote_data.clone()); + accounts.insert(VOTE_ACCOUNT_BLUE, vote_data); + + // create two blank stake accounts + let stake_data = AccountSharedData::create( + STAKE_RENT_EXEMPTION, + vec![0; StakeStateV2::size_of()], + id(), + false, + u64::MAX, + ); + accounts.insert(STAKE_ACCOUNT_BLACK, stake_data.clone()); + accounts.insert(STAKE_ACCOUNT_WHITE, stake_data); + + Env { + run: mollusk, + accounts, + } +} From b9fde691ca32691890992c583410919406955c32 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 29 Nov 2024 02:40:16 -0800 Subject: [PATCH 04/35] example test --- program/tests/mollusk.rs | 138 ++++++++++++++++++++++++++------------- 1 file changed, 93 insertions(+), 45 deletions(-) diff --git a/program/tests/mollusk.rs b/program/tests/mollusk.rs index 8f32b8b..c06defd 100644 --- a/program/tests/mollusk.rs +++ b/program/tests/mollusk.rs @@ -10,7 +10,7 @@ use { entrypoint::ProgramResult, feature_set::{move_stake_and_move_lamports_ixs, stake_raise_minimum_delegation_to_1_sol}, hash::Hash, - instruction::Instruction, + instruction::{AccountMeta, Instruction}, native_token::LAMPORTS_PER_SOL, program_error::ProgramError, pubkey::Pubkey, @@ -30,6 +30,7 @@ use { system_instruction, system_program, sysvar::{ clock::Clock, epoch_schedule::EpochSchedule, rent::Rent, stake_history::StakeHistory, + SysvarId, }, transaction::{Transaction, TransactionError}, vote::{ @@ -93,56 +94,103 @@ fn assert_stake_rent_exemption() { } struct Env { - run: Mollusk, + mollusk: Mollusk, accounts: HashMap, } +impl Env { + fn init() -> Self { + // create a test environment at the execution epoch + let mut accounts = HashMap::new(); + let mut mollusk = Mollusk::new(&id(), "solana_stake_program"); + mollusk.warp_to_slot(EXECUTION_EPOCH * mollusk.sysvars.epoch_schedule.slots_per_epoch + 1); + assert_eq!(mollusk.sysvars.clock.epoch, EXECUTION_EPOCH); -fn setup() -> Env { - // create a test environment at the execution epoch - let mut accounts = HashMap::new(); - let mut mollusk = Mollusk::new(&id(), "solana_stake_program"); - mollusk.warp_to_slot(EXECUTION_EPOCH * mollusk.sysvars.epoch_schedule.slots_per_epoch + 1); - assert_eq!(mollusk.sysvars.clock.epoch, EXECUTION_EPOCH); - - // backfill stake history - for epoch in 0..EXECUTION_EPOCH { - mollusk.sysvars.stake_history.add( - epoch, - StakeHistoryEntry::with_effective(PERSISTANT_ACTIVE_STAKE), + // backfill stake history + for epoch in 0..EXECUTION_EPOCH { + mollusk.sysvars.stake_history.add( + epoch, + StakeHistoryEntry::with_effective(PERSISTANT_ACTIVE_STAKE), + ); + } + + // add a lamports source + let payer_data = + AccountSharedData::new_rent_epoch(PAYER_BALANCE, 0, &system_program::id(), u64::MAX); + accounts.insert(PAYER, payer_data); + + // create two vote accounts + let vote_rent_exemption = Rent::default().minimum_balance(VoteState::size_of()); + let vote_state = bincode::serialize(&VoteState::default()).unwrap(); + let vote_data = AccountSharedData::create( + vote_rent_exemption, + vote_state, + vote_program::id(), + false, + u64::MAX, ); + accounts.insert(VOTE_ACCOUNT_RED, vote_data.clone()); + accounts.insert(VOTE_ACCOUNT_BLUE, vote_data); + + // create two blank stake accounts + let stake_data = AccountSharedData::create( + STAKE_RENT_EXEMPTION, + vec![0; StakeStateV2::size_of()], + id(), + false, + u64::MAX, + ); + accounts.insert(STAKE_ACCOUNT_BLACK, stake_data.clone()); + accounts.insert(STAKE_ACCOUNT_WHITE, stake_data); + + Self { mollusk, accounts } } - // add a lamports source - let payer_data = - AccountSharedData::new_rent_epoch(PAYER_BALANCE, 0, &system_program::id(), u64::MAX); - accounts.insert(PAYER, payer_data); - - // create two vote accounts - let vote_rent_exemption = Rent::default().minimum_balance(VoteState::size_of()); - let vote_state = bincode::serialize(&VoteState::default()).unwrap(); - let vote_data = AccountSharedData::create( - vote_rent_exemption, - vote_state, - vote_program::id(), - false, - u64::MAX, - ); - accounts.insert(VOTE_ACCOUNT_RED, vote_data.clone()); - accounts.insert(VOTE_ACCOUNT_BLUE, vote_data); - - // create two blank stake accounts - let stake_data = AccountSharedData::create( - STAKE_RENT_EXEMPTION, - vec![0; StakeStateV2::size_of()], - id(), - false, - u64::MAX, - ); - accounts.insert(STAKE_ACCOUNT_BLACK, stake_data.clone()); - accounts.insert(STAKE_ACCOUNT_WHITE, stake_data); + fn resolve_accounts(&self, account_metas: &[AccountMeta]) -> Vec<(Pubkey, AccountSharedData)> { + let mut accounts = vec![]; + for account_meta in account_metas { + let key = account_meta.pubkey; + let account_shared_data = if Rent::check_id(&key) { + self.mollusk.sysvars.keyed_account_for_rent_sysvar().1 + } else { + self.accounts.get(&key).cloned().unwrap() + }; - Env { - run: mollusk, - accounts, + accounts.push((key, account_shared_data)); + } + + accounts } } + +fn stake_to_bytes(stake: &StakeStateV2) -> Vec { + let mut data = vec![0; StakeStateV2::size_of()]; + bincode::serialize_into(&mut data[..], stake).unwrap(); + data +} + +#[test] +fn test_initialize() { + let env = Env::init(); + + let authorized = Authorized::default(); + let lockup = Lockup::default(); + + let instruction = ixn::initialize(&STAKE_ACCOUNT_BLACK, &authorized, &lockup); + let accounts = env.resolve_accounts(&instruction.accounts); + + let black_state = StakeStateV2::Initialized(Meta { + rent_exempt_reserve: STAKE_RENT_EXEMPTION, + authorized, + lockup, + }); + let black = stake_to_bytes(&black_state); + + env.mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[ + Check::success(), + Check::account(&STAKE_ACCOUNT_BLACK).data(&black).build(), + ], + ); +} From b8db3ac5930959b25c9c514cd789e5d801ecdbc3 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Mon, 2 Dec 2024 02:35:20 -0800 Subject: [PATCH 05/35] improve runner a bit to try to copy ixn tests --- program/tests/mollusk.rs | 101 +++++++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 10 deletions(-) diff --git a/program/tests/mollusk.rs b/program/tests/mollusk.rs index c06defd..a43fc77 100644 --- a/program/tests/mollusk.rs +++ b/program/tests/mollusk.rs @@ -4,7 +4,7 @@ use { mollusk_svm::{result::Check, Mollusk}, - solana_account::{AccountSharedData, WritableAccount}, + solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, solana_sdk::{ account::Account as SolanaAccount, entrypoint::ProgramResult, @@ -21,6 +21,7 @@ use { instruction::{ self as ixn, LockupArgs, LockupCheckedArgs, StakeError, StakeInstruction, }, + stake_flags::StakeFlags, state::{ warmup_cooldown_rate, Authorized, Delegation, Lockup, Meta, Stake, StakeActivationStatus, StakeAuthorize, StakeStateV2, @@ -53,25 +54,42 @@ use { // ideally we automatically check missing signer failures // need to create a vote account... ugh we need to get credits right for DeactivateDelinquent // for delegate we just need owner, vote account pubkey, and credits (can be 0) +// +// XXX OK i wrote a simple init test +// what to do on monday... i guess go through the stake ixn tests and see what to impl +// main thing we lack is full coverage for lockup and i think a bunch of split edge cases // arbitrary, but gives us room to set up activations/deactivations serveral epochs in the past const EXECUTION_EPOCH: u64 = 8; // mollusk doesnt charge transaction fees, this is just a convenient source of lamports -const PAYER: Pubkey = Pubkey::from_str_const("PAYER7y5empWisbHsxHGE7vgBWKZCemGo1gKw7NpQSK"); +const PAYER: Pubkey = Pubkey::from_str_const("PAYER11111111111111111111111111111111111111"); const PAYER_BALANCE: u64 = 1_000_000 * LAMPORTS_PER_SOL; // two vote accounts with no credits, fine for all stake tests except DeactivateDelinquent const VOTE_ACCOUNT_RED: Pubkey = - Pubkey::from_str_const("REDjn6cyjcZkXAvRHWFtAd4chwHd6MmtqT2u965cDqg"); + Pubkey::from_str_const("RED1111111111111111111111111111111111111111"); const VOTE_ACCOUNT_BLUE: Pubkey = - Pubkey::from_str_const("BLUE7fsMB69ti5fDRZEbZVoWdXbCoA8bwk963vXsZs7"); + Pubkey::from_str_const("BLUE111111111111111111111111111111111111111"); // two blank stake accounts that can be serialized into for tests const STAKE_ACCOUNT_BLACK: Pubkey = - Pubkey::from_str_const("BLACK8oXP6Ar933gupyVZqunKYmmb8rEnrbPSqpxbFt"); + Pubkey::from_str_const("BLACK11111111111111111111111111111111111111"); const STAKE_ACCOUNT_WHITE: Pubkey = - Pubkey::from_str_const("WH1TE3e9czGF33AtbkTBbQ4BQ3EY7BaL8utApeYfSnL"); + Pubkey::from_str_const("WH1TE11111111111111111111111111111111111111"); + +// authorities for tests which use separate ones +const STAKER_BLACK: Pubkey = Pubkey::from_str_const("STAKERBLACK11111111111111111111111111111111"); +const WITHDRAWER_BLACK: Pubkey = + Pubkey::from_str_const("W1THDRAWERBLACK1111111111111111111111111111"); +const STAKER_WHITE: Pubkey = Pubkey::from_str_const("STAKERWH1TE11111111111111111111111111111111"); +const WITHDRAWER_WHITE: Pubkey = + Pubkey::from_str_const("W1THDRAWERWH1TE1111111111111111111111111111"); + +// authorities for tests which use shared ones +const STAKER_GRAY: Pubkey = Pubkey::from_str_const("STAKERGRAY111111111111111111111111111111111"); +const WITHDRAWER_GRAY: Pubkey = + Pubkey::from_str_const("W1THDRAWERGRAY11111111111111111111111111111"); // stake delegated to some imaginary vote account in all epochs // with a warmup/cooldown rate of 9%, routine tests moving under 9sol can ignore stake history @@ -98,6 +116,7 @@ struct Env { accounts: HashMap, } impl Env { + // set up a test environment with valid stake history, two vote accounts, and two blank stake accounts fn init() -> Self { // create a test environment at the execution epoch let mut accounts = HashMap::new(); @@ -145,6 +164,8 @@ impl Env { Self { mollusk, accounts } } + // get the accounts from our account store that this transaction expects to see + // we dont need implicit sysvars, mollusk resolves them internally via syscall stub fn resolve_accounts(&self, account_metas: &[AccountMeta]) -> Vec<(Pubkey, AccountSharedData)> { let mut accounts = vec![]; for account_meta in account_metas { @@ -160,6 +181,68 @@ impl Env { accounts } + + // set up one of the preconfigured blank stake accounts at some starting state + // to mutate the accounts after initial setup, do it directly or execute instructions + // note these accounts are already rent exempt, so lamports specified are stake or extra + fn init_stake( + &mut self, + pubkey: &Pubkey, + stake_state: &StakeStateV2, + additional_lamports: u64, + ) { + assert!(*pubkey == STAKE_ACCOUNT_BLACK || *pubkey == STAKE_ACCOUNT_WHITE); + let stake_account = self.accounts.get_mut(pubkey).unwrap(); + let current_lamports = stake_account.lamports(); + stake_account.set_lamports(current_lamports + additional_lamports); + bincode::serialize_into(stake_account.data_as_mut_slice(), stake_state).unwrap(); + } + + // process an instruction, assert checks, and update internal accounts + fn process(&mut self, instruction: &Instruction, checks: &[Check]) { + let initial_accounts = self.resolve_accounts(&instruction.accounts); + + let result = + self.mollusk + .process_and_validate_instruction(instruction, &initial_accounts, checks); + + for (i, resulting_account) in result.resulting_accounts.into_iter().enumerate() { + let account_meta = &instruction.accounts[i]; + assert_eq!(account_meta.pubkey, resulting_account.0); + if account_meta.is_writable { + if resulting_account.1.lamports() == 0 { + self.accounts.remove(&resulting_account.0); + } else { + self.accounts + .insert(resulting_account.0, resulting_account.1); + } + } + } + } + + // shorthand for process with only a success check + fn process_success(&mut self, instruction: &Instruction) { + self.process(instruction, &[Check::success()]); + } + + // shorthand for process with an expected error + fn process_fail(&mut self, instruction: &Instruction, error: ProgramError) { + self.process(instruction, &[Check::err(error)]); + } +} + +fn just_stake(meta: Meta, stake: u64) -> StakeStateV2 { + StakeStateV2::Stake( + meta, + Stake { + delegation: Delegation { + stake, + ..Delegation::default() + }, + ..Stake::default() + }, + StakeFlags::empty(), + ) } fn stake_to_bytes(stake: &StakeStateV2) -> Vec { @@ -170,13 +253,12 @@ fn stake_to_bytes(stake: &StakeStateV2) -> Vec { #[test] fn test_initialize() { - let env = Env::init(); + let mut env = Env::init(); let authorized = Authorized::default(); let lockup = Lockup::default(); let instruction = ixn::initialize(&STAKE_ACCOUNT_BLACK, &authorized, &lockup); - let accounts = env.resolve_accounts(&instruction.accounts); let black_state = StakeStateV2::Initialized(Meta { rent_exempt_reserve: STAKE_RENT_EXEMPTION, @@ -185,9 +267,8 @@ fn test_initialize() { }); let black = stake_to_bytes(&black_state); - env.mollusk.process_and_validate_instruction( + env.process( &instruction, - &accounts, &[ Check::success(), Check::account(&STAKE_ACCOUNT_BLACK).data(&black).build(), From 55b1a8eaab566650f066b6cec17e79ce1daefcfe Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Mon, 2 Dec 2024 03:08:42 -0800 Subject: [PATCH 06/35] couple cleanups --- program/tests/mollusk.rs | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/program/tests/mollusk.rs b/program/tests/mollusk.rs index a43fc77..956bc8b 100644 --- a/program/tests/mollusk.rs +++ b/program/tests/mollusk.rs @@ -18,9 +18,7 @@ use { signers::Signers, stake::{ self, - instruction::{ - self as ixn, LockupArgs, LockupCheckedArgs, StakeError, StakeInstruction, - }, + instruction::{self, LockupArgs, LockupCheckedArgs, StakeError, StakeInstruction}, stake_flags::StakeFlags, state::{ warmup_cooldown_rate, Authorized, Delegation, Lockup, Meta, Stake, @@ -30,8 +28,8 @@ use { stake_history::StakeHistoryEntry, system_instruction, system_program, sysvar::{ - clock::Clock, epoch_schedule::EpochSchedule, rent::Rent, stake_history::StakeHistory, - SysvarId, + clock::Clock, epoch_rewards::EpochRewards, epoch_schedule::EpochSchedule, rent::Rent, + stake_history::StakeHistory, SysvarId, }, transaction::{Transaction, TransactionError}, vote::{ @@ -172,8 +170,27 @@ impl Env { let key = account_meta.pubkey; let account_shared_data = if Rent::check_id(&key) { self.mollusk.sysvars.keyed_account_for_rent_sysvar().1 + } else if Clock::check_id(&key) { + self.mollusk.sysvars.keyed_account_for_clock_sysvar().1 + } else if EpochSchedule::check_id(&key) { + self.mollusk + .sysvars + .keyed_account_for_epoch_schedule_sysvar() + .1 + } else if EpochRewards::check_id(&key) { + self.mollusk + .sysvars + .keyed_account_for_epoch_rewards_sysvar() + .1 + } else if StakeHistory::check_id(&key) { + self.mollusk + .sysvars + .keyed_account_for_stake_history_sysvar() + .1 + } else if let Some(account) = self.accounts.get(&key).cloned() { + account } else { - self.accounts.get(&key).cloned().unwrap() + AccountSharedData::default() }; accounts.push((key, account_shared_data)); @@ -258,7 +275,7 @@ fn test_initialize() { let authorized = Authorized::default(); let lockup = Lockup::default(); - let instruction = ixn::initialize(&STAKE_ACCOUNT_BLACK, &authorized, &lockup); + let instruction = instruction::initialize(&STAKE_ACCOUNT_BLACK, &authorized, &lockup); let black_state = StakeStateV2::Initialized(Meta { rent_exempt_reserve: STAKE_RENT_EXEMPTION, From 651fe5ed1e15b60f5a4918fb01853ec658734557 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Mon, 2 Dec 2024 04:59:27 -0800 Subject: [PATCH 07/35] builders for all basic stake operations --- program/tests/mollusk.rs | 198 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 4 deletions(-) diff --git a/program/tests/mollusk.rs b/program/tests/mollusk.rs index 956bc8b..e5ae8ca 100644 --- a/program/tests/mollusk.rs +++ b/program/tests/mollusk.rs @@ -37,7 +37,7 @@ use { state::{VoteInit, VoteState, VoteStateVersions}, }, }, - solana_stake_program::{id, processor::Processor}, + solana_stake_program::{get_minimum_delegation, id, processor::Processor}, std::collections::HashMap, test_case::{test_case, test_matrix}, }; @@ -162,6 +162,145 @@ impl Env { Self { mollusk, accounts } } + // creates a test environment and instruction for a given stake operation + // enum contents are sometimes, but not necessarily, ignored + // the success is trivial, this is mostly to allow exhaustive failure tests + // or to do some post-setup for more meaningful success tests + // XXX this is horrible. getting full coverage is fucking insane. this shit is too complicated + // probably need my own enum... or at least add lockup as an arg for everything + fn init_for_instruction(stake_instruction: &StakeInstruction) -> (Self, Instruction) { + let mut env = Self::init(); + let minimum_delegation = get_minimum_delegation(); + + let instruction = match stake_instruction { + StakeInstruction::Initialize(_, _) => instruction::initialize( + &STAKE_ACCOUNT_BLACK, + &Authorized { + staker: STAKER_BLACK, + withdrawer: WITHDRAWER_BLACK, + }, + &Lockup::default(), + ), + // TODO lockup + StakeInstruction::Authorize(_, authorize) => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), + minimum_delegation, + ); + + let (old_authority, new_authority) = match authorize { + StakeAuthorize::Staker => (STAKER_BLACK, STAKER_GRAY), + StakeAuthorize::Withdrawer => (WITHDRAWER_BLACK, WITHDRAWER_GRAY), + }; + + instruction::authorize( + &STAKE_ACCOUNT_BLACK, + &old_authority, + &new_authority, + *authorize, + None, + ) + } + // TODO withdrawer + StakeInstruction::DelegateStake => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), + minimum_delegation, + ); + + instruction::delegate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK, &VOTE_ACCOUNT_RED) + } + // TODO amount, also maybe should use gray + StakeInstruction::Split(_) => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation * 2, + ), + minimum_delegation * 2, + ); + + instruction::split( + &STAKE_ACCOUNT_BLACK, + &STAKER_BLACK, + minimum_delegation, + &STAKE_ACCOUNT_WHITE, + )[2] + .clone() + } + // TODO partial, lockup + StakeInstruction::Withdraw(_) => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &active_stake(VOTE_ACCOUNT_RED, STAKE_ACCOUNT_BLACK, minimum_delegation), + minimum_delegation, + ); + + instruction::withdraw( + &STAKE_ACCOUNT_BLACK, + &WITHDRAWER_BLACK, + &PAYER, + minimum_delegation + STAKE_RENT_EXEMPTION, + None, + ) + } + // TODO withdrawer + StakeInstruction::Deactivate => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &active_stake(VOTE_ACCOUNT_RED, STAKE_ACCOUNT_BLACK, minimum_delegation), + minimum_delegation, + ); + + instruction::deactivate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK) + } + // TODO existing lockup, remove lockup, also hardcoded custodians maybe? + StakeInstruction::SetLockup(_) => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), + minimum_delegation, + ); + + instruction::set_lockup( + &STAKE_ACCOUNT_BLACK, + &LockupArgs { + epoch: Some(EXECUTION_EPOCH * 2), + custodian: Some(Pubkey::new_unique()), + unix_timestamp: None, + }, + &WITHDRAWER_BLACK, + ) + } + // TODO withdrawer + StakeInstruction::Merge => { + // XXX TODO FIXME these need to use gray + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &active_stake(VOTE_ACCOUNT_RED, STAKE_ACCOUNT_BLACK, minimum_delegation), + minimum_delegation, + ); + + env.update_stake( + &STAKE_ACCOUNT_WHITE, + &active_stake(VOTE_ACCOUNT_RED, STAKE_ACCOUNT_WHITE, minimum_delegation), + minimum_delegation, + ); + + instruction::merge(&STAKE_ACCOUNT_WHITE, &STAKE_ACCOUNT_BLACK, &STAKER_GRAY)[0] + .clone() + } + // TODO move, checked, seed, deactivate delinquent, minimum, redelegate + _ => todo!(), + }; + + (env, instruction) + } + // get the accounts from our account store that this transaction expects to see // we dont need implicit sysvars, mollusk resolves them internally via syscall stub fn resolve_accounts(&self, account_metas: &[AccountMeta]) -> Vec<(Pubkey, AccountSharedData)> { @@ -202,7 +341,7 @@ impl Env { // set up one of the preconfigured blank stake accounts at some starting state // to mutate the accounts after initial setup, do it directly or execute instructions // note these accounts are already rent exempt, so lamports specified are stake or extra - fn init_stake( + fn update_stake( &mut self, pubkey: &Pubkey, stake_state: &StakeStateV2, @@ -248,12 +387,63 @@ impl Env { } } -fn just_stake(meta: Meta, stake: u64) -> StakeStateV2 { +fn just_stake(stake_pubkey: Pubkey, stake: u64) -> StakeStateV2 { + let authorized = match stake_pubkey { + STAKE_ACCOUNT_BLACK => Authorized { + staker: STAKER_BLACK, + withdrawer: WITHDRAWER_BLACK, + }, + STAKE_ACCOUNT_WHITE => Authorized { + staker: STAKER_WHITE, + withdrawer: WITHDRAWER_WHITE, + }, + _ => Authorized::default(), + }; + StakeStateV2::Stake( - meta, + Meta { + rent_exempt_reserve: STAKE_RENT_EXEMPTION, + authorized, + lockup: Lockup::default(), + }, + Stake { + delegation: Delegation { + stake, + ..Delegation::default() + }, + ..Stake::default() + }, + StakeFlags::empty(), + ) +} + +fn active_stake(voter_pubkey: Pubkey, stake_pubkey: Pubkey, stake: u64) -> StakeStateV2 { + assert!(stake_pubkey != VOTE_ACCOUNT_RED); + assert!(stake_pubkey != VOTE_ACCOUNT_BLUE); + + let authorized = match stake_pubkey { + STAKE_ACCOUNT_BLACK => Authorized { + staker: STAKER_BLACK, + withdrawer: WITHDRAWER_BLACK, + }, + STAKE_ACCOUNT_WHITE => Authorized { + staker: STAKER_WHITE, + withdrawer: WITHDRAWER_WHITE, + }, + _ => Authorized::default(), + }; + + StakeStateV2::Stake( + Meta { + rent_exempt_reserve: STAKE_RENT_EXEMPTION, + authorized, + lockup: Lockup::default(), + }, Stake { delegation: Delegation { stake, + voter_pubkey, + activation_epoch: EXECUTION_EPOCH - 1, ..Delegation::default() }, ..Stake::default() From aa18a675a56436bd1bbdf192a21379eef0fa95d9 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Mon, 2 Dec 2024 05:02:57 -0800 Subject: [PATCH 08/35] active with gray --- program/tests/mollusk.rs | 53 +++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/program/tests/mollusk.rs b/program/tests/mollusk.rs index e5ae8ca..2be8dea 100644 --- a/program/tests/mollusk.rs +++ b/program/tests/mollusk.rs @@ -212,7 +212,7 @@ impl Env { instruction::delegate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK, &VOTE_ACCOUNT_RED) } - // TODO amount, also maybe should use gray + // TODO amount StakeInstruction::Split(_) => { env.update_stake( &STAKE_ACCOUNT_BLACK, @@ -220,13 +220,14 @@ impl Env { VOTE_ACCOUNT_RED, STAKE_ACCOUNT_BLACK, minimum_delegation * 2, + true, ), minimum_delegation * 2, ); instruction::split( &STAKE_ACCOUNT_BLACK, - &STAKER_BLACK, + &STAKER_GRAY, minimum_delegation, &STAKE_ACCOUNT_WHITE, )[2] @@ -236,7 +237,12 @@ impl Env { StakeInstruction::Withdraw(_) => { env.update_stake( &STAKE_ACCOUNT_BLACK, - &active_stake(VOTE_ACCOUNT_RED, STAKE_ACCOUNT_BLACK, minimum_delegation), + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation, + false, + ), minimum_delegation, ); @@ -252,7 +258,12 @@ impl Env { StakeInstruction::Deactivate => { env.update_stake( &STAKE_ACCOUNT_BLACK, - &active_stake(VOTE_ACCOUNT_RED, STAKE_ACCOUNT_BLACK, minimum_delegation), + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation, + false, + ), minimum_delegation, ); @@ -278,16 +289,25 @@ impl Env { } // TODO withdrawer StakeInstruction::Merge => { - // XXX TODO FIXME these need to use gray env.update_stake( &STAKE_ACCOUNT_BLACK, - &active_stake(VOTE_ACCOUNT_RED, STAKE_ACCOUNT_BLACK, minimum_delegation), + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation, + true, + ), minimum_delegation, ); env.update_stake( &STAKE_ACCOUNT_WHITE, - &active_stake(VOTE_ACCOUNT_RED, STAKE_ACCOUNT_WHITE, minimum_delegation), + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_WHITE, + minimum_delegation, + true, + ), minimum_delegation, ); @@ -417,20 +437,29 @@ fn just_stake(stake_pubkey: Pubkey, stake: u64) -> StakeStateV2 { ) } -fn active_stake(voter_pubkey: Pubkey, stake_pubkey: Pubkey, stake: u64) -> StakeStateV2 { +fn active_stake( + voter_pubkey: Pubkey, + stake_pubkey: Pubkey, + stake: u64, + use_gray: bool, +) -> StakeStateV2 { assert!(stake_pubkey != VOTE_ACCOUNT_RED); assert!(stake_pubkey != VOTE_ACCOUNT_BLUE); - let authorized = match stake_pubkey { - STAKE_ACCOUNT_BLACK => Authorized { + let authorized = match (use_gray, stake_pubkey) { + (true, _) => Authorized { + staker: STAKER_GRAY, + withdrawer: WITHDRAWER_GRAY, + }, + (false, STAKE_ACCOUNT_BLACK) => Authorized { staker: STAKER_BLACK, withdrawer: WITHDRAWER_BLACK, }, - STAKE_ACCOUNT_WHITE => Authorized { + (false, STAKE_ACCOUNT_WHITE) => Authorized { staker: STAKER_WHITE, withdrawer: WITHDRAWER_WHITE, }, - _ => Authorized::default(), + (false, _) => Authorized::default(), }; StakeStateV2::Stake( From 781f87daef7e83164d83f869bc20ce61eacd29ef Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 02:46:29 -0800 Subject: [PATCH 09/35] fd fixture work, save for later --- Cargo.lock | 135 ++++++++++++++++++++++ program/Cargo.toml | 4 +- program/tests/mollusk.rs | 204 +++++++++++++++++++++++++++++++++- program/tests/program_test.rs | 8 +- 4 files changed, 343 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 266139b..d56994b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -744,6 +744,15 @@ dependencies = [ "inout", ] +[[package]] +name = "cmake" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" +dependencies = [ + "cc", +] + [[package]] name = "combine" version = "3.8.1" @@ -1340,6 +1349,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94474d15a76982be62ca8a39570dccce148d98c238ebb7408b0a21b2c4bdddc4" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.35" @@ -1666,6 +1681,15 @@ dependencies = [ "hmac 0.8.1", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.12" @@ -2352,6 +2376,8 @@ version = "0.0.11" dependencies = [ "bincode", "mollusk-svm-error", + "mollusk-svm-fuzz-fixture-firedancer", + "mollusk-svm-fuzz-fs", "mollusk-svm-keys", "solana-bpf-loader-program", "solana-compute-budget", @@ -2370,6 +2396,30 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "mollusk-svm-fuzz-fixture-firedancer" +version = "0.0.11" +dependencies = [ + "mollusk-svm-fuzz-fs", + "prost", + "prost-build", + "serde", + "serde_json", + "solana-compute-budget", + "solana-sdk", +] + +[[package]] +name = "mollusk-svm-fuzz-fs" +version = "0.0.11" +dependencies = [ + "bs58", + "prost", + "serde", + "serde_json", + "solana-sdk", +] + [[package]] name = "mollusk-svm-keys" version = "0.0.11" @@ -2378,6 +2428,12 @@ dependencies = [ "solana-sdk", ] +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + [[package]] name = "nix" version = "0.29.0" @@ -2693,6 +2749,16 @@ dependencies = [ "num", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.6.0", +] + [[package]] name = "pin-project" version = "1.1.7" @@ -2845,6 +2911,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae5a4388762d5815a9fc0dea33c56b021cdc8dde0c55e0c9ca57197254b0cab" +dependencies = [ + "bytes", + "cfg-if", + "cmake", + "heck", + "itertools 0.10.5", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost", + "prost-types", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" +dependencies = [ + "bytes", + "prost", +] + [[package]] name = "qstring" version = "0.7.2" @@ -5108,10 +5229,12 @@ dependencies = [ "bincode", "borsh 1.5.3", "mollusk-svm", + "mollusk-svm-fuzz-fixture-firedancer", "num-derive 0.4.2", "num-traits", "num_enum", "solana-account", + "solana-feature-set", "solana-program", "solana-program-test", "solana-sdk", @@ -6614,6 +6737,18 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/program/Cargo.toml b/program/Cargo.toml index 2264a3c..2e11c1d 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -22,10 +22,12 @@ solana-program = "=2.1.0" thiserror = "1.0.63" [dev-dependencies] -mollusk-svm = { path = "/home/hana/work/misc/mollusk/harness" } +mollusk-svm = { path = "/home/hana/work/misc/mollusk/harness", features = ["fuzz-fd"] } +mollusk-svm-fuzz-fixture-firedancer = { path = "/home/hana/work/misc/mollusk/fuzz/fixture-fd" } solana-account = { version = "=2.1.0", features = ["bincode"] } solana-program-test = "=2.1.0" solana-sdk = "=2.1.0" +solana-feature-set = "=2.1.0" test-case = "3.3.1" [lib] diff --git a/program/tests/mollusk.rs b/program/tests/mollusk.rs index 2be8dea..cabc3c5 100644 --- a/program/tests/mollusk.rs +++ b/program/tests/mollusk.rs @@ -3,12 +3,18 @@ #![allow(clippy::arithmetic_side_effects)] use { - mollusk_svm::{result::Check, Mollusk}, + mollusk_svm::{fuzz::firedancer::load_firedancer_fixture, result::Check, Mollusk}, + mollusk_svm_fuzz_fixture_firedancer::Fixture, solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, solana_sdk::{ account::Account as SolanaAccount, + address_lookup_table, bpf_loader_upgradeable, entrypoint::ProgramResult, - feature_set::{move_stake_and_move_lamports_ixs, stake_raise_minimum_delegation_to_1_sol}, + feature_set::{ + enable_partitioned_epoch_reward, get_sysvar_syscall_enabled, + move_stake_and_move_lamports_ixs, partitioned_epoch_rewards_superfeature, + stake_raise_minimum_delegation_to_1_sol, + }, hash::Hash, instruction::{AccountMeta, Instruction}, native_token::LAMPORTS_PER_SOL, @@ -38,7 +44,7 @@ use { }, }, solana_stake_program::{get_minimum_delegation, id, processor::Processor}, - std::collections::HashMap, + std::{collections::HashMap, fs}, test_case::{test_case, test_matrix}, }; @@ -511,3 +517,195 @@ fn test_initialize() { ], ); } + +/* TODO someday, fd fixtures are very touchy + after a week or two of work, we have found zero "true" failures +#[test] +fn hana() { + // failure counts by ixn: + // 7 HANA ixn: 3 (split) + // bad fixtures, useless accounts + // 6 HANA ixn: 2 (delegate) + // bad fixtures, vote v023 + // 5 HANA ixn: 13 (minimum delegation) + // bad fixtures, cu nonsense or useless accounts + // 4 HANA ixn: 4 (withdraw) + // bad fixtures, malformed stake history + // 2 HANA ixn: 6 (set lockup) + // bad fixtures, useless accounts + // 1 HANA ixn: 8 (auth with seed) + // expected to fail with missing signature, probably the bug we fixed + #[rustfmt::skip] + let failure_paths = vec!["/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/8e377aa2af14a8261c8687e689ea5a2c05937f5b_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/f2597f5e62e88676.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/0d19e6c0dcc5293fd85fa6cbd243b683df1d27f5_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/ac86895e3b04af19.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/bae28d42fc69dfea.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/52c531d4dde001a2.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/103caec03366be1a9bd9132fa8be1428b193528c_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/a9bc98e87eec2acd.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/5aece52dbeb1ceec8c6d5707e45372a2fbbb3e61_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/336973321e077c57.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/0acdc3b3818e466a.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/c97fa85d47757592.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/11cf70f361a4cc77.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/6f632e1ad6075480.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/35b98356227ed1e087868ea9f5062f599f97e96a_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/instr-1111111111111111111111111111111111111111111111111111111111111111-1416.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/9973b46e31edd1780e96ae00a5a6226fba3eb4d7_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/instr-1111111111111111111111111111111111111111111111111111111111111111-0124.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/51ddcae74379237f.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/55b22d9dcb936676fd574fbafe4bb906628e525e_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/133723dc1d779f033aa6a11ac9d61c5c2ad286fe_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/c73f6e8eb34ab99b145b2875a915b3262f592f22_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/adf5434529d66e18db0a10ad5596cb30997c4c8a_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/1ede3ae646d1b82f.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/2ab4c8c82a552c80.bin.fix"]; + + const DIR: &str = "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/"; + + let mut fixture_paths = vec![]; + if let Ok(entries) = fs::read_dir(DIR) { + for entry in entries.filter_map(Result::ok) { + let path = entry.path(); + if path.extension().map_or(false, |ext| ext == "fix") { + if let Some(path_str) = path.to_str() { + fixture_paths.push(path_str.to_string()); + } + } + } + } + + let stake_dummy = AccountSharedData::create(0, vec![], bpf_loader_upgradeable::id(), true, 0); + + let mut passed = 0; + let mut failed = 0; + let mut ok = 0; + let mut err = 0; + let mut bad_sysvar = 0; + let mut bad_cu = 0; + let mut enabled_min = 0; + let mut use_alt = 0; + let mut disabled_rewards = 0; + let mut disabled_move = 0; + for path in &failure_paths /* XXX fixture_paths */ { + // expects to spend no CUs on minimum delegation + if path.contains("c03a06bd0c9c52b0000dc54d4de4fd8d8b350297_3246959.fix") { + bad_cu += 1; + continue; + } + + // set up firedancer fixture test environment + let mut fixture = Fixture::load_from_blob_file(&path); + + // activate these features because the program doesnt function without them + // we may have spurious failures for tests that want to fail with the features off + fixture + .input + .epoch_context + .feature_set + .activate(&enable_partitioned_epoch_reward::id(), 0); + fixture + .input + .epoch_context + .feature_set + .activate(&get_sysvar_syscall_enabled::id(), 0); + + let ref feature_set = fixture.input.epoch_context.feature_set; + + // bpf stake requires the EpochRewards sysvar + if !feature_set.is_active(&partitioned_epoch_rewards_superfeature::id()) + && !feature_set.is_active(&enable_partitioned_epoch_reward::id()) + { + disabled_rewards += 1; + continue; + } + + // bpf stake cannot enable minimum delegation + if feature_set.is_active(&stake_raise_minimum_delegation_to_1_sol::id()) { + enabled_min += 1; + continue; + } + + // some fixtures have mangled sysvar data, but we cannot test these + // mollusk attempts to parse them and unwraps. this is not something we really need to test tho + // since both styles of sysvar object constructor validate the pubkey + let Ok((mut mollusk, instruction, accounts, mut expected)) = + std::panic::catch_unwind(|| load_firedancer_fixture(&fixture)) + else { + bad_sysvar += 1; + continue; + }; + + // XXX + if instruction.data[0] != 4 { + continue; + } + + // alt cannot be resolved by mollusk, which operates on an invoke context + if instruction + .accounts + .iter() + .any(|meta| meta.pubkey == address_lookup_table::program::id()) + { + use_alt += 1; + continue; + } + + // bpf stake cannot disable the move instructions + // so if theyre used with the feature disabled, we skip the fixture + if instruction.data.len() > 1 { + if (instruction.data[0] == 16 || instruction.data[0] == 17) + && !feature_set.is_active(&move_stake_and_move_lamports_ixs::id()) + { + disabled_move += 1; + continue; + } + } + + // TODO a way to have `load_firedancer_fixture` use a preloaded program + //mollusk.add_program(&id(), "solana_stake_program", &bpf_loader_upgradeable::id()); + + // process instruction against bpf stake program + let mut actual = mollusk.process_instruction(&instruction, &accounts); + println!("HANA ixn: {:#?}\nHANa accts: {:#?}\nHANA exp: {:#?}", instruction, accounts, expected); + println!("HANA actual: {:#?}", actual); + + // fixtures and bpf stake often disagree on whether these should be in the account results + // they never change tho so can be filtered out unconditionally + #[allow(deprecated)] + { + expected.resulting_accounts.retain(|(key, _)| { + !solana_sdk::sysvar::is_sysvar_id(key) && key != &stake::config::id() + }); + actual.resulting_accounts.retain(|(key, _)| { + !solana_sdk::sysvar::is_sysvar_id(key) && key != &stake::config::id() + }); + } + + // stake program in the account results is different because fd fixtures use native + expected + .resulting_accounts + .iter_mut() + .find(|(key, _)| key == &id()) + .map(|(_, account)| *account = stake_dummy.clone()); + + // this is a custom function that emulates what we would like solana-conformance consensus mode to be: + // * program_result matches if both are Ok or both are Err, without errors being compared + // * raw_result is ignored (i dont actually know what this is, maybe comparing them is fine) + // * resulting_accounts are only compared for successful transactions + let compare_result = std::panic::catch_unwind(|| expected.compare_consensus(&actual)); + + if compare_result.is_ok() { + if expected.program_result.is_err() { + err += 1; + } else { + ok += 1; + } + + passed += 1; + } else { + failed += 1; + } + } + + println!( + "passed: {} ({} ok, {} err) +failed: {} +skipped: {} +* malformed sysvar: {} +* onerous CU limit: {} +* requires 1sol minimum: {} +* requires ALT: {} +* EpochRewards disabled: {} +* MoveStake/MoveLamports disabled but used: {}", + passed, + ok, + err, + failed, + bad_sysvar + bad_cu + enabled_min + use_alt + disabled_rewards + disabled_move, + bad_sysvar, + bad_cu, + enabled_min, + use_alt, + disabled_rewards, + disabled_move, + ); +} +*/ diff --git a/program/tests/program_test.rs b/program/tests/program_test.rs index 8b09ce8..28fbf7d 100644 --- a/program/tests/program_test.rs +++ b/program/tests/program_test.rs @@ -18,12 +18,12 @@ use { system_instruction, system_program, sysvar::{clock::Clock, stake_history::StakeHistory}, transaction::{Transaction, TransactionError}, + vote::{ + instruction as vote_instruction, + state::{VoteInit, VoteState, VoteStateVersions}, + }, }, solana_stake_program::id, - solana_vote_program::{ - self, vote_instruction, - vote_state::{VoteInit, VoteState, VoteStateVersions}, - }, test_case::{test_case, test_matrix}, }; From 5c64e99a85fd77634019563c407806aa6734aa01 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 03:54:52 -0800 Subject: [PATCH 10/35] ok more changes --- Cargo.lock | 11 ++++++----- program/tests/mollusk.rs | 2 -- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d56994b..b0026b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2372,7 +2372,7 @@ dependencies = [ [[package]] name = "mollusk-svm" -version = "0.0.11" +version = "0.0.12" dependencies = [ "bincode", "mollusk-svm-error", @@ -2384,13 +2384,14 @@ dependencies = [ "solana-logger", "solana-program-runtime", "solana-sdk", + "solana-stake-program 2.1.0", "solana-system-program", "solana-timings", ] [[package]] name = "mollusk-svm-error" -version = "0.0.11" +version = "0.0.12" dependencies = [ "solana-sdk", "thiserror 1.0.69", @@ -2398,7 +2399,7 @@ dependencies = [ [[package]] name = "mollusk-svm-fuzz-fixture-firedancer" -version = "0.0.11" +version = "0.0.12" dependencies = [ "mollusk-svm-fuzz-fs", "prost", @@ -2411,7 +2412,7 @@ dependencies = [ [[package]] name = "mollusk-svm-fuzz-fs" -version = "0.0.11" +version = "0.0.12" dependencies = [ "bs58", "prost", @@ -2422,7 +2423,7 @@ dependencies = [ [[package]] name = "mollusk-svm-keys" -version = "0.0.11" +version = "0.0.12" dependencies = [ "mollusk-svm-error", "solana-sdk", diff --git a/program/tests/mollusk.rs b/program/tests/mollusk.rs index cabc3c5..b5ebea0 100644 --- a/program/tests/mollusk.rs +++ b/program/tests/mollusk.rs @@ -172,8 +172,6 @@ impl Env { // enum contents are sometimes, but not necessarily, ignored // the success is trivial, this is mostly to allow exhaustive failure tests // or to do some post-setup for more meaningful success tests - // XXX this is horrible. getting full coverage is fucking insane. this shit is too complicated - // probably need my own enum... or at least add lockup as an arg for everything fn init_for_instruction(stake_instruction: &StakeInstruction) -> (Self, Instruction) { let mut env = Self::init(); let minimum_delegation = get_minimum_delegation(); From 73dbad975ffb75cc915dff0286d7ea194305eec8 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 03:55:16 -0800 Subject: [PATCH 11/35] ported first nontrivial stake instruction test --- program/tests/stake_instruction.rs | 389 +++++++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 program/tests/stake_instruction.rs diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs new file mode 100644 index 0000000..733322c --- /dev/null +++ b/program/tests/stake_instruction.rs @@ -0,0 +1,389 @@ +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(clippy::arithmetic_side_effects)] + +use { + bincode::serialize, + mollusk_svm::{fuzz::firedancer::load_firedancer_fixture, result::Check, Mollusk}, + mollusk_svm_fuzz_fixture_firedancer::Fixture, + solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, + solana_sdk::{ + account::{create_account_shared_data_for_test, Account as SolanaAccount}, + account_utils::StateMut, + address_lookup_table, bpf_loader_upgradeable, + entrypoint::ProgramResult, + feature_set::{ + enable_partitioned_epoch_reward, get_sysvar_syscall_enabled, + move_stake_and_move_lamports_ixs, partitioned_epoch_rewards_superfeature, + stake_raise_minimum_delegation_to_1_sol, + }, + hash::Hash, + instruction::{AccountMeta, Instruction}, + native_token::LAMPORTS_PER_SOL, + program_error::ProgramError, + pubkey::Pubkey, + signature::{Keypair, Signer}, + signers::Signers, + stake::{ + self, + instruction::{self, LockupArgs, LockupCheckedArgs, StakeError, StakeInstruction}, + stake_flags::StakeFlags, + state::{ + warmup_cooldown_rate, Authorized, Delegation, Lockup, Meta, Stake, + StakeActivationStatus, StakeAuthorize, StakeStateV2, + }, + MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION, + }, + stake_history::{Epoch, StakeHistoryEntry}, + system_instruction, system_program, + sysvar::{ + clock::{self, Clock}, + epoch_rewards::EpochRewards, + epoch_schedule::EpochSchedule, + rent::Rent, + stake_history::{self, StakeHistory}, + SysvarId, + }, + transaction::{Transaction, TransactionError}, + vote::{ + program as solana_vote_program, + state::{VoteInit, VoteState, VoteStateVersions}, + }, + }, + solana_stake_program::{get_minimum_delegation, id, processor::Processor}, + std::{collections::HashMap, fs, sync::Arc}, + test_case::{test_case, test_matrix}, +}; + +fn mollusk_native() -> Mollusk { + let mut mollusk = Mollusk::default(); + mollusk + .feature_set + .deactivate(&stake_raise_minimum_delegation_to_1_sol::id()); + mollusk +} + +fn mollusk_bpf() -> Mollusk { + let mut mollusk = Mollusk::new(&id(), "solana_stake_program"); + mollusk + .feature_set + .deactivate(&stake_raise_minimum_delegation_to_1_sol::id()); + mollusk +} + +fn process_instruction( + mollusk: &Mollusk, + instruction_data: &[u8], + transaction_accounts: Vec<(Pubkey, AccountSharedData)>, + instruction_accounts: Vec, + expected_result: Result<(), ProgramError>, +) -> Vec { + let instruction = Instruction { + program_id: id(), + accounts: instruction_accounts, + data: instruction_data.to_vec(), + }; + + let check = match expected_result { + Ok(()) => Check::success(), + Err(e) => Check::err(e), + }; + + let result = + mollusk.process_and_validate_instruction(&instruction, &transaction_accounts, &[check]); + + result + .resulting_accounts + .into_iter() + .map(|(_, account)| account) + .collect() +} + +fn new_stake( + stake: u64, + voter_pubkey: &Pubkey, + vote_state: &VoteState, + activation_epoch: Epoch, +) -> Stake { + Stake { + delegation: Delegation::new(voter_pubkey, stake, activation_epoch), + credits_observed: vote_state.credits(), + } +} + +fn from>(account: &T) -> Option { + account.state().ok() +} + +fn stake_from>(account: &T) -> Option { + from(account).and_then(|state: StakeStateV2| state.stake()) +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_deactivate_delinquent(mollusk: Mollusk) { + let reference_vote_address = Pubkey::new_unique(); + let vote_address = Pubkey::new_unique(); + let stake_address = Pubkey::new_unique(); + + let initial_stake_state = StakeStateV2::Stake( + Meta::default(), + new_stake( + 1, /* stake */ + &vote_address, + &VoteState::default(), + 1, /* activation_epoch */ + ), + StakeFlags::empty(), + ); + + let stake_account = AccountSharedData::new_data_with_space( + 1, /* lamports */ + &initial_stake_state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + + let mut vote_account = AccountSharedData::new_data_with_space( + 1, /* lamports */ + &VoteStateVersions::new_current(VoteState::default()), + VoteState::size_of(), + &solana_vote_program::id(), + ) + .unwrap(); + + let mut reference_vote_account = AccountSharedData::new_data_with_space( + 1, /* lamports */ + &VoteStateVersions::new_current(VoteState::default()), + VoteState::size_of(), + &solana_vote_program::id(), + ) + .unwrap(); + + let current_epoch = 20; + + let process_instruction_deactivate_delinquent = + |stake_address: &Pubkey, + stake_account: &AccountSharedData, + vote_account: &AccountSharedData, + reference_vote_account: &AccountSharedData, + expected_result| { + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DeactivateDelinquent).unwrap(), + vec![ + (*stake_address, stake_account.clone()), + (vote_address, vote_account.clone()), + (reference_vote_address, reference_vote_account.clone()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock { + epoch: current_epoch, + ..Clock::default() + }), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ], + vec![ + AccountMeta { + pubkey: *stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: reference_vote_address, + is_signer: false, + is_writable: false, + }, + ], + expected_result, + ) + }; + + // `reference_vote_account` has not voted. Instruction will fail + process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Err(StakeError::InsufficientReferenceVotes.into()), + ); + + // `reference_vote_account` has not consistently voted for at least + // `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`. + // Instruction will fail + let mut reference_vote_state = VoteState::default(); + for epoch in 0..MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION / 2 { + reference_vote_state.increment_credits(epoch as Epoch, 1); + } + reference_vote_account + .serialize_data(&VoteStateVersions::new_current(reference_vote_state)) + .unwrap(); + + process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Err(StakeError::InsufficientReferenceVotes.into()), + ); + + // `reference_vote_account` has not consistently voted for the last + // `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`. + // Instruction will fail + let mut reference_vote_state = VoteState::default(); + for epoch in 0..=current_epoch { + reference_vote_state.increment_credits(epoch, 1); + } + assert_eq!( + reference_vote_state.epoch_credits[current_epoch as usize - 2].0, + current_epoch - 2 + ); + reference_vote_state + .epoch_credits + .remove(current_epoch as usize - 2); + assert_eq!( + reference_vote_state.epoch_credits[current_epoch as usize - 2].0, + current_epoch - 1 + ); + reference_vote_account + .serialize_data(&VoteStateVersions::new_current(reference_vote_state)) + .unwrap(); + + process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Err(StakeError::InsufficientReferenceVotes.into()), + ); + + // `reference_vote_account` has consistently voted and `vote_account` has never voted. + // Instruction will succeed + let mut reference_vote_state = VoteState::default(); + for epoch in 0..=current_epoch { + reference_vote_state.increment_credits(epoch, 1); + } + reference_vote_account + .serialize_data(&VoteStateVersions::new_current(reference_vote_state)) + .unwrap(); + + let post_stake_account = &process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Ok(()), + )[0]; + + assert_eq!( + stake_from(post_stake_account) + .unwrap() + .delegation + .deactivation_epoch, + current_epoch + ); + + // `reference_vote_account` has consistently voted and `vote_account` has not voted for the + // last `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`. + // Instruction will succeed + + let mut vote_state = VoteState::default(); + for epoch in 0..MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION / 2 { + vote_state.increment_credits(epoch as Epoch, 1); + } + vote_account + .serialize_data(&VoteStateVersions::new_current(vote_state)) + .unwrap(); + + let post_stake_account = &process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Ok(()), + )[0]; + + assert_eq!( + stake_from(post_stake_account) + .unwrap() + .delegation + .deactivation_epoch, + current_epoch + ); + + // `reference_vote_account` has consistently voted and `vote_account` has not voted for the + // last `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`. Try to deactivate an unrelated stake + // account. Instruction will fail + let unrelated_vote_address = Pubkey::new_unique(); + let unrelated_stake_address = Pubkey::new_unique(); + let mut unrelated_stake_account = stake_account.clone(); + assert_ne!(unrelated_vote_address, vote_address); + unrelated_stake_account + .serialize_data(&StakeStateV2::Stake( + Meta::default(), + new_stake( + 1, /* stake */ + &unrelated_vote_address, + &VoteState::default(), + 1, /* activation_epoch */ + ), + StakeFlags::empty(), + )) + .unwrap(); + + process_instruction_deactivate_delinquent( + &unrelated_stake_address, + &unrelated_stake_account, + &vote_account, + &reference_vote_account, + Err(StakeError::VoteAddressMismatch.into()), + ); + + // `reference_vote_account` has consistently voted and `vote_account` voted once + // `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` ago. + // Instruction will succeed + let mut vote_state = VoteState::default(); + vote_state.increment_credits( + current_epoch - MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch, + 1, + ); + vote_account + .serialize_data(&VoteStateVersions::new_current(vote_state)) + .unwrap(); + process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Ok(()), + ); + + // `reference_vote_account` has consistently voted and `vote_account` voted once + // `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` - 1 epochs ago + // Instruction will fail + let mut vote_state = VoteState::default(); + vote_state.increment_credits( + current_epoch - (MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION - 1) as Epoch, + 1, + ); + vote_account + .serialize_data(&VoteStateVersions::new_current(vote_state)) + .unwrap(); + process_instruction_deactivate_delinquent( + &stake_address, + &stake_account, + &vote_account, + &reference_vote_account, + Err(StakeError::MinimumDelinquentEpochsForDeactivationNotMet.into()), + ); +} From 433795b0376e132ac8ff5962c035762b6f00c7e0 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 03:56:52 -0800 Subject: [PATCH 12/35] remove experimental fd and mollusk stuff --- Cargo.lock | 134 ------ program/Cargo.toml | 3 +- program/tests/mollusk.rs | 709 ----------------------------- program/tests/stake_instruction.rs | 3 +- 4 files changed, 2 insertions(+), 847 deletions(-) delete mode 100644 program/tests/mollusk.rs diff --git a/Cargo.lock b/Cargo.lock index b0026b8..1d34779 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -744,15 +744,6 @@ dependencies = [ "inout", ] -[[package]] -name = "cmake" -version = "0.1.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" -dependencies = [ - "cc", -] - [[package]] name = "combine" version = "3.8.1" @@ -1349,12 +1340,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94474d15a76982be62ca8a39570dccce148d98c238ebb7408b0a21b2c4bdddc4" -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "flate2" version = "1.0.35" @@ -1681,15 +1666,6 @@ dependencies = [ "hmac 0.8.1", ] -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "http" version = "0.2.12" @@ -2376,8 +2352,6 @@ version = "0.0.12" dependencies = [ "bincode", "mollusk-svm-error", - "mollusk-svm-fuzz-fixture-firedancer", - "mollusk-svm-fuzz-fs", "mollusk-svm-keys", "solana-bpf-loader-program", "solana-compute-budget", @@ -2397,30 +2371,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "mollusk-svm-fuzz-fixture-firedancer" -version = "0.0.12" -dependencies = [ - "mollusk-svm-fuzz-fs", - "prost", - "prost-build", - "serde", - "serde_json", - "solana-compute-budget", - "solana-sdk", -] - -[[package]] -name = "mollusk-svm-fuzz-fs" -version = "0.0.12" -dependencies = [ - "bs58", - "prost", - "serde", - "serde_json", - "solana-sdk", -] - [[package]] name = "mollusk-svm-keys" version = "0.0.12" @@ -2429,12 +2379,6 @@ dependencies = [ "solana-sdk", ] -[[package]] -name = "multimap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" - [[package]] name = "nix" version = "0.29.0" @@ -2750,16 +2694,6 @@ dependencies = [ "num", ] -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset", - "indexmap 2.6.0", -] - [[package]] name = "pin-project" version = "1.1.7" @@ -2912,61 +2846,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae5a4388762d5815a9fc0dea33c56b021cdc8dde0c55e0c9ca57197254b0cab" -dependencies = [ - "bytes", - "cfg-if", - "cmake", - "heck", - "itertools 0.10.5", - "lazy_static", - "log", - "multimap", - "petgraph", - "prost", - "prost-types", - "regex", - "tempfile", - "which", -] - -[[package]] -name = "prost-derive" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "prost-types" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" -dependencies = [ - "bytes", - "prost", -] - [[package]] name = "qstring" version = "0.7.2" @@ -5230,7 +5109,6 @@ dependencies = [ "bincode", "borsh 1.5.3", "mollusk-svm", - "mollusk-svm-fuzz-fixture-firedancer", "num-derive 0.4.2", "num-traits", "num_enum", @@ -6738,18 +6616,6 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/program/Cargo.toml b/program/Cargo.toml index 2e11c1d..abd7f02 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -22,8 +22,7 @@ solana-program = "=2.1.0" thiserror = "1.0.63" [dev-dependencies] -mollusk-svm = { path = "/home/hana/work/misc/mollusk/harness", features = ["fuzz-fd"] } -mollusk-svm-fuzz-fixture-firedancer = { path = "/home/hana/work/misc/mollusk/fuzz/fixture-fd" } +mollusk-svm = { path = "/home/hana/work/misc/mollusk/harness" } solana-account = { version = "=2.1.0", features = ["bincode"] } solana-program-test = "=2.1.0" solana-sdk = "=2.1.0" diff --git a/program/tests/mollusk.rs b/program/tests/mollusk.rs deleted file mode 100644 index b5ebea0..0000000 --- a/program/tests/mollusk.rs +++ /dev/null @@ -1,709 +0,0 @@ -#![allow(dead_code)] -#![allow(unused_imports)] -#![allow(clippy::arithmetic_side_effects)] - -use { - mollusk_svm::{fuzz::firedancer::load_firedancer_fixture, result::Check, Mollusk}, - mollusk_svm_fuzz_fixture_firedancer::Fixture, - solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, - solana_sdk::{ - account::Account as SolanaAccount, - address_lookup_table, bpf_loader_upgradeable, - entrypoint::ProgramResult, - feature_set::{ - enable_partitioned_epoch_reward, get_sysvar_syscall_enabled, - move_stake_and_move_lamports_ixs, partitioned_epoch_rewards_superfeature, - stake_raise_minimum_delegation_to_1_sol, - }, - hash::Hash, - instruction::{AccountMeta, Instruction}, - native_token::LAMPORTS_PER_SOL, - program_error::ProgramError, - pubkey::Pubkey, - signature::{Keypair, Signer}, - signers::Signers, - stake::{ - self, - instruction::{self, LockupArgs, LockupCheckedArgs, StakeError, StakeInstruction}, - stake_flags::StakeFlags, - state::{ - warmup_cooldown_rate, Authorized, Delegation, Lockup, Meta, Stake, - StakeActivationStatus, StakeAuthorize, StakeStateV2, - }, - }, - stake_history::StakeHistoryEntry, - system_instruction, system_program, - sysvar::{ - clock::Clock, epoch_rewards::EpochRewards, epoch_schedule::EpochSchedule, rent::Rent, - stake_history::StakeHistory, SysvarId, - }, - transaction::{Transaction, TransactionError}, - vote::{ - program as vote_program, - state::{VoteInit, VoteState, VoteStateVersions}, - }, - }, - solana_stake_program::{get_minimum_delegation, id, processor::Processor}, - std::{collections::HashMap, fs}, - test_case::{test_case, test_matrix}, -}; - -// XXX ok so wow i am going to have to write a lot of shit -// we need a mechanism to create basically arbitrary stake accounts -// this means all states (uninit, init, activating, active, deactivating, deactive) -// we need to be able to make a stake history that gives us partial activation/deactivation -// actually we need to set up stake history ourselves correctly in all cases -// we need to be able to set lockup and authority arbitrarily -// we need helpers to set up with seed pubkeys -// ideally we automatically check missing signer failures -// need to create a vote account... ugh we need to get credits right for DeactivateDelinquent -// for delegate we just need owner, vote account pubkey, and credits (can be 0) -// -// XXX OK i wrote a simple init test -// what to do on monday... i guess go through the stake ixn tests and see what to impl -// main thing we lack is full coverage for lockup and i think a bunch of split edge cases - -// arbitrary, but gives us room to set up activations/deactivations serveral epochs in the past -const EXECUTION_EPOCH: u64 = 8; - -// mollusk doesnt charge transaction fees, this is just a convenient source of lamports -const PAYER: Pubkey = Pubkey::from_str_const("PAYER11111111111111111111111111111111111111"); -const PAYER_BALANCE: u64 = 1_000_000 * LAMPORTS_PER_SOL; - -// two vote accounts with no credits, fine for all stake tests except DeactivateDelinquent -const VOTE_ACCOUNT_RED: Pubkey = - Pubkey::from_str_const("RED1111111111111111111111111111111111111111"); -const VOTE_ACCOUNT_BLUE: Pubkey = - Pubkey::from_str_const("BLUE111111111111111111111111111111111111111"); - -// two blank stake accounts that can be serialized into for tests -const STAKE_ACCOUNT_BLACK: Pubkey = - Pubkey::from_str_const("BLACK11111111111111111111111111111111111111"); -const STAKE_ACCOUNT_WHITE: Pubkey = - Pubkey::from_str_const("WH1TE11111111111111111111111111111111111111"); - -// authorities for tests which use separate ones -const STAKER_BLACK: Pubkey = Pubkey::from_str_const("STAKERBLACK11111111111111111111111111111111"); -const WITHDRAWER_BLACK: Pubkey = - Pubkey::from_str_const("W1THDRAWERBLACK1111111111111111111111111111"); -const STAKER_WHITE: Pubkey = Pubkey::from_str_const("STAKERWH1TE11111111111111111111111111111111"); -const WITHDRAWER_WHITE: Pubkey = - Pubkey::from_str_const("W1THDRAWERWH1TE1111111111111111111111111111"); - -// authorities for tests which use shared ones -const STAKER_GRAY: Pubkey = Pubkey::from_str_const("STAKERGRAY111111111111111111111111111111111"); -const WITHDRAWER_GRAY: Pubkey = - Pubkey::from_str_const("W1THDRAWERGRAY11111111111111111111111111111"); - -// stake delegated to some imaginary vote account in all epochs -// with a warmup/cooldown rate of 9%, routine tests moving under 9sol can ignore stake history -// while also making it easy to write tests involving partial (de)activations -// if the warmup/cooldown rate changes, this number must be adjusted -const PERSISTANT_ACTIVE_STAKE: u64 = 100 * LAMPORTS_PER_SOL; -#[test] -fn assert_warmup_cooldown_rate() { - assert_eq!(warmup_cooldown_rate(0, Some(0)), 0.09); -} - -// hardcoded for convenience -const STAKE_RENT_EXEMPTION: u64 = 2_282_880; -#[test] -fn assert_stake_rent_exemption() { - assert_eq!( - Rent::default().minimum_balance(StakeStateV2::size_of()), - STAKE_RENT_EXEMPTION - ); -} - -struct Env { - mollusk: Mollusk, - accounts: HashMap, -} -impl Env { - // set up a test environment with valid stake history, two vote accounts, and two blank stake accounts - fn init() -> Self { - // create a test environment at the execution epoch - let mut accounts = HashMap::new(); - let mut mollusk = Mollusk::new(&id(), "solana_stake_program"); - mollusk.warp_to_slot(EXECUTION_EPOCH * mollusk.sysvars.epoch_schedule.slots_per_epoch + 1); - assert_eq!(mollusk.sysvars.clock.epoch, EXECUTION_EPOCH); - - // backfill stake history - for epoch in 0..EXECUTION_EPOCH { - mollusk.sysvars.stake_history.add( - epoch, - StakeHistoryEntry::with_effective(PERSISTANT_ACTIVE_STAKE), - ); - } - - // add a lamports source - let payer_data = - AccountSharedData::new_rent_epoch(PAYER_BALANCE, 0, &system_program::id(), u64::MAX); - accounts.insert(PAYER, payer_data); - - // create two vote accounts - let vote_rent_exemption = Rent::default().minimum_balance(VoteState::size_of()); - let vote_state = bincode::serialize(&VoteState::default()).unwrap(); - let vote_data = AccountSharedData::create( - vote_rent_exemption, - vote_state, - vote_program::id(), - false, - u64::MAX, - ); - accounts.insert(VOTE_ACCOUNT_RED, vote_data.clone()); - accounts.insert(VOTE_ACCOUNT_BLUE, vote_data); - - // create two blank stake accounts - let stake_data = AccountSharedData::create( - STAKE_RENT_EXEMPTION, - vec![0; StakeStateV2::size_of()], - id(), - false, - u64::MAX, - ); - accounts.insert(STAKE_ACCOUNT_BLACK, stake_data.clone()); - accounts.insert(STAKE_ACCOUNT_WHITE, stake_data); - - Self { mollusk, accounts } - } - - // creates a test environment and instruction for a given stake operation - // enum contents are sometimes, but not necessarily, ignored - // the success is trivial, this is mostly to allow exhaustive failure tests - // or to do some post-setup for more meaningful success tests - fn init_for_instruction(stake_instruction: &StakeInstruction) -> (Self, Instruction) { - let mut env = Self::init(); - let minimum_delegation = get_minimum_delegation(); - - let instruction = match stake_instruction { - StakeInstruction::Initialize(_, _) => instruction::initialize( - &STAKE_ACCOUNT_BLACK, - &Authorized { - staker: STAKER_BLACK, - withdrawer: WITHDRAWER_BLACK, - }, - &Lockup::default(), - ), - // TODO lockup - StakeInstruction::Authorize(_, authorize) => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), - minimum_delegation, - ); - - let (old_authority, new_authority) = match authorize { - StakeAuthorize::Staker => (STAKER_BLACK, STAKER_GRAY), - StakeAuthorize::Withdrawer => (WITHDRAWER_BLACK, WITHDRAWER_GRAY), - }; - - instruction::authorize( - &STAKE_ACCOUNT_BLACK, - &old_authority, - &new_authority, - *authorize, - None, - ) - } - // TODO withdrawer - StakeInstruction::DelegateStake => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), - minimum_delegation, - ); - - instruction::delegate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK, &VOTE_ACCOUNT_RED) - } - // TODO amount - StakeInstruction::Split(_) => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &active_stake( - VOTE_ACCOUNT_RED, - STAKE_ACCOUNT_BLACK, - minimum_delegation * 2, - true, - ), - minimum_delegation * 2, - ); - - instruction::split( - &STAKE_ACCOUNT_BLACK, - &STAKER_GRAY, - minimum_delegation, - &STAKE_ACCOUNT_WHITE, - )[2] - .clone() - } - // TODO partial, lockup - StakeInstruction::Withdraw(_) => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &active_stake( - VOTE_ACCOUNT_RED, - STAKE_ACCOUNT_BLACK, - minimum_delegation, - false, - ), - minimum_delegation, - ); - - instruction::withdraw( - &STAKE_ACCOUNT_BLACK, - &WITHDRAWER_BLACK, - &PAYER, - minimum_delegation + STAKE_RENT_EXEMPTION, - None, - ) - } - // TODO withdrawer - StakeInstruction::Deactivate => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &active_stake( - VOTE_ACCOUNT_RED, - STAKE_ACCOUNT_BLACK, - minimum_delegation, - false, - ), - minimum_delegation, - ); - - instruction::deactivate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK) - } - // TODO existing lockup, remove lockup, also hardcoded custodians maybe? - StakeInstruction::SetLockup(_) => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), - minimum_delegation, - ); - - instruction::set_lockup( - &STAKE_ACCOUNT_BLACK, - &LockupArgs { - epoch: Some(EXECUTION_EPOCH * 2), - custodian: Some(Pubkey::new_unique()), - unix_timestamp: None, - }, - &WITHDRAWER_BLACK, - ) - } - // TODO withdrawer - StakeInstruction::Merge => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &active_stake( - VOTE_ACCOUNT_RED, - STAKE_ACCOUNT_BLACK, - minimum_delegation, - true, - ), - minimum_delegation, - ); - - env.update_stake( - &STAKE_ACCOUNT_WHITE, - &active_stake( - VOTE_ACCOUNT_RED, - STAKE_ACCOUNT_WHITE, - minimum_delegation, - true, - ), - minimum_delegation, - ); - - instruction::merge(&STAKE_ACCOUNT_WHITE, &STAKE_ACCOUNT_BLACK, &STAKER_GRAY)[0] - .clone() - } - // TODO move, checked, seed, deactivate delinquent, minimum, redelegate - _ => todo!(), - }; - - (env, instruction) - } - - // get the accounts from our account store that this transaction expects to see - // we dont need implicit sysvars, mollusk resolves them internally via syscall stub - fn resolve_accounts(&self, account_metas: &[AccountMeta]) -> Vec<(Pubkey, AccountSharedData)> { - let mut accounts = vec![]; - for account_meta in account_metas { - let key = account_meta.pubkey; - let account_shared_data = if Rent::check_id(&key) { - self.mollusk.sysvars.keyed_account_for_rent_sysvar().1 - } else if Clock::check_id(&key) { - self.mollusk.sysvars.keyed_account_for_clock_sysvar().1 - } else if EpochSchedule::check_id(&key) { - self.mollusk - .sysvars - .keyed_account_for_epoch_schedule_sysvar() - .1 - } else if EpochRewards::check_id(&key) { - self.mollusk - .sysvars - .keyed_account_for_epoch_rewards_sysvar() - .1 - } else if StakeHistory::check_id(&key) { - self.mollusk - .sysvars - .keyed_account_for_stake_history_sysvar() - .1 - } else if let Some(account) = self.accounts.get(&key).cloned() { - account - } else { - AccountSharedData::default() - }; - - accounts.push((key, account_shared_data)); - } - - accounts - } - - // set up one of the preconfigured blank stake accounts at some starting state - // to mutate the accounts after initial setup, do it directly or execute instructions - // note these accounts are already rent exempt, so lamports specified are stake or extra - fn update_stake( - &mut self, - pubkey: &Pubkey, - stake_state: &StakeStateV2, - additional_lamports: u64, - ) { - assert!(*pubkey == STAKE_ACCOUNT_BLACK || *pubkey == STAKE_ACCOUNT_WHITE); - let stake_account = self.accounts.get_mut(pubkey).unwrap(); - let current_lamports = stake_account.lamports(); - stake_account.set_lamports(current_lamports + additional_lamports); - bincode::serialize_into(stake_account.data_as_mut_slice(), stake_state).unwrap(); - } - - // process an instruction, assert checks, and update internal accounts - fn process(&mut self, instruction: &Instruction, checks: &[Check]) { - let initial_accounts = self.resolve_accounts(&instruction.accounts); - - let result = - self.mollusk - .process_and_validate_instruction(instruction, &initial_accounts, checks); - - for (i, resulting_account) in result.resulting_accounts.into_iter().enumerate() { - let account_meta = &instruction.accounts[i]; - assert_eq!(account_meta.pubkey, resulting_account.0); - if account_meta.is_writable { - if resulting_account.1.lamports() == 0 { - self.accounts.remove(&resulting_account.0); - } else { - self.accounts - .insert(resulting_account.0, resulting_account.1); - } - } - } - } - - // shorthand for process with only a success check - fn process_success(&mut self, instruction: &Instruction) { - self.process(instruction, &[Check::success()]); - } - - // shorthand for process with an expected error - fn process_fail(&mut self, instruction: &Instruction, error: ProgramError) { - self.process(instruction, &[Check::err(error)]); - } -} - -fn just_stake(stake_pubkey: Pubkey, stake: u64) -> StakeStateV2 { - let authorized = match stake_pubkey { - STAKE_ACCOUNT_BLACK => Authorized { - staker: STAKER_BLACK, - withdrawer: WITHDRAWER_BLACK, - }, - STAKE_ACCOUNT_WHITE => Authorized { - staker: STAKER_WHITE, - withdrawer: WITHDRAWER_WHITE, - }, - _ => Authorized::default(), - }; - - StakeStateV2::Stake( - Meta { - rent_exempt_reserve: STAKE_RENT_EXEMPTION, - authorized, - lockup: Lockup::default(), - }, - Stake { - delegation: Delegation { - stake, - ..Delegation::default() - }, - ..Stake::default() - }, - StakeFlags::empty(), - ) -} - -fn active_stake( - voter_pubkey: Pubkey, - stake_pubkey: Pubkey, - stake: u64, - use_gray: bool, -) -> StakeStateV2 { - assert!(stake_pubkey != VOTE_ACCOUNT_RED); - assert!(stake_pubkey != VOTE_ACCOUNT_BLUE); - - let authorized = match (use_gray, stake_pubkey) { - (true, _) => Authorized { - staker: STAKER_GRAY, - withdrawer: WITHDRAWER_GRAY, - }, - (false, STAKE_ACCOUNT_BLACK) => Authorized { - staker: STAKER_BLACK, - withdrawer: WITHDRAWER_BLACK, - }, - (false, STAKE_ACCOUNT_WHITE) => Authorized { - staker: STAKER_WHITE, - withdrawer: WITHDRAWER_WHITE, - }, - (false, _) => Authorized::default(), - }; - - StakeStateV2::Stake( - Meta { - rent_exempt_reserve: STAKE_RENT_EXEMPTION, - authorized, - lockup: Lockup::default(), - }, - Stake { - delegation: Delegation { - stake, - voter_pubkey, - activation_epoch: EXECUTION_EPOCH - 1, - ..Delegation::default() - }, - ..Stake::default() - }, - StakeFlags::empty(), - ) -} - -fn stake_to_bytes(stake: &StakeStateV2) -> Vec { - let mut data = vec![0; StakeStateV2::size_of()]; - bincode::serialize_into(&mut data[..], stake).unwrap(); - data -} - -#[test] -fn test_initialize() { - let mut env = Env::init(); - - let authorized = Authorized::default(); - let lockup = Lockup::default(); - - let instruction = instruction::initialize(&STAKE_ACCOUNT_BLACK, &authorized, &lockup); - - let black_state = StakeStateV2::Initialized(Meta { - rent_exempt_reserve: STAKE_RENT_EXEMPTION, - authorized, - lockup, - }); - let black = stake_to_bytes(&black_state); - - env.process( - &instruction, - &[ - Check::success(), - Check::account(&STAKE_ACCOUNT_BLACK).data(&black).build(), - ], - ); -} - -/* TODO someday, fd fixtures are very touchy - after a week or two of work, we have found zero "true" failures -#[test] -fn hana() { - // failure counts by ixn: - // 7 HANA ixn: 3 (split) - // bad fixtures, useless accounts - // 6 HANA ixn: 2 (delegate) - // bad fixtures, vote v023 - // 5 HANA ixn: 13 (minimum delegation) - // bad fixtures, cu nonsense or useless accounts - // 4 HANA ixn: 4 (withdraw) - // bad fixtures, malformed stake history - // 2 HANA ixn: 6 (set lockup) - // bad fixtures, useless accounts - // 1 HANA ixn: 8 (auth with seed) - // expected to fail with missing signature, probably the bug we fixed - #[rustfmt::skip] - let failure_paths = vec!["/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/8e377aa2af14a8261c8687e689ea5a2c05937f5b_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/f2597f5e62e88676.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/0d19e6c0dcc5293fd85fa6cbd243b683df1d27f5_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/ac86895e3b04af19.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/bae28d42fc69dfea.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/52c531d4dde001a2.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/103caec03366be1a9bd9132fa8be1428b193528c_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/a9bc98e87eec2acd.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/5aece52dbeb1ceec8c6d5707e45372a2fbbb3e61_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/336973321e077c57.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/0acdc3b3818e466a.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/c97fa85d47757592.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/11cf70f361a4cc77.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/6f632e1ad6075480.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/35b98356227ed1e087868ea9f5062f599f97e96a_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/instr-1111111111111111111111111111111111111111111111111111111111111111-1416.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/9973b46e31edd1780e96ae00a5a6226fba3eb4d7_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/instr-1111111111111111111111111111111111111111111111111111111111111111-0124.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/51ddcae74379237f.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/55b22d9dcb936676fd574fbafe4bb906628e525e_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/133723dc1d779f033aa6a11ac9d61c5c2ad286fe_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/c73f6e8eb34ab99b145b2875a915b3262f592f22_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/adf5434529d66e18db0a10ad5596cb30997c4c8a_3246959.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/1ede3ae646d1b82f.bin.fix", "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/2ab4c8c82a552c80.bin.fix"]; - - const DIR: &str = "/home/hana/work/firedancer/test-vectors/instr/fixtures/stake/"; - - let mut fixture_paths = vec![]; - if let Ok(entries) = fs::read_dir(DIR) { - for entry in entries.filter_map(Result::ok) { - let path = entry.path(); - if path.extension().map_or(false, |ext| ext == "fix") { - if let Some(path_str) = path.to_str() { - fixture_paths.push(path_str.to_string()); - } - } - } - } - - let stake_dummy = AccountSharedData::create(0, vec![], bpf_loader_upgradeable::id(), true, 0); - - let mut passed = 0; - let mut failed = 0; - let mut ok = 0; - let mut err = 0; - let mut bad_sysvar = 0; - let mut bad_cu = 0; - let mut enabled_min = 0; - let mut use_alt = 0; - let mut disabled_rewards = 0; - let mut disabled_move = 0; - for path in &failure_paths /* XXX fixture_paths */ { - // expects to spend no CUs on minimum delegation - if path.contains("c03a06bd0c9c52b0000dc54d4de4fd8d8b350297_3246959.fix") { - bad_cu += 1; - continue; - } - - // set up firedancer fixture test environment - let mut fixture = Fixture::load_from_blob_file(&path); - - // activate these features because the program doesnt function without them - // we may have spurious failures for tests that want to fail with the features off - fixture - .input - .epoch_context - .feature_set - .activate(&enable_partitioned_epoch_reward::id(), 0); - fixture - .input - .epoch_context - .feature_set - .activate(&get_sysvar_syscall_enabled::id(), 0); - - let ref feature_set = fixture.input.epoch_context.feature_set; - - // bpf stake requires the EpochRewards sysvar - if !feature_set.is_active(&partitioned_epoch_rewards_superfeature::id()) - && !feature_set.is_active(&enable_partitioned_epoch_reward::id()) - { - disabled_rewards += 1; - continue; - } - - // bpf stake cannot enable minimum delegation - if feature_set.is_active(&stake_raise_minimum_delegation_to_1_sol::id()) { - enabled_min += 1; - continue; - } - - // some fixtures have mangled sysvar data, but we cannot test these - // mollusk attempts to parse them and unwraps. this is not something we really need to test tho - // since both styles of sysvar object constructor validate the pubkey - let Ok((mut mollusk, instruction, accounts, mut expected)) = - std::panic::catch_unwind(|| load_firedancer_fixture(&fixture)) - else { - bad_sysvar += 1; - continue; - }; - - // XXX - if instruction.data[0] != 4 { - continue; - } - - // alt cannot be resolved by mollusk, which operates on an invoke context - if instruction - .accounts - .iter() - .any(|meta| meta.pubkey == address_lookup_table::program::id()) - { - use_alt += 1; - continue; - } - - // bpf stake cannot disable the move instructions - // so if theyre used with the feature disabled, we skip the fixture - if instruction.data.len() > 1 { - if (instruction.data[0] == 16 || instruction.data[0] == 17) - && !feature_set.is_active(&move_stake_and_move_lamports_ixs::id()) - { - disabled_move += 1; - continue; - } - } - - // TODO a way to have `load_firedancer_fixture` use a preloaded program - //mollusk.add_program(&id(), "solana_stake_program", &bpf_loader_upgradeable::id()); - - // process instruction against bpf stake program - let mut actual = mollusk.process_instruction(&instruction, &accounts); - println!("HANA ixn: {:#?}\nHANa accts: {:#?}\nHANA exp: {:#?}", instruction, accounts, expected); - println!("HANA actual: {:#?}", actual); - - // fixtures and bpf stake often disagree on whether these should be in the account results - // they never change tho so can be filtered out unconditionally - #[allow(deprecated)] - { - expected.resulting_accounts.retain(|(key, _)| { - !solana_sdk::sysvar::is_sysvar_id(key) && key != &stake::config::id() - }); - actual.resulting_accounts.retain(|(key, _)| { - !solana_sdk::sysvar::is_sysvar_id(key) && key != &stake::config::id() - }); - } - - // stake program in the account results is different because fd fixtures use native - expected - .resulting_accounts - .iter_mut() - .find(|(key, _)| key == &id()) - .map(|(_, account)| *account = stake_dummy.clone()); - - // this is a custom function that emulates what we would like solana-conformance consensus mode to be: - // * program_result matches if both are Ok or both are Err, without errors being compared - // * raw_result is ignored (i dont actually know what this is, maybe comparing them is fine) - // * resulting_accounts are only compared for successful transactions - let compare_result = std::panic::catch_unwind(|| expected.compare_consensus(&actual)); - - if compare_result.is_ok() { - if expected.program_result.is_err() { - err += 1; - } else { - ok += 1; - } - - passed += 1; - } else { - failed += 1; - } - } - - println!( - "passed: {} ({} ok, {} err) -failed: {} -skipped: {} -* malformed sysvar: {} -* onerous CU limit: {} -* requires 1sol minimum: {} -* requires ALT: {} -* EpochRewards disabled: {} -* MoveStake/MoveLamports disabled but used: {}", - passed, - ok, - err, - failed, - bad_sysvar + bad_cu + enabled_min + use_alt + disabled_rewards + disabled_move, - bad_sysvar, - bad_cu, - enabled_min, - use_alt, - disabled_rewards, - disabled_move, - ); -} -*/ diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs index 733322c..73ff668 100644 --- a/program/tests/stake_instruction.rs +++ b/program/tests/stake_instruction.rs @@ -4,8 +4,7 @@ use { bincode::serialize, - mollusk_svm::{fuzz::firedancer::load_firedancer_fixture, result::Check, Mollusk}, - mollusk_svm_fuzz_fixture_firedancer::Fixture, + mollusk_svm::{result::Check, Mollusk}, solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, solana_sdk::{ account::{create_account_shared_data_for_test, Account as SolanaAccount}, From 1f12960bf9c21cae5b160be0ba2e813c71e143ca Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 04:48:49 -0800 Subject: [PATCH 13/35] couple more tests --- Cargo.lock | 1 + program/Cargo.toml | 1 + program/tests/stake_instruction.rs | 381 ++++++++++++++++++++++++++++- 3 files changed, 378 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d34779..5ad70a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5113,6 +5113,7 @@ dependencies = [ "num-traits", "num_enum", "solana-account", + "solana-config-program", "solana-feature-set", "solana-program", "solana-program-test", diff --git a/program/Cargo.toml b/program/Cargo.toml index abd7f02..30233e3 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -25,6 +25,7 @@ thiserror = "1.0.63" mollusk-svm = { path = "/home/hana/work/misc/mollusk/harness" } solana-account = { version = "=2.1.0", features = ["bincode"] } solana-program-test = "=2.1.0" +solana-config-program = "=2.1.0" solana-sdk = "=2.1.0" solana-feature-set = "=2.1.0" test-case = "3.3.1" diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs index 73ff668..cbefdac 100644 --- a/program/tests/stake_instruction.rs +++ b/program/tests/stake_instruction.rs @@ -24,7 +24,7 @@ use { signature::{Keypair, Signer}, signers::Signers, stake::{ - self, + self, config as stake_config, instruction::{self, LockupArgs, LockupCheckedArgs, StakeError, StakeInstruction}, stake_flags::StakeFlags, state::{ @@ -37,9 +37,10 @@ use { system_instruction, system_program, sysvar::{ clock::{self, Clock}, - epoch_rewards::EpochRewards, - epoch_schedule::EpochSchedule, - rent::Rent, + epoch_rewards::{self, EpochRewards}, + epoch_schedule::{self, EpochSchedule}, + rent::{self, Rent}, + rewards, stake_history::{self, StakeHistory}, SysvarId, }, @@ -50,7 +51,12 @@ use { }, }, solana_stake_program::{get_minimum_delegation, id, processor::Processor}, - std::{collections::HashMap, fs, sync::Arc}, + std::{ + collections::{HashMap, HashSet}, + fs, + str::FromStr, + sync::Arc, + }, test_case::{test_case, test_matrix}, }; @@ -70,6 +76,30 @@ fn mollusk_bpf() -> Mollusk { mollusk } +fn create_default_account() -> AccountSharedData { + AccountSharedData::new(0, 0, &Pubkey::new_unique()) +} + +fn create_default_stake_account() -> AccountSharedData { + AccountSharedData::new(0, 0, &id()) +} + +fn invalid_stake_state_pubkey() -> Pubkey { + Pubkey::from_str("BadStake11111111111111111111111111111111111").unwrap() +} + +fn invalid_vote_state_pubkey() -> Pubkey { + Pubkey::from_str("BadVote111111111111111111111111111111111111").unwrap() +} + +fn spoofed_stake_state_pubkey() -> Pubkey { + Pubkey::from_str("SpoofedStake1111111111111111111111111111111").unwrap() +} + +fn spoofed_stake_program_id() -> Pubkey { + Pubkey::from_str("Spoofed111111111111111111111111111111111111").unwrap() +} + fn process_instruction( mollusk: &Mollusk, instruction_data: &[u8], @@ -98,6 +128,47 @@ fn process_instruction( .collect() } +fn get_default_transaction_accounts(instruction: &Instruction) -> Vec<(Pubkey, AccountSharedData)> { + let mut pubkeys: HashSet = instruction + .accounts + .iter() + .map(|meta| meta.pubkey) + .collect(); + pubkeys.insert(clock::id()); + pubkeys.insert(epoch_schedule::id()); + pubkeys.insert(stake_history::id()); + #[allow(deprecated)] + pubkeys + .iter() + .map(|pubkey| { + ( + *pubkey, + if clock::check_id(pubkey) { + create_account_shared_data_for_test(&clock::Clock::default()) + } else if rewards::check_id(pubkey) { + create_account_shared_data_for_test(&rewards::Rewards::new(0.0)) + } else if stake_history::check_id(pubkey) { + create_account_shared_data_for_test(&StakeHistory::default()) + } else if stake_config::check_id(pubkey) { + config::create_account(0, &stake_config::Config::default()) + } else if epoch_schedule::check_id(pubkey) { + create_account_shared_data_for_test(&EpochSchedule::default()) + } else if rent::check_id(pubkey) { + create_account_shared_data_for_test(&Rent::default()) + } else if *pubkey == invalid_stake_state_pubkey() { + AccountSharedData::new(0, 0, &id()) + } else if *pubkey == invalid_vote_state_pubkey() { + AccountSharedData::new(0, 0, &solana_vote_program::id()) + } else if *pubkey == spoofed_stake_state_pubkey() { + AccountSharedData::new(0, 0, &spoofed_stake_program_id()) + } else { + AccountSharedData::new(0, 0, &id()) + }, + ) + }) + .collect() +} + fn new_stake( stake: u64, voter_pubkey: &Pubkey, @@ -118,6 +189,119 @@ fn stake_from>(account: &T) -> Optio from(account).and_then(|state: StakeStateV2| state.stake()) } +mod config { + #[allow(deprecated)] + use solana_sdk::stake::config::{self, *}; + use { + bincode::deserialize, + solana_config_program::{create_config_account, get_config_data}, + solana_sdk::{ + account::{AccountSharedData, ReadableAccount, WritableAccount}, + genesis_config::GenesisConfig, + transaction_context::BorrowedAccount, + }, + }; + + #[allow(deprecated)] + pub fn from(account: &BorrowedAccount) -> Option { + get_config_data(account.get_data()) + .ok() + .and_then(|data| deserialize(data).ok()) + } + + #[allow(deprecated)] + pub fn create_account(lamports: u64, config: &Config) -> AccountSharedData { + create_config_account(vec![], config, lamports) + } + + #[allow(deprecated)] + pub fn add_genesis_account(genesis_config: &mut GenesisConfig) -> u64 { + let mut account = create_config_account(vec![], &Config::default(), 0); + let lamports = genesis_config.rent.minimum_balance(account.data().len()); + + account.set_lamports(lamports.max(1)); + + genesis_config.add_account(config::id(), account); + + lamports + } +} + +// Ensure that the correct errors are returned when processing instructions +// +// The GetMinimumDelegation instruction does not take any accounts; so when it was added, +// `process_instruction()` needed to be updated to *not* need a stake account passed in, which +// changes the error *ordering* conditions. +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_stake_process_instruction_error_ordering(mollusk: Mollusk) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let rent_address = rent::id(); + let rent_account = create_account_shared_data_for_test(&rent); + + let good_stake_address = Pubkey::new_unique(); + let good_stake_account = + AccountSharedData::new(rent_exempt_reserve, StakeStateV2::size_of(), &id()); + let good_instruction = instruction::initialize( + &good_stake_address, + &Authorized::auto(&good_stake_address), + &Lockup::default(), + ); + let good_transaction_accounts = vec![ + (good_stake_address, good_stake_account), + (rent_address, rent_account), + ]; + let good_instruction_accounts = vec![ + AccountMeta { + pubkey: good_stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: rent_address, + is_signer: false, + is_writable: false, + }, + ]; + let good_accounts = (good_transaction_accounts, good_instruction_accounts); + + // The instruction data needs to deserialize to a bogus StakeInstruction. We likely never + // will have `usize::MAX`-number of instructions, so this should be a safe constant to + // always map to an invalid stake instruction. + let bad_instruction = Instruction::new_with_bincode(id(), &usize::MAX, Vec::default()); + let bad_transaction_accounts = Vec::default(); + let bad_instruction_accounts = Vec::default(); + let bad_accounts = (bad_transaction_accounts, bad_instruction_accounts); + + for (instruction, (transaction_accounts, instruction_accounts), expected_result) in [ + (&good_instruction, &good_accounts, Ok(())), + ( + &bad_instruction, + &good_accounts, + Err(ProgramError::InvalidInstructionData), + ), + ( + &good_instruction, + &bad_accounts, + Err(ProgramError::NotEnoughAccountKeys), + ), + ( + &bad_instruction, + &bad_accounts, + Err(ProgramError::InvalidInstructionData), + ), + ] { + process_instruction( + &mollusk, + &instruction.data, + transaction_accounts.clone(), + instruction_accounts.clone(), + expected_result, + ); + } +} + #[test_case(mollusk_native(); "native_stake")] #[test_case(mollusk_bpf(); "bpf_stake")] fn test_deactivate_delinquent(mollusk: Mollusk) { @@ -386,3 +570,190 @@ fn test_deactivate_delinquent(mollusk: Mollusk) { Err(StakeError::MinimumDelinquentEpochsForDeactivationNotMet.into()), ); } + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_stake_process_instruction_with_epoch_rewards_active(mollusk: Mollusk) { + let process_instruction_as_one_arg = |mollusk: &Mollusk, + instruction: &Instruction, + expected_result: Result<(), ProgramError>| + -> Vec { + let mut transaction_accounts = get_default_transaction_accounts(instruction); + + // Initialize EpochRewards sysvar account + let epoch_rewards_sysvar = EpochRewards { + active: true, + ..EpochRewards::default() + }; + transaction_accounts.push(( + epoch_rewards::id(), + create_account_shared_data_for_test(&epoch_rewards_sysvar), + )); + + process_instruction( + &mollusk, + &instruction.data, + transaction_accounts, + instruction.accounts.clone(), + expected_result, + ) + }; + + process_instruction_as_one_arg( + &mollusk, + &instruction::initialize( + &Pubkey::new_unique(), + &Authorized::default(), + &Lockup::default(), + ), + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::authorize( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + StakeAuthorize::Staker, + None, + ), + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::delegate_stake( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &invalid_vote_state_pubkey(), + ), + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::split( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + 100, + &invalid_stake_state_pubkey(), + )[2], + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::withdraw( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + 100, + None, + ), + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::deactivate_stake(&Pubkey::new_unique(), &Pubkey::new_unique()), + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::set_lockup( + &Pubkey::new_unique(), + &LockupArgs::default(), + &Pubkey::new_unique(), + ), + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::merge( + &Pubkey::new_unique(), + &invalid_stake_state_pubkey(), + &Pubkey::new_unique(), + )[0], + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::authorize_with_seed( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + "seed".to_string(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + StakeAuthorize::Staker, + None, + ), + Err(StakeError::EpochRewardsActive.into()), + ); + + process_instruction_as_one_arg( + &mollusk, + &instruction::initialize_checked(&Pubkey::new_unique(), &Authorized::default()), + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::authorize_checked( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + StakeAuthorize::Staker, + None, + ), + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::authorize_checked_with_seed( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + "seed".to_string(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + StakeAuthorize::Staker, + None, + ), + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::set_lockup_checked( + &Pubkey::new_unique(), + &LockupArgs::default(), + &Pubkey::new_unique(), + ), + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::deactivate_delinquent_stake( + &Pubkey::new_unique(), + &invalid_vote_state_pubkey(), + &Pubkey::new_unique(), + ), + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::move_stake( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + 100, + ), + Err(StakeError::EpochRewardsActive.into()), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::move_lamports( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + 100, + ), + Err(StakeError::EpochRewardsActive.into()), + ); + + // Only GetMinimumDelegation should not return StakeError::EpochRewardsActive + process_instruction_as_one_arg(&mollusk, &instruction::get_minimum_delegation(), Ok(())); +} From ed0f9b7cce2f4cd5a01bffbd36dfb976a43b0823 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 05:30:11 -0800 Subject: [PATCH 14/35] merge tests --- Cargo.lock | 1 + program/Cargo.toml | 1 + program/tests/stake_instruction.rs | 770 +++++++++++++++++++++++++++++ 3 files changed, 772 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 5ad70a4..6258e56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5116,6 +5116,7 @@ dependencies = [ "solana-config-program", "solana-feature-set", "solana-program", + "solana-program-runtime", "solana-program-test", "solana-sdk", "test-case", diff --git a/program/Cargo.toml b/program/Cargo.toml index 30233e3..9f31ac5 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -25,6 +25,7 @@ thiserror = "1.0.63" mollusk-svm = { path = "/home/hana/work/misc/mollusk/harness" } solana-account = { version = "=2.1.0", features = ["bincode"] } solana-program-test = "=2.1.0" +solana-program-runtime = "=2.1.0" solana-config-program = "=2.1.0" solana-sdk = "=2.1.0" solana-feature-set = "=2.1.0" diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs index cbefdac..7ff6113 100644 --- a/program/tests/stake_instruction.rs +++ b/program/tests/stake_instruction.rs @@ -6,6 +6,7 @@ use { bincode::serialize, mollusk_svm::{result::Check, Mollusk}, solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, + solana_program_runtime::loaded_programs::ProgramCacheEntryOwner, solana_sdk::{ account::{create_account_shared_data_for_test, Account as SolanaAccount}, account_utils::StateMut, @@ -18,6 +19,7 @@ use { }, hash::Hash, instruction::{AccountMeta, Instruction}, + native_loader, native_token::LAMPORTS_PER_SOL, program_error::ProgramError, pubkey::Pubkey, @@ -76,6 +78,19 @@ fn mollusk_bpf() -> Mollusk { mollusk } +trait IsBpf { + fn is_bpf(&self) -> bool; +} +impl IsBpf for Mollusk { + fn is_bpf(&self) -> bool { + self.program_cache + .load_program(&id()) + .unwrap() + .account_owner + != ProgramCacheEntryOwner::NativeLoader + } +} + fn create_default_account() -> AccountSharedData { AccountSharedData::new(0, 0, &Pubkey::new_unique()) } @@ -189,6 +204,20 @@ fn stake_from>(account: &T) -> Optio from(account).and_then(|state: StakeStateV2| state.stake()) } +fn just_stake(meta: Meta, stake: u64) -> StakeStateV2 { + StakeStateV2::Stake( + meta, + Stake { + delegation: Delegation { + stake, + ..Delegation::default() + }, + ..Stake::default() + }, + StakeFlags::empty(), + ) +} + mod config { #[allow(deprecated)] use solana_sdk::stake::config::{self, *}; @@ -227,6 +256,747 @@ mod config { } } +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_merge(mollusk: Mollusk) { + let stake_address = solana_sdk::pubkey::new_rand(); + let merge_from_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let meta = Meta::auto(&authorized_address); + let stake_lamports = 42; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: merge_from_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + for state in &[ + StakeStateV2::Initialized(meta), + just_stake(meta, stake_lamports), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + for merge_from_state in &[ + StakeStateV2::Initialized(meta), + just_stake(meta, stake_lamports), + ] { + let merge_from_account = AccountSharedData::new_data_with_space( + stake_lamports, + merge_from_state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (merge_from_address, merge_from_account), + (authorized_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + + // Authorized staker signature required... + instruction_accounts[4].is_signer = false; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + instruction_accounts[4].is_signer = true; + + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + + // check lamports + assert_eq!(accounts[0].lamports(), stake_lamports * 2); + assert_eq!(accounts[1].lamports(), 0); + + // check state + match state { + StakeStateV2::Initialized(meta) => { + assert_eq!(accounts[0].state(), Ok(StakeStateV2::Initialized(*meta)),); + } + StakeStateV2::Stake(meta, stake, stake_flags) => { + let expected_stake = stake.delegation.stake + + merge_from_state + .stake() + .map(|stake| stake.delegation.stake) + .unwrap_or_else(|| { + stake_lamports + - merge_from_state.meta().unwrap().rent_exempt_reserve + }); + assert_eq!( + accounts[0].state(), + Ok(StakeStateV2::Stake( + *meta, + Stake { + delegation: Delegation { + stake: expected_stake, + ..stake.delegation + }, + ..*stake + }, + *stake_flags, + )), + ); + } + _ => unreachable!(), + } + assert_eq!(accounts[1].state(), Ok(StakeStateV2::Uninitialized)); + } + } +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_merge_self_fails(mollusk: Mollusk) { + let stake_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_amount = 4242424242; + let stake_lamports = rent_exempt_reserve + stake_amount; + let meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&authorized_address) + }; + let stake = Stake { + delegation: Delegation { + stake: stake_amount, + activation_epoch: 0, + ..Delegation::default() + }, + ..Stake::default() + }; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Stake(meta, stake, StakeFlags::empty()), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (authorized_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts, + instruction_accounts, + Err(ProgramError::InvalidArgument), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_merge_incorrect_authorized_staker(mollusk: Mollusk) { + let stake_address = solana_sdk::pubkey::new_rand(); + let merge_from_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let wrong_authorized_address = solana_sdk::pubkey::new_rand(); + let stake_lamports = 42; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: merge_from_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + for state in &[ + StakeStateV2::Initialized(Meta::auto(&authorized_address)), + just_stake(Meta::auto(&authorized_address), stake_lamports), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + for merge_from_state in &[ + StakeStateV2::Initialized(Meta::auto(&wrong_authorized_address)), + just_stake(Meta::auto(&wrong_authorized_address), stake_lamports), + ] { + let merge_from_account = AccountSharedData::new_data_with_space( + stake_lamports, + merge_from_state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (merge_from_address, merge_from_account), + (authorized_address, AccountSharedData::default()), + (wrong_authorized_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + + instruction_accounts[4].pubkey = wrong_authorized_address; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + instruction_accounts[4].pubkey = authorized_address; + + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Err(StakeError::MergeMismatch.into()), + ); + } + } +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_merge_invalid_account_data(mollusk: Mollusk) { + let stake_address = solana_sdk::pubkey::new_rand(); + let merge_from_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let stake_lamports = 42; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: merge_from_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + for state in &[ + StakeStateV2::Uninitialized, + StakeStateV2::RewardsPool, + StakeStateV2::Initialized(Meta::auto(&authorized_address)), + just_stake(Meta::auto(&authorized_address), stake_lamports), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + for merge_from_state in &[StakeStateV2::Uninitialized, StakeStateV2::RewardsPool] { + let merge_from_account = AccountSharedData::new_data_with_space( + stake_lamports, + merge_from_state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (merge_from_address, merge_from_account), + (authorized_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Err(ProgramError::InvalidAccountData), + ); + } + } +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_merge_fake_stake_source(mollusk: Mollusk) { + let stake_address = solana_sdk::pubkey::new_rand(); + let merge_from_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let stake_lamports = 42; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &just_stake(Meta::auto(&authorized_address), stake_lamports), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let merge_from_account = AccountSharedData::new_data_with_space( + stake_lamports, + &just_stake(Meta::auto(&authorized_address), stake_lamports), + StakeStateV2::size_of(), + &solana_sdk::pubkey::new_rand(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (merge_from_address, merge_from_account), + (authorized_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: merge_from_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts, + instruction_accounts, + Err(if mollusk.is_bpf() { + ProgramError::InvalidAccountOwner + } else { + ProgramError::IncorrectProgramId + }), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_merge_active_stake(mollusk: Mollusk) { + let stake_address = solana_sdk::pubkey::new_rand(); + let merge_from_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let base_lamports = 4242424242; + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_amount = base_lamports; + let stake_lamports = rent_exempt_reserve + stake_amount; + let merge_from_amount = base_lamports; + let merge_from_lamports = rent_exempt_reserve + merge_from_amount; + let meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&authorized_address) + }; + let mut stake = Stake { + delegation: Delegation { + stake: stake_amount, + activation_epoch: 0, + ..Delegation::default() + }, + ..Stake::default() + }; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Stake(meta, stake, StakeFlags::empty()), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let merge_from_activation_epoch = 2; + let mut merge_from_stake = Stake { + delegation: Delegation { + stake: merge_from_amount, + activation_epoch: merge_from_activation_epoch, + ..stake.delegation + }, + ..stake + }; + let merge_from_account = AccountSharedData::new_data_with_space( + merge_from_lamports, + &StakeStateV2::Stake(meta, merge_from_stake, StakeFlags::empty()), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let mut clock = Clock::default(); + let mut stake_history = StakeHistory::default(); + let mut effective = base_lamports; + let mut activating = stake_amount; + let mut deactivating = 0; + stake_history.add( + clock.epoch, + StakeHistoryEntry { + effective, + activating, + deactivating, + }, + ); + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (merge_from_address, merge_from_account), + (authorized_address, AccountSharedData::default()), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: merge_from_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + fn try_merge( + mollusk: &Mollusk, + transaction_accounts: Vec<(Pubkey, AccountSharedData)>, + mut instruction_accounts: Vec, + expected_result: Result<(), ProgramError>, + ) { + for iteration in 0..2 { + if iteration == 1 { + instruction_accounts.swap(0, 1); + } + let accounts = process_instruction( + mollusk, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + expected_result.clone(), + ); + if expected_result.is_ok() { + assert_eq!( + accounts[1 - iteration].state(), + Ok(StakeStateV2::Uninitialized) + ); + } + } + } + + // stake activation epoch, source initialized succeeds + try_merge( + &mollusk, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + let new_warmup_cooldown_rate_epoch = Some(0); + + // both activating fails + loop { + clock.epoch += 1; + if clock.epoch == merge_from_activation_epoch { + activating += merge_from_amount; + } + let delta = activating.min( + (effective as f64 * warmup_cooldown_rate(clock.epoch, new_warmup_cooldown_rate_epoch)) + as u64, + ); + effective += delta; + activating -= delta; + stake_history.add( + clock.epoch - 1, + StakeHistoryEntry { + effective, + activating, + deactivating, + }, + ); + transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock)); + transaction_accounts[4] = ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ); + if stake_amount == stake.stake(clock.epoch, &stake_history, new_warmup_cooldown_rate_epoch) + && merge_from_amount + == merge_from_stake.stake( + clock.epoch, + &stake_history, + new_warmup_cooldown_rate_epoch, + ) + { + break; + } + try_merge( + &mollusk, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::from(StakeError::MergeTransientStake)), + ); + } + + // Both fully activated works + try_merge( + &mollusk, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // deactivate setup for deactivation + let merge_from_deactivation_epoch = clock.epoch + 1; + let stake_deactivation_epoch = clock.epoch + 2; + + // active/deactivating and deactivating/inactive mismatches fail + loop { + clock.epoch += 1; + let delta = deactivating.min( + (effective as f64 * warmup_cooldown_rate(clock.epoch, new_warmup_cooldown_rate_epoch)) + as u64, + ); + effective -= delta; + deactivating -= delta; + if clock.epoch == stake_deactivation_epoch { + deactivating += stake_amount; + stake = Stake { + delegation: Delegation { + deactivation_epoch: stake_deactivation_epoch, + ..stake.delegation + }, + ..stake + }; + transaction_accounts[0] + .1 + .set_state(&StakeStateV2::Stake(meta, stake, StakeFlags::empty())) + .unwrap(); + } + if clock.epoch == merge_from_deactivation_epoch { + deactivating += merge_from_amount; + merge_from_stake = Stake { + delegation: Delegation { + deactivation_epoch: merge_from_deactivation_epoch, + ..merge_from_stake.delegation + }, + ..merge_from_stake + }; + transaction_accounts[1] + .1 + .set_state(&StakeStateV2::Stake( + meta, + merge_from_stake, + StakeFlags::empty(), + )) + .unwrap(); + } + stake_history.add( + clock.epoch - 1, + StakeHistoryEntry { + effective, + activating, + deactivating, + }, + ); + transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock)); + transaction_accounts[4] = ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ); + if 0 == stake.stake(clock.epoch, &stake_history, new_warmup_cooldown_rate_epoch) + && 0 == merge_from_stake.stake( + clock.epoch, + &stake_history, + new_warmup_cooldown_rate_epoch, + ) + { + break; + } + try_merge( + &mollusk, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::from(StakeError::MergeTransientStake)), + ); + } + + // Both fully deactivated works + try_merge(&mollusk, transaction_accounts, instruction_accounts, Ok(())); +} + +// XXX SKIP test_stake_get_minimum_delegation + // Ensure that the correct errors are returned when processing instructions // // The GetMinimumDelegation instruction does not take any accounts; so when it was added, From 4512b07558d4a74f77cdde5a77f2ca8cdb2eb487 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 06:03:40 -0800 Subject: [PATCH 15/35] more tests --- Cargo.lock | 1 + program/Cargo.toml | 1 + program/tests/stake_instruction.rs | 1396 +++++++++++++++++++++++++++- 3 files changed, 1392 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6258e56..16d0cec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5119,6 +5119,7 @@ dependencies = [ "solana-program-runtime", "solana-program-test", "solana-sdk", + "solana-vote-program", "test-case", "thiserror 1.0.69", ] diff --git a/program/Cargo.toml b/program/Cargo.toml index 9f31ac5..deba94b 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -27,6 +27,7 @@ solana-account = { version = "=2.1.0", features = ["bincode"] } solana-program-test = "=2.1.0" solana-program-runtime = "=2.1.0" solana-config-program = "=2.1.0" +solana-vote-program = "=2.1.0" solana-sdk = "=2.1.0" solana-feature-set = "=2.1.0" test-case = "3.3.1" diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs index 7ff6113..f8a0324 100644 --- a/program/tests/stake_instruction.rs +++ b/program/tests/stake_instruction.rs @@ -27,7 +27,11 @@ use { signers::Signers, stake::{ self, config as stake_config, - instruction::{self, LockupArgs, LockupCheckedArgs, StakeError, StakeInstruction}, + instruction::{ + self, authorize_checked, authorize_checked_with_seed, initialize_checked, + set_lockup_checked, AuthorizeCheckedWithSeedArgs, AuthorizeWithSeedArgs, + LockupArgs, StakeError, StakeInstruction, + }, stake_flags::StakeFlags, state::{ warmup_cooldown_rate, Authorized, Delegation, Lockup, Meta, Stake, @@ -47,12 +51,12 @@ use { SysvarId, }, transaction::{Transaction, TransactionError}, - vote::{ - program as solana_vote_program, - state::{VoteInit, VoteState, VoteStateVersions}, - }, }, solana_stake_program::{get_minimum_delegation, id, processor::Processor}, + solana_vote_program::{ + self, + vote_state::{self, VoteState, VoteStateVersions}, + }, std::{ collections::{HashMap, HashSet}, fs, @@ -118,10 +122,19 @@ fn spoofed_stake_program_id() -> Pubkey { fn process_instruction( mollusk: &Mollusk, instruction_data: &[u8], - transaction_accounts: Vec<(Pubkey, AccountSharedData)>, + mut transaction_accounts: Vec<(Pubkey, AccountSharedData)>, instruction_accounts: Vec, expected_result: Result<(), ProgramError>, ) -> Vec { + for ixn_key in instruction_accounts.iter().map(|meta| meta.pubkey) { + if !transaction_accounts + .iter() + .any(|(txn_key, _)| *txn_key == ixn_key) + { + transaction_accounts.push((ixn_key, AccountSharedData::default())); + } + } + let instruction = Instruction { program_id: id(), accounts: instruction_accounts, @@ -184,6 +197,21 @@ fn get_default_transaction_accounts(instruction: &Instruction) -> Vec<(Pubkey, A .collect() } +fn process_instruction_as_one_arg( + mollusk: &Mollusk, + instruction: &Instruction, + expected_result: Result<(), ProgramError>, +) -> Vec { + let transaction_accounts = get_default_transaction_accounts(instruction); + process_instruction( + &mollusk, + &instruction.data, + transaction_accounts, + instruction.accounts.clone(), + expected_result, + ) +} + fn new_stake( stake: u64, voter_pubkey: &Pubkey, @@ -204,6 +232,22 @@ fn stake_from>(account: &T) -> Optio from(account).and_then(|state: StakeStateV2| state.stake()) } +pub fn delegation_from(account: &AccountSharedData) -> Option { + from(account).and_then(|state: StakeStateV2| state.delegation()) +} + +pub fn authorized_from(account: &AccountSharedData) -> Option { + from(account).and_then(|state: StakeStateV2| state.authorized()) +} + +pub fn lockup_from>(account: &T) -> Option { + from(account).and_then(|state: StakeStateV2| state.lockup()) +} + +pub fn meta_from(account: &AccountSharedData) -> Option { + from(account).and_then(|state: StakeStateV2| state.meta()) +} + fn just_stake(meta: Meta, stake: u64) -> StakeStateV2 { StakeStateV2::Stake( meta, @@ -256,6 +300,1346 @@ mod config { } } +// XXX SKIP BEOFRE THIS +// the tests are kind of dumb but i mihgt grab them anyway +// just annoying bc they test errors that changed +// and are kind of useless, we should actually test the interface systematically + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_stake_checked_instructions(mollusk: Mollusk) { + let stake_address = Pubkey::new_unique(); + let staker = Pubkey::new_unique(); + let staker_account = create_default_account(); + let withdrawer = Pubkey::new_unique(); + let withdrawer_account = create_default_account(); + let authorized_address = Pubkey::new_unique(); + let authorized_account = create_default_account(); + let new_authorized_account = create_default_account(); + let clock_address = clock::id(); + let clock_account = create_account_shared_data_for_test(&Clock::default()); + let custodian = Pubkey::new_unique(); + let custodian_account = create_default_account(); + let rent = Rent::default(); + let rent_address = rent::id(); + let rent_account = create_account_shared_data_for_test(&rent); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let minimum_delegation = crate::get_minimum_delegation(); + + // Test InitializeChecked with non-signing withdrawer + let mut instruction = initialize_checked(&stake_address, &Authorized { staker, withdrawer }); + instruction.accounts[3] = AccountMeta::new_readonly(withdrawer, false); + process_instruction_as_one_arg( + &mollusk, + &instruction, + Err(ProgramError::MissingRequiredSignature), + ); + + // Test InitializeChecked with withdrawer signer + let stake_account = AccountSharedData::new( + rent_exempt_reserve + minimum_delegation, + StakeStateV2::size_of(), + &id(), + ); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::InitializeChecked).unwrap(), + vec![ + (stake_address, stake_account), + (rent_address, rent_account), + (staker, staker_account), + (withdrawer, withdrawer_account.clone()), + ], + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: rent_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: staker, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: withdrawer, + is_signer: true, + is_writable: false, + }, + ], + Ok(()), + ); + + // Test AuthorizeChecked with non-signing authority + let mut instruction = authorize_checked( + &stake_address, + &authorized_address, + &staker, + StakeAuthorize::Staker, + None, + ); + instruction.accounts[3] = AccountMeta::new_readonly(staker, false); + process_instruction_as_one_arg( + &mollusk, + &instruction, + Err(ProgramError::MissingRequiredSignature), + ); + + let mut instruction = authorize_checked( + &stake_address, + &authorized_address, + &withdrawer, + StakeAuthorize::Withdrawer, + None, + ); + instruction.accounts[3] = AccountMeta::new_readonly(withdrawer, false); + process_instruction_as_one_arg( + &mollusk, + &instruction, + Err(ProgramError::MissingRequiredSignature), + ); + + // Test AuthorizeChecked with authority signer + let stake_account = AccountSharedData::new_data_with_space( + 42, + &StakeStateV2::Initialized(Meta::auto(&authorized_address)), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::AuthorizeChecked(StakeAuthorize::Staker)).unwrap(), + vec![ + (stake_address, stake_account.clone()), + (clock_address, clock_account.clone()), + (authorized_address, authorized_account.clone()), + (staker, new_authorized_account.clone()), + ], + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: staker, + is_signer: true, + is_writable: false, + }, + ], + Ok(()), + ); + + process_instruction( + &mollusk, + &serialize(&StakeInstruction::AuthorizeChecked( + StakeAuthorize::Withdrawer, + )) + .unwrap(), + vec![ + (stake_address, stake_account), + (clock_address, clock_account.clone()), + (authorized_address, authorized_account.clone()), + (withdrawer, new_authorized_account.clone()), + ], + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: withdrawer, + is_signer: true, + is_writable: false, + }, + ], + Ok(()), + ); + + // Test AuthorizeCheckedWithSeed with non-signing authority + let authorized_owner = Pubkey::new_unique(); + let seed = "test seed"; + let address_with_seed = + Pubkey::create_with_seed(&authorized_owner, seed, &authorized_owner).unwrap(); + let mut instruction = authorize_checked_with_seed( + &stake_address, + &authorized_owner, + seed.to_string(), + &authorized_owner, + &staker, + StakeAuthorize::Staker, + None, + ); + instruction.accounts[3] = AccountMeta::new_readonly(staker, false); + process_instruction_as_one_arg( + &mollusk, + &instruction, + Err(ProgramError::MissingRequiredSignature), + ); + + let mut instruction = authorize_checked_with_seed( + &stake_address, + &authorized_owner, + seed.to_string(), + &authorized_owner, + &staker, + StakeAuthorize::Withdrawer, + None, + ); + instruction.accounts[3] = AccountMeta::new_readonly(staker, false); + process_instruction_as_one_arg( + &mollusk, + &instruction, + Err(ProgramError::MissingRequiredSignature), + ); + + // Test AuthorizeCheckedWithSeed with authority signer + let stake_account = AccountSharedData::new_data_with_space( + 42, + &StakeStateV2::Initialized(Meta::auto(&address_with_seed)), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::AuthorizeCheckedWithSeed( + AuthorizeCheckedWithSeedArgs { + stake_authorize: StakeAuthorize::Staker, + authority_seed: seed.to_string(), + authority_owner: authorized_owner, + }, + )) + .unwrap(), + vec![ + (address_with_seed, stake_account.clone()), + (authorized_owner, authorized_account.clone()), + (clock_address, clock_account.clone()), + (staker, new_authorized_account.clone()), + ], + vec![ + AccountMeta { + pubkey: address_with_seed, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: authorized_owner, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: clock_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: staker, + is_signer: true, + is_writable: false, + }, + ], + Ok(()), + ); + + process_instruction( + &mollusk, + &serialize(&StakeInstruction::AuthorizeCheckedWithSeed( + AuthorizeCheckedWithSeedArgs { + stake_authorize: StakeAuthorize::Withdrawer, + authority_seed: seed.to_string(), + authority_owner: authorized_owner, + }, + )) + .unwrap(), + vec![ + (address_with_seed, stake_account), + (authorized_owner, authorized_account), + (clock_address, clock_account.clone()), + (withdrawer, new_authorized_account), + ], + vec![ + AccountMeta { + pubkey: address_with_seed, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: authorized_owner, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: clock_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: withdrawer, + is_signer: true, + is_writable: false, + }, + ], + Ok(()), + ); + + // Test SetLockupChecked with non-signing lockup custodian + let mut instruction = set_lockup_checked( + &stake_address, + &LockupArgs { + unix_timestamp: None, + epoch: Some(1), + custodian: Some(custodian), + }, + &withdrawer, + ); + instruction.accounts[2] = AccountMeta::new_readonly(custodian, false); + process_instruction_as_one_arg( + &mollusk, + &instruction, + Err(ProgramError::MissingRequiredSignature), + ); + + // Test SetLockupChecked with lockup custodian signer + let stake_account = AccountSharedData::new_data_with_space( + 42, + &StakeStateV2::Initialized(Meta::auto(&withdrawer)), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + + process_instruction( + &mollusk, + &instruction.data, + vec![ + (clock_address, clock_account), + (stake_address, stake_account), + (withdrawer, withdrawer_account), + (custodian, custodian_account), + ], + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: withdrawer, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: custodian, + is_signer: true, + is_writable: false, + }, + ], + Ok(()), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_stake_initialize(mollusk: Mollusk) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_lamports = rent_exempt_reserve; + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new(stake_lamports, StakeStateV2::size_of(), &id()); + let custodian_address = solana_sdk::pubkey::new_rand(); + let lockup = Lockup { + epoch: 1, + unix_timestamp: 0, + custodian: custodian_address, + }; + let instruction_data = serialize(&StakeInstruction::Initialize( + Authorized::auto(&stake_address), + lockup, + )) + .unwrap(); + let mut transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (rent::id(), create_account_shared_data_for_test(&rent)), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: rent::id(), + is_signer: false, + is_writable: false, + }, + ]; + + // should pass + let accounts = process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + // check that we see what we expect + assert_eq!( + from(&accounts[0]).unwrap(), + StakeStateV2::Initialized(Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + lockup, + }), + ); + + // 2nd time fails, can't move it from anything other than uninit->init + transaction_accounts[0] = (stake_address, accounts[0].clone()); + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InvalidAccountData), + ); + transaction_accounts[0] = (stake_address, stake_account); + + // not enough balance for rent + transaction_accounts[1] = ( + rent::id(), + create_account_shared_data_for_test(&Rent { + lamports_per_byte_year: rent.lamports_per_byte_year + 1, + ..rent + }), + ); + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + + // incorrect account sizes + let stake_account = AccountSharedData::new(stake_lamports, StakeStateV2::size_of() + 1, &id()); + transaction_accounts[0] = (stake_address, stake_account); + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InvalidAccountData), + ); + + let stake_account = AccountSharedData::new(stake_lamports, StakeStateV2::size_of() - 1, &id()); + transaction_accounts[0] = (stake_address, stake_account); + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts, + instruction_accounts, + Err(ProgramError::InvalidAccountData), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_authorize(mollusk: Mollusk) { + let authority_address = solana_sdk::pubkey::new_rand(); + let authority_address_2 = solana_sdk::pubkey::new_rand(); + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_lamports = 42; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::default(), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let to_address = solana_sdk::pubkey::new_rand(); + let to_account = AccountSharedData::new(1, 0, &system_program::id()); + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (to_address, to_account), + (authority_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authority_address, + is_signer: false, + is_writable: false, + }, + ]; + + // should fail, uninit + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Authorize( + authority_address, + StakeAuthorize::Staker, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InvalidAccountData), + ); + + // should pass + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Initialized(Meta::auto(&stake_address)), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + transaction_accounts[0] = (stake_address, stake_account); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Authorize( + authority_address, + StakeAuthorize::Staker, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Authorize( + authority_address, + StakeAuthorize::Withdrawer, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + if let StakeStateV2::Initialized(Meta { authorized, .. }) = from(&accounts[0]).unwrap() { + assert_eq!(authorized.staker, authority_address); + assert_eq!(authorized.withdrawer, authority_address); + } else { + panic!(); + } + + // A second authorization signed by the stake account should fail + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Authorize( + authority_address_2, + StakeAuthorize::Staker, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + + // Test a second authorization by the new authority_address + instruction_accounts[0].is_signer = false; + instruction_accounts[2].is_signer = true; + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Authorize( + authority_address_2, + StakeAuthorize::Staker, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + if let StakeStateV2::Initialized(Meta { authorized, .. }) = from(&accounts[0]).unwrap() { + assert_eq!(authorized.staker, authority_address_2); + } else { + panic!(); + } + + // Test a successful action by the currently authorized withdrawer + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: to_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authority_address, + is_signer: true, + is_writable: false, + }, + ]; + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(from(&accounts[0]).unwrap(), StakeStateV2::Uninitialized); + + // Test that withdrawal to account fails without authorized withdrawer + instruction_accounts[4].is_signer = false; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts, + Err(ProgramError::MissingRequiredSignature), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_authorize_override(mollusk: Mollusk) { + let authority_address = solana_sdk::pubkey::new_rand(); + let mallory_address = solana_sdk::pubkey::new_rand(); + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_lamports = 42; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Initialized(Meta::auto(&stake_address)), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (authority_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authority_address, + is_signer: false, + is_writable: false, + }, + ]; + + // Authorize a staker pubkey and move the withdrawer key into cold storage. + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Authorize( + authority_address, + StakeAuthorize::Staker, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // Attack! The stake key (a hot key) is stolen and used to authorize a new staker. + instruction_accounts[0].is_signer = false; + instruction_accounts[2].is_signer = true; + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Authorize( + mallory_address, + StakeAuthorize::Staker, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // Verify the original staker no longer has access. + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Authorize( + authority_address, + StakeAuthorize::Staker, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + + // Verify the withdrawer (pulled from cold storage) can save the day. + instruction_accounts[0].is_signer = true; + instruction_accounts[2].is_signer = false; + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Authorize( + authority_address, + StakeAuthorize::Withdrawer, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // Attack! Verify the staker cannot be used to authorize a withdraw. + instruction_accounts[0].is_signer = false; + instruction_accounts[2] = AccountMeta { + pubkey: mallory_address, + is_signer: true, + is_writable: false, + }; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Authorize( + authority_address, + StakeAuthorize::Withdrawer, + )) + .unwrap(), + transaction_accounts, + instruction_accounts, + Err(ProgramError::MissingRequiredSignature), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_authorize_with_seed(mollusk: Mollusk) { + let authority_base_address = solana_sdk::pubkey::new_rand(); + let authority_address = solana_sdk::pubkey::new_rand(); + let seed = "42"; + let stake_address = Pubkey::create_with_seed(&authority_base_address, seed, &id()).unwrap(); + let stake_lamports = 42; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Initialized(Meta::auto(&stake_address)), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (authority_base_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: authority_base_address, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + ]; + + // Wrong seed + process_instruction( + &mollusk, + &serialize(&StakeInstruction::AuthorizeWithSeed( + AuthorizeWithSeedArgs { + new_authorized_pubkey: authority_address, + stake_authorize: StakeAuthorize::Staker, + authority_seed: "".to_string(), + authority_owner: id(), + }, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + + // Wrong base + instruction_accounts[1].pubkey = authority_address; + let instruction_data = serialize(&StakeInstruction::AuthorizeWithSeed( + AuthorizeWithSeedArgs { + new_authorized_pubkey: authority_address, + stake_authorize: StakeAuthorize::Staker, + authority_seed: seed.to_string(), + authority_owner: id(), + }, + )) + .unwrap(); + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + instruction_accounts[1].pubkey = authority_base_address; + + // Set stake authority + let accounts = process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // Set withdraw authority + let instruction_data = serialize(&StakeInstruction::AuthorizeWithSeed( + AuthorizeWithSeedArgs { + new_authorized_pubkey: authority_address, + stake_authorize: StakeAuthorize::Withdrawer, + authority_seed: seed.to_string(), + authority_owner: id(), + }, + )) + .unwrap(); + let accounts = process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // No longer withdraw authority + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts, + instruction_accounts, + Err(ProgramError::MissingRequiredSignature), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_authorize_delegated_stake(mollusk: Mollusk) { + let authority_address = solana_sdk::pubkey::new_rand(); + let stake_address = solana_sdk::pubkey::new_rand(); + let minimum_delegation = crate::get_minimum_delegation(); + let stake_lamports = minimum_delegation; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Initialized(Meta::auto(&stake_address)), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let vote_address = solana_sdk::pubkey::new_rand(); + let vote_account = + vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100); + let vote_address_2 = solana_sdk::pubkey::new_rand(); + let mut vote_account_2 = + vote_state::create_account(&vote_address_2, &solana_sdk::pubkey::new_rand(), 0, 100); + vote_account_2 + .set_state(&VoteStateVersions::new_current(VoteState::default())) + .unwrap(); + #[allow(deprecated)] + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (vote_address, vote_account), + (vote_address_2, vote_account_2), + ( + authority_address, + AccountSharedData::new(42, 0, &system_program::id()), + ), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + #[allow(deprecated)] + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ]; + + // delegate stake + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // deactivate, so we can re-delegate + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // authorize + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Authorize( + authority_address, + StakeAuthorize::Staker, + )) + .unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authority_address, + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + assert_eq!( + authorized_from(&accounts[0]).unwrap().staker, + authority_address + ); + + // Random other account should fail + instruction_accounts[0].is_signer = false; + instruction_accounts[1].pubkey = vote_address_2; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + + // Authorized staker should succeed + instruction_accounts.push(AccountMeta { + pubkey: authority_address, + is_signer: true, + is_writable: false, + }); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts, + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + assert_eq!( + stake_from(&accounts[0]).unwrap().delegation.voter_pubkey, + vote_address_2, + ); + + // Test another staking action + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts, + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authority_address, + is_signer: true, + is_writable: false, + }, + ], + Ok(()), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_stake_delegate(mollusk: Mollusk) { + let mut vote_state = VoteState::default(); + for i in 0..1000 { + vote_state::process_slot_vote_unchecked(&mut vote_state, i); + } + let vote_state_credits = vote_state.credits(); + let vote_address = solana_sdk::pubkey::new_rand(); + let vote_address_2 = solana_sdk::pubkey::new_rand(); + let mut vote_account = + vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100); + let mut vote_account_2 = + vote_state::create_account(&vote_address_2, &solana_sdk::pubkey::new_rand(), 0, 100); + vote_account + .set_state(&VoteStateVersions::new_current(vote_state.clone())) + .unwrap(); + vote_account_2 + .set_state(&VoteStateVersions::new_current(vote_state)) + .unwrap(); + let minimum_delegation = crate::get_minimum_delegation(); + let stake_lamports = minimum_delegation; + let stake_address = solana_sdk::pubkey::new_rand(); + let mut stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Initialized(Meta { + authorized: Authorized { + staker: stake_address, + withdrawer: stake_address, + }, + ..Meta::default() + }), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let mut clock = Clock { + epoch: 1, + ..Clock::default() + }; + #[allow(deprecated)] + let mut transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (vote_address, vote_account), + (vote_address_2, vote_account_2.clone()), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + #[allow(deprecated)] + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ]; + + // should fail, unsigned stake account + instruction_accounts[0].is_signer = false; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + instruction_accounts[0].is_signer = true; + + // should pass + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + // verify that delegate() looks right, compare against hand-rolled + assert_eq!( + stake_from(&accounts[0]).unwrap(), + Stake { + delegation: Delegation { + voter_pubkey: vote_address, + stake: stake_lamports, + activation_epoch: clock.epoch, + deactivation_epoch: u64::MAX, + ..Delegation::default() + }, + credits_observed: vote_state_credits, + } + ); + + // verify that delegate fails as stake is active and not deactivating + clock.epoch += 1; + transaction_accounts[0] = (stake_address, accounts[0].clone()); + transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock)); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(StakeError::TooSoonToRedelegate.into()), + ); + + // deactivate + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + + // verify that delegate to a different vote account fails + // during deactivation + transaction_accounts[0] = (stake_address, accounts[0].clone()); + instruction_accounts[1].pubkey = vote_address_2; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(StakeError::TooSoonToRedelegate.into()), + ); + instruction_accounts[1].pubkey = vote_address; + + // verify that delegate succeeds to same vote account + // when stake is deactivating + let accounts_2 = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + // verify that deactivation has been cleared + let stake = stake_from(&accounts_2[0]).unwrap(); + assert_eq!(stake.delegation.deactivation_epoch, u64::MAX); + + // verify that delegate to a different vote account fails + // if stake is still active + transaction_accounts[0] = (stake_address, accounts_2[0].clone()); + instruction_accounts[1].pubkey = vote_address_2; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(StakeError::TooSoonToRedelegate.into()), + ); + + // without stake history, cool down is instantaneous + clock.epoch += 1; + transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock)); + + // verify that delegate can be called to new vote account, 2nd is redelegate + transaction_accounts[0] = (stake_address, accounts[0].clone()); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + instruction_accounts[1].pubkey = vote_address; + // verify that delegate() looks right, compare against hand-rolled + assert_eq!( + stake_from(&accounts[0]).unwrap(), + Stake { + delegation: Delegation { + voter_pubkey: vote_address_2, + stake: stake_lamports, + activation_epoch: clock.epoch, + deactivation_epoch: u64::MAX, + ..Delegation::default() + }, + credits_observed: vote_state_credits, + } + ); + + // signed but faked vote account + transaction_accounts[1] = (vote_address_2, vote_account_2); + transaction_accounts[1] + .1 + .set_owner(solana_sdk::pubkey::new_rand()); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::IncorrectProgramId), + ); + + // verify that non-stakes fail delegate() + let stake_state = StakeStateV2::RewardsPool; + stake_account.set_state(&stake_state).unwrap(); + transaction_accounts[0] = (stake_address, stake_account); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts, + instruction_accounts, + Err(ProgramError::IncorrectProgramId), + ); +} + +// XXX NOTE DIVIDE HERE +// below, i have everything up to the end of the file +// but working backwards is fucking annoying + #[test_case(mollusk_native(); "native_stake")] #[test_case(mollusk_bpf(); "bpf_stake")] fn test_merge(mollusk: Mollusk) { From 5170ef1a100399c31fb25fc4866ad940e6b47727 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 06:17:36 -0800 Subject: [PATCH 16/35] more tests --- program/tests/stake_instruction.rs | 1477 +++++++++++++++++++++++++++- 1 file changed, 1473 insertions(+), 4 deletions(-) diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs index f8a0324..14c431f 100644 --- a/program/tests/stake_instruction.rs +++ b/program/tests/stake_instruction.rs @@ -232,19 +232,19 @@ fn stake_from>(account: &T) -> Optio from(account).and_then(|state: StakeStateV2| state.stake()) } -pub fn delegation_from(account: &AccountSharedData) -> Option { +fn delegation_from(account: &AccountSharedData) -> Option { from(account).and_then(|state: StakeStateV2| state.delegation()) } -pub fn authorized_from(account: &AccountSharedData) -> Option { +fn authorized_from(account: &AccountSharedData) -> Option { from(account).and_then(|state: StakeStateV2| state.authorized()) } -pub fn lockup_from>(account: &T) -> Option { +fn lockup_from>(account: &T) -> Option { from(account).and_then(|state: StakeStateV2| state.lockup()) } -pub fn meta_from(account: &AccountSharedData) -> Option { +fn meta_from(account: &AccountSharedData) -> Option { from(account).and_then(|state: StakeStateV2| state.meta()) } @@ -262,6 +262,70 @@ fn just_stake(meta: Meta, stake: u64) -> StakeStateV2 { ) } +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, + stake_history, + None, + ); + active_stake += stake_status.effective; + } + } + active_stake +} + +fn new_stake_history_entry<'a, I>( + epoch: Epoch, + stakes: I, + history: &StakeHistory, + new_rate_activation_epoch: Option, +) -> StakeHistoryEntry +where + I: Iterator, +{ + stakes.fold(StakeHistoryEntry::default(), |sum, stake| { + sum + stake.stake_activating_and_deactivating(epoch, history, new_rate_activation_epoch) + }) +} + +fn create_stake_history_from_delegations( + bootstrap: Option, + epochs: std::ops::Range, + delegations: &[Delegation], + new_rate_activation_epoch: Option, +) -> StakeHistory { + let mut stake_history = StakeHistory::default(); + + let bootstrap_delegation = if let Some(bootstrap) = bootstrap { + vec![Delegation { + activation_epoch: u64::MAX, + stake: bootstrap, + ..Delegation::default() + }] + } else { + vec![] + }; + + for epoch in epochs { + let entry = new_stake_history_entry( + epoch, + delegations.iter().chain(bootstrap_delegation.iter()), + &stake_history, + new_rate_activation_epoch, + ); + stake_history.add(epoch, entry); + } + + stake_history +} + mod config { #[allow(deprecated)] use solana_sdk::stake::config::{self, *}; @@ -1636,6 +1700,1411 @@ fn test_stake_delegate(mollusk: Mollusk) { ); } +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_redelegate_consider_balance_changes(mollusk: Mollusk) { + let mut clock = Clock::default(); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let initial_lamports = 4242424242; + let stake_lamports = rent_exempt_reserve + initial_lamports; + let recipient_address = solana_sdk::pubkey::new_rand(); + let authority_address = solana_sdk::pubkey::new_rand(); + let vote_address = solana_sdk::pubkey::new_rand(); + let vote_account = + vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100); + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Initialized(Meta { + rent_exempt_reserve, + ..Meta::auto(&authority_address) + }), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + #[allow(deprecated)] + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (vote_address, vote_account), + ( + recipient_address, + AccountSharedData::new(1, 0, &system_program::id()), + ), + (authority_address, AccountSharedData::default()), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + #[allow(deprecated)] + let delegate_instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authority_address, + is_signer: true, + is_writable: false, + }, + ]; + let deactivate_instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authority_address, + is_signer: true, + is_writable: false, + }, + ]; + + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + delegate_instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + clock.epoch += 1; + transaction_accounts[2] = (clock::id(), create_account_shared_data_for_test(&clock)); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + deactivate_instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // Once deactivated, we withdraw stake to new account + clock.epoch += 1; + transaction_accounts[2] = (clock::id(), create_account_shared_data_for_test(&clock)); + let withdraw_lamports = initial_lamports / 2; + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(withdraw_lamports)).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authority_address, + is_signer: true, + is_writable: false, + }, + ], + Ok(()), + ); + let expected_balance = rent_exempt_reserve + initial_lamports - withdraw_lamports; + assert_eq!(accounts[0].lamports(), expected_balance); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + clock.epoch += 1; + transaction_accounts[2] = (clock::id(), create_account_shared_data_for_test(&clock)); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + delegate_instruction_accounts.clone(), + Ok(()), + ); + assert_eq!( + stake_from(&accounts[0]).unwrap().delegation.stake, + accounts[0].lamports() - rent_exempt_reserve, + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + clock.epoch += 1; + transaction_accounts[2] = (clock::id(), create_account_shared_data_for_test(&clock)); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + deactivate_instruction_accounts, + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // Out of band deposit + transaction_accounts[0] + .1 + .checked_add_lamports(withdraw_lamports) + .unwrap(); + + clock.epoch += 1; + transaction_accounts[2] = (clock::id(), create_account_shared_data_for_test(&clock)); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts, + delegate_instruction_accounts, + Ok(()), + ); + assert_eq!( + stake_from(&accounts[0]).unwrap().delegation.stake, + accounts[0].lamports() - rent_exempt_reserve, + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split(mollusk: Mollusk) { + 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(); + let stake_lamports = minimum_delegation * 2; + let split_to_address = solana_sdk::pubkey::new_rand(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let mut transaction_accounts = vec![ + (stake_address, AccountSharedData::default()), + (split_to_address, split_to_account.clone()), + ( + rent::id(), + create_account_shared_data_for_test(&Rent { + lamports_per_byte_year: 0, + ..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 { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: true, + }, + ]; + + for state in [ + StakeStateV2::Initialized(Meta::auto(&stake_address)), + just_stake(Meta::auto(&stake_address), stake_lamports), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeStateV2::size_of(), + &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 + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + + // should pass + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + // no lamport leakage + assert_eq!( + 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) + ); + + assert_eq!(from(&accounts[0]).unwrap(), from(&accounts[1]).unwrap()); + match state { + StakeStateV2::Initialized(_meta) => { + assert_eq!(from(&accounts[0]).unwrap(), state); + } + StakeStateV2::Stake(_meta, _stake, _) => { + let stake_0 = from(&accounts[0]).unwrap().stake(); + assert_eq!(stake_0.unwrap().delegation.stake, stake_lamports / 2); + } + _ => unreachable!(), + } + } + + // should fail, fake owner of destination + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &solana_sdk::pubkey::new_rand(), + ) + .unwrap(); + transaction_accounts[1] = (split_to_address, split_to_account); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts, + Err(if mollusk.is_bpf() { + ProgramError::InvalidAccountOwner + } else { + ProgramError::IncorrectProgramId + }), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_withdraw_stake(mollusk: Mollusk) { + let recipient_address = solana_sdk::pubkey::new_rand(); + let authority_address = solana_sdk::pubkey::new_rand(); + let custodian_address = solana_sdk::pubkey::new_rand(); + let stake_address = solana_sdk::pubkey::new_rand(); + let minimum_delegation = crate::get_minimum_delegation(); + let stake_lamports = minimum_delegation; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let vote_address = solana_sdk::pubkey::new_rand(); + let mut vote_account = + vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100); + vote_account + .set_state(&VoteStateVersions::new_current(VoteState::default())) + .unwrap(); + #[allow(deprecated)] + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (vote_address, vote_account), + (recipient_address, AccountSharedData::default()), + ( + authority_address, + AccountSharedData::new(42, 0, &system_program::id()), + ), + (custodian_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + rent::id(), + create_account_shared_data_for_test(&Rent::free()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ]; + + // should fail, no signer + instruction_accounts[4].is_signer = false; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + instruction_accounts[4].is_signer = true; + + // should pass, signed keyed account and uninitialized + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(accounts[0].lamports(), 0); + assert_eq!(from(&accounts[0]).unwrap(), StakeStateV2::Uninitialized); + + // initialize stake + let lockup = Lockup { + unix_timestamp: 0, + epoch: 0, + custodian: custodian_address, + }; + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Initialize( + Authorized::auto(&stake_address), + lockup, + )) + .unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: rent::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // should fail, signed keyed account and locked up, more than available + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + + // Stake some lamports (available lamports for withdrawals will reduce to zero) + #[allow(deprecated)] + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // simulate rewards + transaction_accounts[0].1.checked_add_lamports(10).unwrap(); + + // withdrawal before deactivate works for rewards amount + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(10)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // withdrawal of rewards fails if not in excess of stake + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(11)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + + // deactivate the stake before withdrawal + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // simulate time passing + let clock = Clock { + epoch: 100, + ..Clock::default() + }; + transaction_accounts[5] = (clock::id(), create_account_shared_data_for_test(&clock)); + + // Try to withdraw more than what's available + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(stake_lamports + 11)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + + // Try to withdraw all lamports + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(stake_lamports + 10)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(accounts[0].lamports(), 0); + assert_eq!(from(&accounts[0]).unwrap(), StakeStateV2::Uninitialized); + + // overflow + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_account = AccountSharedData::new_data_with_space( + 1_000_000_000, + &StakeStateV2::Initialized(Meta { + rent_exempt_reserve, + authorized: Authorized { + staker: authority_address, + withdrawer: authority_address, + }, + lockup: Lockup::default(), + }), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + transaction_accounts[0] = (stake_address, stake_account.clone()); + transaction_accounts[2] = (recipient_address, stake_account); + instruction_accounts[4].pubkey = authority_address; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(u64::MAX - 10)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + + // should fail, invalid state + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::RewardsPool, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + transaction_accounts[0] = (stake_address, stake_account); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts, + Err(ProgramError::InvalidAccountData), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_withdraw_stake_before_warmup(mollusk: Mollusk) { + let recipient_address = solana_sdk::pubkey::new_rand(); + let stake_address = solana_sdk::pubkey::new_rand(); + let minimum_delegation = crate::get_minimum_delegation(); + let stake_lamports = minimum_delegation; + let total_lamports = stake_lamports + 33; + let stake_account = AccountSharedData::new_data_with_space( + total_lamports, + &StakeStateV2::Initialized(Meta::auto(&stake_address)), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let vote_address = solana_sdk::pubkey::new_rand(); + let mut vote_account = + vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100); + vote_account + .set_state(&VoteStateVersions::new_current(VoteState::default())) + .unwrap(); + let mut clock = Clock { + epoch: 16, + ..Clock::default() + }; + #[allow(deprecated)] + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (vote_address, vote_account), + (recipient_address, AccountSharedData::default()), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ]; + + // Stake some lamports (available lamports for withdrawals will reduce to zero) + #[allow(deprecated)] + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // Try to withdraw stake + let stake_history = create_stake_history_from_delegations( + None, + 0..clock.epoch, + &[stake_from(&accounts[0]).unwrap().delegation], + None, + ); + transaction_accounts[4] = ( + stake_history::id(), + create_account_shared_data_for_test(&stake_history), + ); + clock.epoch = 0; + transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock)); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw( + total_lamports - stake_lamports + 1, + )) + .unwrap(), + transaction_accounts, + instruction_accounts, + Err(ProgramError::InsufficientFunds), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_withdraw_lockup(mollusk: Mollusk) { + let recipient_address = solana_sdk::pubkey::new_rand(); + let custodian_address = solana_sdk::pubkey::new_rand(); + let stake_address = solana_sdk::pubkey::new_rand(); + let total_lamports = 100; + let mut meta = Meta { + lockup: Lockup { + unix_timestamp: 0, + epoch: 1, + custodian: custodian_address, + }, + ..Meta::auto(&stake_address) + }; + let stake_account = AccountSharedData::new_data_with_space( + total_lamports, + &StakeStateV2::Initialized(meta), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let mut clock = Clock::default(); + let mut transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (recipient_address, AccountSharedData::default()), + (custodian_address, AccountSharedData::default()), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ]; + + // should fail, lockup is still in force + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(StakeError::LockupInForce.into()), + ); + + // should pass + instruction_accounts.push(AccountMeta { + pubkey: custodian_address, + is_signer: true, + is_writable: false, + }); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(from(&accounts[0]).unwrap(), StakeStateV2::Uninitialized); + + // should pass, custodian is the same as the withdraw authority + instruction_accounts[5].pubkey = stake_address; + meta.lockup.custodian = stake_address; + let stake_account_self_as_custodian = AccountSharedData::new_data_with_space( + total_lamports, + &StakeStateV2::Initialized(meta), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + transaction_accounts[0] = (stake_address, stake_account_self_as_custodian); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(from(&accounts[0]).unwrap(), StakeStateV2::Uninitialized); + transaction_accounts[0] = (stake_address, stake_account); + + // should pass, lockup has expired + instruction_accounts.pop(); + clock.epoch += 1; + transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock)); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(total_lamports)).unwrap(), + transaction_accounts, + instruction_accounts, + Ok(()), + ); + assert_eq!(from(&accounts[0]).unwrap(), StakeStateV2::Uninitialized); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_withdraw_rent_exempt(mollusk: Mollusk) { + let recipient_address = solana_sdk::pubkey::new_rand(); + let custodian_address = solana_sdk::pubkey::new_rand(); + let stake_address = solana_sdk::pubkey::new_rand(); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let minimum_delegation = crate::get_minimum_delegation(); + let stake_lamports = 7 * minimum_delegation; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports + rent_exempt_reserve, + &StakeStateV2::Initialized(Meta { + rent_exempt_reserve, + ..Meta::auto(&stake_address) + }), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (recipient_address, AccountSharedData::default()), + (custodian_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ]; + + // should pass, withdrawing initialized account down to minimum balance + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(stake_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // should fail, withdrawal that would leave less than rent-exempt reserve + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + + // should pass, withdrawal of complete account + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw( + stake_lamports + rent_exempt_reserve, + )) + .unwrap(), + transaction_accounts, + instruction_accounts, + Ok(()), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_deactivate(mollusk: Mollusk) { + let stake_address = solana_sdk::pubkey::new_rand(); + let minimum_delegation = crate::get_minimum_delegation(); + let stake_lamports = minimum_delegation; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Initialized(Meta::auto(&stake_address)), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let vote_address = solana_sdk::pubkey::new_rand(); + let mut vote_account = + vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100); + vote_account + .set_state(&VoteStateVersions::new_current(VoteState::default())) + .unwrap(); + #[allow(deprecated)] + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (vote_address, vote_account), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + ]; + + // should fail, not signed + instruction_accounts[0].is_signer = false; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InvalidAccountData), + ); + instruction_accounts[0].is_signer = true; + + // should fail, not staked yet + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InvalidAccountData), + ); + + // Staking + #[allow(deprecated)] + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // should pass + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // should fail, only works once + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts, + instruction_accounts, + Err(StakeError::AlreadyDeactivated.into()), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_set_lockup(mollusk: Mollusk) { + let custodian_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let stake_address = solana_sdk::pubkey::new_rand(); + let minimum_delegation = crate::get_minimum_delegation(); + let stake_lamports = minimum_delegation; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let vote_address = solana_sdk::pubkey::new_rand(); + let mut vote_account = + vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100); + vote_account + .set_state(&VoteStateVersions::new_current(VoteState::default())) + .unwrap(); + let instruction_data = serialize(&StakeInstruction::SetLockup(LockupArgs { + unix_timestamp: Some(1), + epoch: Some(1), + custodian: Some(custodian_address), + })) + .unwrap(); + #[allow(deprecated)] + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (vote_address, vote_account), + (authorized_address, AccountSharedData::default()), + (custodian_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + rent::id(), + create_account_shared_data_for_test(&Rent::free()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: custodian_address, + is_signer: true, + is_writable: false, + }, + ]; + + // should fail, wrong state + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InvalidAccountData), + ); + + // initialize stake + let lockup = Lockup { + unix_timestamp: 1, + epoch: 1, + custodian: custodian_address, + }; + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Initialize( + Authorized::auto(&stake_address), + lockup, + )) + .unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: rent::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // should fail, not signed + instruction_accounts[2].is_signer = false; + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + instruction_accounts[2].is_signer = true; + + // should pass + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // Staking + #[allow(deprecated)] + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // should fail, not signed + instruction_accounts[2].is_signer = false; + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + instruction_accounts[2].is_signer = true; + + // should pass + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // Lockup in force + let instruction_data = serialize(&StakeInstruction::SetLockup(LockupArgs { + unix_timestamp: Some(2), + epoch: None, + custodian: None, + })) + .unwrap(); + + // should fail, authorized withdrawer cannot change it + instruction_accounts[0].is_signer = true; + instruction_accounts[2].is_signer = false; + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + instruction_accounts[0].is_signer = false; + instruction_accounts[2].is_signer = true; + + // should pass, custodian can change it + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // Lockup expired + let clock = Clock { + unix_timestamp: i64::MAX, + epoch: Epoch::MAX, + ..Clock::default() + }; + transaction_accounts[4] = (clock::id(), create_account_shared_data_for_test(&clock)); + + // should fail, custodian cannot change it + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + + // should pass, authorized withdrawer can change it + instruction_accounts[0].is_signer = true; + instruction_accounts[2].is_signer = false; + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // Change authorized withdrawer + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Authorize( + authorized_address, + StakeAuthorize::Withdrawer, + )) + .unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + // should fail, previous authorized withdrawer cannot change the lockup anymore + process_instruction( + &mollusk, + &instruction_data, + transaction_accounts, + instruction_accounts, + Err(ProgramError::MissingRequiredSignature), + ); +} + // XXX NOTE DIVIDE HERE // below, i have everything up to the end of the file // but working backwards is fucking annoying From 0b174dc11d9654d23af394fabb14c3aa3f30e4f0 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 10:09:41 -0800 Subject: [PATCH 17/35] minidel test --- program/tests/stake_instruction.rs | 34 +++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs index 14c431f..4f4bd48 100644 --- a/program/tests/stake_instruction.rs +++ b/program/tests/stake_instruction.rs @@ -51,6 +51,7 @@ use { SysvarId, }, transaction::{Transaction, TransactionError}, + transaction_context::TransactionReturnData, }, solana_stake_program::{get_minimum_delegation, id, processor::Processor}, solana_vote_program::{ @@ -3848,7 +3849,38 @@ fn test_merge_active_stake(mollusk: Mollusk) { try_merge(&mollusk, transaction_accounts, instruction_accounts, Ok(())); } -// XXX SKIP test_stake_get_minimum_delegation +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_stake_get_minimum_delegation(mollusk: Mollusk) { + let stake_address = Pubkey::new_unique(); + let stake_account = create_default_stake_account(); + let minimum_delegation = crate::get_minimum_delegation(); + let instruction_data = serialize(&StakeInstruction::GetMinimumDelegation).unwrap(); + let transaction_accounts = vec![(stake_address, stake_account)]; + let instruction_accounts = vec![AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }]; + + let instruction = Instruction { + program_id: id(), + accounts: instruction_accounts, + data: instruction_data, + }; + + mollusk.process_and_validate_instruction( + &instruction, + &transaction_accounts, + &[ + Check::success(), + Check::return_data(TransactionReturnData { + program_id: id(), + data: minimum_delegation.to_le_bytes().to_vec(), + }), + ], + ); +} // Ensure that the correct errors are returned when processing instructions // From 1228e8e72aede9162651eab332587e68ee1638d1 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:28:20 -0800 Subject: [PATCH 18/35] more tests --- Cargo.lock | 1 + program/Cargo.toml | 1 + program/tests/stake_instruction.rs | 2616 ++++++++++++++++++++++++++-- 3 files changed, 2467 insertions(+), 151 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16d0cec..c01f060 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5106,6 +5106,7 @@ name = "solana-stake-program" version = "1.0.0" dependencies = [ "arrayref", + "assert_matches", "bincode", "borsh 1.5.3", "mollusk-svm", diff --git a/program/Cargo.toml b/program/Cargo.toml index deba94b..43870e6 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -31,6 +31,7 @@ solana-vote-program = "=2.1.0" solana-sdk = "=2.1.0" solana-feature-set = "=2.1.0" test-case = "3.3.1" +assert_matches = "1.5.0" [lib] crate-type = ["cdylib", "lib"] diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs index 4f4bd48..c15b725 100644 --- a/program/tests/stake_instruction.rs +++ b/program/tests/stake_instruction.rs @@ -3,6 +3,7 @@ #![allow(clippy::arithmetic_side_effects)] use { + assert_matches::assert_matches, bincode::serialize, mollusk_svm::{result::Check, Mollusk}, solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, @@ -205,7 +206,7 @@ fn process_instruction_as_one_arg( ) -> Vec { let transaction_accounts = get_default_transaction_accounts(instruction); process_instruction( - &mollusk, + mollusk, &instruction.data, transaction_accounts, instruction.accounts.clone(), @@ -3106,29 +3107,90 @@ fn test_set_lockup(mollusk: Mollusk) { ); } -// XXX NOTE DIVIDE HERE -// below, i have everything up to the end of the file -// but working backwards is fucking annoying - +/// Ensure that `initialize()` respects the minimum balance requirements +/// - Assert 1: accounts with a balance equal-to the rent exemption initialize OK +/// - Assert 2: accounts with a balance less-than the rent exemption do not initialize #[test_case(mollusk_native(); "native_stake")] #[test_case(mollusk_bpf(); "bpf_stake")] -fn test_merge(mollusk: Mollusk) { +fn test_initialize_minimum_balance(mollusk: Mollusk) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); let stake_address = solana_sdk::pubkey::new_rand(); - let merge_from_address = solana_sdk::pubkey::new_rand(); - let authorized_address = solana_sdk::pubkey::new_rand(); - let meta = Meta::auto(&authorized_address); - let stake_lamports = 42; - let mut instruction_accounts = vec![ + let instruction_data = serialize(&StakeInstruction::Initialize( + Authorized::auto(&stake_address), + Lockup::default(), + )) + .unwrap(); + let instruction_accounts = vec![ AccountMeta { pubkey: stake_address, is_signer: false, is_writable: true, }, AccountMeta { - pubkey: merge_from_address, + pubkey: rent::id(), is_signer: false, + is_writable: false, + }, + ]; + for (lamports, expected_result) in [ + (rent_exempt_reserve, Ok(())), + ( + rent_exempt_reserve - 1, + Err(ProgramError::InsufficientFunds), + ), + ] { + let stake_account = AccountSharedData::new(lamports, StakeStateV2::size_of(), &id()); + process_instruction( + &mollusk, + &instruction_data, + vec![ + (stake_address, stake_account), + (rent::id(), create_account_shared_data_for_test(&rent)), + ], + instruction_accounts.clone(), + expected_result, + ); + } +} + +/// Ensure that `delegate()` respects the minimum delegation requirements +/// - Assert 1: delegating an amount equal-to the minimum succeeds +/// - Assert 2: delegating an amount less-than the minimum fails +/// Also test both asserts above over both StakeStateV2::{Initialized and Stake}, since the logic +/// is slightly different for the variants. +/// +/// NOTE: Even though new stake accounts must have a minimum balance that is at least +/// the minimum delegation (plus rent exempt reserve), the old behavior allowed +/// withdrawing below the minimum delegation, then re-delegating successfully (see +/// `test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation()` for +/// more information.) +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_delegate_minimum_stake_delegation(mollusk: Mollusk) { + let minimum_delegation = crate::get_minimum_delegation(); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&stake_address) + }; + let vote_address = solana_sdk::pubkey::new_rand(); + let vote_account = + vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100); + #[allow(deprecated)] + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, is_writable: true, }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, AccountMeta { pubkey: clock::id(), is_signer: false, @@ -3140,182 +3202,2434 @@ fn test_merge(mollusk: Mollusk) { is_writable: false, }, AccountMeta { - pubkey: authorized_address, - is_signer: true, + pubkey: stake_config::id(), + is_signer: false, is_writable: false, }, ]; - - for state in &[ - StakeStateV2::Initialized(meta), - just_stake(meta, stake_lamports), + for (stake_delegation, expected_result) in &[ + (minimum_delegation, Ok(())), + ( + minimum_delegation - 1, + Err(StakeError::InsufficientDelegation), + ), ] { - let stake_account = AccountSharedData::new_data_with_space( - stake_lamports, - state, - StakeStateV2::size_of(), - &id(), - ) - .unwrap(); - for merge_from_state in &[ + for stake_state in &[ StakeStateV2::Initialized(meta), - just_stake(meta, stake_lamports), + just_stake(meta, *stake_delegation), ] { - let merge_from_account = AccountSharedData::new_data_with_space( - stake_lamports, - merge_from_state, + let stake_account = AccountSharedData::new_data_with_space( + stake_delegation + rent_exempt_reserve, + stake_state, StakeStateV2::size_of(), &id(), ) .unwrap(); - let transaction_accounts = vec![ - (stake_address, stake_account.clone()), - (merge_from_address, merge_from_account), - (authorized_address, AccountSharedData::default()), - ( - clock::id(), - create_account_shared_data_for_test(&Clock::default()), - ), - ( - stake_history::id(), - create_account_shared_data_for_test(&StakeHistory::default()), - ), - ( - epoch_schedule::id(), - create_account_shared_data_for_test(&EpochSchedule::default()), - ), - ]; - - // Authorized staker signature required... - instruction_accounts[4].is_signer = false; + #[allow(deprecated)] process_instruction( &mollusk, - &serialize(&StakeInstruction::Merge).unwrap(), - transaction_accounts.clone(), - instruction_accounts.clone(), - Err(ProgramError::MissingRequiredSignature), - ); - instruction_accounts[4].is_signer = true; - - let accounts = process_instruction( - &mollusk, - &serialize(&StakeInstruction::Merge).unwrap(), - transaction_accounts, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + vec![ + (stake_address, stake_account), + (vote_address, vote_account.clone()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ], instruction_accounts.clone(), - Ok(()), + expected_result.clone().map_err(|e| e.into()), ); - - // check lamports - assert_eq!(accounts[0].lamports(), stake_lamports * 2); - assert_eq!(accounts[1].lamports(), 0); - - // check state - match state { - StakeStateV2::Initialized(meta) => { - assert_eq!(accounts[0].state(), Ok(StakeStateV2::Initialized(*meta)),); - } - StakeStateV2::Stake(meta, stake, stake_flags) => { - let expected_stake = stake.delegation.stake - + merge_from_state - .stake() - .map(|stake| stake.delegation.stake) - .unwrap_or_else(|| { - stake_lamports - - merge_from_state.meta().unwrap().rent_exempt_reserve - }); - assert_eq!( - accounts[0].state(), - Ok(StakeStateV2::Stake( - *meta, - Stake { - delegation: Delegation { - stake: expected_stake, - ..stake.delegation - }, - ..*stake - }, - *stake_flags, - )), - ); - } - _ => unreachable!(), - } - assert_eq!(accounts[1].state(), Ok(StakeStateV2::Uninitialized)); } } } +/// Ensure that `split()` respects the minimum delegation requirements. This applies to +/// both the source and destination acounts. Thus, we have four permutations possible based on +/// if each account's post-split delegation is equal-to (EQ) or less-than (LT) the minimum: +/// +/// source | dest | result +/// --------+------+-------- +/// EQ | EQ | Ok +/// EQ | LT | Err +/// LT | EQ | Err +/// LT | LT | Err #[test_case(mollusk_native(); "native_stake")] #[test_case(mollusk_bpf(); "bpf_stake")] -fn test_merge_self_fails(mollusk: Mollusk) { - let stake_address = solana_sdk::pubkey::new_rand(); - let authorized_address = solana_sdk::pubkey::new_rand(); +fn test_split_minimum_stake_delegation(mollusk: Mollusk) { + let minimum_delegation = crate::get_minimum_delegation(); let rent = Rent::default(); let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); - let stake_amount = 4242424242; - let stake_lamports = rent_exempt_reserve + stake_amount; - let meta = Meta { - rent_exempt_reserve, - ..Meta::auto(&authorized_address) + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let clock = Clock { + epoch: current_epoch, + ..Clock::default() }; - let stake = Stake { - delegation: Delegation { - stake: stake_amount, - activation_epoch: 0, - ..Delegation::default() - }, - ..Stake::default() + let source_address = Pubkey::new_unique(); + let source_meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&source_address) }; - let stake_account = AccountSharedData::new_data_with_space( - stake_lamports, - &StakeStateV2::Stake(meta, stake, StakeFlags::empty()), + let dest_address = Pubkey::new_unique(); + let dest_account = AccountSharedData::new_data_with_space( + rent_exempt_reserve, + &StakeStateV2::Uninitialized, StakeStateV2::size_of(), &id(), ) .unwrap(); - let transaction_accounts = vec![ - (stake_address, stake_account), - (authorized_address, AccountSharedData::default()), - ( - clock::id(), - create_account_shared_data_for_test(&Clock::default()), - ), - ( - stake_history::id(), - create_account_shared_data_for_test(&StakeHistory::default()), - ), - ]; let instruction_accounts = vec![ AccountMeta { - pubkey: stake_address, - is_signer: false, + pubkey: source_address, + is_signer: true, is_writable: true, }, AccountMeta { - pubkey: stake_address, + pubkey: dest_address, is_signer: false, is_writable: true, }, - AccountMeta { - pubkey: clock::id(), - is_signer: false, - is_writable: false, - }, - AccountMeta { - pubkey: stake_history::id(), - is_signer: false, - is_writable: false, - }, - AccountMeta { - pubkey: authorized_address, - is_signer: true, - is_writable: false, - }, ]; + for (source_delegation, split_amount, expected_result) in [ + (minimum_delegation * 2, minimum_delegation, Ok(())), + ( + minimum_delegation * 2, + minimum_delegation - 1, + Err(ProgramError::InsufficientFunds), + ), + ( + (minimum_delegation * 2) - 1, + minimum_delegation, + Err(ProgramError::InsufficientFunds), + ), + ( + (minimum_delegation - 1) * 2, + minimum_delegation - 1, + Err(ProgramError::InsufficientFunds), + ), + ] { + 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( + &mollusk, + &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) + ); + } +} - process_instruction( - &mollusk, - &serialize(&StakeInstruction::Merge).unwrap(), +/// Ensure that splitting the full amount from an account respects the minimum delegation +/// requirements. This ensures that we are future-proofing/testing any raises to the minimum +/// delegation. +/// - Assert 1: splitting the full amount from an account that has at least the minimum +/// delegation is OK +/// - Assert 2: splitting the full amount from an account that has less than the minimum +/// delegation is not OK +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split_full_amount_minimum_stake_delegation(mollusk: Mollusk) { + let minimum_delegation = crate::get_minimum_delegation(); + 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 source_address = Pubkey::new_unique(); + let source_meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&source_address) + }; + let dest_address = Pubkey::new_unique(); + let dest_account = AccountSharedData::new_data_with_space( + 0, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: source_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: dest_address, + is_signer: false, + is_writable: true, + }, + ]; + for (reserve, expected_result) in [ + (rent_exempt_reserve, Ok(())), + ( + rent_exempt_reserve - 1, + Err(ProgramError::InsufficientFunds), + ), + ] { + for (stake_delegation, source_stake_state) in &[ + (0, StakeStateV2::Initialized(source_meta)), + ( + minimum_delegation, + just_stake(source_meta, minimum_delegation), + ), + ] { + let source_account = AccountSharedData::new_data_with_space( + stake_delegation + reserve, + source_stake_state, + 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( + &mollusk, + &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) + ); + } + } +} + +/// Ensure that `split()` correctly handles prefunded destination accounts from +/// initialized stakes. When a destination account already has funds, ensure +/// the minimum split amount reduces accordingly. +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_initialized_split_destination_minimum_balance(mollusk: Mollusk) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let source_address = Pubkey::new_unique(); + let destination_address = Pubkey::new_unique(); + 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 (destination_starting_balance, split_amount, expected_result) in [ + // split amount must be non zero + (rent_exempt_reserve, 0, Err(ProgramError::InsufficientFunds)), + // any split amount is OK when destination account is already fully funded + (rent_exempt_reserve, 1, Ok(())), + // if destination is only short by 1 lamport, then split amount can be 1 lamport + (rent_exempt_reserve - 1, 1, Ok(())), + // destination short by 2 lamports, then 1 isn't enough (non-zero split amount) + ( + rent_exempt_reserve - 2, + 1, + Err(ProgramError::InsufficientFunds), + ), + // destination has smallest non-zero balance, so can split the minimum balance + // requirements minus what destination already has + (1, rent_exempt_reserve - 1, Ok(())), + // destination has smallest non-zero balance, but cannot split less than the minimum + // balance requirements minus what destination already has + ( + 1, + rent_exempt_reserve - 2, + Err(ProgramError::InsufficientFunds), + ), + // destination has zero lamports, so split must be at least rent exempt reserve + (0, rent_exempt_reserve, Ok(())), + // destination has zero lamports, but split amount is less than rent exempt reserve + ( + 0, + rent_exempt_reserve - 1, + Err(ProgramError::InsufficientFunds), + ), + ] { + // Set the source's starting balance to something large to ensure its post-split + // balance meets all the requirements + let source_balance = rent_exempt_reserve + split_amount; + let source_meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&source_address) + }; + let source_account = AccountSharedData::new_data_with_space( + source_balance, + &StakeStateV2::Initialized(source_meta), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let destination_account = AccountSharedData::new_data_with_space( + destination_starting_balance, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(split_amount)).unwrap(), + vec![ + (source_address, source_account), + (destination_address, destination_account), + (rent::id(), create_account_shared_data_for_test(&rent)), + ], + instruction_accounts.clone(), + expected_result.clone(), + ); + } +} + +/// Ensure that `split()` correctly handles prefunded destination accounts from staked stakes. +/// When a destination account already has funds, ensure the minimum split amount reduces +/// accordingly. +#[test_case(mollusk_native(), &[Ok(()), Ok(())]; "native_stake")] +#[test_case(mollusk_bpf(), &[Ok(()), Ok(())]; "bpf_stake")] +// NOTE it is not presently possible to test 1sol minimum delegation +// #[test_case(feature_set_all_enabled(), &[Err(StakeError::InsufficientDelegation.into()), Err(StakeError::InsufficientDelegation.into())]; "all_enabled")] +fn test_staked_split_destination_minimum_balance( + mollusk: Mollusk, + expected_results: &[Result<(), ProgramError>], +) { + let minimum_delegation = crate::get_minimum_delegation(); + 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 source_address = Pubkey::new_unique(); + let destination_address = Pubkey::new_unique(); + 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 (destination_starting_balance, split_amount, expected_result) in [ + // split amount must be non zero + ( + rent_exempt_reserve + minimum_delegation, + 0, + Err(ProgramError::InsufficientFunds), + ), + // destination is fully funded: + // - old behavior: any split amount is OK + // - new behavior: split amount must be at least the minimum delegation + ( + rent_exempt_reserve + minimum_delegation, + 1, + expected_results[0].clone(), + ), + // if destination is only short by 1 lamport, then... + // - old behavior: split amount can be 1 lamport + // - new behavior: split amount must be at least the minimum delegation + ( + rent_exempt_reserve + minimum_delegation - 1, + 1, + expected_results[1].clone(), + ), + // destination short by 2 lamports, so 1 isn't enough (non-zero split amount) + ( + rent_exempt_reserve + minimum_delegation - 2, + 1, + Err(ProgramError::InsufficientFunds), + ), + // destination is rent exempt, so split enough for minimum delegation + (rent_exempt_reserve, minimum_delegation, Ok(())), + // destination is rent exempt, but split amount less than minimum delegation + ( + rent_exempt_reserve, + minimum_delegation.saturating_sub(1), // when minimum is 0, this blows up! + Err(ProgramError::InsufficientFunds), + ), + // 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(ProgramError::InsufficientFunds), + ), + // destination is not rent exempt, but split amount only for minimum delegation + ( + rent_exempt_reserve - 1, + minimum_delegation, + Err(ProgramError::InsufficientFunds), + ), + // 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(ProgramError::InsufficientFunds), + ), + // destination has smallest non-zero balance, but cannot split less than the minimum + // balance requirements minus what destination already has + ( + 1, + rent_exempt_reserve + minimum_delegation - 2, + Err(ProgramError::InsufficientFunds), + ), + // 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(ProgramError::InsufficientFunds), + ), + // destination has zero lamports, but split amount is less than rent exempt reserve + // plus minimum delegation + ( + 0, + rent_exempt_reserve + minimum_delegation - 1, + Err(ProgramError::InsufficientFunds), + ), + ] { + // Set the source's starting balance to something large to ensure its post-split + // balance meets all the requirements + let source_balance = rent_exempt_reserve + minimum_delegation + split_amount; + let source_meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&source_address) + }; + let source_stake_delegation = source_balance - rent_exempt_reserve; + let source_account = AccountSharedData::new_data_with_space( + source_balance, + &just_stake(source_meta, source_stake_delegation), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let destination_account = AccountSharedData::new_data_with_space( + destination_starting_balance, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let expected_active_stake = get_active_stake_for_tests( + &[source_account.clone(), destination_account.clone()], + &clock, + &stake_history, + ); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(split_amount)).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()), + ), + ], + instruction_accounts.clone(), + expected_result.clone(), + ); + 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, + // 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 + // make the destination rent exempt, and then the leftover lamports are delegated. + if expected_result.is_ok() { + assert_matches!(accounts[0].state().unwrap(), StakeStateV2::Stake(_, _, _)); + if let StakeStateV2::Stake(_, destination_stake, _) = accounts[1].state().unwrap() { + let destination_initial_rent_deficit = + rent_exempt_reserve.saturating_sub(destination_starting_balance); + let expected_destination_stake_delegation = + split_amount - destination_initial_rent_deficit; + assert_eq!( + expected_destination_stake_delegation, + destination_stake.delegation.stake + ); + assert!(destination_stake.delegation.stake >= minimum_delegation,); + } else { + panic!("destination state must be StakeStake::Stake after successful split when source is also StakeStateV2::Stake!"); + } + } + } +} + +/// Ensure that `withdraw()` respects the minimum delegation requirements +/// - Assert 1: withdrawing so remaining stake is equal-to the minimum is OK +/// - Assert 2: withdrawing so remaining stake is less-than the minimum is not OK +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_withdraw_minimum_stake_delegation(mollusk: Mollusk) { + let minimum_delegation = crate::get_minimum_delegation(); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&stake_address) + }; + let recipient_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ]; + let starting_stake_delegation = minimum_delegation; + for (ending_stake_delegation, expected_result) in [ + (minimum_delegation, Ok(())), + (minimum_delegation - 1, Err(ProgramError::InsufficientFunds)), + ] { + for (stake_delegation, stake_state) in &[ + (0, StakeStateV2::Initialized(meta)), + (minimum_delegation, just_stake(meta, minimum_delegation)), + ] { + let rewards_balance = 123; + let stake_account = AccountSharedData::new_data_with_space( + stake_delegation + rent_exempt_reserve + rewards_balance, + stake_state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let withdraw_amount = + (starting_stake_delegation + rewards_balance) - ending_stake_delegation; + #[allow(deprecated)] + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(withdraw_amount)).unwrap(), + vec![ + (stake_address, stake_account), + ( + recipient_address, + AccountSharedData::new(rent_exempt_reserve, 0, &system_program::id()), + ), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + rent::id(), + create_account_shared_data_for_test(&Rent::free()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ], + instruction_accounts.clone(), + expected_result.clone(), + ); + } + } +} + +/// The stake program's old behavior allowed delegations below the minimum stake delegation +/// (see also `test_delegate_minimum_stake_delegation()`). This was not the desired behavior, +/// and has been fixed in the new behavior. This test ensures the behavior is not changed +/// inadvertently. +/// +/// This test: +/// 1. Initialises a stake account (with sufficient balance for both rent and minimum delegation) +/// 2. Delegates the minimum amount +/// 3. Deactives the delegation +/// 4. Withdraws from the account such that the ending balance is *below* rent + minimum delegation +/// 5. Re-delegates, now with less than the minimum delegation, but it still succeeds +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegation( + mollusk: Mollusk, +) { + let minimum_delegation = crate::get_minimum_delegation(); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new( + rent_exempt_reserve + minimum_delegation, + StakeStateV2::size_of(), + &id(), + ); + let vote_address = solana_sdk::pubkey::new_rand(); + let vote_account = + vote_state::create_account(&vote_address, &solana_sdk::pubkey::new_rand(), 0, 100); + let recipient_address = solana_sdk::pubkey::new_rand(); + let mut clock = Clock::default(); + #[allow(deprecated)] + let mut transaction_accounts = vec![ + (stake_address, stake_account), + (vote_address, vote_account), + ( + recipient_address, + AccountSharedData::new(rent_exempt_reserve, 0, &system_program::id()), + ), + (clock::id(), create_account_shared_data_for_test(&clock)), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + stake_config::id(), + config::create_account(0, &stake_config::Config::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + (rent::id(), create_account_shared_data_for_test(&rent)), + ]; + #[allow(deprecated)] + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_config::id(), + is_signer: false, + is_writable: false, + }, + ]; + + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Initialize( + Authorized::auto(&stake_address), + Lockup::default(), + )) + .unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: rent::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + transaction_accounts[1] = (vote_address, accounts[1].clone()); + + clock.epoch += 1; + transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock)); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + clock.epoch += 1; + transaction_accounts[3] = (clock::id(), create_account_shared_data_for_test(&clock)); + let withdraw_amount = accounts[0].lamports() - (rent_exempt_reserve + minimum_delegation - 1); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(withdraw_amount)).unwrap(), + transaction_accounts.clone(), + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: recipient_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ], + Ok(()), + ); + transaction_accounts[0] = (stake_address, accounts[0].clone()); + + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + transaction_accounts, + instruction_accounts, + Err(StakeError::InsufficientDelegation.into()), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split_source_uninitialized(mollusk: Mollusk) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let minimum_delegation = crate::get_minimum_delegation(); + let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (split_to_address, split_to_account), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + ]; + + // splitting an uninitialized account where the destination is the same as the source + { + // splitting should work when... + // - when split amount is the full balance + // - when split amount is zero + // - when split amount is non-zero and less than the full balance + // + // and splitting should fail when the split amount is greater than the balance + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(0)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + } + + // this should work + instruction_accounts[1].pubkey = split_to_address; + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + assert_eq!(accounts[0].lamports(), accounts[1].lamports()); + + // no signers should fail + instruction_accounts[0].is_signer = false; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts, + Err(ProgramError::MissingRequiredSignature), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split_split_not_uninitialized(mollusk: Mollusk) { + let stake_lamports = 42; + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &just_stake(Meta::auto(&stake_address), stake_lamports), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + ]; + + for split_to_state in &[ + StakeStateV2::Initialized(Meta::default()), + StakeStateV2::Stake(Meta::default(), Stake::default(), StakeFlags::default()), + StakeStateV2::RewardsPool, + ] { + let split_to_account = AccountSharedData::new_data_with_space( + 0, + split_to_state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + vec![ + (stake_address, stake_account.clone()), + (split_to_address, split_to_account), + ], + instruction_accounts.clone(), + Err(ProgramError::InvalidAccountData), + ); + } +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split_more_than_staked(mollusk: Mollusk) { + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_history = StakeHistory::default(); + let current_epoch = 100; + let minimum_delegation = crate::get_minimum_delegation(); + let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &just_stake( + Meta { + rent_exempt_reserve, + ..Meta::auto(&stake_address) + }, + stake_lamports / 2 - 1, + ), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let split_to_account = AccountSharedData::new_data_with_space( + rent_exempt_reserve, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + 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 instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: true, + }, + ]; + + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts, + Err(StakeError::InsufficientDelegation.into()), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split_with_rent(mollusk: Mollusk) { + 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(); + let stake_address = solana_sdk::pubkey::new_rand(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: true, + }, + ]; + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + ..Meta::default() + }; + + // test splitting both an Initialized stake and a Staked stake + for (minimum_balance, state) in &[ + (rent_exempt_reserve, StakeStateV2::Initialized(meta)), + ( + rent_exempt_reserve + minimum_delegation, + just_stake(meta, minimum_delegation * 2 + rent_exempt_reserve), + ), + ] { + let stake_lamports = minimum_balance * 2; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + state, + StakeStateV2::size_of(), + &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 + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(minimum_balance - 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + + // doesn't leave enough for initial stake to be non-zero + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split( + stake_lamports - minimum_balance + 1, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + + // split account already has enough lamports + transaction_accounts[1].1.set_lamports(*minimum_balance); + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports - minimum_balance)).unwrap(), + transaction_accounts, + 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 StakeStateV2::Stake(meta, stake, stake_flags) = state { + assert_eq!( + accounts[1].state(), + Ok(StakeStateV2::Stake( + *meta, + Stake { + delegation: Delegation { + stake: stake_lamports - minimum_balance, + ..stake.delegation + }, + ..*stake + }, + *stake_flags, + )) + ); + assert_eq!(accounts[0].lamports(), *minimum_balance,); + assert_eq!(accounts[1].lamports(), stake_lamports,); + } + } +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split_to_account_with_rent_exempt_reserve(mollusk: Mollusk) { + 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(); + let stake_lamports = (rent_exempt_reserve + minimum_delegation) * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + ..Meta::default() + }; + let state = just_stake(meta, stake_lamports - rent_exempt_reserve); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: true, + }, + ]; + + let transaction_accounts = |initial_balance: u64| -> Vec<(Pubkey, AccountSharedData)> { + let split_to_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + 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( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + // split to insufficiently funded dest fails + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Err(ProgramError::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( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + + // should work + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + // no lamport leakage + assert_eq!( + 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 StakeStateV2::Stake(meta, stake, stake_flags) = state { + let expected_stake = + stake_lamports / 2 - (rent_exempt_reserve.saturating_sub(initial_balance)); + assert_eq!( + Ok(StakeStateV2::Stake( + meta, + Stake { + delegation: Delegation { + stake: stake_lamports / 2 + - (rent_exempt_reserve.saturating_sub(initial_balance)), + ..stake.delegation + }, + ..stake + }, + stake_flags + )), + accounts[1].state(), + ); + assert_eq!( + accounts[1].lamports(), + expected_stake + + rent_exempt_reserve + + initial_balance.saturating_sub(rent_exempt_reserve), + ); + assert_eq!( + Ok(StakeStateV2::Stake( + meta, + Stake { + delegation: Delegation { + stake: stake_lamports / 2 - rent_exempt_reserve, + ..stake.delegation + }, + ..stake + }, + stake_flags, + )), + accounts[0].state(), + ); + } + } +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split_from_larger_sized_account(mollusk: Mollusk) { + let rent = Rent::default(); + 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() + }; + let minimum_delegation = crate::get_minimum_delegation(); + let stake_lamports = (source_larger_rent_exempt_reserve + minimum_delegation) * 2; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: source_larger_rent_exempt_reserve, + ..Meta::default() + }; + let state = just_stake(meta, stake_lamports - source_larger_rent_exempt_reserve); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeStateV2::size_of() + 100, + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: true, + }, + ]; + + let transaction_accounts = |initial_balance: u64| -> Vec<(Pubkey, AccountSharedData)> { + let split_to_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + 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( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts(initial_balance), + instruction_accounts.clone(), + Err(ProgramError::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( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports + 1)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InsufficientFunds), + ); + + // should work + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports / 2)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + // no lamport leakage + assert_eq!( + 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 StakeStateV2::Stake(meta, stake, stake_flags) = state { + let expected_split_meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: split_rent_exempt_reserve, + ..Meta::default() + }; + let expected_stake = + stake_lamports / 2 - (split_rent_exempt_reserve.saturating_sub(initial_balance)); + + assert_eq!( + Ok(StakeStateV2::Stake( + expected_split_meta, + Stake { + delegation: Delegation { + stake: expected_stake, + ..stake.delegation + }, + ..stake + }, + stake_flags, + )), + accounts[1].state() + ); + assert_eq!( + accounts[1].lamports(), + expected_stake + + split_rent_exempt_reserve + + initial_balance.saturating_sub(split_rent_exempt_reserve) + ); + assert_eq!( + Ok(StakeStateV2::Stake( + meta, + Stake { + delegation: Delegation { + stake: stake_lamports / 2 - source_larger_rent_exempt_reserve, + ..stake.delegation + }, + ..stake + }, + stake_flags, + )), + accounts[0].state() + ); + } + } +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split_from_smaller_sized_account(mollusk: Mollusk) { + let rent = Rent::default(); + 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; + let stake_lamports = split_rent_exempt_reserve + 1; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: source_smaller_rent_exempt_reserve, + ..Meta::default() + }; + let state = just_stake(meta, stake_lamports - source_smaller_rent_exempt_reserve); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: true, + }, + ]; + + let split_amount = stake_lamports - (source_smaller_rent_exempt_reserve + 1); // Enough so that split stake is > 0 + let split_lamport_balances = vec![ + 0, + 1, + split_rent_exempt_reserve, + split_rent_exempt_reserve + 1, + ]; + for initial_balance in split_lamport_balances { + let split_to_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of() + 100, + &id(), + ) + .unwrap(); + 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: current_epoch, + ..Clock::default() + }), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + + // should always return error when splitting to larger account + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(split_amount)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::InvalidAccountData), + ); + + // Splitting 100% of source should not make a difference + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Err(ProgramError::InvalidAccountData), + ); + } +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split_100_percent_of_source(mollusk: Mollusk) { + 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(); + let stake_lamports = rent_exempt_reserve + minimum_delegation; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + ..Meta::default() + }; + let split_to_address = solana_sdk::pubkey::new_rand(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: true, + }, + ]; + + // test splitting both an Initialized stake and a Staked stake + for state in &[ + StakeStateV2::Initialized(meta), + just_stake(meta, stake_lamports - rent_exempt_reserve), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeStateV2::size_of(), + &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 + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + + // no lamport leakage + assert_eq!( + 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 { + StakeStateV2::Initialized(_) => { + assert_eq!(Ok(*state), accounts[1].state()); + assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state()); + } + StakeStateV2::Stake(meta, stake, stake_flags) => { + assert_eq!( + Ok(StakeStateV2::Stake( + *meta, + Stake { + delegation: Delegation { + stake: stake_lamports - rent_exempt_reserve, + ..stake.delegation + }, + ..*stake + }, + *stake_flags + )), + accounts[1].state() + ); + assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state()); + } + _ => unreachable!(), + } + } +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split_100_percent_of_source_to_account_with_lamports(mollusk: Mollusk) { + 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(); + let stake_lamports = rent_exempt_reserve + minimum_delegation; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve, + ..Meta::default() + }; + let state = just_stake(meta, stake_lamports - rent_exempt_reserve); + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: true, + }, + ]; + + // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly + // rent_exempt_reserve, and more than rent_exempt_reserve. Technically, the empty case is + // covered in test_split_100_percent_of_source, but included here as well for readability + 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 split_to_account = AccountSharedData::new_data_with_space( + initial_balance, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &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 + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + + // no lamport leakage + assert_eq!( + 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 StakeStateV2::Stake(meta, stake, stake_flags) = state { + assert_eq!( + Ok(StakeStateV2::Stake( + meta, + Stake { + delegation: Delegation { + stake: stake_lamports - rent_exempt_reserve, + ..stake.delegation + }, + ..stake + }, + stake_flags, + )), + accounts[1].state() + ); + assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state()); + } + } +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split_rent_exemptness(mollusk: Mollusk) { + let rent = Rent::default(); + 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() + }; + let minimum_delegation = crate::get_minimum_delegation(); + let stake_lamports = source_rent_exempt_reserve + minimum_delegation; + let stake_address = solana_sdk::pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&stake_address), + rent_exempt_reserve: source_rent_exempt_reserve, + ..Meta::default() + }; + let split_to_address = solana_sdk::pubkey::new_rand(); + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: split_to_address, + is_signer: false, + is_writable: true, + }, + ]; + + for state in &[ + StakeStateV2::Initialized(meta), + just_stake(meta, stake_lamports - source_rent_exempt_reserve), + ] { + // Test that splitting to a larger account fails + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of() + 10000, + &id(), + ) + .unwrap(); + 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_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Err(ProgramError::InvalidAccountData), + ); + + // Test that splitting from a larger account to a smaller one works. + // Split amount should not matter, assuming other fund criteria are met + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &state, + StakeStateV2::size_of() + 100, + &id(), + ) + .unwrap(); + let split_to_account = AccountSharedData::new_data_with_space( + 0, + &StakeStateV2::Uninitialized, + StakeStateV2::size_of(), + &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( + &mollusk, + &serialize(&StakeInstruction::Split(stake_lamports)).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + 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), + rent_exempt_reserve: split_rent_exempt_reserve, + ..Meta::default() + }; + match state { + StakeStateV2::Initialized(_) => { + assert_eq!( + Ok(StakeStateV2::Initialized(expected_split_meta)), + accounts[1].state() + ); + assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state()); + } + StakeStateV2::Stake(_meta, stake, stake_flags) => { + // Expected stake should reflect original stake amount so that extra lamports + // from the rent_exempt_reserve inequality do not magically activate + let expected_stake = stake_lamports - source_rent_exempt_reserve; + + assert_eq!( + Ok(StakeStateV2::Stake( + expected_split_meta, + Stake { + delegation: Delegation { + stake: expected_stake, + ..stake.delegation + }, + ..*stake + }, + *stake_flags, + )), + accounts[1].state() + ); + assert_eq!( + accounts[1].lamports(), + expected_stake + source_rent_exempt_reserve, + ); + assert_eq!(Ok(StakeStateV2::Uninitialized), accounts[0].state()); + } + _ => unreachable!(), + } + } +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_split_require_rent_exempt_destination(mollusk: Mollusk) { + 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(); + 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, + }, + ]; + let expected_result = Err(ProgramError::InsufficientFunds); + + 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 + 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( + &mollusk, + &serialize(&StakeInstruction::Split(split_amount)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + if initial_balance + split_amount < rent_exempt_reserve { + Err(ProgramError::InsufficientFunds) + } else { + 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( + &mollusk, + &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(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_merge(mollusk: Mollusk) { + let stake_address = solana_sdk::pubkey::new_rand(); + let merge_from_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let meta = Meta::auto(&authorized_address); + let stake_lamports = 42; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: merge_from_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + for state in &[ + StakeStateV2::Initialized(meta), + just_stake(meta, stake_lamports), + ] { + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + for merge_from_state in &[ + StakeStateV2::Initialized(meta), + just_stake(meta, stake_lamports), + ] { + let merge_from_account = AccountSharedData::new_data_with_space( + stake_lamports, + merge_from_state, + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account.clone()), + (merge_from_address, merge_from_account), + (authorized_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ( + epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + ), + ]; + + // Authorized staker signature required... + instruction_accounts[4].is_signer = false; + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(ProgramError::MissingRequiredSignature), + ); + instruction_accounts[4].is_signer = true; + + let accounts = process_instruction( + &mollusk, + &serialize(&StakeInstruction::Merge).unwrap(), + transaction_accounts, + instruction_accounts.clone(), + Ok(()), + ); + + // check lamports + assert_eq!(accounts[0].lamports(), stake_lamports * 2); + assert_eq!(accounts[1].lamports(), 0); + + // check state + match state { + StakeStateV2::Initialized(meta) => { + assert_eq!(accounts[0].state(), Ok(StakeStateV2::Initialized(*meta)),); + } + StakeStateV2::Stake(meta, stake, stake_flags) => { + let expected_stake = stake.delegation.stake + + merge_from_state + .stake() + .map(|stake| stake.delegation.stake) + .unwrap_or_else(|| { + stake_lamports + - merge_from_state.meta().unwrap().rent_exempt_reserve + }); + assert_eq!( + accounts[0].state(), + Ok(StakeStateV2::Stake( + *meta, + Stake { + delegation: Delegation { + stake: expected_stake, + ..stake.delegation + }, + ..*stake + }, + *stake_flags, + )), + ); + } + _ => unreachable!(), + } + assert_eq!(accounts[1].state(), Ok(StakeStateV2::Uninitialized)); + } + } +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_merge_self_fails(mollusk: Mollusk) { + let stake_address = solana_sdk::pubkey::new_rand(); + let authorized_address = solana_sdk::pubkey::new_rand(); + let rent = Rent::default(); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let stake_amount = 4242424242; + let stake_lamports = rent_exempt_reserve + stake_amount; + let meta = Meta { + rent_exempt_reserve, + ..Meta::auto(&authorized_address) + }; + let stake = Stake { + delegation: Delegation { + stake: stake_amount, + activation_epoch: 0, + ..Delegation::default() + }, + ..Stake::default() + }; + let stake_account = AccountSharedData::new_data_with_space( + stake_lamports, + &StakeStateV2::Stake(meta, stake, StakeFlags::empty()), + StakeStateV2::size_of(), + &id(), + ) + .unwrap(); + let transaction_accounts = vec![ + (stake_address, stake_account), + (authorized_address, AccountSharedData::default()), + ( + clock::id(), + create_account_shared_data_for_test(&Clock::default()), + ), + ( + stake_history::id(), + create_account_shared_data_for_test(&StakeHistory::default()), + ), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: authorized_address, + is_signer: true, + is_writable: false, + }, + ]; + + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Merge).unwrap(), transaction_accounts, instruction_accounts, Err(ProgramError::InvalidArgument), @@ -4246,7 +6560,7 @@ fn test_stake_process_instruction_with_epoch_rewards_active(mollusk: Mollusk) { )); process_instruction( - &mollusk, + mollusk, &instruction.data, transaction_accounts, instruction.accounts.clone(), From 18efd9ffc8718a089a77f881d0fb0478d3951fde Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:43:32 -0800 Subject: [PATCH 19/35] removed some tests. haha jk added more tests --- program/tests/stake_instruction.rs | 413 ++++++++++++++++++++++++++++- 1 file changed, 409 insertions(+), 4 deletions(-) diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs index c15b725..d4d4bd3 100644 --- a/program/tests/stake_instruction.rs +++ b/program/tests/stake_instruction.rs @@ -366,10 +366,415 @@ mod config { } } -// XXX SKIP BEOFRE THIS -// the tests are kind of dumb but i mihgt grab them anyway -// just annoying bc they test errors that changed -// and are kind of useless, we should actually test the interface systematically +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_stake_process_instruction(mollusk: Mollusk) { + process_instruction_as_one_arg( + &mollusk, + &instruction::initialize( + &Pubkey::new_unique(), + &Authorized::default(), + &Lockup::default(), + ), + Err(ProgramError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::authorize( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + StakeAuthorize::Staker, + None, + ), + Err(ProgramError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::split( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + 100, + &invalid_stake_state_pubkey(), + )[2], + Err(ProgramError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::merge( + &Pubkey::new_unique(), + &invalid_stake_state_pubkey(), + &Pubkey::new_unique(), + )[0], + Err(ProgramError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::split_with_seed( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + 100, + &invalid_stake_state_pubkey(), + &Pubkey::new_unique(), + "seed", + )[1], + Err(ProgramError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::delegate_stake( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &invalid_vote_state_pubkey(), + ), + Err(ProgramError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::withdraw( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + 100, + None, + ), + Err(ProgramError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::deactivate_stake(&Pubkey::new_unique(), &Pubkey::new_unique()), + Err(ProgramError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::set_lockup( + &Pubkey::new_unique(), + &LockupArgs::default(), + &Pubkey::new_unique(), + ), + Err(ProgramError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::deactivate_delinquent_stake( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &invalid_vote_state_pubkey(), + ), + Err(ProgramError::IncorrectProgramId), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::deactivate_delinquent_stake( + &Pubkey::new_unique(), + &invalid_vote_state_pubkey(), + &Pubkey::new_unique(), + ), + Err(ProgramError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::deactivate_delinquent_stake( + &Pubkey::new_unique(), + &invalid_vote_state_pubkey(), + &invalid_vote_state_pubkey(), + ), + Err(ProgramError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::move_stake( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + 100, + ), + Err(ProgramError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &mollusk, + &instruction::move_lamports( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + 100, + ), + Err(ProgramError::InvalidAccountData), + ); +} + +#[test_case(mollusk_native(); "native_stake")] +#[test_case(mollusk_bpf(); "bpf_stake")] +fn test_stake_process_instruction_decode_bail(mollusk: Mollusk) { + // these will not call stake_state, have bogus contents + let stake_address = Pubkey::new_unique(); + let stake_account = create_default_stake_account(); + let rent_address = rent::id(); + let rent = Rent::default(); + let rent_account = create_account_shared_data_for_test(&rent); + let rewards_address = rewards::id(); + let rewards_account = create_account_shared_data_for_test(&rewards::Rewards::new(0.0)); + let stake_history_address = stake_history::id(); + let stake_history_account = create_account_shared_data_for_test(&StakeHistory::default()); + let vote_address = Pubkey::new_unique(); + let vote_account = AccountSharedData::new(0, 0, &solana_vote_program::id()); + let clock_address = clock::id(); + let clock_account = create_account_shared_data_for_test(&clock::Clock::default()); + #[allow(deprecated)] + let config_address = stake_config::id(); + #[allow(deprecated)] + let config_account = config::create_account(0, &stake_config::Config::default()); + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let minimum_delegation = crate::get_minimum_delegation(); + let withdrawal_amount = rent_exempt_reserve + minimum_delegation; + + // gets the "is_empty()" check + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Initialize( + Authorized::default(), + Lockup::default(), + )) + .unwrap(), + Vec::new(), + Vec::new(), + Err(ProgramError::NotEnoughAccountKeys), + ); + + // no account for rent + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Initialize( + Authorized::default(), + Lockup::default(), + )) + .unwrap(), + vec![(stake_address, stake_account.clone())], + vec![AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }], + Err(ProgramError::NotEnoughAccountKeys), + ); + + // fails to deserialize stake state + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Initialize( + Authorized::default(), + Lockup::default(), + )) + .unwrap(), + vec![ + (stake_address, stake_account.clone()), + (rent_address, rent_account), + ], + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: rent_address, + is_signer: false, + is_writable: false, + }, + ], + Err(ProgramError::InvalidAccountData), + ); + + // gets the first check in delegate, wrong number of accounts + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + vec![(stake_address, stake_account.clone())], + vec![AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }], + Err(ProgramError::NotEnoughAccountKeys), + ); + + // gets the sub-check for number of args + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + vec![(stake_address, stake_account.clone())], + vec![AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }], + Err(ProgramError::NotEnoughAccountKeys), + ); + + // gets the check non-deserialize-able account in delegate_stake + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DelegateStake).unwrap(), + vec![ + (stake_address, stake_account.clone()), + (vote_address, vote_account.clone()), + (clock_address, clock_account), + (stake_history_address, stake_history_account.clone()), + (config_address, config_account), + ], + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: clock_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: config_address, + is_signer: false, + is_writable: false, + }, + ], + Err(ProgramError::InvalidAccountData), + ); + + // Tests 3rd keyed account is of correct type (Clock instead of rewards) in withdraw + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(withdrawal_amount)).unwrap(), + vec![ + (stake_address, stake_account.clone()), + (vote_address, vote_account.clone()), + (rewards_address, rewards_account.clone()), + (stake_history_address, stake_history_account), + ], + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: rewards_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_history_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: stake_address, + is_signer: true, + is_writable: false, + }, + ], + Err(ProgramError::InvalidArgument), + ); + + // Tests correct number of accounts are provided in withdraw + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Withdraw(withdrawal_amount)).unwrap(), + vec![(stake_address, stake_account.clone())], + vec![AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }], + Err(ProgramError::NotEnoughAccountKeys), + ); + + // Tests 2nd keyed account is of correct type (Clock instead of rewards) in deactivate + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + vec![ + (stake_address, stake_account.clone()), + (rewards_address, rewards_account), + ], + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: rewards_address, + is_signer: false, + is_writable: false, + }, + ], + Err(ProgramError::InvalidArgument), + ); + + // Tests correct number of accounts are provided in deactivate + process_instruction( + &mollusk, + &serialize(&StakeInstruction::Deactivate).unwrap(), + Vec::new(), + Vec::new(), + Err(ProgramError::NotEnoughAccountKeys), + ); + + // Tests correct number of accounts are provided in deactivate_delinquent + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DeactivateDelinquent).unwrap(), + Vec::new(), + Vec::new(), + Err(ProgramError::NotEnoughAccountKeys), + ); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DeactivateDelinquent).unwrap(), + vec![(stake_address, stake_account.clone())], + vec![AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }], + Err(ProgramError::NotEnoughAccountKeys), + ); + process_instruction( + &mollusk, + &serialize(&StakeInstruction::DeactivateDelinquent).unwrap(), + vec![(stake_address, stake_account), (vote_address, vote_account)], + vec![ + AccountMeta { + pubkey: stake_address, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: vote_address, + is_signer: false, + is_writable: false, + }, + ], + Err(ProgramError::NotEnoughAccountKeys), + ); +} #[test_case(mollusk_native(); "native_stake")] #[test_case(mollusk_bpf(); "bpf_stake")] From 9c7029c3c70e1431df0ea0197f9c7ee43b5b3eb0 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:23:48 -0800 Subject: [PATCH 20/35] remove unused --- program/tests/stake_instruction.rs | 79 +++++------------------------- 1 file changed, 11 insertions(+), 68 deletions(-) diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs index d4d4bd3..4362a2a 100644 --- a/program/tests/stake_instruction.rs +++ b/program/tests/stake_instruction.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] -#![allow(unused_imports)] #![allow(clippy::arithmetic_side_effects)] use { @@ -9,25 +7,14 @@ use { solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, solana_program_runtime::loaded_programs::ProgramCacheEntryOwner, solana_sdk::{ - account::{create_account_shared_data_for_test, Account as SolanaAccount}, + account::create_account_shared_data_for_test, account_utils::StateMut, - address_lookup_table, bpf_loader_upgradeable, - entrypoint::ProgramResult, - feature_set::{ - enable_partitioned_epoch_reward, get_sysvar_syscall_enabled, - move_stake_and_move_lamports_ixs, partitioned_epoch_rewards_superfeature, - stake_raise_minimum_delegation_to_1_sol, - }, - hash::Hash, + feature_set::stake_raise_minimum_delegation_to_1_sol, instruction::{AccountMeta, Instruction}, - native_loader, - native_token::LAMPORTS_PER_SOL, program_error::ProgramError, pubkey::Pubkey, - signature::{Keypair, Signer}, - signers::Signers, stake::{ - self, config as stake_config, + config as stake_config, instruction::{ self, authorize_checked, authorize_checked_with_seed, initialize_checked, set_lockup_checked, AuthorizeCheckedWithSeedArgs, AuthorizeWithSeedArgs, @@ -35,13 +22,13 @@ use { }, stake_flags::StakeFlags, state::{ - warmup_cooldown_rate, Authorized, Delegation, Lockup, Meta, Stake, - StakeActivationStatus, StakeAuthorize, StakeStateV2, + warmup_cooldown_rate, Authorized, Delegation, Lockup, Meta, Stake, StakeAuthorize, + StakeStateV2, }, MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION, }, stake_history::{Epoch, StakeHistoryEntry}, - system_instruction, system_program, + system_program, sysvar::{ clock::{self, Clock}, epoch_rewards::{self, EpochRewards}, @@ -49,23 +36,16 @@ use { rent::{self, Rent}, rewards, stake_history::{self, StakeHistory}, - SysvarId, }, - transaction::{Transaction, TransactionError}, transaction_context::TransactionReturnData, }, - solana_stake_program::{get_minimum_delegation, id, processor::Processor}, + solana_stake_program::{get_minimum_delegation, id}, solana_vote_program::{ self, vote_state::{self, VoteState, VoteStateVersions}, }, - std::{ - collections::{HashMap, HashSet}, - fs, - str::FromStr, - sync::Arc, - }, - test_case::{test_case, test_matrix}, + std::{collections::HashSet, str::FromStr}, + test_case::test_case, }; fn mollusk_native() -> Mollusk { @@ -234,22 +214,10 @@ fn stake_from>(account: &T) -> Optio from(account).and_then(|state: StakeStateV2| state.stake()) } -fn delegation_from(account: &AccountSharedData) -> Option { - from(account).and_then(|state: StakeStateV2| state.delegation()) -} - fn authorized_from(account: &AccountSharedData) -> Option { from(account).and_then(|state: StakeStateV2| state.authorized()) } -fn lockup_from>(account: &T) -> Option { - from(account).and_then(|state: StakeStateV2| state.lockup()) -} - -fn meta_from(account: &AccountSharedData) -> Option { - from(account).and_then(|state: StakeStateV2| state.meta()) -} - fn just_stake(meta: Meta, stake: u64) -> StakeStateV2 { StakeStateV2::Stake( meta, @@ -330,40 +298,15 @@ fn create_stake_history_from_delegations( mod config { #[allow(deprecated)] - use solana_sdk::stake::config::{self, *}; use { - bincode::deserialize, - solana_config_program::{create_config_account, get_config_data}, - solana_sdk::{ - account::{AccountSharedData, ReadableAccount, WritableAccount}, - genesis_config::GenesisConfig, - transaction_context::BorrowedAccount, - }, + solana_config_program::create_config_account, + solana_sdk::{account::AccountSharedData, stake::config::Config}, }; - #[allow(deprecated)] - pub fn from(account: &BorrowedAccount) -> Option { - get_config_data(account.get_data()) - .ok() - .and_then(|data| deserialize(data).ok()) - } - #[allow(deprecated)] pub fn create_account(lamports: u64, config: &Config) -> AccountSharedData { create_config_account(vec![], config, lamports) } - - #[allow(deprecated)] - pub fn add_genesis_account(genesis_config: &mut GenesisConfig) -> u64 { - let mut account = create_config_account(vec![], &Config::default(), 0); - let lamports = genesis_config.rent.minimum_balance(account.data().len()); - - account.set_lamports(lamports.max(1)); - - genesis_config.add_account(config::id(), account); - - lamports - } } #[test_case(mollusk_native(); "native_stake")] From 1712b6306bd8ab2c040f85e4f45784c22866dda5 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:08:16 -0800 Subject: [PATCH 21/35] cleanup --- program/tests/program_test.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/program/tests/program_test.rs b/program/tests/program_test.rs index 28fbf7d..df1cd67 100644 --- a/program/tests/program_test.rs +++ b/program/tests/program_test.rs @@ -977,8 +977,6 @@ impl StakeLifecycle { } } -// TODO test whole-balance split (there are a lot more split tests i didnt port -// yet tho) #[test_case(StakeLifecycle::Uninitialized; "uninitialized")] #[test_case(StakeLifecycle::Initialized; "initialized")] #[test_case(StakeLifecycle::Activating; "activating")] @@ -1129,7 +1127,6 @@ async fn test_split(split_source_type: StakeLifecycle) { } } -// TODO lockup... unenforced and enforced? also maybe for split #[test_case(StakeLifecycle::Uninitialized; "uninitialized")] #[test_case(StakeLifecycle::Initialized; "initialized")] #[test_case(StakeLifecycle::Activating; "activating")] From 929dd4dcd521d696acd11928f113c9b19fc30be3 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Tue, 10 Dec 2024 19:20:15 -0800 Subject: [PATCH 22/35] update to latest mollusk --- program/Cargo.toml | 2 +- program/tests/stake_instruction.rs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/program/Cargo.toml b/program/Cargo.toml index 43870e6..6b31770 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -22,7 +22,7 @@ solana-program = "=2.1.0" thiserror = "1.0.63" [dev-dependencies] -mollusk-svm = { path = "/home/hana/work/misc/mollusk/harness" } +mollusk-svm = { path = "/home/hana/work/misc/mollusk/harness", features = ["all-builtins"] } solana-account = { version = "=2.1.0", features = ["bincode"] } solana-program-test = "=2.1.0" solana-program-runtime = "=2.1.0" diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs index 4362a2a..cdd6397 100644 --- a/program/tests/stake_instruction.rs +++ b/program/tests/stake_instruction.rs @@ -37,7 +37,6 @@ use { rewards, stake_history::{self, StakeHistory}, }, - transaction_context::TransactionReturnData, }, solana_stake_program::{get_minimum_delegation, id}, solana_vote_program::{ @@ -6536,10 +6535,7 @@ fn test_stake_get_minimum_delegation(mollusk: Mollusk) { &transaction_accounts, &[ Check::success(), - Check::return_data(TransactionReturnData { - program_id: id(), - data: minimum_delegation.to_le_bytes().to_vec(), - }), + Check::return_data(minimum_delegation.to_le_bytes().to_vec()), ], ); } From d2fcbd91a1c661d3dfb0f0b552839867f9e11483 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:18:32 -0800 Subject: [PATCH 23/35] fixed some tests --- program/tests/stake_instruction.rs | 40 ++++++++++++++++++------------ 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs index cdd6397..57faf95 100644 --- a/program/tests/stake_instruction.rs +++ b/program/tests/stake_instruction.rs @@ -250,6 +250,10 @@ fn get_active_stake_for_tests( active_stake } +fn create_empty_stake_history_for_test() -> AccountSharedData { + AccountSharedData::create(1, vec![0; 8], solana_program::sysvar::id(), false, u64::MAX) +} + fn new_stake_history_entry<'a, I>( epoch: Epoch, stakes: I, @@ -295,6 +299,18 @@ fn create_stake_history_from_delegations( stake_history } +fn create_stake_history_at_epoch(current_epoch: Epoch) -> StakeHistory { + unsafe { + std::mem::transmute::, StakeHistory>( + (0..current_epoch) + .rev() + .take(512) + .map(|past_epoch| (past_epoch, StakeHistoryEntry::with_effective(u64::MAX))) + .collect(), + ) + } +} + mod config { #[allow(deprecated)] use { @@ -1848,10 +1864,7 @@ fn test_stake_delegate(mollusk: Mollusk) { (vote_address, vote_account), (vote_address_2, vote_account_2.clone()), (clock::id(), create_account_shared_data_for_test(&clock)), - ( - stake_history::id(), - create_account_shared_data_for_test(&StakeHistory::default()), - ), + (stake_history::id(), create_empty_stake_history_for_test()), ( stake_config::id(), config::create_account(0, &stake_config::Config::default()), @@ -3909,7 +3922,6 @@ fn test_staked_split_destination_minimum_balance( let minimum_delegation = crate::get_minimum_delegation(); 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, @@ -4033,7 +4045,7 @@ fn test_staked_split_destination_minimum_balance( let expected_active_stake = get_active_stake_for_tests( &[source_account.clone(), destination_account.clone()], &clock, - &stake_history, + &StakeHistory::default(), ); let accounts = process_instruction( &mollusk, @@ -4042,10 +4054,7 @@ fn test_staked_split_destination_minimum_balance( (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), - ), + (stake_history::id(), create_empty_stake_history_for_test()), (clock::id(), create_account_shared_data_for_test(&clock)), ( epoch_schedule::id(), @@ -4057,7 +4066,7 @@ fn test_staked_split_destination_minimum_balance( ); assert_eq!( expected_active_stake, - get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + get_active_stake_for_tests(&accounts[0..2], &clock, &StakeHistory::default()) ); // 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, @@ -4222,10 +4231,7 @@ fn test_behavior_withdrawal_then_redelegate_with_less_than_minimum_stake_delegat AccountSharedData::new(rent_exempt_reserve, 0, &system_program::id()), ), (clock::id(), create_account_shared_data_for_test(&clock)), - ( - stake_history::id(), - create_account_shared_data_for_test(&StakeHistory::default()), - ), + (stake_history::id(), create_empty_stake_history_for_test()), ( stake_config::id(), config::create_account(0, &stake_config::Config::default()), @@ -4907,12 +4913,14 @@ fn test_split_from_larger_sized_account(mollusk: Mollusk) { let rent = Rent::default(); 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() }; + // XXX + //let stake_history = create_stake_history_at_epoch(current_epoch); + let stake_history = StakeHistory::default(); let minimum_delegation = crate::get_minimum_delegation(); let stake_lamports = (source_larger_rent_exempt_reserve + minimum_delegation) * 2; let stake_address = solana_sdk::pubkey::new_rand(); From 65b7fb7ac76337da91b6358a896edadcd37aa837 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:24:56 -0800 Subject: [PATCH 24/35] fix all tests --- program/tests/stake_instruction.rs | 58 +++++++++--------------------- 1 file changed, 16 insertions(+), 42 deletions(-) diff --git a/program/tests/stake_instruction.rs b/program/tests/stake_instruction.rs index 57faf95..441b633 100644 --- a/program/tests/stake_instruction.rs +++ b/program/tests/stake_instruction.rs @@ -299,18 +299,6 @@ fn create_stake_history_from_delegations( stake_history } -fn create_stake_history_at_epoch(current_epoch: Epoch) -> StakeHistory { - unsafe { - std::mem::transmute::, StakeHistory>( - (0..current_epoch) - .rev() - .take(512) - .map(|past_epoch| (past_epoch, StakeHistoryEntry::with_effective(u64::MAX))) - .collect(), - ) - } -} - mod config { #[allow(deprecated)] use { @@ -2096,10 +2084,7 @@ fn test_redelegate_consider_balance_changes(mollusk: Mollusk) { ), (authority_address, AccountSharedData::default()), (clock::id(), create_account_shared_data_for_test(&clock)), - ( - stake_history::id(), - create_account_shared_data_for_test(&StakeHistory::default()), - ), + (stake_history::id(), create_empty_stake_history_for_test()), ( stake_config::id(), config::create_account(0, &stake_config::Config::default()), @@ -4735,7 +4720,6 @@ fn test_split_with_rent(mollusk: Mollusk) { fn test_split_to_account_with_rent_exempt_reserve(mollusk: Mollusk) { 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, @@ -4783,10 +4767,7 @@ fn test_split_to_account_with_rent_exempt_reserve(mollusk: Mollusk) { (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), - ), + (stake_history::id(), create_empty_stake_history_for_test()), (clock::id(), create_account_shared_data_for_test(&clock)), ( epoch_schedule::id(), @@ -4834,7 +4815,7 @@ fn test_split_to_account_with_rent_exempt_reserve(mollusk: Mollusk) { transaction_accounts[1].1.clone(), ], &clock, - &stake_history, + &StakeHistory::default(), ); // split more than available fails @@ -4862,7 +4843,7 @@ fn test_split_to_account_with_rent_exempt_reserve(mollusk: Mollusk) { // no deactivated stake assert_eq!( expected_active_stake, - get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + get_active_stake_for_tests(&accounts[0..2], &clock, &StakeHistory::default()) ); if let StakeStateV2::Stake(meta, stake, stake_flags) = state { @@ -4918,9 +4899,6 @@ fn test_split_from_larger_sized_account(mollusk: Mollusk) { epoch: current_epoch, ..Clock::default() }; - // XXX - //let stake_history = create_stake_history_at_epoch(current_epoch); - let stake_history = StakeHistory::default(); let minimum_delegation = crate::get_minimum_delegation(); let stake_lamports = (source_larger_rent_exempt_reserve + minimum_delegation) * 2; let stake_address = solana_sdk::pubkey::new_rand(); @@ -4963,10 +4941,7 @@ fn test_split_from_larger_sized_account(mollusk: Mollusk) { (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), - ), + (stake_history::id(), create_empty_stake_history_for_test()), (clock::id(), create_account_shared_data_for_test(&clock)), ( epoch_schedule::id(), @@ -5003,7 +4978,7 @@ fn test_split_from_larger_sized_account(mollusk: Mollusk) { transaction_accounts[1].1.clone(), ], &clock, - &stake_history, + &StakeHistory::default(), ); // split more than available fails @@ -5031,7 +5006,7 @@ fn test_split_from_larger_sized_account(mollusk: Mollusk) { // no deactivated stake assert_eq!( expected_active_stake, - get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + get_active_stake_for_tests(&accounts[0..2], &clock, &StakeHistory::default()) ); if let StakeStateV2::Stake(meta, stake, stake_flags) = state { @@ -5589,7 +5564,6 @@ fn test_split_rent_exemptness(mollusk: Mollusk) { fn test_split_require_rent_exempt_destination(mollusk: Mollusk) { 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, @@ -5647,10 +5621,7 @@ fn test_split_require_rent_exempt_destination(mollusk: Mollusk) { (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), - ), + (stake_history::id(), create_empty_stake_history_for_test()), (clock::id(), create_account_shared_data_for_test(&clock)), ( epoch_schedule::id(), @@ -5666,7 +5637,7 @@ fn test_split_require_rent_exempt_destination(mollusk: Mollusk) { let expected_active_stake = get_active_stake_for_tests( &[source_account.clone(), transaction_accounts[1].1.clone()], &clock, - &stake_history, + &StakeHistory::default(), ); let result_accounts = process_instruction( &mollusk, @@ -5679,8 +5650,11 @@ fn test_split_require_rent_exempt_destination(mollusk: Mollusk) { expected_result.clone() }, ); - let result_active_stake = - get_active_stake_for_tests(&result_accounts[0..2], &clock, &stake_history); + let result_active_stake = get_active_stake_for_tests( + &result_accounts[0..2], + &clock, + &StakeHistory::default(), + ); if expected_active_stake > 0 // starting stake was delegated // partial split && result_accounts[0].lamports() > 0 @@ -5701,7 +5675,7 @@ fn test_split_require_rent_exempt_destination(mollusk: Mollusk) { let expected_active_stake = get_active_stake_for_tests( &[source_account.clone(), transaction_accounts[1].1.clone()], &clock, - &stake_history, + &StakeHistory::default(), ); let accounts = process_instruction( &mollusk, @@ -5720,7 +5694,7 @@ fn test_split_require_rent_exempt_destination(mollusk: Mollusk) { // no deactivated stake assert_eq!( expected_active_stake, - get_active_stake_for_tests(&accounts[0..2], &clock, &stake_history) + get_active_stake_for_tests(&accounts[0..2], &clock, &StakeHistory::default()) ); if let StakeStateV2::Stake(meta, stake, stake_flags) = state { From 4a61435d7a46c402b5ba069478bc2b6b1c760f2e Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Sat, 14 Dec 2024 10:39:50 -0800 Subject: [PATCH 25/35] update to mollusk from crates --- Cargo.lock | 12 +++++++++--- program/Cargo.toml | 6 +++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c01f060..d53ecf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2348,7 +2348,9 @@ dependencies = [ [[package]] name = "mollusk-svm" -version = "0.0.12" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fceaf67fe3f95a9478f4f5b0d71e77c073eee7a795a74d6143317a22454c289" dependencies = [ "bincode", "mollusk-svm-error", @@ -2365,7 +2367,9 @@ dependencies = [ [[package]] name = "mollusk-svm-error" -version = "0.0.12" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8738bc85a52d123012209a573f17faffa1db440493396ae2e1f64fbb8f3579bf" dependencies = [ "solana-sdk", "thiserror 1.0.69", @@ -2373,7 +2377,9 @@ dependencies = [ [[package]] name = "mollusk-svm-keys" -version = "0.0.12" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a7656d86d743de0a9788ce4c0e9ff63028a42e350131ebe67c476cdde6ac9f" dependencies = [ "mollusk-svm-error", "solana-sdk", diff --git a/program/Cargo.toml b/program/Cargo.toml index 6b31770..f106344 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -18,11 +18,12 @@ borsh = { version = "1.5.1", features = ["derive", "unstable__schema"] } num-derive = "0.4" num-traits = "0.2" num_enum = "0.7.3" -solana-program = "=2.1.0" +solana-program = "2.1" thiserror = "1.0.63" [dev-dependencies] -mollusk-svm = { path = "/home/hana/work/misc/mollusk/harness", features = ["all-builtins"] } +assert_matches = "1.5.0" +mollusk-svm = { version = "=0.0.13", features = ["all-builtins"] } solana-account = { version = "=2.1.0", features = ["bincode"] } solana-program-test = "=2.1.0" solana-program-runtime = "=2.1.0" @@ -31,7 +32,6 @@ solana-vote-program = "=2.1.0" solana-sdk = "=2.1.0" solana-feature-set = "=2.1.0" test-case = "3.3.1" -assert_matches = "1.5.0" [lib] crate-type = ["cdylib", "lib"] From fd6835ac696aa15cef976d82e8058f10569b624f Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:22:05 -0800 Subject: [PATCH 26/35] add some stuff --- Cargo.lock | 23 +++++++++++++++++++++++ program/Cargo.toml | 3 +++ 2 files changed, 26 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d53ecf8..e1e4005 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "ark-bn254" version = "0.4.0" @@ -1057,6 +1066,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "difflib" version = "0.4.0" @@ -5111,6 +5131,7 @@ dependencies = [ name = "solana-stake-program" version = "1.0.0" dependencies = [ + "arbitrary", "arrayref", "assert_matches", "bincode", @@ -5119,9 +5140,11 @@ dependencies = [ "num-derive 0.4.2", "num-traits", "num_enum", + "rand 0.8.5", "solana-account", "solana-config-program", "solana-feature-set", + "solana-logger", "solana-program", "solana-program-runtime", "solana-program-test", diff --git a/program/Cargo.toml b/program/Cargo.toml index f106344..49a49ff 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -23,14 +23,17 @@ thiserror = "1.0.63" [dev-dependencies] assert_matches = "1.5.0" +arbitrary = { version = "1.4.1", features = ["derive"] } mollusk-svm = { version = "=0.0.13", features = ["all-builtins"] } solana-account = { version = "=2.1.0", features = ["bincode"] } +solana-logger = "=2.1.0" solana-program-test = "=2.1.0" solana-program-runtime = "=2.1.0" solana-config-program = "=2.1.0" solana-vote-program = "=2.1.0" solana-sdk = "=2.1.0" solana-feature-set = "=2.1.0" +rand = "0.8.5" test-case = "3.3.1" [lib] From 681528cbffca547659a5048f2b076cae8d02328d Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:22:32 -0800 Subject: [PATCH 27/35] start adding interface tests, based on first mollusk tests --- program/tests/interface.rs | 761 +++++++++++++++++++++++++++++++++++++ 1 file changed, 761 insertions(+) create mode 100644 program/tests/interface.rs diff --git a/program/tests/interface.rs b/program/tests/interface.rs new file mode 100644 index 0000000..b81b5cf --- /dev/null +++ b/program/tests/interface.rs @@ -0,0 +1,761 @@ +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(clippy::arithmetic_side_effects)] + +use { + arbitrary::{Arbitrary, Unstructured}, + mollusk_svm::{result::Check, Mollusk}, + solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, + solana_sdk::{ + account::Account as SolanaAccount, + address_lookup_table, bpf_loader_upgradeable, + entrypoint::ProgramResult, + feature_set::{ + enable_partitioned_epoch_reward, get_sysvar_syscall_enabled, + move_stake_and_move_lamports_ixs, partitioned_epoch_rewards_superfeature, + stake_raise_minimum_delegation_to_1_sol, + }, + hash::Hash, + instruction::{AccountMeta, Instruction}, + native_token::LAMPORTS_PER_SOL, + program_error::ProgramError, + pubkey::Pubkey, + signature::{Keypair, Signer}, + signers::Signers, + stake::{ + self, + instruction::{self, LockupArgs, LockupCheckedArgs, StakeError, StakeInstruction}, + stake_flags::StakeFlags, + state::{ + warmup_cooldown_rate, Authorized, Delegation, Lockup, Meta, Stake, + StakeActivationStatus, StakeAuthorize, StakeStateV2, + }, + }, + stake_history::StakeHistoryEntry, + system_instruction, system_program, + sysvar::{ + clock::Clock, epoch_rewards::EpochRewards, epoch_schedule::EpochSchedule, rent::Rent, + stake_history::StakeHistory, SysvarId, + }, + transaction::{Transaction, TransactionError}, + vote::{ + program as vote_program, + state::{VoteInit, VoteState, VoteStateVersions}, + }, + }, + solana_stake_program::{get_minimum_delegation, id, processor::Processor}, + std::{collections::HashMap, fs}, + test_case::{test_case, test_matrix}, +}; + +// XXX ok so wow i am going to have to write a lot of shit +// we need a mechanism to create basically arbitrary stake accounts +// this means all states (uninit, init, activating, active, deactivating, deactive) +// we need to be able to make a stake history that gives us partial activation/deactivation +// actually we need to set up stake history ourselves correctly in all cases +// we need to be able to set lockup and authority arbitrarily +// we need helpers to set up with seed pubkeys +// ideally we automatically check missing signer failures +// need to create a vote account... ugh we need to get credits right for DeactivateDelinquent +// for delegate we just need owner, vote account pubkey, and credits (can be 0) +// +// XXX OK i wrote a simple init test +// what to do on monday... i guess go through the stake ixn tests and see what to impl +// main thing we lack is full coverage for lockup and i think a bunch of split edge cases + +// arbitrary, but gives us room to set up activations/deactivations serveral epochs in the past +const EXECUTION_EPOCH: u64 = 8; + +// mollusk doesnt charge transaction fees, this is just a convenient source of lamports +const PAYER: Pubkey = Pubkey::from_str_const("PAYER11111111111111111111111111111111111111"); +const PAYER_BALANCE: u64 = 1_000_000 * LAMPORTS_PER_SOL; + +// two vote accounts with no credits, fine for all stake tests except DeactivateDelinquent +const VOTE_ACCOUNT_RED: Pubkey = + Pubkey::from_str_const("RED1111111111111111111111111111111111111111"); +const VOTE_ACCOUNT_BLUE: Pubkey = + Pubkey::from_str_const("BLUE111111111111111111111111111111111111111"); + +// two blank stake accounts that can be serialized into for tests +const STAKE_ACCOUNT_BLACK: Pubkey = + Pubkey::from_str_const("BLACK11111111111111111111111111111111111111"); +const STAKE_ACCOUNT_WHITE: Pubkey = + Pubkey::from_str_const("WH1TE11111111111111111111111111111111111111"); + +// authorities for tests which use separate ones +const STAKER_BLACK: Pubkey = Pubkey::from_str_const("STAKERBLACK11111111111111111111111111111111"); +const WITHDRAWER_BLACK: Pubkey = + Pubkey::from_str_const("W1THDRAWERBLACK1111111111111111111111111111"); +const STAKER_WHITE: Pubkey = Pubkey::from_str_const("STAKERWH1TE11111111111111111111111111111111"); +const WITHDRAWER_WHITE: Pubkey = + Pubkey::from_str_const("W1THDRAWERWH1TE1111111111111111111111111111"); + +// authorities for tests which use shared ones +const STAKER_GRAY: Pubkey = Pubkey::from_str_const("STAKERGRAY111111111111111111111111111111111"); +const WITHDRAWER_GRAY: Pubkey = + Pubkey::from_str_const("W1THDRAWERGRAY11111111111111111111111111111"); + +// valid custodians for any stake account +const CUSTODIAN_LEFT: Pubkey = + Pubkey::from_str_const("CUSTXD1ANLEFT111111111111111111111111111111"); +const CUSTODIAN_RIGHT: Pubkey = + Pubkey::from_str_const("CUSTXD1ANR1GHT11111111111111111111111111111"); + +// stake delegated to some imaginary vote account in all epochs +// with a warmup/cooldown rate of 9%, routine tests moving under 9sol can ignore stake history +// while also making it easy to write tests involving partial (de)activations +// if the warmup/cooldown rate changes, this number must be adjusted +const PERSISTANT_ACTIVE_STAKE: u64 = 100 * LAMPORTS_PER_SOL; +#[test] +fn assert_warmup_cooldown_rate() { + assert_eq!(warmup_cooldown_rate(0, Some(0)), 0.09); +} + +// hardcoded for convenience +const STAKE_RENT_EXEMPTION: u64 = 2_282_880; +#[test] +fn assert_stake_rent_exemption() { + assert_eq!( + Rent::default().minimum_balance(StakeStateV2::size_of()), + STAKE_RENT_EXEMPTION + ); +} + +// we use two hashmaps because cloning mollusk is impossible and creating it is expensive +// doing this we let base_accounts be immutable and can set and clear override_accounts +struct Env { + mollusk: Mollusk, + base_accounts: HashMap, + override_accounts: HashMap, +} +impl Env { + // set up a test environment with valid stake history, two vote accounts, and two blank stake accounts + fn init() -> Self { + // create a test environment at the execution epoch + let mut base_accounts = HashMap::new(); + let mut mollusk = Mollusk::new(&id(), "solana_stake_program"); + solana_logger::setup_with(""); + mollusk.warp_to_slot(EXECUTION_EPOCH * mollusk.sysvars.epoch_schedule.slots_per_epoch + 1); + assert_eq!(mollusk.sysvars.clock.epoch, EXECUTION_EPOCH); + + // backfill stake history + for epoch in 0..EXECUTION_EPOCH { + mollusk.sysvars.stake_history.add( + epoch, + StakeHistoryEntry::with_effective(PERSISTANT_ACTIVE_STAKE), + ); + } + + // add a lamports source + let payer_data = + AccountSharedData::new_rent_epoch(PAYER_BALANCE, 0, &system_program::id(), u64::MAX); + base_accounts.insert(PAYER, payer_data); + + // create two vote accounts + let vote_rent_exemption = Rent::default().minimum_balance(VoteState::size_of()); + let vote_state = bincode::serialize(&VoteState::default()).unwrap(); + let vote_data = AccountSharedData::create( + vote_rent_exemption, + vote_state, + vote_program::id(), + false, + u64::MAX, + ); + base_accounts.insert(VOTE_ACCOUNT_RED, vote_data.clone()); + base_accounts.insert(VOTE_ACCOUNT_BLUE, vote_data); + + // create two blank stake accounts + let stake_data = AccountSharedData::create( + STAKE_RENT_EXEMPTION, + vec![0; StakeStateV2::size_of()], + id(), + false, + u64::MAX, + ); + base_accounts.insert(STAKE_ACCOUNT_BLACK, stake_data.clone()); + base_accounts.insert(STAKE_ACCOUNT_WHITE, stake_data); + + Self { + mollusk, + base_accounts, + override_accounts: HashMap::new(), + } + } + + // creates a test environment and instruction for a given stake operation + // enum contents are sometimes, but not necessarily, ignored + // the success is trivial, this is mostly to allow exhaustive failure tests + // or to do some post-setup for more meaningful success tests + /* + fn init_for_instruction(stake_instruction: &StakeInstruction) -> (Self, Instruction) { + let mut env = Self::init(); + let minimum_delegation = get_minimum_delegation(); + + let instruction = match stake_instruction { + StakeInstruction::Initialize(_, _) => instruction::initialize( + &STAKE_ACCOUNT_BLACK, + &Authorized { + staker: STAKER_BLACK, + withdrawer: WITHDRAWER_BLACK, + }, + &Lockup::default(), + ), + // TODO lockup + StakeInstruction::Authorize(_, authorize) => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), + minimum_delegation, + ); + + let (old_authority, new_authority) = match authorize { + StakeAuthorize::Staker => (STAKER_BLACK, STAKER_GRAY), + StakeAuthorize::Withdrawer => (WITHDRAWER_BLACK, WITHDRAWER_GRAY), + }; + + instruction::authorize( + &STAKE_ACCOUNT_BLACK, + &old_authority, + &new_authority, + *authorize, + None, + ) + } + // TODO withdrawer + StakeInstruction::DelegateStake => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), + minimum_delegation, + ); + + instruction::delegate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK, &VOTE_ACCOUNT_RED) + } + // TODO amount + StakeInstruction::Split(_) => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation * 2, + true, + ), + minimum_delegation * 2, + ); + + instruction::split( + &STAKE_ACCOUNT_BLACK, + &STAKER_GRAY, + minimum_delegation, + &STAKE_ACCOUNT_WHITE, + )[2] + .clone() + } + // TODO partial, lockup + StakeInstruction::Withdraw(_) => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation, + false, + ), + minimum_delegation, + ); + + instruction::withdraw( + &STAKE_ACCOUNT_BLACK, + &WITHDRAWER_BLACK, + &PAYER, + minimum_delegation + STAKE_RENT_EXEMPTION, + None, + ) + } + // TODO withdrawer + StakeInstruction::Deactivate => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation, + false, + ), + minimum_delegation, + ); + + instruction::deactivate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK) + } + // TODO existing lockup, remove lockup, also hardcoded custodians maybe? + StakeInstruction::SetLockup(_) => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), + minimum_delegation, + ); + + instruction::set_lockup( + &STAKE_ACCOUNT_BLACK, + &LockupArgs { + epoch: Some(EXECUTION_EPOCH * 2), + custodian: Some(Pubkey::new_unique()), + unix_timestamp: None, + }, + &WITHDRAWER_BLACK, + ) + } + // TODO withdrawer + StakeInstruction::Merge => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation, + true, + ), + minimum_delegation, + ); + + env.update_stake( + &STAKE_ACCOUNT_WHITE, + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_WHITE, + minimum_delegation, + true, + ), + minimum_delegation, + ); + + instruction::merge(&STAKE_ACCOUNT_WHITE, &STAKE_ACCOUNT_BLACK, &STAKER_GRAY)[0] + .clone() + } + // TODO move, checked, seed, deactivate delinquent, minimum, redelegate + _ => todo!(), + }; + + (env, instruction) + } + */ + + // get the accounts from our account store that this transaction expects to see + // we dont need implicit sysvars, mollusk resolves them internally via syscall stub + fn resolve_accounts(&self, account_metas: &[AccountMeta]) -> Vec<(Pubkey, AccountSharedData)> { + let mut accounts = vec![]; + for account_meta in account_metas { + let key = account_meta.pubkey; + let account_shared_data = if Rent::check_id(&key) { + self.mollusk.sysvars.keyed_account_for_rent_sysvar().1 + } else if Clock::check_id(&key) { + self.mollusk.sysvars.keyed_account_for_clock_sysvar().1 + } else if EpochSchedule::check_id(&key) { + self.mollusk + .sysvars + .keyed_account_for_epoch_schedule_sysvar() + .1 + } else if EpochRewards::check_id(&key) { + self.mollusk + .sysvars + .keyed_account_for_epoch_rewards_sysvar() + .1 + } else if StakeHistory::check_id(&key) { + self.mollusk + .sysvars + .keyed_account_for_stake_history_sysvar() + .1 + } else if let Some(account) = self.override_accounts.get(&key).cloned() { + account + } else if let Some(account) = self.base_accounts.get(&key).cloned() { + account + } else { + AccountSharedData::default() + }; + + accounts.push((key, account_shared_data)); + } + + accounts + } + + // set up one of the preconfigured blank stake accounts at some starting state + // to mutate the accounts after initial setup, do it directly or execute instructions + // note these accounts are already rent exempt, so lamports specified are stake or extra + fn update_stake( + &mut self, + pubkey: &Pubkey, + stake_state: &StakeStateV2, + additional_lamports: u64, + ) { + assert!(*pubkey == STAKE_ACCOUNT_BLACK || *pubkey == STAKE_ACCOUNT_WHITE); + let stake_account = self.override_accounts.get_mut(pubkey).unwrap(); + let current_lamports = stake_account.lamports(); + stake_account.set_lamports(current_lamports + additional_lamports); + bincode::serialize_into(stake_account.data_as_mut_slice(), stake_state).unwrap(); + } + + fn reset(&mut self) { + self.override_accounts.clear() + } + + /* XXX + // process an instruction, assert checks, and update internal accounts + // XXX dont think i need to mutate accounts, im doing all one-off + fn process(&mut self, instruction: &Instruction, checks: &[Check]) { + let initial_accounts = self.resolve_accounts(&instruction.accounts); + + let result = + self.mollusk + .process_and_validate_instruction(instruction, &initial_accounts, checks); + + for (i, resulting_account) in result.resulting_accounts.into_iter().enumerate() { + let account_meta = &instruction.accounts[i]; + assert_eq!(account_meta.pubkey, resulting_account.0); + if account_meta.is_writable { + if resulting_account.1.lamports() == 0 { + self.accounts.remove(&resulting_account.0); + } else { + self.accounts + .insert(resulting_account.0, resulting_account.1); + } + } + } + } + */ + + // shorthand for process with only a success check + fn process_success(&self, instruction: &Instruction) { + let accounts = self.resolve_accounts(&instruction.accounts); + self.mollusk + .process_and_validate_instruction(instruction, &accounts, &[Check::success()]); + } + + // shorthand for process with an expected error + fn process_fail(&self, instruction: &Instruction, error: ProgramError) { + let accounts = self.resolve_accounts(&instruction.accounts); + self.mollusk + .process_and_validate_instruction(instruction, &accounts, &[Check::err(error)]); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Arbitrary)] +enum StakeInterface { + Initialize(LockupState), + Authorize(AuthorityType, LockupState), + /* + DelegateStake(StakeAuthorize), + Split(AmountFraction), + Withdraw(LockupState, AmountFraction), + Deactivate(StakeAuthorize), + SetLockup(LockupState, LockupState), + Merge(StakeAuthorize), + */ + // TODO move, checked, seed, deactivate delinquent, minimum, redelegate +} + +impl StakeInterface { + // creates an instruction with the given combination of settings that is guaranteed to succeed + fn to_instruction(&self, env: &mut Env) -> Instruction { + let minimum_delegation = get_minimum_delegation(); + + match self { + Self::Initialize(lockup_state) => instruction::initialize( + &STAKE_ACCOUNT_BLACK, + &Authorized { + staker: STAKER_BLACK, + withdrawer: WITHDRAWER_BLACK, + }, + &lockup_state.to_lockup(CUSTODIAN_LEFT), + ), + Self::Authorize(authority_type, lockup_state) => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + ¬_just_stake( + STAKE_ACCOUNT_BLACK, + minimum_delegation, + false, + lockup_state.to_lockup(CUSTODIAN_LEFT), + ), + minimum_delegation, + ); + + let authorize = authority_type.into(); + let (old_authority, new_authority) = match authorize { + StakeAuthorize::Staker => (STAKER_BLACK, STAKER_GRAY), + StakeAuthorize::Withdrawer => (WITHDRAWER_BLACK, WITHDRAWER_GRAY), + }; + + instruction::authorize( + &STAKE_ACCOUNT_BLACK, + &old_authority, + &new_authority, + authorize, + lockup_state.to_custodian(&CUSTODIAN_LEFT), + ) + } + /* + // TODO withdrawer + StakeInstruction::DelegateStake => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), + minimum_delegation, + ); + + instruction::delegate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK, &VOTE_ACCOUNT_RED) + } + // TODO amount + StakeInstruction::Split(_) => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation * 2, + true, + ), + minimum_delegation * 2, + ); + + instruction::split( + &STAKE_ACCOUNT_BLACK, + &STAKER_GRAY, + minimum_delegation, + &STAKE_ACCOUNT_WHITE, + )[2] + .clone() + } + // TODO partial, lockup + StakeInstruction::Withdraw(_) => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation, + false, + ), + minimum_delegation, + ); + + instruction::withdraw( + &STAKE_ACCOUNT_BLACK, + &WITHDRAWER_BLACK, + &PAYER, + minimum_delegation + STAKE_RENT_EXEMPTION, + None, + ) + } + // TODO withdrawer + StakeInstruction::Deactivate => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation, + false, + ), + minimum_delegation, + ); + + instruction::deactivate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK) + } + // TODO existing lockup, remove lockup, also hardcoded custodians maybe? + StakeInstruction::SetLockup(_) => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), + minimum_delegation, + ); + + instruction::set_lockup( + &STAKE_ACCOUNT_BLACK, + &LockupArgs { + epoch: Some(EXECUTION_EPOCH * 2), + custodian: Some(Pubkey::new_unique()), + unix_timestamp: None, + }, + &WITHDRAWER_BLACK, + ) + } + // TODO withdrawer + StakeInstruction::Merge => { + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation, + true, + ), + minimum_delegation, + ); + + env.update_stake( + &STAKE_ACCOUNT_WHITE, + &active_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_WHITE, + minimum_delegation, + true, + ), + minimum_delegation, + ); + + instruction::merge(&STAKE_ACCOUNT_WHITE, &STAKE_ACCOUNT_BLACK, &STAKER_GRAY)[0] + .clone() + } + */ + // TODO move, checked, seed, deactivate delinquent, minimum, redelegate + _ => todo!(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Arbitrary)] +enum AuthorityType { + Staker, + Withdrawer, +} + +impl From<&AuthorityType> for StakeAuthorize { + fn from(authority_type: &AuthorityType) -> Self { + match authority_type { + AuthorityType::Staker => Self::Staker, + AuthorityType::Withdrawer => Self::Withdrawer, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Arbitrary)] +enum LockupState { + Active, + Inactive, + None, +} + +impl LockupState { + fn to_lockup(&self, custodian: Pubkey) -> Lockup { + match self { + Self::Active => Lockup { + custodian, + epoch: EXECUTION_EPOCH + 1, + unix_timestamp: 0, + }, + Self::Inactive => Lockup { + custodian, + epoch: EXECUTION_EPOCH - 1, + unix_timestamp: 0, + }, + Self::None => Lockup::default(), + } + } + + fn to_custodian<'a>(&self, custodian: &'a Pubkey) -> Option<&'a Pubkey> { + match self { + Self::None => None, + _ => Some(custodian), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Arbitrary)] +enum AmountFraction { + Partial, + Full, +} + +fn just_stake(stake_pubkey: Pubkey, stake: u64) -> StakeStateV2 { + not_just_stake(stake_pubkey, stake, false, Lockup::default()) +} + +fn not_just_stake( + stake_pubkey: Pubkey, + stake: u64, + common_authority: bool, + lockup: Lockup, +) -> StakeStateV2 { + i_cant_believe_its_not_stake( + Pubkey::default(), + stake_pubkey, + stake, + common_authority, + lockup, + false, + ) +} + +fn i_cant_believe_its_not_stake( + voter_pubkey: Pubkey, + stake_pubkey: Pubkey, + stake: u64, + common_authority: bool, + lockup: Lockup, + is_active: bool, +) -> StakeStateV2 { + assert!(stake_pubkey != VOTE_ACCOUNT_RED); + assert!(stake_pubkey != VOTE_ACCOUNT_BLUE); + + let authorized = match stake_pubkey { + _ if common_authority => Authorized { + staker: STAKER_GRAY, + withdrawer: WITHDRAWER_GRAY, + }, + STAKE_ACCOUNT_BLACK => Authorized { + staker: STAKER_BLACK, + withdrawer: WITHDRAWER_BLACK, + }, + STAKE_ACCOUNT_WHITE => Authorized { + staker: STAKER_WHITE, + withdrawer: WITHDRAWER_WHITE, + }, + _ => Authorized::default(), + }; + + let activation_epoch = if is_active { EXECUTION_EPOCH - 1 } else { 0 }; + + StakeStateV2::Stake( + Meta { + rent_exempt_reserve: STAKE_RENT_EXEMPTION, + authorized, + lockup, + }, + Stake { + delegation: Delegation { + stake, + voter_pubkey, + activation_epoch, + ..Delegation::default() + }, + ..Stake::default() + }, + StakeFlags::empty(), + ) +} + +fn stake_to_bytes(stake: &StakeStateV2) -> Vec { + let mut data = vec![0; StakeStateV2::size_of()]; + bincode::serialize_into(&mut data[..], stake).unwrap(); + data +} + +#[test] +fn test_all_success() { + let mut env = Env::init(); + + for _ in 0..1000 { + let raw_data: Vec = (0..StakeInterface::size_hint(99).0) + .map(|_| rand::random::()) + .collect(); + let mut unstructured = Unstructured::new(&raw_data); + + let instruction = StakeInterface::arbitrary(&mut unstructured) + .unwrap() + .to_instruction(&mut env); + env.process_success(&instruction); + env.reset(); + } +} From 32f9e91577671638fb8000d34c59d7864c9077c2 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:33:39 -0800 Subject: [PATCH 28/35] cleanup --- program/tests/interface.rs | 262 ++++++------------------------------- 1 file changed, 43 insertions(+), 219 deletions(-) diff --git a/program/tests/interface.rs b/program/tests/interface.rs index b81b5cf..7e3692d 100644 --- a/program/tests/interface.rs +++ b/program/tests/interface.rs @@ -48,25 +48,10 @@ use { test_case::{test_case, test_matrix}, }; -// XXX ok so wow i am going to have to write a lot of shit -// we need a mechanism to create basically arbitrary stake accounts -// this means all states (uninit, init, activating, active, deactivating, deactive) -// we need to be able to make a stake history that gives us partial activation/deactivation -// actually we need to set up stake history ourselves correctly in all cases -// we need to be able to set lockup and authority arbitrarily -// we need helpers to set up with seed pubkeys -// ideally we automatically check missing signer failures -// need to create a vote account... ugh we need to get credits right for DeactivateDelinquent -// for delegate we just need owner, vote account pubkey, and credits (can be 0) -// -// XXX OK i wrote a simple init test -// what to do on monday... i guess go through the stake ixn tests and see what to impl -// main thing we lack is full coverage for lockup and i think a bunch of split edge cases - -// arbitrary, but gives us room to set up activations/deactivations serveral epochs in the past +// arbitrary, gives us room to set up activations/deactivations const EXECUTION_EPOCH: u64 = 8; -// mollusk doesnt charge transaction fees, this is just a convenient source of lamports +// mollusk doesnt charge transaction fees, this is just a convenient source/sink for lamports const PAYER: Pubkey = Pubkey::from_str_const("PAYER11111111111111111111111111111111111111"); const PAYER_BALANCE: u64 = 1_000_000 * LAMPORTS_PER_SOL; @@ -82,7 +67,7 @@ const STAKE_ACCOUNT_BLACK: Pubkey = const STAKE_ACCOUNT_WHITE: Pubkey = Pubkey::from_str_const("WH1TE11111111111111111111111111111111111111"); -// authorities for tests which use separate ones +// separate authorities for two stake accounts const STAKER_BLACK: Pubkey = Pubkey::from_str_const("STAKERBLACK11111111111111111111111111111111"); const WITHDRAWER_BLACK: Pubkey = Pubkey::from_str_const("W1THDRAWERBLACK1111111111111111111111111111"); @@ -90,7 +75,7 @@ const STAKER_WHITE: Pubkey = Pubkey::from_str_const("STAKERWH1TE1111111111111111 const WITHDRAWER_WHITE: Pubkey = Pubkey::from_str_const("W1THDRAWERWH1TE1111111111111111111111111111"); -// authorities for tests which use shared ones +// shared authorities for two stake accounts, clearly distinguished from the above const STAKER_GRAY: Pubkey = Pubkey::from_str_const("STAKERGRAY111111111111111111111111111111111"); const WITHDRAWER_GRAY: Pubkey = Pubkey::from_str_const("W1THDRAWERGRAY11111111111111111111111111111"); @@ -182,164 +167,21 @@ impl Env { } } - // creates a test environment and instruction for a given stake operation - // enum contents are sometimes, but not necessarily, ignored - // the success is trivial, this is mostly to allow exhaustive failure tests - // or to do some post-setup for more meaningful success tests - /* - fn init_for_instruction(stake_instruction: &StakeInstruction) -> (Self, Instruction) { - let mut env = Self::init(); - let minimum_delegation = get_minimum_delegation(); - - let instruction = match stake_instruction { - StakeInstruction::Initialize(_, _) => instruction::initialize( - &STAKE_ACCOUNT_BLACK, - &Authorized { - staker: STAKER_BLACK, - withdrawer: WITHDRAWER_BLACK, - }, - &Lockup::default(), - ), - // TODO lockup - StakeInstruction::Authorize(_, authorize) => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), - minimum_delegation, - ); - - let (old_authority, new_authority) = match authorize { - StakeAuthorize::Staker => (STAKER_BLACK, STAKER_GRAY), - StakeAuthorize::Withdrawer => (WITHDRAWER_BLACK, WITHDRAWER_GRAY), - }; - - instruction::authorize( - &STAKE_ACCOUNT_BLACK, - &old_authority, - &new_authority, - *authorize, - None, - ) - } - // TODO withdrawer - StakeInstruction::DelegateStake => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), - minimum_delegation, - ); - - instruction::delegate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK, &VOTE_ACCOUNT_RED) - } - // TODO amount - StakeInstruction::Split(_) => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &active_stake( - VOTE_ACCOUNT_RED, - STAKE_ACCOUNT_BLACK, - minimum_delegation * 2, - true, - ), - minimum_delegation * 2, - ); - - instruction::split( - &STAKE_ACCOUNT_BLACK, - &STAKER_GRAY, - minimum_delegation, - &STAKE_ACCOUNT_WHITE, - )[2] - .clone() - } - // TODO partial, lockup - StakeInstruction::Withdraw(_) => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &active_stake( - VOTE_ACCOUNT_RED, - STAKE_ACCOUNT_BLACK, - minimum_delegation, - false, - ), - minimum_delegation, - ); - - instruction::withdraw( - &STAKE_ACCOUNT_BLACK, - &WITHDRAWER_BLACK, - &PAYER, - minimum_delegation + STAKE_RENT_EXEMPTION, - None, - ) - } - // TODO withdrawer - StakeInstruction::Deactivate => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &active_stake( - VOTE_ACCOUNT_RED, - STAKE_ACCOUNT_BLACK, - minimum_delegation, - false, - ), - minimum_delegation, - ); - - instruction::deactivate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK) - } - // TODO existing lockup, remove lockup, also hardcoded custodians maybe? - StakeInstruction::SetLockup(_) => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), - minimum_delegation, - ); - - instruction::set_lockup( - &STAKE_ACCOUNT_BLACK, - &LockupArgs { - epoch: Some(EXECUTION_EPOCH * 2), - custodian: Some(Pubkey::new_unique()), - unix_timestamp: None, - }, - &WITHDRAWER_BLACK, - ) - } - // TODO withdrawer - StakeInstruction::Merge => { - env.update_stake( - &STAKE_ACCOUNT_BLACK, - &active_stake( - VOTE_ACCOUNT_RED, - STAKE_ACCOUNT_BLACK, - minimum_delegation, - true, - ), - minimum_delegation, - ); - - env.update_stake( - &STAKE_ACCOUNT_WHITE, - &active_stake( - VOTE_ACCOUNT_RED, - STAKE_ACCOUNT_WHITE, - minimum_delegation, - true, - ), - minimum_delegation, - ); - - instruction::merge(&STAKE_ACCOUNT_WHITE, &STAKE_ACCOUNT_BLACK, &STAKER_GRAY)[0] - .clone() - } - // TODO move, checked, seed, deactivate delinquent, minimum, redelegate - _ => todo!(), - }; - - (env, instruction) + // set up one of the preconfigured blank stake accounts at some starting state + // to mutate the accounts after initial setup, do it directly or execute instructions + // note these accounts are already rent exempt, so lamports specified are stake or extra + fn update_stake( + &mut self, + pubkey: &Pubkey, + stake_state: &StakeStateV2, + additional_lamports: u64, + ) { + assert!(*pubkey == STAKE_ACCOUNT_BLACK || *pubkey == STAKE_ACCOUNT_WHITE); + let stake_account = self.override_accounts.get_mut(pubkey).unwrap(); + let current_lamports = stake_account.lamports(); + stake_account.set_lamports(current_lamports + additional_lamports); + bincode::serialize_into(stake_account.data_as_mut_slice(), stake_state).unwrap(); } - */ // get the accounts from our account store that this transaction expects to see // we dont need implicit sysvars, mollusk resolves them internally via syscall stub @@ -380,64 +222,45 @@ impl Env { accounts } - // set up one of the preconfigured blank stake accounts at some starting state - // to mutate the accounts after initial setup, do it directly or execute instructions - // note these accounts are already rent exempt, so lamports specified are stake or extra - fn update_stake( - &mut self, - pubkey: &Pubkey, - stake_state: &StakeStateV2, - additional_lamports: u64, - ) { - assert!(*pubkey == STAKE_ACCOUNT_BLACK || *pubkey == STAKE_ACCOUNT_WHITE); - let stake_account = self.override_accounts.get_mut(pubkey).unwrap(); - let current_lamports = stake_account.lamports(); - stake_account.set_lamports(current_lamports + additional_lamports); - bincode::serialize_into(stake_account.data_as_mut_slice(), stake_state).unwrap(); - } - - fn reset(&mut self) { - self.override_accounts.clear() - } - - /* XXX - // process an instruction, assert checks, and update internal accounts - // XXX dont think i need to mutate accounts, im doing all one-off - fn process(&mut self, instruction: &Instruction, checks: &[Check]) { - let initial_accounts = self.resolve_accounts(&instruction.accounts); - - let result = - self.mollusk - .process_and_validate_instruction(instruction, &initial_accounts, checks); - - for (i, resulting_account) in result.resulting_accounts.into_iter().enumerate() { - let account_meta = &instruction.accounts[i]; - assert_eq!(account_meta.pubkey, resulting_account.0); - if account_meta.is_writable { - if resulting_account.1.lamports() == 0 { - self.accounts.remove(&resulting_account.0); - } else { - self.accounts - .insert(resulting_account.0, resulting_account.1); - } + // process an instruction, assert checks, and update override accounts + fn process(&mut self, instruction: &Instruction, checks: &[Check]) { + let initial_accounts = self.resolve_accounts(&instruction.accounts); + + let result = + self.mollusk + .process_and_validate_instruction(instruction, &initial_accounts, checks); + + for (i, resulting_account) in result.resulting_accounts.into_iter().enumerate() { + let account_meta = &instruction.accounts[i]; + assert_eq!(account_meta.pubkey, resulting_account.0); + if account_meta.is_writable { + if resulting_account.1.lamports() == 0 { + self.override_accounts.remove(&resulting_account.0); + } else { + self.override_accounts + .insert(resulting_account.0, resulting_account.1); } } } - */ + } - // shorthand for process with only a success check + // immutable process with only a success check fn process_success(&self, instruction: &Instruction) { let accounts = self.resolve_accounts(&instruction.accounts); self.mollusk .process_and_validate_instruction(instruction, &accounts, &[Check::success()]); } - // shorthand for process with an expected error + // immutable process with an expected error fn process_fail(&self, instruction: &Instruction, error: ProgramError) { let accounts = self.resolve_accounts(&instruction.accounts); self.mollusk .process_and_validate_instruction(instruction, &accounts, &[Check::err(error)]); } + + fn reset(&mut self) { + self.override_accounts.clear() + } } #[derive(Debug, Clone, PartialEq, Eq, Arbitrary)] @@ -755,6 +578,7 @@ fn test_all_success() { let instruction = StakeInterface::arbitrary(&mut unstructured) .unwrap() .to_instruction(&mut env); + env.process_success(&instruction); env.reset(); } From 4e8be5ecfb16a28ddd7984d05f73fcfe35ded042 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:34:12 -0800 Subject: [PATCH 29/35] three instructions work --- program/tests/interface.rs | 132 +++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 50 deletions(-) diff --git a/program/tests/interface.rs b/program/tests/interface.rs index 7e3692d..2204899 100644 --- a/program/tests/interface.rs +++ b/program/tests/interface.rs @@ -5,7 +5,7 @@ use { arbitrary::{Arbitrary, Unstructured}, mollusk_svm::{result::Check, Mollusk}, - solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, + solana_account::{Account, AccountSharedData, ReadableAccount, WritableAccount}, solana_sdk::{ account::Account as SolanaAccount, address_lookup_table, bpf_loader_upgradeable, @@ -110,8 +110,8 @@ fn assert_stake_rent_exemption() { // doing this we let base_accounts be immutable and can set and clear override_accounts struct Env { mollusk: Mollusk, - base_accounts: HashMap, - override_accounts: HashMap, + base_accounts: HashMap, + override_accounts: HashMap, } impl Env { // set up a test environment with valid stake history, two vote accounts, and two blank stake accounts @@ -119,7 +119,7 @@ impl Env { // create a test environment at the execution epoch let mut base_accounts = HashMap::new(); let mut mollusk = Mollusk::new(&id(), "solana_stake_program"); - solana_logger::setup_with(""); + // XXX solana_logger::setup_with(""); mollusk.warp_to_slot(EXECUTION_EPOCH * mollusk.sysvars.epoch_schedule.slots_per_epoch + 1); assert_eq!(mollusk.sysvars.clock.epoch, EXECUTION_EPOCH); @@ -132,33 +132,34 @@ impl Env { } // add a lamports source - let payer_data = - AccountSharedData::new_rent_epoch(PAYER_BALANCE, 0, &system_program::id(), u64::MAX); - base_accounts.insert(PAYER, payer_data); + let payer_account = + Account::new_rent_epoch(PAYER_BALANCE, 0, &system_program::id(), u64::MAX); + base_accounts.insert(PAYER, payer_account); // create two vote accounts let vote_rent_exemption = Rent::default().minimum_balance(VoteState::size_of()); - let vote_state = bincode::serialize(&VoteState::default()).unwrap(); - let vote_data = AccountSharedData::create( + let vote_state_versions = VoteStateVersions::new_current(VoteState::default()); + let vote_data = bincode::serialize(&vote_state_versions).unwrap(); + let vote_account = Account::create( vote_rent_exemption, - vote_state, + vote_data, vote_program::id(), false, u64::MAX, ); - base_accounts.insert(VOTE_ACCOUNT_RED, vote_data.clone()); - base_accounts.insert(VOTE_ACCOUNT_BLUE, vote_data); + base_accounts.insert(VOTE_ACCOUNT_RED, vote_account.clone()); + base_accounts.insert(VOTE_ACCOUNT_BLUE, vote_account); // create two blank stake accounts - let stake_data = AccountSharedData::create( + let stake_account = Account::create( STAKE_RENT_EXEMPTION, vec![0; StakeStateV2::size_of()], id(), false, u64::MAX, ); - base_accounts.insert(STAKE_ACCOUNT_BLACK, stake_data.clone()); - base_accounts.insert(STAKE_ACCOUNT_WHITE, stake_data); + base_accounts.insert(STAKE_ACCOUNT_BLACK, stake_account.clone()); + base_accounts.insert(STAKE_ACCOUNT_WHITE, stake_account); Self { mollusk, @@ -177,10 +178,18 @@ impl Env { additional_lamports: u64, ) { assert!(*pubkey == STAKE_ACCOUNT_BLACK || *pubkey == STAKE_ACCOUNT_WHITE); - let stake_account = self.override_accounts.get_mut(pubkey).unwrap(); + + let mut stake_account = if let Some(stake_account) = self.override_accounts.get(pubkey) { + stake_account.clone() + } else { + self.base_accounts.get(pubkey).cloned().unwrap() + }; + let current_lamports = stake_account.lamports(); stake_account.set_lamports(current_lamports + additional_lamports); bincode::serialize_into(stake_account.data_as_mut_slice(), stake_state).unwrap(); + + self.override_accounts.insert(*pubkey, stake_account); } // get the accounts from our account store that this transaction expects to see @@ -209,9 +218,9 @@ impl Env { .keyed_account_for_stake_history_sysvar() .1 } else if let Some(account) = self.override_accounts.get(&key).cloned() { - account + account.into() } else if let Some(account) = self.base_accounts.get(&key).cloned() { - account + account.into() } else { AccountSharedData::default() }; @@ -222,31 +231,34 @@ impl Env { accounts } - // process an instruction, assert checks, and update override accounts - fn process(&mut self, instruction: &Instruction, checks: &[Check]) { - let initial_accounts = self.resolve_accounts(&instruction.accounts); - - let result = - self.mollusk - .process_and_validate_instruction(instruction, &initial_accounts, checks); - - for (i, resulting_account) in result.resulting_accounts.into_iter().enumerate() { - let account_meta = &instruction.accounts[i]; - assert_eq!(account_meta.pubkey, resulting_account.0); - if account_meta.is_writable { - if resulting_account.1.lamports() == 0 { - self.override_accounts.remove(&resulting_account.0); - } else { - self.override_accounts - .insert(resulting_account.0, resulting_account.1); + /* XXX + // process an instruction, assert checks, and update override accounts + fn process(&mut self, instruction: &Instruction, checks: &[Check]) { + let initial_accounts = self.resolve_accounts(&instruction.accounts); + + let result = + self.mollusk + .process_and_validate_instruction(instruction, &initial_accounts, checks); + + for (i, resulting_account) in result.resulting_accounts.into_iter().enumerate() { + let account_meta = &instruction.accounts[i]; + assert_eq!(account_meta.pubkey, resulting_account.0); + if account_meta.is_writable { + if resulting_account.1.lamports() == 0 { + self.override_accounts.remove(&resulting_account.0); + } else { + self.override_accounts + .insert(resulting_account.0, resulting_account.1); + } } } } - } + */ // immutable process with only a success check fn process_success(&self, instruction: &Instruction) { let accounts = self.resolve_accounts(&instruction.accounts); + //println!("HANA ixn: {:#?}\n accts: {:#?}\n hm1: {:#?}\n hm2: {:#?}", instruction, accounts, self.base_accounts, self.override_accounts); self.mollusk .process_and_validate_instruction(instruction, &accounts, &[Check::success()]); } @@ -267,18 +279,24 @@ impl Env { enum StakeInterface { Initialize(LockupState), Authorize(AuthorityType, LockupState), + DelegateStake(AuthorityType), /* - DelegateStake(StakeAuthorize), Split(AmountFraction), Withdraw(LockupState, AmountFraction), - Deactivate(StakeAuthorize), + Deactivate(AuthorityType), SetLockup(LockupState, LockupState), - Merge(StakeAuthorize), + Merge(AuthorityType), */ // TODO move, checked, seed, deactivate delinquent, minimum, redelegate } impl StakeInterface { + // unfortunately `size_hint()` is useless + // we substantially overshoot to avoid mistakes + fn max_size() -> usize { + 32 + } + // creates an instruction with the given combination of settings that is guaranteed to succeed fn to_instruction(&self, env: &mut Env) -> Instruction { let minimum_delegation = get_minimum_delegation(); @@ -318,19 +336,21 @@ impl StakeInterface { lockup_state.to_custodian(&CUSTODIAN_LEFT), ) } - /* - // TODO withdrawer - StakeInstruction::DelegateStake => { + // XXX FIXME withdrawer doesnt work, working as intended? also maybe add lockup + Self::DelegateStake(_authority_type) => { env.update_stake( &STAKE_ACCOUNT_BLACK, &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), minimum_delegation, ); + // XXX let authority = authority_type.pubkey(STAKE_ACCOUNT_BLACK); + instruction::delegate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK, &VOTE_ACCOUNT_RED) } + /* // TODO amount - StakeInstruction::Split(_) => { + Self::Split(_) => { env.update_stake( &STAKE_ACCOUNT_BLACK, &active_stake( @@ -351,7 +371,7 @@ impl StakeInterface { .clone() } // TODO partial, lockup - StakeInstruction::Withdraw(_) => { + Self::Withdraw(_) => { env.update_stake( &STAKE_ACCOUNT_BLACK, &active_stake( @@ -372,7 +392,7 @@ impl StakeInterface { ) } // TODO withdrawer - StakeInstruction::Deactivate => { + Self::Deactivate => { env.update_stake( &STAKE_ACCOUNT_BLACK, &active_stake( @@ -387,7 +407,7 @@ impl StakeInterface { instruction::deactivate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK) } // TODO existing lockup, remove lockup, also hardcoded custodians maybe? - StakeInstruction::SetLockup(_) => { + Self::SetLockup(_) => { env.update_stake( &STAKE_ACCOUNT_BLACK, &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), @@ -405,7 +425,7 @@ impl StakeInterface { ) } // TODO withdrawer - StakeInstruction::Merge => { + Self::Merge => { env.update_stake( &STAKE_ACCOUNT_BLACK, &active_stake( @@ -444,6 +464,18 @@ enum AuthorityType { Withdrawer, } +impl AuthorityType { + fn pubkey(&self, stake_pubkey: Pubkey) -> Pubkey { + match (stake_pubkey, self) { + (STAKE_ACCOUNT_BLACK, Self::Staker) => STAKER_BLACK, + (STAKE_ACCOUNT_BLACK, Self::Withdrawer) => WITHDRAWER_BLACK, + (STAKE_ACCOUNT_WHITE, Self::Staker) => STAKER_WHITE, + (STAKE_ACCOUNT_WHITE, Self::Withdrawer) => WITHDRAWER_WHITE, + _ => panic!("expected a hardcoded stake pubkey, got {}", stake_pubkey), + } + } +} + impl From<&AuthorityType> for StakeAuthorize { fn from(authority_type: &AuthorityType) -> Self { match authority_type { @@ -535,7 +567,7 @@ fn i_cant_believe_its_not_stake( staker: STAKER_WHITE, withdrawer: WITHDRAWER_WHITE, }, - _ => Authorized::default(), + _ => panic!("expected a hardcoded stake pubkey, got {}", stake_pubkey), }; let activation_epoch = if is_active { EXECUTION_EPOCH - 1 } else { 0 }; @@ -569,8 +601,8 @@ fn stake_to_bytes(stake: &StakeStateV2) -> Vec { fn test_all_success() { let mut env = Env::init(); - for _ in 0..1000 { - let raw_data: Vec = (0..StakeInterface::size_hint(99).0) + for _ in 0..10 { + let raw_data: Vec = (0..StakeInterface::max_size()) .map(|_| rand::random::()) .collect(); let mut unstructured = Unstructured::new(&raw_data); From a8e2e16984bdb7fbb625a364fa11f605ba55393e Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Sat, 14 Dec 2024 06:02:20 -0800 Subject: [PATCH 30/35] instead of generating randomly, make exhaustive list --- program/tests/interface.rs | 58 +++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/program/tests/interface.rs b/program/tests/interface.rs index 2204899..7b74ba4 100644 --- a/program/tests/interface.rs +++ b/program/tests/interface.rs @@ -44,7 +44,11 @@ use { }, }, solana_stake_program::{get_minimum_delegation, id, processor::Processor}, - std::{collections::HashMap, fs}, + std::{ + collections::{HashMap, HashSet}, + fs, + sync::LazyLock, + }, test_case::{test_case, test_matrix}, }; @@ -106,6 +110,22 @@ fn assert_stake_rent_exemption() { ); } +// exhaustive set of all test instruction declarations +// this is probabalistic but should exceed ten nines +// implementing it by hand would be extremely annoying +static INSTRUCTION_DECLARATIONS: LazyLock> = LazyLock::new(|| { + let mut declarations = HashSet::new(); + for _ in 0..10_000 { + let raw_data: Vec = (0..StakeInterface::max_size()) + .map(|_| rand::random::()) + .collect(); + let mut unstructured = Unstructured::new(&raw_data); + declarations.insert(StakeInterface::arbitrary(&mut unstructured).unwrap()); + } + + declarations +}); + // we use two hashmaps because cloning mollusk is impossible and creating it is expensive // doing this we let base_accounts be immutable and can set and clear override_accounts struct Env { @@ -119,7 +139,7 @@ impl Env { // create a test environment at the execution epoch let mut base_accounts = HashMap::new(); let mut mollusk = Mollusk::new(&id(), "solana_stake_program"); - // XXX solana_logger::setup_with(""); + solana_logger::setup_with(""); mollusk.warp_to_slot(EXECUTION_EPOCH * mollusk.sysvars.epoch_schedule.slots_per_epoch + 1); assert_eq!(mollusk.sysvars.clock.epoch, EXECUTION_EPOCH); @@ -275,11 +295,11 @@ impl Env { } } -#[derive(Debug, Clone, PartialEq, Eq, Arbitrary)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Arbitrary)] enum StakeInterface { Initialize(LockupState), Authorize(AuthorityType, LockupState), - DelegateStake(AuthorityType), + DelegateStake(LockupState), /* Split(AmountFraction), Withdraw(LockupState, AmountFraction), @@ -336,16 +356,18 @@ impl StakeInterface { lockup_state.to_custodian(&CUSTODIAN_LEFT), ) } - // XXX FIXME withdrawer doesnt work, working as intended? also maybe add lockup - Self::DelegateStake(_authority_type) => { + Self::DelegateStake(lockup_state) => { env.update_stake( &STAKE_ACCOUNT_BLACK, - &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), + ¬_just_stake( + STAKE_ACCOUNT_BLACK, + minimum_delegation, + false, + lockup_state.to_lockup(CUSTODIAN_LEFT), + ), minimum_delegation, ); - // XXX let authority = authority_type.pubkey(STAKE_ACCOUNT_BLACK); - instruction::delegate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK, &VOTE_ACCOUNT_RED) } /* @@ -458,7 +480,7 @@ impl StakeInterface { } } -#[derive(Debug, Clone, PartialEq, Eq, Arbitrary)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Arbitrary)] enum AuthorityType { Staker, Withdrawer, @@ -485,7 +507,7 @@ impl From<&AuthorityType> for StakeAuthorize { } } -#[derive(Debug, Clone, PartialEq, Eq, Arbitrary)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Arbitrary)] enum LockupState { Active, Inactive, @@ -517,7 +539,7 @@ impl LockupState { } } -#[derive(Debug, Clone, PartialEq, Eq, Arbitrary)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Arbitrary)] enum AmountFraction { Partial, Full, @@ -601,16 +623,8 @@ fn stake_to_bytes(stake: &StakeStateV2) -> Vec { fn test_all_success() { let mut env = Env::init(); - for _ in 0..10 { - let raw_data: Vec = (0..StakeInterface::max_size()) - .map(|_| rand::random::()) - .collect(); - let mut unstructured = Unstructured::new(&raw_data); - - let instruction = StakeInterface::arbitrary(&mut unstructured) - .unwrap() - .to_instruction(&mut env); - + for declaration in &*INSTRUCTION_DECLARATIONS { + let instruction = declaration.to_instruction(&mut env); env.process_success(&instruction); env.reset(); } From 8e490007ed1d34dbce24aa42e5b2b360146d78a1 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Sat, 14 Dec 2024 09:22:41 -0800 Subject: [PATCH 31/35] add back all current instructions --- program/tests/interface.rs | 232 ++++++++++++++++++++++++++----------- 1 file changed, 164 insertions(+), 68 deletions(-) diff --git a/program/tests/interface.rs b/program/tests/interface.rs index 7b74ba4..33e35a6 100644 --- a/program/tests/interface.rs +++ b/program/tests/interface.rs @@ -111,7 +111,7 @@ fn assert_stake_rent_exemption() { } // exhaustive set of all test instruction declarations -// this is probabalistic but should exceed ten nines +// this is probabilistic but should exceed ten nines // implementing it by hand would be extremely annoying static INSTRUCTION_DECLARATIONS: LazyLock> = LazyLock::new(|| { let mut declarations = HashSet::new(); @@ -139,7 +139,6 @@ impl Env { // create a test environment at the execution epoch let mut base_accounts = HashMap::new(); let mut mollusk = Mollusk::new(&id(), "solana_stake_program"); - solana_logger::setup_with(""); mollusk.warp_to_slot(EXECUTION_EPOCH * mollusk.sysvars.epoch_schedule.slots_per_epoch + 1); assert_eq!(mollusk.sysvars.clock.epoch, EXECUTION_EPOCH); @@ -278,7 +277,6 @@ impl Env { // immutable process with only a success check fn process_success(&self, instruction: &Instruction) { let accounts = self.resolve_accounts(&instruction.accounts); - //println!("HANA ixn: {:#?}\n accts: {:#?}\n hm1: {:#?}\n hm2: {:#?}", instruction, accounts, self.base_accounts, self.override_accounts); self.mollusk .process_and_validate_instruction(instruction, &accounts, &[Check::success()]); } @@ -295,18 +293,16 @@ impl Env { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Arbitrary)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)] enum StakeInterface { Initialize(LockupState), Authorize(AuthorityType, LockupState), DelegateStake(LockupState), - /* - Split(AmountFraction), - Withdraw(LockupState, AmountFraction), - Deactivate(AuthorityType), + Split(LockupState, AmountFraction), + Withdraw(SimpleStakeStatus, LockupState, AmountFraction), + Deactivate(LockupState), SetLockup(LockupState, LockupState), - Merge(AuthorityType), - */ + Merge(LockupState), // TODO move, checked, seed, deactivate delinquent, minimum, redelegate } @@ -370,117 +366,172 @@ impl StakeInterface { instruction::delegate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK, &VOTE_ACCOUNT_RED) } - /* - // TODO amount - Self::Split(_) => { + Self::Split(lockup_state, amount_fraction) => { + let delegated_stake = minimum_delegation * 2; + let split_amount = match amount_fraction { + AmountFraction::Partial => delegated_stake / 2, + AmountFraction::Full => delegated_stake + STAKE_RENT_EXEMPTION, + }; + env.update_stake( &STAKE_ACCOUNT_BLACK, - &active_stake( + &i_cant_believe_its_not_stake( VOTE_ACCOUNT_RED, STAKE_ACCOUNT_BLACK, - minimum_delegation * 2, + delegated_stake, + StakeStatus::Active, true, + lockup_state.to_lockup(CUSTODIAN_LEFT), ), - minimum_delegation * 2, + delegated_stake, ); instruction::split( &STAKE_ACCOUNT_BLACK, &STAKER_GRAY, - minimum_delegation, + split_amount, &STAKE_ACCOUNT_WHITE, - )[2] - .clone() + ) + .remove(2) } - // TODO partial, lockup - Self::Withdraw(_) => { + Self::Withdraw(simple_status, lockup_state, amount_fraction) => { + let status = simple_status.into(); + let free_lamports = LAMPORTS_PER_SOL; + env.update_stake( &STAKE_ACCOUNT_BLACK, - &active_stake( + &i_cant_believe_its_not_stake( VOTE_ACCOUNT_RED, STAKE_ACCOUNT_BLACK, minimum_delegation, + status, false, + lockup_state.to_lockup(CUSTODIAN_LEFT), ), - minimum_delegation, + minimum_delegation + free_lamports, ); + let withdraw_amount = match amount_fraction { + AmountFraction::Full if status != StakeStatus::Active => { + free_lamports + minimum_delegation + STAKE_RENT_EXEMPTION + } + _ => free_lamports, + }; + + let authority = if status == StakeStatus::Uninitialized { + STAKE_ACCOUNT_BLACK + } else { + WITHDRAWER_BLACK + }; + instruction::withdraw( &STAKE_ACCOUNT_BLACK, - &WITHDRAWER_BLACK, + &authority, &PAYER, - minimum_delegation + STAKE_RENT_EXEMPTION, - None, + withdraw_amount, + lockup_state.to_custodian(&CUSTODIAN_LEFT), ) } - // TODO withdrawer - Self::Deactivate => { + Self::Deactivate(lockup_state) => { env.update_stake( &STAKE_ACCOUNT_BLACK, - &active_stake( + &i_cant_believe_its_not_stake( VOTE_ACCOUNT_RED, STAKE_ACCOUNT_BLACK, minimum_delegation, + StakeStatus::Active, false, + lockup_state.to_lockup(CUSTODIAN_LEFT), ), minimum_delegation, ); instruction::deactivate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK) } - // TODO existing lockup, remove lockup, also hardcoded custodians maybe? - Self::SetLockup(_) => { + Self::SetLockup(existing_lockup_state, new_lockup_state) => { env.update_stake( &STAKE_ACCOUNT_BLACK, - &just_stake(STAKE_ACCOUNT_BLACK, minimum_delegation), + ¬_just_stake( + STAKE_ACCOUNT_BLACK, + minimum_delegation, + false, + existing_lockup_state.to_lockup(CUSTODIAN_LEFT), + ), minimum_delegation, ); instruction::set_lockup( &STAKE_ACCOUNT_BLACK, - &LockupArgs { - epoch: Some(EXECUTION_EPOCH * 2), - custodian: Some(Pubkey::new_unique()), - unix_timestamp: None, - }, - &WITHDRAWER_BLACK, + &new_lockup_state.to_args(CUSTODIAN_RIGHT), + existing_lockup_state + .to_custodian(&CUSTODIAN_LEFT) + .unwrap_or(&WITHDRAWER_BLACK), ) } - // TODO withdrawer - Self::Merge => { + Self::Merge(lockup_state) => { env.update_stake( &STAKE_ACCOUNT_BLACK, - &active_stake( + &i_cant_believe_its_not_stake( VOTE_ACCOUNT_RED, STAKE_ACCOUNT_BLACK, minimum_delegation, + StakeStatus::Active, true, + lockup_state.to_lockup(CUSTODIAN_LEFT), ), minimum_delegation, ); env.update_stake( &STAKE_ACCOUNT_WHITE, - &active_stake( + &i_cant_believe_its_not_stake( VOTE_ACCOUNT_RED, STAKE_ACCOUNT_WHITE, minimum_delegation, + StakeStatus::Active, true, + lockup_state.to_lockup(CUSTODIAN_LEFT), ), minimum_delegation, ); - instruction::merge(&STAKE_ACCOUNT_WHITE, &STAKE_ACCOUNT_BLACK, &STAKER_GRAY)[0] - .clone() + instruction::merge(&STAKE_ACCOUNT_WHITE, &STAKE_ACCOUNT_BLACK, &STAKER_GRAY) + .remove(0) } - */ // TODO move, checked, seed, deactivate delinquent, minimum, redelegate _ => todo!(), } } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Arbitrary)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)] +enum StakeStatus { + Uninitialized, + Initialized, + Activating, + Active, + Deactivating, + Deactive, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)] +enum SimpleStakeStatus { + Uninitialized, + Initialized, + Active, +} + +impl From<&SimpleStakeStatus> for StakeStatus { + fn from(simple_status: &SimpleStakeStatus) -> Self { + match simple_status { + SimpleStakeStatus::Uninitialized => Self::Uninitialized, + SimpleStakeStatus::Initialized => Self::Initialized, + SimpleStakeStatus::Active => Self::Active, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)] enum AuthorityType { Staker, Withdrawer, @@ -507,7 +558,7 @@ impl From<&AuthorityType> for StakeAuthorize { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Arbitrary)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)] enum LockupState { Active, Inactive, @@ -533,22 +584,35 @@ impl LockupState { fn to_custodian<'a>(&self, custodian: &'a Pubkey) -> Option<&'a Pubkey> { match self { - Self::None => None, - _ => Some(custodian), + Self::Active => Some(custodian), + _ => None, + } + } + + fn to_args(&self, custodian: Pubkey) -> LockupArgs { + match self { + Self::None => LockupArgs::default(), + _ => LockupArgs { + custodian: self.to_custodian(&custodian).cloned(), + epoch: Some(self.to_lockup(custodian).epoch), + unix_timestamp: None, + }, } } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Arbitrary)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Arbitrary)] enum AmountFraction { Partial, Full, } +// initialized with appropriate authority, no lockup fn just_stake(stake_pubkey: Pubkey, stake: u64) -> StakeStateV2 { not_just_stake(stake_pubkey, stake, false, Lockup::default()) } +// initialized with settable authority and lockup fn not_just_stake( stake_pubkey: Pubkey, stake: u64, @@ -559,19 +623,22 @@ fn not_just_stake( Pubkey::default(), stake_pubkey, stake, + StakeStatus::Initialized, common_authority, lockup, - false, ) } +// XXX FIXME for inactive lockup we should NOT sign + +// any point in the stake lifecycle with settable vote account, authority, and lockup fn i_cant_believe_its_not_stake( voter_pubkey: Pubkey, stake_pubkey: Pubkey, stake: u64, + stake_status: StakeStatus, common_authority: bool, lockup: Lockup, - is_active: bool, ) -> StakeStateV2 { assert!(stake_pubkey != VOTE_ACCOUNT_RED); assert!(stake_pubkey != VOTE_ACCOUNT_BLUE); @@ -592,25 +659,54 @@ fn i_cant_believe_its_not_stake( _ => panic!("expected a hardcoded stake pubkey, got {}", stake_pubkey), }; - let activation_epoch = if is_active { EXECUTION_EPOCH - 1 } else { 0 }; + let meta = Meta { + rent_exempt_reserve: STAKE_RENT_EXEMPTION, + authorized, + lockup, + }; - StakeStateV2::Stake( - Meta { - rent_exempt_reserve: STAKE_RENT_EXEMPTION, - authorized, - lockup, + let delegation = match stake_status { + StakeStatus::Uninitialized | StakeStatus::Initialized => Delegation::default(), + StakeStatus::Activating => Delegation { + stake, + voter_pubkey, + activation_epoch: EXECUTION_EPOCH, + ..Delegation::default() }, - Stake { - delegation: Delegation { - stake, - voter_pubkey, - activation_epoch, - ..Delegation::default() - }, - ..Stake::default() + StakeStatus::Active => Delegation { + stake, + voter_pubkey, + activation_epoch: EXECUTION_EPOCH - 1, + ..Delegation::default() }, - StakeFlags::empty(), - ) + StakeStatus::Deactivating => Delegation { + stake, + voter_pubkey, + activation_epoch: EXECUTION_EPOCH - 1, + deactivation_epoch: EXECUTION_EPOCH, + ..Delegation::default() + }, + StakeStatus::Deactive => Delegation { + stake, + voter_pubkey, + activation_epoch: EXECUTION_EPOCH - 2, + deactivation_epoch: EXECUTION_EPOCH - 1, + ..Delegation::default() + }, + }; + + match stake_status { + StakeStatus::Uninitialized => StakeStateV2::Uninitialized, + StakeStatus::Initialized => StakeStateV2::Initialized(meta), + _ => StakeStateV2::Stake( + meta, + Stake { + delegation, + ..Stake::default() + }, + StakeFlags::empty(), + ), + } } fn stake_to_bytes(stake: &StakeStateV2) -> Vec { From 8996d484947e1d799d2f5bb846ba0cd73b3e56f8 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Sat, 14 Dec 2024 09:54:40 -0800 Subject: [PATCH 32/35] check all signers --- program/tests/interface.rs | 55 +++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/program/tests/interface.rs b/program/tests/interface.rs index 33e35a6..748edb5 100644 --- a/program/tests/interface.rs +++ b/program/tests/interface.rs @@ -250,42 +250,18 @@ impl Env { accounts } - /* XXX - // process an instruction, assert checks, and update override accounts - fn process(&mut self, instruction: &Instruction, checks: &[Check]) { - let initial_accounts = self.resolve_accounts(&instruction.accounts); - - let result = - self.mollusk - .process_and_validate_instruction(instruction, &initial_accounts, checks); - - for (i, resulting_account) in result.resulting_accounts.into_iter().enumerate() { - let account_meta = &instruction.accounts[i]; - assert_eq!(account_meta.pubkey, resulting_account.0); - if account_meta.is_writable { - if resulting_account.1.lamports() == 0 { - self.override_accounts.remove(&resulting_account.0); - } else { - self.override_accounts - .insert(resulting_account.0, resulting_account.1); - } - } - } - } - */ - - // immutable process with only a success check + // immutable process that should succeed fn process_success(&self, instruction: &Instruction) { let accounts = self.resolve_accounts(&instruction.accounts); self.mollusk .process_and_validate_instruction(instruction, &accounts, &[Check::success()]); } - // immutable process with an expected error - fn process_fail(&self, instruction: &Instruction, error: ProgramError) { + // immutable process that should fail + fn process_fail(&self, instruction: &Instruction) { let accounts = self.resolve_accounts(&instruction.accounts); - self.mollusk - .process_and_validate_instruction(instruction, &accounts, &[Check::err(error)]); + let result = self.mollusk.process_instruction(instruction, &accounts); + assert!(result.program_result.is_err()); } fn reset(&mut self) { @@ -629,8 +605,6 @@ fn not_just_stake( ) } -// XXX FIXME for inactive lockup we should NOT sign - // any point in the stake lifecycle with settable vote account, authority, and lockup fn i_cant_believe_its_not_stake( voter_pubkey: Pubkey, @@ -725,3 +699,22 @@ fn test_all_success() { env.reset(); } } + +#[test] +fn test_no_signer_bypass() { + let mut env = Env::init(); + + for declaration in &*INSTRUCTION_DECLARATIONS { + let instruction = declaration.to_instruction(&mut env); + for i in 0..instruction.accounts.len() { + if !instruction.accounts[i].is_signer { + continue; + } + + let mut instruction = instruction.clone(); + instruction.accounts[i].is_signer = false; + env.process_fail(&instruction); + env.reset(); + } + } +} From c4bf1037de63beb08d9425c6acdec8d447cb0d40 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Mon, 16 Dec 2024 08:58:32 -0800 Subject: [PATCH 33/35] dont need special enum for ternary --- program/tests/interface.rs | 39 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/program/tests/interface.rs b/program/tests/interface.rs index 748edb5..29714e5 100644 --- a/program/tests/interface.rs +++ b/program/tests/interface.rs @@ -275,7 +275,7 @@ enum StakeInterface { Authorize(AuthorityType, LockupState), DelegateStake(LockupState), Split(LockupState, AmountFraction), - Withdraw(SimpleStakeStatus, LockupState, AmountFraction), + Withdraw(Option, LockupState, AmountFraction), Deactivate(LockupState), SetLockup(LockupState, LockupState), Merge(LockupState), @@ -290,7 +290,7 @@ impl StakeInterface { } // creates an instruction with the given combination of settings that is guaranteed to succeed - fn to_instruction(&self, env: &mut Env) -> Instruction { + fn to_instruction(self, env: &mut Env) -> Instruction { let minimum_delegation = get_minimum_delegation(); match self { @@ -370,9 +370,13 @@ impl StakeInterface { ) .remove(2) } - Self::Withdraw(simple_status, lockup_state, amount_fraction) => { - let status = simple_status.into(); + Self::Withdraw(source_has_delegation, lockup_state, amount_fraction) => { let free_lamports = LAMPORTS_PER_SOL; + let status = match source_has_delegation { + None => StakeStatus::Uninitialized, + Some(false) => StakeStatus::Initialized, + Some(true) => StakeStatus::Active, + }; env.update_stake( &STAKE_ACCOUNT_BLACK, @@ -490,23 +494,6 @@ enum StakeStatus { Deactive, } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)] -enum SimpleStakeStatus { - Uninitialized, - Initialized, - Active, -} - -impl From<&SimpleStakeStatus> for StakeStatus { - fn from(simple_status: &SimpleStakeStatus) -> Self { - match simple_status { - SimpleStakeStatus::Uninitialized => Self::Uninitialized, - SimpleStakeStatus::Initialized => Self::Initialized, - SimpleStakeStatus::Active => Self::Active, - } - } -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)] enum AuthorityType { Staker, @@ -525,8 +512,8 @@ impl AuthorityType { } } -impl From<&AuthorityType> for StakeAuthorize { - fn from(authority_type: &AuthorityType) -> Self { +impl From for StakeAuthorize { + fn from(authority_type: AuthorityType) -> Self { match authority_type { AuthorityType::Staker => Self::Staker, AuthorityType::Withdrawer => Self::Withdrawer, @@ -542,7 +529,7 @@ enum LockupState { } impl LockupState { - fn to_lockup(&self, custodian: Pubkey) -> Lockup { + fn to_lockup(self, custodian: Pubkey) -> Lockup { match self { Self::Active => Lockup { custodian, @@ -558,14 +545,14 @@ impl LockupState { } } - fn to_custodian<'a>(&self, custodian: &'a Pubkey) -> Option<&'a Pubkey> { + fn to_custodian(self, custodian: &Pubkey) -> Option<&Pubkey> { match self { Self::Active => Some(custodian), _ => None, } } - fn to_args(&self, custodian: Pubkey) -> LockupArgs { + fn to_args(self, custodian: Pubkey) -> LockupArgs { match self { Self::None => LockupArgs::default(), _ => LockupArgs { From f44222c57428eca31ce0ce349bc838c83042871f Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Mon, 16 Dec 2024 09:01:59 -0800 Subject: [PATCH 34/35] dont need that either --- program/tests/interface.rs | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/program/tests/interface.rs b/program/tests/interface.rs index 29714e5..660fd20 100644 --- a/program/tests/interface.rs +++ b/program/tests/interface.rs @@ -274,8 +274,8 @@ enum StakeInterface { Initialize(LockupState), Authorize(AuthorityType, LockupState), DelegateStake(LockupState), - Split(LockupState, AmountFraction), - Withdraw(Option, LockupState, AmountFraction), + Split(LockupState, bool), + Withdraw(Option, LockupState, bool), Deactivate(LockupState), SetLockup(LockupState, LockupState), Merge(LockupState), @@ -342,11 +342,12 @@ impl StakeInterface { instruction::delegate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK, &VOTE_ACCOUNT_RED) } - Self::Split(lockup_state, amount_fraction) => { + Self::Split(lockup_state, full_split) => { let delegated_stake = minimum_delegation * 2; - let split_amount = match amount_fraction { - AmountFraction::Partial => delegated_stake / 2, - AmountFraction::Full => delegated_stake + STAKE_RENT_EXEMPTION, + let split_amount = if full_split { + delegated_stake + STAKE_RENT_EXEMPTION + } else { + delegated_stake / 2 }; env.update_stake( @@ -370,7 +371,7 @@ impl StakeInterface { ) .remove(2) } - Self::Withdraw(source_has_delegation, lockup_state, amount_fraction) => { + Self::Withdraw(source_has_delegation, lockup_state, full_withdraw) => { let free_lamports = LAMPORTS_PER_SOL; let status = match source_has_delegation { None => StakeStatus::Uninitialized, @@ -391,11 +392,10 @@ impl StakeInterface { minimum_delegation + free_lamports, ); - let withdraw_amount = match amount_fraction { - AmountFraction::Full if status != StakeStatus::Active => { - free_lamports + minimum_delegation + STAKE_RENT_EXEMPTION - } - _ => free_lamports, + let withdraw_amount = if full_withdraw && status != StakeStatus::Active { + free_lamports + minimum_delegation + STAKE_RENT_EXEMPTION + } else { + free_lamports }; let authority = if status == StakeStatus::Uninitialized { @@ -564,12 +564,6 @@ impl LockupState { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Arbitrary)] -enum AmountFraction { - Partial, - Full, -} - // initialized with appropriate authority, no lockup fn just_stake(stake_pubkey: Pubkey, stake: u64) -> StakeStateV2 { not_just_stake(stake_pubkey, stake, false, Lockup::default()) From 9d451f40b226e0f83986a3ab332f3cd8559c0612 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:11:36 -0800 Subject: [PATCH 35/35] impl move ixns but something wrong --- program/tests/interface.rs | 109 ++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 8 deletions(-) diff --git a/program/tests/interface.rs b/program/tests/interface.rs index 660fd20..24006c7 100644 --- a/program/tests/interface.rs +++ b/program/tests/interface.rs @@ -52,6 +52,11 @@ use { test_case::{test_case, test_matrix}, }; +// NOTE ideas for future tests: +// * fail with different vote accounts on operations that require them to match +// * fail with different authorities on operations that require them to match +// * adding/changing lockups to ensure we always fail when violating lockup + // arbitrary, gives us room to set up activations/deactivations const EXECUTION_EPOCH: u64 = 8; @@ -279,7 +284,9 @@ enum StakeInterface { Deactivate(LockupState), SetLockup(LockupState, LockupState), Merge(LockupState), - // TODO move, checked, seed, deactivate delinquent, minimum, redelegate + //MoveStake(LockupState, bool, bool), + //MoveLamports(LockupState, bool, Option), + // TODO checked, seed, deactivate delinquent, minimum, redelegate } impl StakeInterface { @@ -373,7 +380,7 @@ impl StakeInterface { } Self::Withdraw(source_has_delegation, lockup_state, full_withdraw) => { let free_lamports = LAMPORTS_PER_SOL; - let status = match source_has_delegation { + let source_status = match source_has_delegation { None => StakeStatus::Uninitialized, Some(false) => StakeStatus::Initialized, Some(true) => StakeStatus::Active, @@ -385,20 +392,20 @@ impl StakeInterface { VOTE_ACCOUNT_RED, STAKE_ACCOUNT_BLACK, minimum_delegation, - status, + source_status, false, lockup_state.to_lockup(CUSTODIAN_LEFT), ), minimum_delegation + free_lamports, ); - let withdraw_amount = if full_withdraw && status != StakeStatus::Active { + let withdraw_amount = if full_withdraw && source_status != StakeStatus::Active { free_lamports + minimum_delegation + STAKE_RENT_EXEMPTION } else { free_lamports }; - let authority = if status == StakeStatus::Uninitialized { + let authority = if source_status == StakeStatus::Uninitialized { STAKE_ACCOUNT_BLACK } else { WITHDRAWER_BLACK @@ -477,9 +484,95 @@ impl StakeInterface { instruction::merge(&STAKE_ACCOUNT_WHITE, &STAKE_ACCOUNT_BLACK, &STAKER_GRAY) .remove(0) - } - // TODO move, checked, seed, deactivate delinquent, minimum, redelegate - _ => todo!(), + } // XXX these have VERY unexpected behavior + // when we try to get MergeKind, it doesnt show the stake history we expect, but an empty one + // however mollusk does have the stake history we want in its sysvar cache it creates + // i need to depend on a local monorepo to debug this which means i need to redo my whole account-decoder shit + // do this after all the other instructions so that i know theyre good + /* + Self::MoveStake(lockup_state, active_destination, full_move) => { + let source_delegation = minimum_delegation * 2; + let move_amount = if full_move { + source_delegation + } else { + source_delegation / 2 + }; + + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &i_cant_believe_its_not_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + source_delegation, + StakeStatus::Active, + true, + lockup_state.to_lockup(CUSTODIAN_LEFT), + ), + source_delegation, + ); + + env.update_stake( + &STAKE_ACCOUNT_WHITE, + &i_cant_believe_its_not_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_WHITE, + minimum_delegation, + if active_destination { + StakeStatus::Active + } else { + StakeStatus::Initialized + }, + true, + lockup_state.to_lockup(CUSTODIAN_LEFT), + ), + minimum_delegation, + ); + + instruction::move_stake(&STAKE_ACCOUNT_BLACK, &STAKE_ACCOUNT_WHITE, &STAKER_GRAY, move_amount) + + } + Self::MoveLamports(lockup_state, active_source, fully_activated_destination) => { + let free_lamports = LAMPORTS_PER_SOL; + let destination_status = match fully_activated_destination { + None => StakeStatus::Initialized, + Some(false) => StakeStatus::Activating, + Some(true) => StakeStatus::Active, + }; + + env.update_stake( + &STAKE_ACCOUNT_BLACK, + &i_cant_believe_its_not_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_BLACK, + minimum_delegation, + if active_source { + StakeStatus::Active + } else { + StakeStatus::Initialized + }, + true, + lockup_state.to_lockup(CUSTODIAN_LEFT), + ), + minimum_delegation + free_lamports, + ); + + env.update_stake( + &STAKE_ACCOUNT_WHITE, + &i_cant_believe_its_not_stake( + VOTE_ACCOUNT_RED, + STAKE_ACCOUNT_WHITE, + minimum_delegation, + destination_status, + true, + lockup_state.to_lockup(CUSTODIAN_LEFT), + ), + minimum_delegation, + ); + + instruction::move_lamports(&STAKE_ACCOUNT_BLACK, &STAKE_ACCOUNT_WHITE, &STAKER_GRAY, free_lamports) + } + */ + // TODO checked, seed, deactivate delinquent, minimum, redelegate } } }