Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cli: Add stake redelegation support
Browse files Browse the repository at this point in the history
mvines committed Jun 30, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent c3e5fc6 commit 9fcf10c
Showing 4 changed files with 384 additions and 9 deletions.
6 changes: 6 additions & 0 deletions cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -214,6 +214,7 @@ pub enum CliCommand {
nonce_authority: SignerIndex,
memo: Option<String>,
fee_payer: SignerIndex,
redelegate_stake_account_pubkey: Option<Pubkey>,
},
SplitStake {
stake_account_pubkey: Pubkey,
@@ -680,6 +681,9 @@ pub fn parse_command(
("delegate-stake", Some(matches)) => {
parse_stake_delegate_stake(matches, default_signer, wallet_manager)
}
("redelegate-stake", Some(matches)) => {
parse_stake_delegate_stake(matches, default_signer, wallet_manager)
}
("withdraw-stake", Some(matches)) => {
parse_stake_withdraw_stake(matches, default_signer, wallet_manager)
}
@@ -1132,6 +1136,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
nonce_authority,
memo,
fee_payer,
redelegate_stake_account_pubkey,
} => process_delegate_stake(
&rpc_client,
config,
@@ -1146,6 +1151,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
*nonce_authority,
memo.as_ref(),
*fee_payer,
redelegate_stake_account_pubkey.as_ref(),
),
CliCommand::SplitStake {
stake_account_pubkey,
133 changes: 127 additions & 6 deletions cli/src/stake.rs
Original file line number Diff line number Diff line change
@@ -284,6 +284,51 @@ impl StakeSubCommands for App<'_, '_> {
.arg(fee_payer_arg())
.arg(memo_arg())
)
.subcommand(
SubCommand::with_name("redelegate-stake")
.about("Redelegate active stake to another vote account")
.arg(
Arg::with_name("force")
.long("force")
.takes_value(false)
.hidden(true) // Don't document this argument to discourage its use
.help("Override vote account sanity checks (use carefully!)")
)
.arg(
pubkey!(Arg::with_name("stake_account_pubkey")
.index(1)
.value_name("STAKE_ACCOUNT_ADDRESS")
.required(true),
"Existing delegated stake account that has been fully activated. \
On success this stake account will be scheduled for deactivation and the rent-exempt balance \
may be withdrawn once fully deactivated")
)
.arg(
pubkey!(Arg::with_name("vote_account_pubkey")
.index(2)
.value_name("REDELEGATED_VOTE_ACCOUNT_ADDRESS")
.required(true),
"The vote account to which the stake will be redelegated")
)
.arg(
Arg::with_name("redelegate_stake_account")
.index(3)
.value_name("REDELEGATED_STAKE_ACCOUNT")
.takes_value(true)
.required(true)
.validator(is_valid_signer)
.help("Stake account to create for the redelegation. \
On success this stake account will be created and scheduled for activation with all \
the stake in the existing stake account, exclusive of the rent-exempt balance retained \
in the existing account")
)
.arg(stake_authority_arg())
.offline_args()
.nonce_args(false)
.arg(fee_payer_arg())
.arg(memo_arg())
)

.subcommand(
SubCommand::with_name("stake-authorize")
.about("Authorize a new signing keypair for the given stake account")
@@ -742,6 +787,8 @@ pub fn parse_stake_delegate_stake(
pubkey_of_signer(matches, "stake_account_pubkey", wallet_manager)?.unwrap();
let vote_account_pubkey =
pubkey_of_signer(matches, "vote_account_pubkey", wallet_manager)?.unwrap();
let (redelegate_stake_account, redelegate_stake_account_pubkey) =
signer_of(matches, "redelegate_stake_account", wallet_manager)?;
let force = matches.is_present("force");
let sign_only = matches.is_present(SIGN_ONLY_ARG.name);
let dump_transaction_message = matches.is_present(DUMP_TRANSACTION_MESSAGE.name);
@@ -754,7 +801,7 @@ pub fn parse_stake_delegate_stake(
signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?;
let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?;

let mut bulk_signers = vec![stake_authority, fee_payer];
let mut bulk_signers = vec![stake_authority, fee_payer, redelegate_stake_account];
if nonce_account.is_some() {
bulk_signers.push(nonce_authority);
}
@@ -774,6 +821,7 @@ pub fn parse_stake_delegate_stake(
nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(),
memo,
fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(),
redelegate_stake_account_pubkey,
},
signers: signer_info.signers,
})
@@ -2393,11 +2441,21 @@ pub fn process_delegate_stake(
nonce_authority: SignerIndex,
memo: Option<&String>,
fee_payer: SignerIndex,
redelegate_stake_account_pubkey: Option<&Pubkey>,
) -> ProcessResult {
check_unique_pubkeys(
(&config.signers[0].pubkey(), "cli keypair".to_string()),
(stake_account_pubkey, "stake_account_pubkey".to_string()),
)?;
if let Some(redelegate_stake_account_pubkey) = &redelegate_stake_account_pubkey {
check_unique_pubkeys(
(stake_account_pubkey, "stake_account_pubkey".to_string()),
(
redelegate_stake_account_pubkey,
"redelegate_stake_account".to_string(),
),
)?;
}
let stake_authority = config.signers[stake_authority];

if !sign_only {
@@ -2450,12 +2508,22 @@ pub fn process_delegate_stake(

let recent_blockhash = blockhash_query.get_blockhash(rpc_client, config.commitment)?;

let ixs = vec![stake_instruction::delegate_stake(
stake_account_pubkey,
&stake_authority.pubkey(),
vote_account_pubkey,
)]
let ixs = if let Some(redelegate_stake_account_pubkey) = &redelegate_stake_account_pubkey {
stake_instruction::redelegate(
stake_account_pubkey,
&stake_authority.pubkey(),
vote_account_pubkey,
redelegate_stake_account_pubkey,
)
} else {
vec![stake_instruction::delegate_stake(
stake_account_pubkey,
&stake_authority.pubkey(),
vote_account_pubkey,
)]
}
.with_memo(memo);

let nonce_authority = config.signers[nonce_authority];
let fee_payer = config.signers[fee_payer];

@@ -3824,6 +3892,7 @@ mod tests {
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
},
signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
}
@@ -3855,6 +3924,7 @@ mod tests {
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@@ -3888,6 +3958,7 @@ mod tests {
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
},
signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
}
@@ -3922,6 +3993,7 @@ mod tests {
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
},
signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
}
@@ -3951,6 +4023,7 @@ mod tests {
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
},
signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
}
@@ -3990,6 +4063,7 @@ mod tests {
nonce_authority: 0,
memo: None,
fee_payer: 1,
redelegate_stake_account_pubkey: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@@ -4038,6 +4112,7 @@ mod tests {
nonce_authority: 2,
memo: None,
fee_payer: 1,
redelegate_stake_account_pubkey: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@@ -4074,6 +4149,7 @@ mod tests {
nonce_authority: 0,
memo: None,
fee_payer: 1,
redelegate_stake_account_pubkey: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@@ -4082,6 +4158,51 @@ mod tests {
}
);

// Test RedelegateStake Subcommand (minimal test due to the significant implementation
// overlap with DelegateStake)
let (redelegate_stake_account_keypair_file, mut redelegate_stake_account_tmp_file) =
make_tmp_file();
let redelegate_stake_account_keypair = Keypair::new();
write_keypair(
&redelegate_stake_account_keypair,
redelegate_stake_account_tmp_file.as_file_mut(),
)
.unwrap();
let redelegate_stake_account_pubkey = redelegate_stake_account_keypair.pubkey();

let test_redelegate_stake = test_commands.clone().get_matches_from(vec![
"test",
"redelegate-stake",
&stake_account_string,
&vote_account_string,
&redelegate_stake_account_keypair_file,
]);
assert_eq!(
parse_command(&test_redelegate_stake, &default_signer, &mut None).unwrap(),
CliCommandInfo {
command: CliCommand::DelegateStake {
stake_account_pubkey,
vote_account_pubkey,
stake_authority: 0,
force: false,
sign_only: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::default(),
nonce_account: None,
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: Some(redelegate_stake_account_pubkey),
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
read_keypair_file(&redelegate_stake_account_keypair_file)
.unwrap()
.into()
],
}
);

// Test WithdrawStake Subcommand
let test_withdraw_stake = test_commands.clone().get_matches_from(vec![
"test",
18 changes: 17 additions & 1 deletion cli/src/test_utils.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use {
solana_client::rpc_client::RpcClient,
solana_sdk::{clock::DEFAULT_MS_PER_SLOT, commitment_config::CommitmentConfig},
solana_sdk::{
clock::{Epoch, DEFAULT_MS_PER_SLOT},
commitment_config::CommitmentConfig,
},
std::{thread::sleep, time::Duration},
};

@@ -35,3 +38,16 @@ pub fn check_ready(rpc_client: &RpcClient) {
sleep(Duration::from_millis(DEFAULT_MS_PER_SLOT));
}
}

pub fn wait_for_next_epoch(rpc_client: &RpcClient) -> Epoch {
let current_epoch = rpc_client.get_epoch_info().unwrap().epoch;
println!("waiting for epoch {}", current_epoch + 1);
loop {
sleep(Duration::from_millis(DEFAULT_MS_PER_SLOT));

let next_epoch = rpc_client.get_epoch_info().unwrap().epoch;
if next_epoch > current_epoch {
return next_epoch;
}
}
}
236 changes: 234 additions & 2 deletions cli/tests/stake.rs
Original file line number Diff line number Diff line change
@@ -6,21 +6,25 @@ use {
cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig},
spend_utils::SpendAmount,
stake::StakeAuthorizationIndexed,
test_utils::check_ready,
test_utils::{check_ready, wait_for_next_epoch},
},
solana_cli_output::{parse_sign_only_reply_string, OutputFormat},
solana_client::{
blockhash_query::{self, BlockhashQuery},
nonce_utils,
rpc_client::RpcClient,
rpc_response::{RpcStakeActivation, StakeActivationState},
},
solana_faucet::faucet::run_local_faucet,
solana_sdk::{
account_utils::StateMut,
commitment_config::CommitmentConfig,
epoch_schedule::EpochSchedule,
fee::FeeStructure,
fee_calculator::FeeRateGovernor,
nonce::State as NonceState,
pubkey::Pubkey,
rent::Rent,
signature::{keypair_from_seed, Keypair, Signer},
stake::{
self,
@@ -29,9 +33,230 @@ use {
},
},
solana_streamer::socket::SocketAddrSpace,
solana_test_validator::TestValidator,
solana_test_validator::{TestValidator, TestValidatorGenesis},
};

#[test]
fn test_stake_redelegation() {
let mint_keypair = Keypair::new();
let mint_pubkey = mint_keypair.pubkey();
let authorized_withdrawer = Keypair::new().pubkey();
let faucet_addr = run_local_faucet(mint_keypair, None);

let slots_per_epoch = 32;
let test_validator = TestValidatorGenesis::default()
.fee_rate_governor(FeeRateGovernor::new(0, 0))
.rent(Rent {
lamports_per_byte_year: 1,
exemption_threshold: 1.0,
..Rent::default()
})
.epoch_schedule(EpochSchedule::custom(
slots_per_epoch,
slots_per_epoch,
/* enable_warmup_epochs = */ false,
))
.faucet_addr(Some(faucet_addr))
.start_with_mint_address(mint_pubkey, SocketAddrSpace::Unspecified)
.expect("validator start failed");

let rpc_client =
RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed());
let default_signer = Keypair::new();

let mut config = CliConfig::recent_for_tests();
config.json_rpc_url = test_validator.rpc_url();
config.signers = vec![&default_signer];

request_and_confirm_airdrop(
&rpc_client,
&config,
&config.signers[0].pubkey(),
100_000_000_000,
)
.unwrap();

// Create vote account
let vote_keypair = Keypair::new();
config.signers = vec![&default_signer, &vote_keypair];
config.command = CliCommand::CreateVoteAccount {
vote_account: 1,
seed: None,
identity_account: 0,
authorized_voter: None,
authorized_withdrawer,
commission: 0,
sign_only: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
nonce_account: None,
nonce_authority: 0,
memo: None,
fee_payer: 0,
};
process_command(&config).unwrap();

// Create second vote account
let vote2_keypair = Keypair::new();
config.signers = vec![&default_signer, &vote2_keypair];
config.command = CliCommand::CreateVoteAccount {
vote_account: 1,
seed: None,
identity_account: 0,
authorized_voter: None,
authorized_withdrawer,
commission: 0,
sign_only: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
nonce_account: None,
nonce_authority: 0,
memo: None,
fee_payer: 0,
};
process_command(&config).unwrap();

// Create stake account
let stake_keypair = Keypair::new();
config.signers = vec![&default_signer, &stake_keypair];
config.command = CliCommand::CreateStakeAccount {
stake_account: 1,
seed: None,
staker: None,
withdrawer: None,
withdrawer_signer: None,
lockup: Lockup::default(),
amount: SpendAmount::Some(50_000_000_000),
sign_only: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
nonce_account: None,
nonce_authority: 0,
memo: None,
fee_payer: 0,
from: 0,
};
process_command(&config).unwrap();

// Delegate stake to `vote_keypair`
config.signers = vec![&default_signer];
config.command = CliCommand::DelegateStake {
stake_account_pubkey: stake_keypair.pubkey(),
vote_account_pubkey: vote_keypair.pubkey(),
stake_authority: 0,
force: true,
sign_only: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::default(),
nonce_account: None,
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
};
process_command(&config).unwrap();

// wait for new epoch
wait_for_next_epoch(&rpc_client);

// `stake_keypair` should now be delegated to `vote_keypair` and fully activated
let stake_account = rpc_client.get_account(&stake_keypair.pubkey()).unwrap();
let stake_state: StakeState = stake_account.state().unwrap();

let rent_exempt_reserve = match stake_state {
StakeState::Stake(meta, stake) => {
assert_eq!(stake.delegation.voter_pubkey, vote_keypair.pubkey());
meta.rent_exempt_reserve
}
_ => panic!("Unexpected stake state!"),
};

assert_eq!(
rpc_client
.get_stake_activation(stake_keypair.pubkey(), None)
.unwrap(),
RpcStakeActivation {
state: StakeActivationState::Active,
active: 50_000_000_000 - rent_exempt_reserve,
inactive: 0
}
);
check_balance!(50_000_000_000, &rpc_client, &stake_keypair.pubkey());

let stake2_keypair = Keypair::new();

// Add an extra `rent_exempt_reserve` amount into `stake2_keypair` before redelegation to
// account for the `rent_exempt_reserve` balance that'll be pealed off the stake during the
// redelegation process
request_and_confirm_airdrop(
&rpc_client,
&config,
&stake2_keypair.pubkey(),
rent_exempt_reserve,
)
.unwrap();

// Redelegate to `vote2_keypair` via `stake2_keypair1
config.signers = vec![&default_signer, &stake2_keypair];
config.command = CliCommand::DelegateStake {
stake_account_pubkey: stake_keypair.pubkey(),
vote_account_pubkey: vote2_keypair.pubkey(),
stake_authority: 0,
force: true,
sign_only: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::default(),
nonce_account: None,
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: Some(stake2_keypair.pubkey()),
};
process_command(&config).unwrap();

// check that all the stake, save `rent_exempt_reserve`, have been moved from `stake_keypair`
// to `stake2_keypair`
check_balance!(rent_exempt_reserve, &rpc_client, &stake_keypair.pubkey());
check_balance!(50_000_000_000, &rpc_client, &stake2_keypair.pubkey(),);

// wait for new epoch
wait_for_next_epoch(&rpc_client);

// `stake_keypair` should now be deactivated
assert_eq!(
rpc_client
.get_stake_activation(stake_keypair.pubkey(), None)
.unwrap(),
RpcStakeActivation {
state: StakeActivationState::Inactive,
active: 0,
inactive: 50_000_000_000 - rent_exempt_reserve,
}
);

// `stake2_keypair` should now be delegated to `vote2_keypair` and fully activated
let stake2_account = rpc_client.get_account(&stake2_keypair.pubkey()).unwrap();
let stake2_state: StakeState = stake2_account.state().unwrap();

match stake2_state {
StakeState::Stake(_meta, stake) => {
assert_eq!(stake.delegation.voter_pubkey, vote2_keypair.pubkey());
}
_ => panic!("Unexpected stake2 state!"),
};

assert_eq!(
rpc_client
.get_stake_activation(stake2_keypair.pubkey(), None)
.unwrap(),
RpcStakeActivation {
state: StakeActivationState::Active,
active: 50_000_000_000 - rent_exempt_reserve,
inactive: 0
}
);
}

#[test]
fn test_stake_delegation_force() {
let mint_keypair = Keypair::new();
@@ -113,6 +338,7 @@ fn test_stake_delegation_force() {
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
};
process_command(&config).unwrap_err();

@@ -129,6 +355,7 @@ fn test_stake_delegation_force() {
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
};
process_command(&config).unwrap();
}
@@ -205,6 +432,7 @@ fn test_seed_stake_delegation_and_deactivation() {
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
};
process_command(&config_validator).unwrap();

@@ -293,6 +521,7 @@ fn test_stake_delegation_and_deactivation() {
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
};
process_command(&config_validator).unwrap();

@@ -405,6 +634,7 @@ fn test_offline_stake_delegation_and_deactivation() {
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
};
config_offline.output_format = OutputFormat::JsonCompact;
let sig_response = process_command(&config_offline).unwrap();
@@ -426,6 +656,7 @@ fn test_offline_stake_delegation_and_deactivation() {
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
};
process_command(&config_payer).unwrap();

@@ -558,6 +789,7 @@ fn test_nonced_stake_delegation_and_deactivation() {
nonce_authority: 0,
memo: None,
fee_payer: 0,
redelegate_stake_account_pubkey: None,
};
process_command(&config).unwrap();

0 comments on commit 9fcf10c

Please sign in to comment.