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

Commit ef17887

Browse files
authored
stake-pool: Add DecreaseAdditionalValidatorStake instruction (#3925)
* stake-pool: Add `DecreaseAdditionalValidatorStake` instruction * Update checks for deduping
1 parent 9aac29c commit ef17887

File tree

4 files changed

+654
-198
lines changed

4 files changed

+654
-198
lines changed

stake-pool/program/src/instruction.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,37 @@ pub enum StakePoolInstruction {
442442
/// seed used to create ephemeral account.
443443
ephemeral_stake_seed: u64,
444444
},
445+
446+
/// (Staker only) Decrease active stake again from a validator, eventually moving it to the reserve
447+
///
448+
/// Works regardless if the transient stake account already exists.
449+
///
450+
/// Internally, this instruction splits a validator stake account into an
451+
/// ephemeral stake account, deactivates it, then merges or splits it into
452+
/// the transient stake account delegated to the appropriate validator.
453+
///
454+
/// The amount of lamports to move must be at least rent-exemption plus
455+
/// `max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation())`.
456+
///
457+
/// 0. `[]` Stake pool
458+
/// 1. `[s]` Stake pool staker
459+
/// 2. `[]` Stake pool withdraw authority
460+
/// 3. `[w]` Validator list
461+
/// 4. `[w]` Canonical stake account to split from
462+
/// 5. `[w]` Uninitialized ephemeral stake account to receive stake
463+
/// 6. `[w]` Transient stake account
464+
/// 7. `[]` Clock sysvar
465+
/// 8. '[]' Stake history sysvar
466+
/// 9. `[]` System program
467+
/// 10. `[]` Stake program
468+
DecreaseAdditionalValidatorStake {
469+
/// amount of lamports to split into the transient stake account
470+
lamports: u64,
471+
/// seed used to create transient stake account
472+
transient_stake_seed: u64,
473+
/// seed used to create ephemeral account.
474+
ephemeral_stake_seed: u64,
475+
},
445476
}
446477

447478
/// Creates an 'initialize' instruction.
@@ -595,6 +626,47 @@ pub fn decrease_validator_stake(
595626
}
596627
}
597628

629+
/// Creates `DecreaseAdditionalValidatorStake` instruction (rebalance from
630+
/// validator account to transient account)
631+
pub fn decrease_additional_validator_stake(
632+
program_id: &Pubkey,
633+
stake_pool: &Pubkey,
634+
staker: &Pubkey,
635+
stake_pool_withdraw_authority: &Pubkey,
636+
validator_list: &Pubkey,
637+
validator_stake: &Pubkey,
638+
ephemeral_stake: &Pubkey,
639+
transient_stake: &Pubkey,
640+
lamports: u64,
641+
transient_stake_seed: u64,
642+
ephemeral_stake_seed: u64,
643+
) -> Instruction {
644+
let accounts = vec![
645+
AccountMeta::new_readonly(*stake_pool, false),
646+
AccountMeta::new_readonly(*staker, true),
647+
AccountMeta::new_readonly(*stake_pool_withdraw_authority, false),
648+
AccountMeta::new(*validator_list, false),
649+
AccountMeta::new(*validator_stake, false),
650+
AccountMeta::new(*ephemeral_stake, false),
651+
AccountMeta::new(*transient_stake, false),
652+
AccountMeta::new_readonly(sysvar::clock::id(), false),
653+
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
654+
AccountMeta::new_readonly(system_program::id(), false),
655+
AccountMeta::new_readonly(stake::program::id(), false),
656+
];
657+
Instruction {
658+
program_id: *program_id,
659+
accounts,
660+
data: StakePoolInstruction::DecreaseAdditionalValidatorStake {
661+
lamports,
662+
transient_stake_seed,
663+
ephemeral_stake_seed,
664+
}
665+
.try_to_vec()
666+
.unwrap(),
667+
}
668+
}
669+
598670
/// Creates `IncreaseValidatorStake` instruction (rebalance from reserve account to
599671
/// transient account)
600672
pub fn increase_validator_stake(
@@ -895,6 +967,49 @@ pub fn decrease_validator_stake_with_vote(
895967
)
896968
}
897969

970+
/// Create a `DecreaseAdditionalValidatorStake` instruction given an existing
971+
/// stake pool and vote account
972+
pub fn decrease_additional_validator_stake_with_vote(
973+
program_id: &Pubkey,
974+
stake_pool: &StakePool,
975+
stake_pool_address: &Pubkey,
976+
vote_account_address: &Pubkey,
977+
lamports: u64,
978+
validator_stake_seed: Option<NonZeroU32>,
979+
transient_stake_seed: u64,
980+
ephemeral_stake_seed: u64,
981+
) -> Instruction {
982+
let pool_withdraw_authority =
983+
find_withdraw_authority_program_address(program_id, stake_pool_address).0;
984+
let (validator_stake_address, _) = find_stake_program_address(
985+
program_id,
986+
vote_account_address,
987+
stake_pool_address,
988+
validator_stake_seed,
989+
);
990+
let (ephemeral_stake_address, _) =
991+
find_ephemeral_stake_program_address(program_id, stake_pool_address, ephemeral_stake_seed);
992+
let (transient_stake_address, _) = find_transient_stake_program_address(
993+
program_id,
994+
vote_account_address,
995+
stake_pool_address,
996+
transient_stake_seed,
997+
);
998+
decrease_additional_validator_stake(
999+
program_id,
1000+
stake_pool_address,
1001+
&stake_pool.staker,
1002+
&pool_withdraw_authority,
1003+
&stake_pool.validator_list,
1004+
&validator_stake_address,
1005+
&ephemeral_stake_address,
1006+
&transient_stake_address,
1007+
lamports,
1008+
transient_stake_seed,
1009+
ephemeral_stake_seed,
1010+
)
1011+
}
1012+
8981013
/// Creates `UpdateValidatorListBalance` instruction (update validator stake account balances)
8991014
pub fn update_validator_list_balance(
9001015
program_id: &Pubkey,

stake-pool/program/src/processor.rs

Lines changed: 154 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,18 +1124,31 @@ impl Processor {
11241124
accounts: &[AccountInfo],
11251125
lamports: u64,
11261126
transient_stake_seed: u64,
1127+
maybe_ephemeral_stake_seed: Option<u64>,
11271128
) -> ProgramResult {
11281129
let account_info_iter = &mut accounts.iter();
11291130
let stake_pool_info = next_account_info(account_info_iter)?;
11301131
let staker_info = next_account_info(account_info_iter)?;
11311132
let withdraw_authority_info = next_account_info(account_info_iter)?;
11321133
let validator_list_info = next_account_info(account_info_iter)?;
11331134
let validator_stake_account_info = next_account_info(account_info_iter)?;
1135+
let maybe_ephemeral_stake_account_info = maybe_ephemeral_stake_seed
1136+
.map(|_| next_account_info(account_info_iter))
1137+
.transpose()?;
11341138
let transient_stake_account_info = next_account_info(account_info_iter)?;
11351139
let clock_info = next_account_info(account_info_iter)?;
11361140
let clock = &Clock::from_account_info(clock_info)?;
1137-
let rent_info = next_account_info(account_info_iter)?;
1138-
let rent = &Rent::from_account_info(rent_info)?;
1141+
let rent = if maybe_ephemeral_stake_seed.is_some() {
1142+
// instruction with ephemeral account doesn't take the rent account
1143+
Rent::get()?
1144+
} else {
1145+
// legacy instruction takes the rent account
1146+
let rent_info = next_account_info(account_info_iter)?;
1147+
Rent::from_account_info(rent_info)?
1148+
};
1149+
let maybe_stake_history_info = maybe_ephemeral_stake_seed
1150+
.map(|_| next_account_info(account_info_iter))
1151+
.transpose()?;
11391152
let system_program_info = next_account_info(account_info_iter)?;
11401153
let stake_program_info = next_account_info(account_info_iter)?;
11411154

@@ -1191,24 +1204,21 @@ impl Processor {
11911204
NonZeroU32::new(validator_stake_info.validator_seed_suffix),
11921205
)?;
11931206
if validator_stake_info.transient_stake_lamports > 0 {
1194-
return Err(StakePoolError::TransientAccountInUse.into());
1207+
if maybe_ephemeral_stake_seed.is_none() {
1208+
msg!("Attempting to decrease stake on a validator with pending transient stake, use DecreaseAdditionalValidatorStake with the existing seed");
1209+
return Err(StakePoolError::TransientAccountInUse.into());
1210+
}
1211+
if transient_stake_seed != validator_stake_info.transient_seed_suffix {
1212+
msg!(
1213+
"Transient stake already exists with seed {}, you must use that one",
1214+
validator_stake_info.transient_seed_suffix
1215+
);
1216+
return Err(ProgramError::InvalidSeeds);
1217+
}
1218+
// Let the runtime check to see if the merge is valid, so there's no
1219+
// explicit check here that the transient stake is decreasing
11951220
}
11961221

1197-
let transient_stake_bump_seed = check_transient_stake_address(
1198-
program_id,
1199-
stake_pool_info.key,
1200-
transient_stake_account_info.key,
1201-
&vote_account_address,
1202-
transient_stake_seed,
1203-
)?;
1204-
let transient_stake_account_signer_seeds: &[&[_]] = &[
1205-
TRANSIENT_STAKE_SEED_PREFIX,
1206-
&vote_account_address.to_bytes(),
1207-
&stake_pool_info.key.to_bytes(),
1208-
&transient_stake_seed.to_le_bytes(),
1209-
&[transient_stake_bump_seed],
1210-
];
1211-
12121222
let stake_minimum_delegation = stake::tools::get_minimum_delegation()?;
12131223
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake::state::StakeState>());
12141224
let current_minimum_lamports =
@@ -1236,38 +1246,126 @@ impl Processor {
12361246
return Err(ProgramError::InsufficientFunds);
12371247
}
12381248

1239-
create_stake_account(
1240-
transient_stake_account_info.clone(),
1241-
transient_stake_account_signer_seeds,
1242-
system_program_info.clone(),
1243-
)?;
1249+
let source_stake_account_info =
1250+
if let Some((ephemeral_stake_seed, ephemeral_stake_account_info)) =
1251+
maybe_ephemeral_stake_seed.zip(maybe_ephemeral_stake_account_info)
1252+
{
1253+
let ephemeral_stake_bump_seed = check_ephemeral_stake_address(
1254+
program_id,
1255+
stake_pool_info.key,
1256+
ephemeral_stake_account_info.key,
1257+
ephemeral_stake_seed,
1258+
)?;
1259+
let ephemeral_stake_account_signer_seeds: &[&[_]] = &[
1260+
EPHEMERAL_STAKE_SEED_PREFIX,
1261+
&stake_pool_info.key.to_bytes(),
1262+
&ephemeral_stake_seed.to_le_bytes(),
1263+
&[ephemeral_stake_bump_seed],
1264+
];
1265+
create_stake_account(
1266+
ephemeral_stake_account_info.clone(),
1267+
ephemeral_stake_account_signer_seeds,
1268+
system_program_info.clone(),
1269+
)?;
12441270

1245-
// split into transient stake account
1246-
Self::stake_split(
1247-
stake_pool_info.key,
1248-
validator_stake_account_info.clone(),
1249-
withdraw_authority_info.clone(),
1250-
AUTHORITY_WITHDRAW,
1251-
stake_pool.stake_withdraw_bump_seed,
1252-
lamports,
1253-
transient_stake_account_info.clone(),
1254-
)?;
1271+
// split into ephemeral stake account
1272+
Self::stake_split(
1273+
stake_pool_info.key,
1274+
validator_stake_account_info.clone(),
1275+
withdraw_authority_info.clone(),
1276+
AUTHORITY_WITHDRAW,
1277+
stake_pool.stake_withdraw_bump_seed,
1278+
lamports,
1279+
ephemeral_stake_account_info.clone(),
1280+
)?;
12551281

1256-
// deactivate transient stake
1257-
Self::stake_deactivate(
1258-
transient_stake_account_info.clone(),
1259-
clock_info.clone(),
1260-
withdraw_authority_info.clone(),
1282+
Self::stake_deactivate(
1283+
ephemeral_stake_account_info.clone(),
1284+
clock_info.clone(),
1285+
withdraw_authority_info.clone(),
1286+
stake_pool_info.key,
1287+
AUTHORITY_WITHDRAW,
1288+
stake_pool.stake_withdraw_bump_seed,
1289+
)?;
1290+
1291+
ephemeral_stake_account_info
1292+
} else {
1293+
// if no ephemeral account is provided, split everything from the
1294+
// validator stake account, into the transient stake account
1295+
validator_stake_account_info
1296+
};
1297+
1298+
let transient_stake_bump_seed = check_transient_stake_address(
1299+
program_id,
12611300
stake_pool_info.key,
1262-
AUTHORITY_WITHDRAW,
1263-
stake_pool.stake_withdraw_bump_seed,
1301+
transient_stake_account_info.key,
1302+
&vote_account_address,
1303+
transient_stake_seed,
12641304
)?;
12651305

1306+
if validator_stake_info.transient_stake_lamports > 0 {
1307+
let stake_history_info = maybe_stake_history_info.unwrap();
1308+
// transient stake exists, try to merge from the source account,
1309+
// which is always an ephemeral account
1310+
Self::stake_merge(
1311+
stake_pool_info.key,
1312+
source_stake_account_info.clone(),
1313+
withdraw_authority_info.clone(),
1314+
AUTHORITY_WITHDRAW,
1315+
stake_pool.stake_withdraw_bump_seed,
1316+
transient_stake_account_info.clone(),
1317+
clock_info.clone(),
1318+
stake_history_info.clone(),
1319+
stake_program_info.clone(),
1320+
)?;
1321+
} else {
1322+
let transient_stake_account_signer_seeds: &[&[_]] = &[
1323+
TRANSIENT_STAKE_SEED_PREFIX,
1324+
&vote_account_address.to_bytes(),
1325+
&stake_pool_info.key.to_bytes(),
1326+
&transient_stake_seed.to_le_bytes(),
1327+
&[transient_stake_bump_seed],
1328+
];
1329+
1330+
create_stake_account(
1331+
transient_stake_account_info.clone(),
1332+
transient_stake_account_signer_seeds,
1333+
system_program_info.clone(),
1334+
)?;
1335+
1336+
// split into transient stake account
1337+
Self::stake_split(
1338+
stake_pool_info.key,
1339+
source_stake_account_info.clone(),
1340+
withdraw_authority_info.clone(),
1341+
AUTHORITY_WITHDRAW,
1342+
stake_pool.stake_withdraw_bump_seed,
1343+
lamports,
1344+
transient_stake_account_info.clone(),
1345+
)?;
1346+
1347+
// Deactivate transient stake if necessary
1348+
let (_, stake) = get_stake_state(transient_stake_account_info)?;
1349+
if stake.delegation.deactivation_epoch == Epoch::MAX {
1350+
Self::stake_deactivate(
1351+
transient_stake_account_info.clone(),
1352+
clock_info.clone(),
1353+
withdraw_authority_info.clone(),
1354+
stake_pool_info.key,
1355+
AUTHORITY_WITHDRAW,
1356+
stake_pool.stake_withdraw_bump_seed,
1357+
)?;
1358+
}
1359+
}
1360+
12661361
validator_stake_info.active_stake_lamports = validator_stake_info
12671362
.active_stake_lamports
12681363
.checked_sub(lamports)
12691364
.ok_or(StakePoolError::CalculationFailure)?;
1270-
validator_stake_info.transient_stake_lamports = lamports;
1365+
validator_stake_info.transient_stake_lamports = validator_stake_info
1366+
.transient_stake_lamports
1367+
.checked_add(lamports)
1368+
.ok_or(StakePoolError::CalculationFailure)?;
12711369
validator_stake_info.transient_seed_suffix = transient_stake_seed;
12721370

12731371
Ok(())
@@ -3248,6 +3346,21 @@ impl Processor {
32483346
accounts,
32493347
lamports,
32503348
transient_stake_seed,
3349+
None,
3350+
)
3351+
}
3352+
StakePoolInstruction::DecreaseAdditionalValidatorStake {
3353+
lamports,
3354+
transient_stake_seed,
3355+
ephemeral_stake_seed,
3356+
} => {
3357+
msg!("Instruction: DecreaseAdditionalValidatorStake");
3358+
Self::process_decrease_validator_stake(
3359+
program_id,
3360+
accounts,
3361+
lamports,
3362+
transient_stake_seed,
3363+
Some(ephemeral_stake_seed),
32513364
)
32523365
}
32533366
StakePoolInstruction::IncreaseValidatorStake {

0 commit comments

Comments
 (0)