Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit c324f6c

Browse files
authored
stake-pool: Handle force destaked accounts (#3152)
1 parent b207760 commit c324f6c

File tree

13 files changed

+595
-228
lines changed

13 files changed

+595
-228
lines changed

stake-pool/js/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,12 @@ export async function increaseValidatorStake(
612612
transientStakeSeed,
613613
);
614614

615+
const validatorStake = await findStakeProgramAddress(
616+
STAKE_POOL_PROGRAM_ID,
617+
validatorInfo.voteAccountAddress,
618+
stakePoolAddress,
619+
);
620+
615621
const instructions: TransactionInstruction[] = [];
616622
instructions.push(
617623
StakePoolInstruction.increaseValidatorStake({
@@ -622,6 +628,7 @@ export async function increaseValidatorStake(
622628
transientStakeSeed: transientStakeSeed.toNumber(),
623629
withdrawAuthority,
624630
transientStake,
631+
validatorStake,
625632
validatorVote,
626633
lamports,
627634
}),

stake-pool/js/src/instructions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export type IncreaseValidatorStakeParams = {
157157
validatorList: PublicKey;
158158
reserveStake: PublicKey;
159159
transientStake: PublicKey;
160+
validatorStake: PublicKey;
160161
validatorVote: PublicKey;
161162
// Amount of lamports to split into the transient stake account.
162163
lamports: number;
@@ -343,6 +344,7 @@ export class StakePoolInstruction {
343344
validatorList,
344345
reserveStake,
345346
transientStake,
347+
validatorStake,
346348
validatorVote,
347349
lamports,
348350
transientStakeSeed,
@@ -358,6 +360,7 @@ export class StakePoolInstruction {
358360
{ pubkey: validatorList, isSigner: false, isWritable: true },
359361
{ pubkey: reserveStake, isSigner: false, isWritable: true },
360362
{ pubkey: transientStake, isSigner: false, isWritable: true },
363+
{ pubkey: validatorStake, isSigner: false, isWritable: false },
361364
{ pubkey: validatorVote, isSigner: false, isWritable: false },
362365
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
363366
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },

stake-pool/program/src/instruction.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,14 @@ pub enum StakePoolInstruction {
166166
/// 3. `[w]` Validator list
167167
/// 4. `[w]` Stake pool reserve stake
168168
/// 5. `[w]` Transient stake account
169-
/// 6. `[]` Validator vote account to delegate to
170-
/// 7. '[]' Clock sysvar
171-
/// 8. '[]' Rent sysvar
172-
/// 9. `[]` Stake History sysvar
173-
/// 10. `[]` Stake Config sysvar
174-
/// 11. `[]` System program
175-
/// 12. `[]` Stake program
169+
/// 6. `[]` Validator stake account
170+
/// 7. `[]` Validator vote account to delegate to
171+
/// 8. '[]' Clock sysvar
172+
/// 9. '[]' Rent sysvar
173+
/// 10. `[]` Stake History sysvar
174+
/// 11. `[]` Stake Config sysvar
175+
/// 12. `[]` System program
176+
/// 13. `[]` Stake program
176177
/// userdata: amount of lamports to increase on the given validator.
177178
/// The actual amount split into the transient stake account is:
178179
/// `lamports + stake_rent_exemption`
@@ -538,6 +539,7 @@ pub fn increase_validator_stake(
538539
validator_list: &Pubkey,
539540
reserve_stake: &Pubkey,
540541
transient_stake: &Pubkey,
542+
validator_stake: &Pubkey,
541543
validator: &Pubkey,
542544
lamports: u64,
543545
transient_stake_seed: u64,
@@ -549,6 +551,7 @@ pub fn increase_validator_stake(
549551
AccountMeta::new(*validator_list, false),
550552
AccountMeta::new(*reserve_stake, false),
551553
AccountMeta::new(*transient_stake, false),
554+
AccountMeta::new_readonly(*validator_stake, false),
552555
AccountMeta::new_readonly(*validator, false),
553556
AccountMeta::new_readonly(sysvar::clock::id(), false),
554557
AccountMeta::new_readonly(sysvar::rent::id(), false),
@@ -671,6 +674,8 @@ pub fn increase_validator_stake_with_vote(
671674
stake_pool_address,
672675
transient_stake_seed,
673676
);
677+
let (validator_stake_address, _) =
678+
find_stake_program_address(program_id, vote_account_address, stake_pool_address);
674679

675680
increase_validator_stake(
676681
program_id,
@@ -680,6 +685,7 @@ pub fn increase_validator_stake_with_vote(
680685
&stake_pool.validator_list,
681686
&stake_pool.reserve_stake,
682687
&transient_stake_address,
688+
&validator_stake_address,
683689
vote_account_address,
684690
lamports,
685691
transient_stake_seed,

stake-pool/program/src/processor.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,7 @@ impl Processor {
12401240
let validator_list_info = next_account_info(account_info_iter)?;
12411241
let reserve_stake_account_info = next_account_info(account_info_iter)?;
12421242
let transient_stake_account_info = next_account_info(account_info_iter)?;
1243+
let validator_stake_account_info = next_account_info(account_info_iter)?;
12431244
let validator_vote_account_info = next_account_info(account_info_iter)?;
12441245
let clock_info = next_account_info(account_info_iter)?;
12451246
let clock = &Clock::from_account_info(clock_info)?;
@@ -1300,6 +1301,32 @@ impl Processor {
13001301
return Err(StakePoolError::TransientAccountInUse.into());
13011302
}
13021303

1304+
// Check that the validator stake account is actually delegated to the right
1305+
// validator. This can happen if a validator was force destaked during a
1306+
// cluster restart.
1307+
{
1308+
check_account_owner(validator_stake_account_info, stake_program_info.key)?;
1309+
check_validator_stake_address(
1310+
program_id,
1311+
stake_pool_info.key,
1312+
validator_stake_account_info.key,
1313+
vote_account_address,
1314+
)?;
1315+
let (meta, stake) = get_stake_state(validator_stake_account_info)?;
1316+
if !stake_is_usable_by_pool(&meta, withdraw_authority_info.key, &stake_pool.lockup) {
1317+
msg!("Validator stake for {} not usable by pool, must be owned by withdraw authority", vote_account_address);
1318+
return Err(StakePoolError::WrongStakeState.into());
1319+
}
1320+
if stake.delegation.voter_pubkey != *vote_account_address {
1321+
msg!(
1322+
"Validator stake {} not delegated to {}",
1323+
validator_stake_account_info.key,
1324+
vote_account_address
1325+
);
1326+
return Err(StakePoolError::WrongStakeState.into());
1327+
}
1328+
}
1329+
13031330
let transient_stake_bump_seed = check_transient_stake_address(
13041331
program_id,
13051332
stake_pool_info.key,
@@ -1683,6 +1710,30 @@ impl Processor {
16831710
msg!("Validator stake account no longer part of the pool, ignoring");
16841711
}
16851712
}
1713+
Some(stake::state::StakeState::Initialized(meta))
1714+
if stake_is_usable_by_pool(
1715+
&meta,
1716+
withdraw_authority_info.key,
1717+
&stake_pool.lockup,
1718+
) =>
1719+
{
1720+
// If a validator stake is `Initialized`, the validator could
1721+
// have been destaked during a cluster restart. Either way,
1722+
// absorb those lamports into the reserve. The transient
1723+
// stake was likely absorbed into the reserve earlier.
1724+
Self::stake_merge(
1725+
stake_pool_info.key,
1726+
validator_stake_info.clone(),
1727+
withdraw_authority_info.clone(),
1728+
AUTHORITY_WITHDRAW,
1729+
stake_pool.stake_withdraw_bump_seed,
1730+
reserve_stake_info.clone(),
1731+
clock_info.clone(),
1732+
stake_history_info.clone(),
1733+
stake_program_info.clone(),
1734+
)?;
1735+
validator_stake_record.status = StakeStatus::ReadyForRemoval;
1736+
}
16861737
Some(stake::state::StakeState::Initialized(_))
16871738
| Some(stake::state::StakeState::Uninitialized)
16881739
| Some(stake::state::StakeState::RewardsPool)
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#![cfg(feature = "test-bpf")]
2+
3+
mod helpers;
4+
5+
use {
6+
helpers::*,
7+
solana_program::{instruction::InstructionError, pubkey::Pubkey, stake},
8+
solana_program_test::*,
9+
solana_sdk::{
10+
account::{Account, WritableAccount},
11+
clock::Epoch,
12+
signature::Signer,
13+
transaction::TransactionError,
14+
},
15+
spl_stake_pool::{
16+
error::StakePoolError,
17+
find_stake_program_address, find_transient_stake_program_address, id,
18+
state::{StakeStatus, ValidatorStakeInfo},
19+
MINIMUM_ACTIVE_STAKE,
20+
},
21+
};
22+
23+
async fn setup() -> (ProgramTestContext, StakePoolAccounts, Pubkey) {
24+
let mut program_test = program_test();
25+
let stake_pool_accounts = StakePoolAccounts::new();
26+
27+
let stake_pool_pubkey = stake_pool_accounts.stake_pool.pubkey();
28+
let (mut stake_pool, mut validator_list) = stake_pool_accounts.state();
29+
30+
let voter_pubkey = add_vote_account(&mut program_test);
31+
let meta = stake::state::Meta {
32+
rent_exempt_reserve: STAKE_ACCOUNT_RENT_EXEMPTION,
33+
authorized: stake::state::Authorized {
34+
staker: stake_pool_accounts.withdraw_authority,
35+
withdrawer: stake_pool_accounts.withdraw_authority,
36+
},
37+
lockup: stake_pool.lockup,
38+
};
39+
40+
let stake_account = Account::create(
41+
TEST_STAKE_AMOUNT + STAKE_ACCOUNT_RENT_EXEMPTION,
42+
bincode::serialize::<stake::state::StakeState>(&stake::state::StakeState::Initialized(
43+
meta,
44+
))
45+
.unwrap(),
46+
stake::program::id(),
47+
false,
48+
Epoch::default(),
49+
);
50+
51+
let (stake_address, _) = find_stake_program_address(&id(), &voter_pubkey, &stake_pool_pubkey);
52+
program_test.add_account(stake_address, stake_account);
53+
let active_stake_lamports = TEST_STAKE_AMOUNT - MINIMUM_ACTIVE_STAKE;
54+
// add to validator list
55+
validator_list.validators.push(ValidatorStakeInfo {
56+
status: StakeStatus::Active,
57+
vote_account_address: voter_pubkey,
58+
active_stake_lamports,
59+
transient_stake_lamports: 0,
60+
last_update_epoch: 0,
61+
transient_seed_suffix_start: 0,
62+
transient_seed_suffix_end: 0,
63+
});
64+
65+
stake_pool.total_lamports += active_stake_lamports;
66+
stake_pool.pool_token_supply += active_stake_lamports;
67+
68+
add_reserve_stake_account(
69+
&mut program_test,
70+
&stake_pool_accounts.reserve_stake.pubkey(),
71+
&stake_pool_accounts.withdraw_authority,
72+
TEST_STAKE_AMOUNT,
73+
);
74+
add_stake_pool_account(
75+
&mut program_test,
76+
&stake_pool_accounts.stake_pool.pubkey(),
77+
&stake_pool,
78+
);
79+
add_validator_list_account(
80+
&mut program_test,
81+
&stake_pool_accounts.validator_list.pubkey(),
82+
&validator_list,
83+
stake_pool_accounts.max_validators,
84+
);
85+
86+
add_mint_account(
87+
&mut program_test,
88+
&stake_pool_accounts.pool_mint.pubkey(),
89+
&stake_pool_accounts.withdraw_authority,
90+
stake_pool.pool_token_supply,
91+
);
92+
add_token_account(
93+
&mut program_test,
94+
&stake_pool_accounts.pool_fee_account.pubkey(),
95+
&stake_pool_accounts.pool_mint.pubkey(),
96+
&stake_pool_accounts.manager.pubkey(),
97+
);
98+
99+
let context = program_test.start_with_context().await;
100+
(context, stake_pool_accounts, voter_pubkey)
101+
}
102+
103+
#[tokio::test]
104+
async fn success_update() {
105+
let (mut context, stake_pool_accounts, voter_pubkey) = setup().await;
106+
let pre_reserve_lamports = context
107+
.banks_client
108+
.get_account(stake_pool_accounts.reserve_stake.pubkey())
109+
.await
110+
.unwrap()
111+
.unwrap()
112+
.lamports;
113+
let (stake_address, _) = find_stake_program_address(
114+
&id(),
115+
&voter_pubkey,
116+
&stake_pool_accounts.stake_pool.pubkey(),
117+
);
118+
let validator_stake_lamports = context
119+
.banks_client
120+
.get_account(stake_address)
121+
.await
122+
.unwrap()
123+
.unwrap()
124+
.lamports;
125+
// update should merge the destaked validator stake account into the reserve
126+
let error = stake_pool_accounts
127+
.update_all(
128+
&mut context.banks_client,
129+
&context.payer,
130+
&context.last_blockhash,
131+
&[voter_pubkey],
132+
false,
133+
)
134+
.await;
135+
assert!(error.is_none());
136+
let post_reserve_lamports = context
137+
.banks_client
138+
.get_account(stake_pool_accounts.reserve_stake.pubkey())
139+
.await
140+
.unwrap()
141+
.unwrap()
142+
.lamports;
143+
assert_eq!(
144+
post_reserve_lamports,
145+
pre_reserve_lamports + validator_stake_lamports
146+
);
147+
// test no more validator stake account
148+
assert!(context
149+
.banks_client
150+
.get_account(stake_address)
151+
.await
152+
.unwrap()
153+
.is_none());
154+
}
155+
156+
#[tokio::test]
157+
async fn fail_increase() {
158+
let (mut context, stake_pool_accounts, voter_pubkey) = setup().await;
159+
let (stake_address, _) = find_stake_program_address(
160+
&id(),
161+
&voter_pubkey,
162+
&stake_pool_accounts.stake_pool.pubkey(),
163+
);
164+
let transient_stake_seed = 0;
165+
let transient_stake_address = find_transient_stake_program_address(
166+
&id(),
167+
&voter_pubkey,
168+
&stake_pool_accounts.stake_pool.pubkey(),
169+
transient_stake_seed,
170+
)
171+
.0;
172+
let error = stake_pool_accounts
173+
.increase_validator_stake(
174+
&mut context.banks_client,
175+
&context.payer,
176+
&context.last_blockhash,
177+
&transient_stake_address,
178+
&stake_address,
179+
&voter_pubkey,
180+
MINIMUM_ACTIVE_STAKE,
181+
transient_stake_seed,
182+
)
183+
.await
184+
.unwrap()
185+
.unwrap();
186+
assert_eq!(
187+
error,
188+
TransactionError::InstructionError(
189+
0,
190+
InstructionError::Custom(StakePoolError::WrongStakeState as u32)
191+
)
192+
);
193+
}

0 commit comments

Comments
 (0)