Skip to content

Commit 69d3bd2

Browse files
committed
deactivate tests, StakeLifecycle
1 parent 695ce95 commit 69d3bd2

File tree

5 files changed

+416
-98
lines changed

5 files changed

+416
-98
lines changed

program/tests/deactivate.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#![allow(clippy::arithmetic_side_effects)]
2+
3+
mod helpers;
4+
5+
use {
6+
helpers::{context::StakeTestContext, lifecycle::StakeLifecycle},
7+
mollusk_svm::result::Check,
8+
solana_account::ReadableAccount,
9+
solana_program_error::ProgramError,
10+
solana_pubkey::Pubkey,
11+
solana_stake_client::instructions::{DeactivateBuilder, DelegateStakeBuilder},
12+
solana_stake_interface::{error::StakeError, state::StakeStateV2},
13+
solana_stake_program::id,
14+
test_case::test_case,
15+
};
16+
17+
#[test_case(false; "activating")]
18+
#[test_case(true; "active")]
19+
fn test_deactivate(activate: bool) {
20+
let mut ctx = StakeTestContext::with_delegation();
21+
let min_delegation = ctx.minimum_delegation.unwrap();
22+
23+
let (stake, mut stake_account) = ctx
24+
.stake_account(StakeLifecycle::Initialized)
25+
.staked_amount(min_delegation)
26+
.build();
27+
28+
let vote = *ctx.vote_account.as_ref().unwrap();
29+
let vote_account_data = ctx.vote_account_data.clone().unwrap();
30+
31+
// Deactivating an undelegated account fails
32+
ctx.process(
33+
DeactivateBuilder::new()
34+
.stake(stake)
35+
.stake_authority(ctx.staker)
36+
.instruction(),
37+
&[(&stake, &stake_account)],
38+
)
39+
.checks(&[Check::err(ProgramError::InvalidAccountData)])
40+
.test_missing_signers(false)
41+
.execute();
42+
43+
// Delegate
44+
let result = ctx
45+
.process(
46+
DelegateStakeBuilder::new()
47+
.stake(stake)
48+
.vote(vote)
49+
.unused(Pubkey::new_unique())
50+
.stake_authority(ctx.staker)
51+
.instruction(),
52+
&[(&stake, &stake_account), (&vote, &vote_account_data)],
53+
)
54+
.execute();
55+
stake_account = result.resulting_accounts[0].1.clone().into();
56+
57+
if activate {
58+
// Advance epoch to activate
59+
let current_slot = ctx.mollusk.sysvars.clock.slot;
60+
let slots_per_epoch = ctx.mollusk.sysvars.epoch_schedule.slots_per_epoch;
61+
ctx.mollusk.warp_to_slot(current_slot + slots_per_epoch);
62+
}
63+
64+
// Deactivate with withdrawer fails
65+
ctx.process(
66+
DeactivateBuilder::new()
67+
.stake(stake)
68+
.stake_authority(ctx.withdrawer)
69+
.instruction(),
70+
&[(&stake, &stake_account)],
71+
)
72+
.checks(&[Check::err(ProgramError::MissingRequiredSignature)])
73+
.test_missing_signers(false)
74+
.execute();
75+
76+
// Deactivate succeeds
77+
let result = ctx
78+
.process(
79+
DeactivateBuilder::new()
80+
.stake(stake)
81+
.stake_authority(ctx.staker)
82+
.instruction(),
83+
&[(&stake, &stake_account)],
84+
)
85+
.checks(&[
86+
Check::success(),
87+
Check::all_rent_exempt(),
88+
Check::account(&stake)
89+
.lamports(ctx.rent_exempt_reserve + min_delegation)
90+
.owner(&id())
91+
.space(StakeStateV2::size_of())
92+
.build(),
93+
])
94+
.test_missing_signers(true)
95+
.execute();
96+
stake_account = result.resulting_accounts[0].1.clone().into();
97+
98+
let clock = ctx.mollusk.sysvars.clock.clone();
99+
let stake_state: StakeStateV2 = bincode::deserialize(stake_account.data()).unwrap();
100+
if let StakeStateV2::Stake(_, stake_data, _) = stake_state {
101+
assert_eq!(stake_data.delegation.deactivation_epoch, clock.epoch);
102+
} else {
103+
panic!("Expected StakeStateV2::Stake");
104+
}
105+
106+
// Deactivate again fails
107+
ctx.process(
108+
DeactivateBuilder::new()
109+
.stake(stake)
110+
.stake_authority(ctx.staker)
111+
.instruction(),
112+
&[(&stake, &stake_account)],
113+
)
114+
.checks(&[Check::err(StakeError::AlreadyDeactivated.into())])
115+
.test_missing_signers(false)
116+
.execute();
117+
118+
// Advance epoch
119+
let current_slot = ctx.mollusk.sysvars.clock.slot;
120+
let slots_per_epoch = ctx.mollusk.sysvars.epoch_schedule.slots_per_epoch;
121+
ctx.mollusk.warp_to_slot(current_slot + slots_per_epoch);
122+
123+
// Deactivate again still fails
124+
ctx.process(
125+
DeactivateBuilder::new()
126+
.stake(stake)
127+
.stake_authority(ctx.staker)
128+
.instruction(),
129+
&[(&stake, &stake_account)],
130+
)
131+
.checks(&[Check::err(StakeError::AlreadyDeactivated.into())])
132+
.test_missing_signers(false)
133+
.execute();
134+
}

program/tests/helpers/context.rs

Lines changed: 122 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,169 @@ use {
22
super::{
33
instruction_builders::InstructionExecution,
44
lifecycle::StakeLifecycle,
5-
utils::{add_sysvars, STAKE_RENT_EXEMPTION},
5+
utils::{add_sysvars, create_vote_account, STAKE_RENT_EXEMPTION},
66
},
77
mollusk_svm::{result::Check, Mollusk},
88
solana_account::AccountSharedData,
99
solana_instruction::Instruction,
1010
solana_pubkey::Pubkey,
11+
solana_stake_interface::state::Lockup,
1112
solana_stake_program::id,
1213
};
1314

1415
/// Builder for creating stake accounts with customizable parameters
15-
pub struct StakeAccountBuilder {
16+
pub struct StakeAccountBuilder<'a> {
17+
ctx: &'a mut StakeTestContext,
1618
lifecycle: StakeLifecycle,
19+
staked_amount: u64,
20+
stake_authority: Option<Pubkey>,
21+
withdraw_authority: Option<Pubkey>,
22+
lockup: Option<Lockup>,
23+
vote_account: Option<Pubkey>,
24+
stake_pubkey: Option<Pubkey>,
1725
}
1826

19-
impl StakeAccountBuilder {
27+
impl StakeAccountBuilder<'_> {
28+
/// Set the staked amount (lamports delegated to validator)
29+
pub fn staked_amount(mut self, amount: u64) -> Self {
30+
self.staked_amount = amount;
31+
self
32+
}
33+
34+
/// Set a custom stake authority (defaults to ctx.staker)
35+
pub fn stake_authority(mut self, authority: &Pubkey) -> Self {
36+
self.stake_authority = Some(*authority);
37+
self
38+
}
39+
40+
/// Set a custom withdraw authority (defaults to ctx.withdrawer)
41+
pub fn withdraw_authority(mut self, authority: &Pubkey) -> Self {
42+
self.withdraw_authority = Some(*authority);
43+
self
44+
}
45+
46+
/// Set a custom lockup (defaults to Lockup::default())
47+
pub fn lockup(mut self, lockup: &Lockup) -> Self {
48+
self.lockup = Some(*lockup);
49+
self
50+
}
51+
52+
/// Set a custom vote account (defaults to ctx.vote_account)
53+
pub fn vote_account(mut self, vote_account: &Pubkey) -> Self {
54+
self.vote_account = Some(*vote_account);
55+
self
56+
}
57+
58+
/// Set a specific stake account pubkey (defaults to Pubkey::new_unique())
59+
pub fn stake_pubkey(mut self, pubkey: &Pubkey) -> Self {
60+
self.stake_pubkey = Some(*pubkey);
61+
self
62+
}
63+
64+
/// Build the stake account and return (pubkey, account_data)
2065
pub fn build(self) -> (Pubkey, AccountSharedData) {
21-
let stake_pubkey = Pubkey::new_unique();
22-
let account = self.lifecycle.create_uninitialized_account();
66+
let stake_pubkey = self.stake_pubkey.unwrap_or_else(Pubkey::new_unique);
67+
let account = self.lifecycle.create_stake_account_fully_specified(
68+
&mut self.ctx.mollusk,
69+
&stake_pubkey,
70+
self.vote_account.as_ref().unwrap_or(
71+
self.ctx
72+
.vote_account
73+
.as_ref()
74+
.expect("vote_account required for this lifecycle"),
75+
),
76+
self.staked_amount,
77+
self.stake_authority.as_ref().unwrap_or(&self.ctx.staker),
78+
self.withdraw_authority
79+
.as_ref()
80+
.unwrap_or(&self.ctx.withdrawer),
81+
self.lockup.as_ref().unwrap_or(&Lockup::default()),
82+
);
2383
(stake_pubkey, account)
2484
}
2585
}
2686

27-
/// Consolidated test context for stake account tests
2887
pub struct StakeTestContext {
2988
pub mollusk: Mollusk,
3089
pub rent_exempt_reserve: u64,
3190
pub staker: Pubkey,
3291
pub withdrawer: Pubkey,
92+
pub minimum_delegation: Option<u64>,
93+
pub vote_account: Option<Pubkey>,
94+
pub vote_account_data: Option<AccountSharedData>,
3395
}
3496

3597
impl StakeTestContext {
36-
/// Create a new test context with all standard setup
37-
pub fn new() -> Self {
98+
pub fn minimal() -> Self {
3899
let mollusk = Mollusk::new(&id(), "solana_stake_program");
100+
Self {
101+
mollusk,
102+
rent_exempt_reserve: STAKE_RENT_EXEMPTION,
103+
staker: Pubkey::new_unique(),
104+
withdrawer: Pubkey::new_unique(),
105+
minimum_delegation: None,
106+
vote_account: None,
107+
vote_account_data: None,
108+
}
109+
}
39110

111+
pub fn with_delegation() -> Self {
112+
let mollusk = Mollusk::new(&id(), "solana_stake_program");
113+
let minimum_delegation = solana_stake_program::get_minimum_delegation();
40114
Self {
41115
mollusk,
42116
rent_exempt_reserve: STAKE_RENT_EXEMPTION,
43117
staker: Pubkey::new_unique(),
44118
withdrawer: Pubkey::new_unique(),
119+
minimum_delegation: Some(minimum_delegation),
120+
vote_account: Some(Pubkey::new_unique()),
121+
vote_account_data: Some(create_vote_account()),
45122
}
46123
}
47124

125+
pub fn new() -> Self {
126+
Self::with_delegation()
127+
}
128+
48129
/// Create a stake account builder for the specified lifecycle stage
49130
///
50131
/// Example:
51132
/// ```
52133
/// let (stake, account) = ctx
53-
/// .stake_account(StakeLifecycle::Uninitialized)
134+
/// .stake_account(StakeLifecycle::Active)
135+
/// .staked_amount(1_000_000)
54136
/// .build();
55137
/// ```
56-
pub fn stake_account(&mut self, lifecycle: StakeLifecycle) -> StakeAccountBuilder {
57-
StakeAccountBuilder { lifecycle }
138+
pub fn stake_account(&mut self, lifecycle: StakeLifecycle) -> StakeAccountBuilder<'_> {
139+
StakeAccountBuilder {
140+
ctx: self,
141+
lifecycle,
142+
staked_amount: 0,
143+
stake_authority: None,
144+
withdraw_authority: None,
145+
lockup: None,
146+
vote_account: None,
147+
stake_pubkey: None,
148+
}
149+
}
150+
151+
/// Create a lockup that expires in the future
152+
pub fn create_future_lockup(&self, epochs_ahead: u64) -> Lockup {
153+
Lockup {
154+
unix_timestamp: 0,
155+
epoch: self.mollusk.sysvars.clock.epoch + epochs_ahead,
156+
custodian: Pubkey::new_unique(),
157+
}
158+
}
159+
160+
/// Create a lockup that's currently in force (far future)
161+
pub fn create_in_force_lockup(&self) -> Lockup {
162+
self.create_future_lockup(1_000_000)
163+
}
164+
165+
/// Create a second vote account (for testing different vote accounts)
166+
pub fn create_second_vote_account(&self) -> (Pubkey, AccountSharedData) {
167+
(Pubkey::new_unique(), create_vote_account())
58168
}
59169

60170
/// Process an instruction with account data provided as a slice of (pubkey, data) pairs.
@@ -71,7 +181,7 @@ impl StakeTestContext {
71181
InstructionExecution::new(instruction, accounts_vec, self)
72182
}
73183

74-
/// Process an instruction with optional missing signer testing
184+
/// Internal helper to process an instruction with optional missing signer testing
75185
pub(crate) fn process_instruction_maybe_test_signers(
76186
&self,
77187
instruction: &Instruction,

0 commit comments

Comments
 (0)