cli now supports a custodian for stake authorize operations

This commit is contained in:
Michael Vines 2021-01-26 09:44:44 -08:00
parent ffa5c7dcc8
commit 119e2c75dd
8 changed files with 182 additions and 36 deletions

View File

@ -260,6 +260,7 @@ pub enum CliCommand {
nonce_account: Option<Pubkey>,
nonce_authority: SignerIndex,
fee_payer: SignerIndex,
custodian: Option<SignerIndex>,
},
StakeSetLockup {
stake_account_pubkey: Pubkey,
@ -1520,11 +1521,13 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
nonce_account,
nonce_authority,
fee_payer,
custodian,
} => process_stake_authorize(
&rpc_client,
config,
&stake_account_pubkey,
new_authorizations,
*custodian,
*sign_only,
blockhash_query,
*nonce_account,

View File

@ -64,6 +64,12 @@ pub const WITHDRAW_AUTHORITY_ARG: ArgConstant<'static> = ArgConstant {
help: "Authorized withdrawer [default: cli config keypair]",
};
pub const CUSTODIAN_ARG: ArgConstant<'static> = ArgConstant {
name: "custodian",
long: "custodian",
help: "Authority to override account lockup",
};
fn stake_authority_arg<'a, 'b>() -> Arg<'a, 'b> {
Arg::with_name(STAKE_AUTHORITY_ARG.name)
.long(STAKE_AUTHORITY_ARG.long)
@ -82,6 +88,15 @@ fn withdraw_authority_arg<'a, 'b>() -> Arg<'a, 'b> {
.help(WITHDRAW_AUTHORITY_ARG.help)
}
fn custodian_arg<'a, 'b>() -> Arg<'a, 'b> {
Arg::with_name(CUSTODIAN_ARG.name)
.long(CUSTODIAN_ARG.long)
.takes_value(true)
.value_name("KEYPAIR")
.validator(is_valid_signer)
.help(CUSTODIAN_ARG.help)
}
pub trait StakeSubCommands {
fn stake_subcommands(self) -> Self;
}
@ -223,6 +238,7 @@ impl StakeSubCommands for App<'_, '_> {
.offline_args()
.nonce_args(false)
.arg(fee_payer_arg())
.arg(custodian_arg())
)
.subcommand(
SubCommand::with_name("deactivate-stake")
@ -331,14 +347,7 @@ impl StakeSubCommands for App<'_, '_> {
.offline_args()
.nonce_args(false)
.arg(fee_payer_arg())
.arg(
Arg::with_name("custodian")
.long("custodian")
.takes_value(true)
.value_name("KEYPAIR")
.validator(is_valid_signer)
.help("Authority to override account lockup")
)
.arg(custodian_arg())
)
.subcommand(
SubCommand::with_name("stake-set-lockup")
@ -561,11 +570,15 @@ pub fn parse_stake_authorize(
let (nonce_authority, nonce_authority_pubkey) =
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 (custodian, custodian_pubkey) = signer_of(matches, "custodian", wallet_manager)?;
bulk_signers.push(fee_payer);
if nonce_account.is_some() {
bulk_signers.push(nonce_authority);
}
if custodian.is_some() {
bulk_signers.push(custodian);
}
let signer_info =
default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?;
@ -591,6 +604,7 @@ pub fn parse_stake_authorize(
nonce_account,
nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(),
fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(),
custodian: custodian_pubkey.and_then(|_| signer_info.index_of(custodian_pubkey)),
},
signers: signer_info.signers,
})
@ -970,6 +984,7 @@ pub fn process_stake_authorize(
config: &CliConfig,
stake_account_pubkey: &Pubkey,
new_authorizations: &[(StakeAuthorize, Pubkey, SignerIndex)],
custodian: Option<SignerIndex>,
sign_only: bool,
blockhash_query: &BlockhashQuery,
nonce_account: Option<Pubkey>,
@ -977,6 +992,7 @@ pub fn process_stake_authorize(
fee_payer: SignerIndex,
) -> ProcessResult {
let mut ixs = Vec::new();
let custodian = custodian.map(|index| config.signers[index]);
for (stake_authorize, authorized_pubkey, authority) in new_authorizations.iter() {
check_unique_pubkeys(
(stake_account_pubkey, "stake_account_pubkey".to_string()),
@ -988,6 +1004,7 @@ pub fn process_stake_authorize(
&authority.pubkey(), // currently authorized
authorized_pubkey, // new stake signer
*stake_authorize, // stake or withdraw
custodian.map(|signer| signer.pubkey()).as_ref(),
));
}
@ -1939,6 +1956,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
},
signers: vec![read_keypair_file(&default_keypair_file).unwrap().into(),],
},
@ -1973,6 +1991,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@ -2011,6 +2030,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@ -2038,6 +2058,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
},
signers: vec![read_keypair_file(&default_keypair_file).unwrap().into(),],
},
@ -2062,6 +2083,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@ -2092,6 +2114,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@ -2123,6 +2146,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
},
signers: vec![read_keypair_file(&default_keypair_file).unwrap().into(),],
},
@ -2151,6 +2175,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@ -2185,6 +2210,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
},
signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
}
@ -2221,6 +2247,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 1,
custodian: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@ -2267,6 +2294,7 @@ mod tests {
nonce_account: Some(nonce_account),
nonce_authority: 2,
fee_payer: 1,
custodian: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@ -2299,6 +2327,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
},
signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
}
@ -2336,6 +2365,7 @@ mod tests {
nonce_account: Some(nonce_account_pubkey),
nonce_authority: 1,
fee_payer: 0,
custodian: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@ -2369,6 +2399,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 1,
custodian: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
@ -2406,6 +2437,7 @@ mod tests {
nonce_account: None,
nonce_authority: 0,
fee_payer: 1,
custodian: None,
},
signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),

View File

@ -592,6 +592,7 @@ fn test_stake_authorize() {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
};
process_command(&config).unwrap();
let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap();
@ -619,6 +620,7 @@ fn test_stake_authorize() {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
};
process_command(&config).unwrap();
let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap();
@ -641,6 +643,7 @@ fn test_stake_authorize() {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
};
process_command(&config).unwrap();
let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap();
@ -663,6 +666,7 @@ fn test_stake_authorize() {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
};
config_offline.output_format = OutputFormat::JsonCompact;
let sign_reply = process_command(&config_offline).unwrap();
@ -678,6 +682,7 @@ fn test_stake_authorize() {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
};
process_command(&config).unwrap();
let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap();
@ -724,6 +729,7 @@ fn test_stake_authorize() {
nonce_account: Some(nonce_account.pubkey()),
nonce_authority: 0,
fee_payer: 0,
custodian: None,
};
let sign_reply = process_command(&config_offline).unwrap();
let sign_only = parse_sign_only_reply_string(&sign_reply);
@ -743,6 +749,7 @@ fn test_stake_authorize() {
nonce_account: Some(nonce_account.pubkey()),
nonce_authority: 0,
fee_payer: 0,
custodian: None,
};
process_command(&config).unwrap();
let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap();
@ -845,6 +852,7 @@ fn test_stake_authorize_with_fee_payer() {
nonce_account: None,
nonce_authority: 0,
fee_payer: 1,
custodian: None,
};
process_command(&config).unwrap();
// `config` balance has not changed, despite submitting the TX
@ -863,6 +871,7 @@ fn test_stake_authorize_with_fee_payer() {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
};
config_offline.output_format = OutputFormat::JsonCompact;
let sign_reply = process_command(&config_offline).unwrap();
@ -878,6 +887,7 @@ fn test_stake_authorize_with_fee_payer() {
nonce_account: None,
nonce_authority: 0,
fee_payer: 0,
custodian: None,
};
process_command(&config).unwrap();
// `config`'s balance again has not changed

View File

@ -781,6 +781,7 @@ mod tests {
&stake_authority.pubkey(),
&new_stake_authority,
StakeAuthorize::Staker,
None,
);
let message = Message::new(&[ix], Some(&stake_authority.pubkey()));
let tx = Transaction::new(&[&stake_authority], message, blockhash);

View File

@ -68,6 +68,8 @@ pub enum StakeInstruction {
/// 0. [WRITE] Stake account to be updated
/// 1. [] Clock sysvar
/// 2. [SIGNER] The stake or withdraw authority
/// 3. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before
/// lockup expiration
Authorize(Pubkey, StakeAuthorize),
/// Delegate a stake to a particular vote account
@ -139,6 +141,8 @@ pub enum StakeInstruction {
/// 0. [WRITE] Stake account to be updated
/// 1. [SIGNER] Base key of stake or withdraw authority
/// 2. [] Clock sysvar
/// 3. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before
/// lockup expiration
AuthorizeWithSeed(AuthorizeWithSeedArgs),
}
@ -344,13 +348,18 @@ pub fn authorize(
authorized_pubkey: &Pubkey,
new_authorized_pubkey: &Pubkey,
stake_authorize: StakeAuthorize,
custodian_pubkey: Option<&Pubkey>,
) -> Instruction {
let account_metas = vec![
let mut account_metas = vec![
AccountMeta::new(*stake_pubkey, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(*authorized_pubkey, true),
];
if let Some(custodian_pubkey) = custodian_pubkey {
account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true));
}
Instruction::new(
id(),
&StakeInstruction::Authorize(*new_authorized_pubkey, stake_authorize),
@ -365,13 +374,18 @@ pub fn authorize_with_seed(
authority_owner: &Pubkey,
new_authorized_pubkey: &Pubkey,
stake_authorize: StakeAuthorize,
custodian_pubkey: Option<&Pubkey>,
) -> Instruction {
let account_metas = vec![
let mut account_metas = vec![
AccountMeta::new(*stake_pubkey, false),
AccountMeta::new_readonly(*authority_base, true),
AccountMeta::new_readonly(sysvar::clock::id(), false),
];
if let Some(custodian_pubkey) = custodian_pubkey {
account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true));
}
let args = AuthorizeWithSeedArgs {
new_authorized_pubkey: *new_authorized_pubkey,
stake_authorize,
@ -637,7 +651,8 @@ mod tests {
&Pubkey::default(),
&Pubkey::default(),
&Pubkey::default(),
StakeAuthorize::Staker
StakeAuthorize::Staker,
None,
)),
Err(InstructionError::InvalidAccountData),
);
@ -722,7 +737,8 @@ mod tests {
&spoofed_stake_state_pubkey(),
&Pubkey::default(),
&Pubkey::default(),
StakeAuthorize::Staker
StakeAuthorize::Staker,
None,
)),
Err(InstructionError::IncorrectProgramId),
);

View File

@ -66,12 +66,14 @@ fn authorize_stake_accounts_instructions(
stake_authority_pubkey,
new_stake_authority_pubkey,
StakeAuthorize::Staker,
None,
);
let instruction1 = stake_instruction::authorize(
&stake_account_address,
withdraw_authority_pubkey,
new_withdraw_authority_pubkey,
StakeAuthorize::Withdrawer,
None,
);
vec![instruction0, instruction1]
}

View File

@ -211,6 +211,7 @@ fn distribution_instructions(
&stake_authority,
&recipient,
StakeAuthorize::Staker,
None,
));
// Make the recipient the new withdraw authority
@ -219,6 +220,7 @@ fn distribution_instructions(
&withdraw_authority,
&recipient,
StakeAuthorize::Withdrawer,
None,
));
// Add lockup

View File

@ -45,15 +45,23 @@ pub fn parse_stake(
}
StakeInstruction::Authorize(new_authorized, authority_type) => {
check_num_stake_accounts(&instruction.accounts, 3)?;
let mut value = json!({
"stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(),
"clockSysvar": account_keys[instruction.accounts[1] as usize].to_string(),
"authority": account_keys[instruction.accounts[2] as usize].to_string(),
"newAuthority": new_authorized.to_string(),
"authorityType": authority_type,
});
let map = value.as_object_mut().unwrap();
if instruction.accounts.len() >= 4 {
map.insert(
"custodian".to_string(),
json!(account_keys[instruction.accounts[3] as usize].to_string()),
);
}
Ok(ParsedInstructionEnum {
instruction_type: "authorize".to_string(),
info: json!({
"stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(),
"clockSysvar": account_keys[instruction.accounts[1] as usize].to_string(),
"authority": account_keys[instruction.accounts[2] as usize].to_string(),
"newAuthority": new_authorized.to_string(),
"authorityType": authority_type,
}),
info: value,
})
}
StakeInstruction::DelegateStake => {
@ -93,7 +101,7 @@ pub fn parse_stake(
"lamports": lamports,
});
let map = value.as_object_mut().unwrap();
if instruction.accounts.len() == 6 {
if instruction.accounts.len() >= 6 {
map.insert(
"custodian".to_string(),
json!(account_keys[instruction.accounts[5] as usize].to_string()),
@ -151,16 +159,30 @@ pub fn parse_stake(
}
StakeInstruction::AuthorizeWithSeed(args) => {
check_num_stake_accounts(&instruction.accounts, 2)?;
Ok(ParsedInstructionEnum {
instruction_type: "authorizeWithSeed".to_string(),
info: json!({
let mut value = json!({
"stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(),
"authorityBase": account_keys[instruction.accounts[1] as usize].to_string(),
"newAuthorized": args.new_authorized_pubkey.to_string(),
"authorityType": args.stake_authorize,
"authoritySeed": args.authority_seed,
"authorityOwner": args.authority_owner.to_string(),
}),
});
let map = value.as_object_mut().unwrap();
if instruction.accounts.len() >= 3 {
map.insert(
"clockSysvar".to_string(),
json!(account_keys[instruction.accounts[2] as usize].to_string()),
);
}
if instruction.accounts.len() >= 4 {
map.insert(
"custodian".to_string(),
json!(account_keys[instruction.accounts[3] as usize].to_string()),
);
}
Ok(ParsedInstructionEnum {
instruction_type: "authorizeWithSeed".to_string(),
info: value,
})
}
}
@ -184,17 +206,17 @@ mod test {
fn test_parse_stake_instruction() {
let mut keys: Vec<Pubkey> = vec![];
for _ in 0..6 {
keys.push(solana_sdk::pubkey::new_rand());
keys.push(Pubkey::new_unique());
}
let authorized = Authorized {
staker: solana_sdk::pubkey::new_rand(),
withdrawer: solana_sdk::pubkey::new_rand(),
staker: Pubkey::new_unique(),
withdrawer: Pubkey::new_unique(),
};
let lockup = Lockup {
unix_timestamp: 1_234_567_890,
epoch: 11,
custodian: solana_sdk::pubkey::new_rand(),
custodian: Pubkey::new_unique(),
};
let lamports = 55;
@ -222,9 +244,13 @@ mod test {
);
assert!(parse_stake(&message.instructions[1], &keys[0..2]).is_err());
let authority_type = StakeAuthorize::Staker;
let instruction =
stake_instruction::authorize(&keys[1], &keys[0], &keys[3], authority_type);
let instruction = stake_instruction::authorize(
&keys[1],
&keys[0],
&keys[3],
StakeAuthorize::Staker,
None,
);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_stake(&message.instructions[0], &keys[0..3]).unwrap(),
@ -235,7 +261,31 @@ mod test {
"clockSysvar": keys[2].to_string(),
"authority": keys[0].to_string(),
"newAuthority": keys[3].to_string(),
"authorityType": authority_type,
"authorityType": StakeAuthorize::Staker,
}),
}
);
assert!(parse_stake(&message.instructions[0], &keys[0..2]).is_err());
let instruction = stake_instruction::authorize(
&keys[1],
&keys[0],
&keys[3],
StakeAuthorize::Withdrawer,
Some(&keys[1]),
);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_stake(&message.instructions[0], &keys[0..3]).unwrap(),
ParsedInstructionEnum {
instruction_type: "authorize".to_string(),
info: json!({
"stakeAccount": keys[1].to_string(),
"clockSysvar": keys[2].to_string(),
"authority": keys[0].to_string(),
"newAuthority": keys[3].to_string(),
"authorityType": StakeAuthorize::Withdrawer,
"custodian": keys[1].to_string(),
}),
}
);
@ -350,7 +400,8 @@ mod test {
seed.to_string(),
&keys[2],
&keys[3],
authority_type,
StakeAuthorize::Staker,
None,
);
let message = Message::new(&[instruction], None);
assert_eq!(
@ -363,7 +414,36 @@ mod test {
"newAuthorized": keys[3].to_string(),
"authorityBase": keys[0].to_string(),
"authoritySeed": seed,
"authorityType": authority_type,
"authorityType": StakeAuthorize::Staker,
"clockSysvar": keys[2].to_string(),
}),
}
);
assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err());
let instruction = stake_instruction::authorize_with_seed(
&keys[1],
&keys[0],
seed.to_string(),
&keys[2],
&keys[3],
StakeAuthorize::Withdrawer,
Some(&keys[4]),
);
let message = Message::new(&[instruction], None);
assert_eq!(
parse_stake(&message.instructions[0], &keys[0..5]).unwrap(),
ParsedInstructionEnum {
instruction_type: "authorizeWithSeed".to_string(),
info: json!({
"stakeAccount": keys[2].to_string(),
"authorityOwner": keys[2].to_string(),
"newAuthorized": keys[3].to_string(),
"authorityBase": keys[0].to_string(),
"authoritySeed": seed,
"authorityType": StakeAuthorize::Withdrawer,
"clockSysvar": keys[3].to_string(),
"custodian": keys[1].to_string(),
}),
}
);
@ -375,11 +455,11 @@ mod test {
fn test_parse_set_lockup() {
let mut keys: Vec<Pubkey> = vec![];
for _ in 0..2 {
keys.push(solana_sdk::pubkey::new_rand());
keys.push(Pubkey::new_unique());
}
let unix_timestamp = 1_234_567_890;
let epoch = 11;
let custodian = solana_sdk::pubkey::new_rand();
let custodian = Pubkey::new_unique();
let lockup = LockupArgs {
unix_timestamp: Some(unix_timestamp),