Skip to content

Commit c4e9deb

Browse files
committed
StakeTracker, migrate Delegate tests
1 parent b5a94f0 commit c4e9deb

File tree

8 files changed

+1700
-156
lines changed

8 files changed

+1700
-156
lines changed

program/tests/delegate.rs

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
#![allow(clippy::arithmetic_side_effects)]
2+
3+
mod helpers;
4+
5+
use {
6+
helpers::{
7+
context::StakeTestContext,
8+
instruction_builders::{DeactivateConfig, DelegateConfig},
9+
lifecycle::StakeLifecycle,
10+
stake_tracker::MolluskStakeExt,
11+
utils::{create_vote_account, increment_vote_account_credits, parse_stake_account},
12+
},
13+
mollusk_svm::result::Check,
14+
solana_account::{AccountSharedData, WritableAccount},
15+
solana_program_error::ProgramError,
16+
solana_pubkey::Pubkey,
17+
solana_stake_interface::{
18+
error::StakeError,
19+
state::{Delegation, Stake, StakeStateV2},
20+
},
21+
solana_stake_program::id,
22+
};
23+
24+
#[test]
25+
fn test_delegate() {
26+
let mut ctx = StakeTestContext::with_delegation();
27+
let vote_account = *ctx.vote_account.as_ref().unwrap();
28+
let mut vote_account_data = ctx.vote_account_data.as_ref().unwrap().clone();
29+
let min_delegation = ctx.minimum_delegation.unwrap();
30+
31+
let vote_state_credits = 100u64;
32+
increment_vote_account_credits(&mut vote_account_data, 0, vote_state_credits);
33+
34+
let (stake, mut stake_account) = ctx
35+
.stake_account(StakeLifecycle::Initialized)
36+
.staked_amount(min_delegation)
37+
.build();
38+
39+
// Delegate stake
40+
let result = ctx
41+
.process_with(DelegateConfig {
42+
stake: (&stake, &stake_account),
43+
vote: (&vote_account, &vote_account_data),
44+
})
45+
.checks(&[
46+
Check::success(),
47+
Check::all_rent_exempt(),
48+
Check::account(&stake)
49+
.lamports(ctx.rent_exempt_reserve + min_delegation)
50+
.owner(&id())
51+
.space(StakeStateV2::size_of())
52+
.build(),
53+
])
54+
.test_missing_signers(true)
55+
.execute();
56+
stake_account = result.resulting_accounts[0].1.clone().into();
57+
58+
// Verify that delegate() looks right
59+
let clock = ctx.mollusk.sysvars.clock.clone();
60+
let (_, stake_data, _) = parse_stake_account(&stake_account);
61+
assert_eq!(
62+
stake_data.unwrap(),
63+
Stake {
64+
delegation: Delegation {
65+
voter_pubkey: vote_account,
66+
stake: min_delegation,
67+
activation_epoch: clock.epoch,
68+
deactivation_epoch: u64::MAX,
69+
..Delegation::default()
70+
},
71+
credits_observed: vote_state_credits,
72+
}
73+
);
74+
75+
// Advance epoch to activate the stake
76+
let activation_epoch = ctx.mollusk.sysvars.clock.epoch;
77+
ctx.tracker.as_mut().unwrap().track_delegation(
78+
&stake,
79+
min_delegation,
80+
activation_epoch,
81+
&vote_account,
82+
);
83+
84+
let slots_per_epoch = ctx.mollusk.sysvars.epoch_schedule.slots_per_epoch;
85+
let current_slot = ctx.mollusk.sysvars.clock.slot;
86+
ctx.mollusk.warp_to_slot_with_stake_tracking(
87+
ctx.tracker.as_ref().unwrap(),
88+
current_slot + slots_per_epoch,
89+
Some(0),
90+
);
91+
92+
// Verify that delegate fails as stake is active and not deactivating
93+
ctx.process_with(DelegateConfig {
94+
stake: (&stake, &stake_account),
95+
vote: (&vote_account, ctx.vote_account_data.as_ref().unwrap()),
96+
})
97+
.checks(&[Check::err(StakeError::TooSoonToRedelegate.into())])
98+
.test_missing_signers(false)
99+
.execute();
100+
101+
// Deactivate
102+
let result = ctx
103+
.process_with(DeactivateConfig {
104+
stake: (&stake, &stake_account),
105+
override_signer: None,
106+
})
107+
.execute();
108+
stake_account = result.resulting_accounts[0].1.clone().into();
109+
110+
// Create second vote account
111+
let (vote_account2, vote_account2_data) = ctx.create_second_vote_account();
112+
113+
// Verify that delegate to a different vote account fails during deactivation
114+
ctx.process_with(DelegateConfig {
115+
stake: (&stake, &stake_account),
116+
vote: (&vote_account2, &vote_account2_data),
117+
})
118+
.checks(&[Check::err(StakeError::TooSoonToRedelegate.into())])
119+
.test_missing_signers(false)
120+
.execute();
121+
122+
// Verify that delegate succeeds to same vote account when stake is deactivating
123+
let result = ctx
124+
.process_with(DelegateConfig {
125+
stake: (&stake, &stake_account),
126+
vote: (&vote_account, ctx.vote_account_data.as_ref().unwrap()),
127+
})
128+
.execute();
129+
stake_account = result.resulting_accounts[0].1.clone().into();
130+
131+
// Verify that deactivation has been cleared
132+
let (_, stake_data, _) = parse_stake_account(&stake_account);
133+
assert_eq!(stake_data.unwrap().delegation.deactivation_epoch, u64::MAX);
134+
135+
// Verify that delegate to a different vote account fails if stake is still active
136+
ctx.process_with(DelegateConfig {
137+
stake: (&stake, &stake_account),
138+
vote: (&vote_account2, &vote_account2_data),
139+
})
140+
.checks(&[Check::err(StakeError::TooSoonToRedelegate.into())])
141+
.test_missing_signers(false)
142+
.execute();
143+
144+
// Advance epoch again using tracker
145+
let current_slot = ctx.mollusk.sysvars.clock.slot;
146+
let slots_per_epoch = ctx.mollusk.sysvars.epoch_schedule.slots_per_epoch;
147+
ctx.mollusk.warp_to_slot_with_stake_tracking(
148+
ctx.tracker.as_ref().unwrap(),
149+
current_slot + slots_per_epoch,
150+
Some(0),
151+
);
152+
153+
// Delegate still fails after stake is fully activated; redelegate is not supported
154+
let (vote_account2, vote_account2_data) = ctx.create_second_vote_account();
155+
156+
ctx.process_with(DelegateConfig {
157+
stake: (&stake, &stake_account),
158+
vote: (&vote_account2, &vote_account2_data),
159+
})
160+
.checks(&[Check::err(StakeError::TooSoonToRedelegate.into())])
161+
.test_missing_signers(false)
162+
.execute();
163+
}
164+
165+
#[test]
166+
fn test_delegate_fake_vote_account() {
167+
let mut ctx = StakeTestContext::with_delegation();
168+
169+
// Create fake vote account (not owned by vote program)
170+
let fake_vote_account = Pubkey::new_unique();
171+
let mut fake_vote_data = create_vote_account();
172+
fake_vote_data.set_owner(Pubkey::new_unique()); // Wrong owner
173+
174+
let min_delegation = ctx.minimum_delegation.unwrap();
175+
let (stake, stake_account) = ctx
176+
.stake_account(StakeLifecycle::Initialized)
177+
.staked_amount(min_delegation)
178+
.build();
179+
180+
// Try to delegate to fake vote account
181+
ctx.process_with(DelegateConfig {
182+
stake: (&stake, &stake_account),
183+
vote: (&fake_vote_account, &fake_vote_data),
184+
})
185+
.checks(&[Check::err(ProgramError::IncorrectProgramId)])
186+
.test_missing_signers(false)
187+
.execute();
188+
}
189+
190+
#[test]
191+
fn test_delegate_non_stake_account() {
192+
let ctx = StakeTestContext::with_delegation();
193+
194+
// Create a rewards pool account (program-owned but not a stake account)
195+
let rewards_pool = Pubkey::new_unique();
196+
let rewards_pool_data = AccountSharedData::new_data_with_space(
197+
ctx.rent_exempt_reserve,
198+
&StakeStateV2::RewardsPool,
199+
StakeStateV2::size_of(),
200+
&id(),
201+
)
202+
.unwrap();
203+
204+
ctx.process_with(DelegateConfig {
205+
stake: (&rewards_pool, &rewards_pool_data),
206+
vote: (
207+
ctx.vote_account.as_ref().unwrap(),
208+
ctx.vote_account_data.as_ref().unwrap(),
209+
),
210+
})
211+
.checks(&[Check::err(ProgramError::InvalidAccountData)])
212+
.test_missing_signers(false)
213+
.execute();
214+
}

program/tests/helpers/context.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use {
22
super::{
33
instruction_builders::{InstructionConfig, InstructionExecution},
44
lifecycle::StakeLifecycle,
5+
stake_tracker::StakeTracker,
56
utils::{add_sysvars, create_vote_account, STAKE_RENT_EXEMPTION},
67
},
78
mollusk_svm::{result::Check, Mollusk},
@@ -66,6 +67,10 @@ impl StakeAccountBuilder<'_> {
6667
let stake_pubkey = self.stake_pubkey.unwrap_or_else(Pubkey::new_unique);
6768
let account = self.lifecycle.create_stake_account_fully_specified(
6869
&mut self.ctx.mollusk,
70+
self.ctx
71+
.tracker
72+
.as_mut()
73+
.expect("tracker required for stake account builder"),
6974
&stake_pubkey,
7075
self.vote_account.as_ref().unwrap_or(
7176
self.ctx
@@ -84,6 +89,7 @@ impl StakeAccountBuilder<'_> {
8489
}
8590
}
8691

92+
#[allow(dead_code)] // can be removed once later tests are in
8793
pub struct StakeTestContext {
8894
pub mollusk: Mollusk,
8995
pub rent_exempt_reserve: u64,
@@ -92,8 +98,10 @@ pub struct StakeTestContext {
9298
pub minimum_delegation: Option<u64>,
9399
pub vote_account: Option<Pubkey>,
94100
pub vote_account_data: Option<AccountSharedData>,
101+
pub tracker: Option<StakeTracker>,
95102
}
96103

104+
#[allow(dead_code)] // can be removed once later tests are in
97105
impl StakeTestContext {
98106
pub fn minimal() -> Self {
99107
let mollusk = Mollusk::new(&id(), "solana_stake_program");
@@ -105,12 +113,14 @@ impl StakeTestContext {
105113
minimum_delegation: None,
106114
vote_account: None,
107115
vote_account_data: None,
116+
tracker: None,
108117
}
109118
}
110119

111120
pub fn with_delegation() -> Self {
112121
let mollusk = Mollusk::new(&id(), "solana_stake_program");
113122
let minimum_delegation = solana_stake_program::get_minimum_delegation();
123+
let tracker: StakeTracker = StakeLifecycle::create_tracker_for_test(minimum_delegation);
114124
Self {
115125
mollusk,
116126
rent_exempt_reserve: STAKE_RENT_EXEMPTION,
@@ -119,6 +129,7 @@ impl StakeTestContext {
119129
minimum_delegation: Some(minimum_delegation),
120130
vote_account: Some(Pubkey::new_unique()),
121131
vote_account_data: Some(create_vote_account()),
132+
tracker: Some(tracker),
122133
}
123134
}
124135

@@ -127,6 +138,7 @@ impl StakeTestContext {
127138
}
128139

129140
/// Create a stake account builder for the specified lifecycle stage
141+
/// This is the primary method for creating stake accounts in tests.
130142
///
131143
/// Example:
132144
/// ```
@@ -214,7 +226,6 @@ impl StakeTestContext {
214226
.process_and_validate_instruction(instruction, &accounts_with_sysvars, checks)
215227
}
216228
}
217-
218229
impl Default for StakeTestContext {
219230
fn default() -> Self {
220231
Self::new()

program/tests/helpers/lifecycle.rs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
use {
2-
super::utils::{add_sysvars, create_vote_account, STAKE_RENT_EXEMPTION},
2+
super::{
3+
stake_tracker::MolluskStakeExt,
4+
utils::{add_sysvars, create_vote_account, STAKE_RENT_EXEMPTION},
5+
},
6+
crate::helpers::stake_tracker::StakeTracker,
37
mollusk_svm::Mollusk,
48
solana_account::{Account, AccountSharedData, WritableAccount},
59
solana_pubkey::Pubkey,
@@ -23,12 +27,22 @@ pub enum StakeLifecycle {
2327
}
2428

2529
impl StakeLifecycle {
30+
/// Helper to create tracker with appropriate background stake for tests
31+
/// Returns a tracker seeded with background cluster stake
32+
pub fn create_tracker_for_test(minimum_delegation: u64) -> StakeTracker {
33+
// Use a moderate background stake amount
34+
// This mimics Banks' cluster-wide effective stake from all validators
35+
// Calculation: needs to be >> test stakes to provide stable warmup base
36+
let background_stake = minimum_delegation.saturating_mul(100);
37+
StakeTracker::with_background_stake(background_stake)
38+
}
39+
2640
/// Create a stake account with full specification of authorities and lockup
2741
#[allow(clippy::too_many_arguments)]
2842
pub fn create_stake_account_fully_specified(
2943
self,
3044
mollusk: &mut Mollusk,
31-
// tracker: &mut StakeTracker, // added in subsequent PR
45+
tracker: &mut StakeTracker,
3246
stake_pubkey: &Pubkey,
3347
vote_account: &Pubkey,
3448
staked_amount: u64,
@@ -91,9 +105,8 @@ impl StakeLifecycle {
91105
stake_account = result.resulting_accounts[0].1.clone().into();
92106

93107
// Track delegation in the tracker
94-
// let activation_epoch = mollusk.sysvars.clock.epoch;
95-
// TODO: uncomment in subsequent PR (add `tracker.track_delegation` here)
96-
// tracker.track_delegation(stake_pubkey, staked_amount, activation_epoch, vote_account);
108+
let activation_epoch = mollusk.sysvars.clock.epoch;
109+
tracker.track_delegation(stake_pubkey, staked_amount, activation_epoch, vote_account);
97110
}
98111

99112
// Advance epoch to activate if needed (Active and beyond)
@@ -104,8 +117,7 @@ impl StakeLifecycle {
104117
let current_slot = mollusk.sysvars.clock.slot;
105118
let target_slot = current_slot + slots_per_epoch;
106119

107-
// TODO: use `warp_to_slot_with_stake_tracking` here (in subsequent PR)
108-
mollusk.warp_to_slot(target_slot);
120+
mollusk.warp_to_slot_with_stake_tracking(tracker, target_slot, Some(0));
109121
}
110122

111123
// Deactivate if needed
@@ -120,9 +132,8 @@ impl StakeLifecycle {
120132
stake_account = result.resulting_accounts[0].1.clone().into();
121133

122134
// Track deactivation in the tracker
123-
// let deactivation_epoch = mollusk.sysvars.clock.epoch;
124-
// TODO: uncomment in subsequent PR
125-
// tracker.track_deactivation(stake_pubkey, deactivation_epoch);
135+
let deactivation_epoch = mollusk.sysvars.clock.epoch;
136+
tracker.track_deactivation(stake_pubkey, deactivation_epoch);
126137
}
127138

128139
// Advance epoch to fully deactivate if needed (Deactive lifecycle)
@@ -134,8 +145,7 @@ impl StakeLifecycle {
134145
let current_slot = mollusk.sysvars.clock.slot;
135146
let target_slot = current_slot + slots_per_epoch;
136147

137-
// TODO: use `warp_to_slot_with_stake_tracking` here (in subsequent PR)
138-
mollusk.warp_to_slot(target_slot);
148+
mollusk.warp_to_slot_with_stake_tracking(tracker, target_slot, Some(0));
139149
}
140150

141151
stake_account

program/tests/helpers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
pub mod context;
55
pub mod instruction_builders;
66
pub mod lifecycle;
7+
pub mod stake_tracker;
78
pub mod utils;

0 commit comments

Comments
 (0)