Add StakeInstruction::AuthorizeWithSeed (#11700)
* Add StakeInstruction::AuthorizeWithSeed * chore: add authorize-with-seed to web.js * fix: add address_owner * Add SystemInstruction::TransferWithSeed * Update ABI hash * chore: better variable names * Add AuthorizeWithSeedArgs * Reorder and rename arguments for clarity
This commit is contained in:
@ -131,6 +131,13 @@ pub enum StakeInstruction {
|
||||
/// 3. [] Stake history sysvar that carries stake warmup/cooldown history
|
||||
/// 4. [SIGNER] Stake authority
|
||||
Merge,
|
||||
|
||||
/// Authorize a key to manage stake or withdrawal with a derived key
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. [WRITE] Stake account to be updated
|
||||
/// 1. [SIGNER] Base key of stake or withdraw authority
|
||||
AuthorizeWithSeed(AuthorizeWithSeedArgs),
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||
@ -140,6 +147,14 @@ pub struct LockupArgs {
|
||||
pub custodian: Option<Pubkey>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct AuthorizeWithSeedArgs {
|
||||
pub new_authorized_pubkey: Pubkey,
|
||||
pub stake_authorize: StakeAuthorize,
|
||||
pub authority_seed: String,
|
||||
pub authority_owner: Pubkey,
|
||||
}
|
||||
|
||||
fn initialize(stake_pubkey: &Pubkey, authorized: &Authorized, lockup: &Lockup) -> Instruction {
|
||||
Instruction::new(
|
||||
id(),
|
||||
@ -341,6 +356,33 @@ pub fn authorize(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn authorize_with_seed(
|
||||
stake_pubkey: &Pubkey,
|
||||
authority_base: &Pubkey,
|
||||
authority_seed: String,
|
||||
authority_owner: &Pubkey,
|
||||
new_authorized_pubkey: &Pubkey,
|
||||
stake_authorize: StakeAuthorize,
|
||||
) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*stake_pubkey, false),
|
||||
AccountMeta::new_readonly(*authority_base, true),
|
||||
];
|
||||
|
||||
let args = AuthorizeWithSeedArgs {
|
||||
new_authorized_pubkey: *new_authorized_pubkey,
|
||||
stake_authorize,
|
||||
authority_seed,
|
||||
authority_owner: *authority_owner,
|
||||
};
|
||||
|
||||
Instruction::new(
|
||||
id(),
|
||||
&StakeInstruction::AuthorizeWithSeed(args),
|
||||
account_metas,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delegate_stake(
|
||||
stake_pubkey: &Pubkey,
|
||||
authorized_pubkey: &Pubkey,
|
||||
@ -420,7 +462,17 @@ pub fn process_instruction(
|
||||
&Rent::from_keyed_account(next_keyed_account(keyed_accounts)?)?,
|
||||
),
|
||||
StakeInstruction::Authorize(authorized_pubkey, stake_authorize) => {
|
||||
me.authorize(&authorized_pubkey, stake_authorize, &signers)
|
||||
me.authorize(&signers, &authorized_pubkey, stake_authorize)
|
||||
}
|
||||
StakeInstruction::AuthorizeWithSeed(args) => {
|
||||
let authority_base = next_keyed_account(keyed_accounts)?;
|
||||
me.authorize_with_seed(
|
||||
&authority_base,
|
||||
&args.authority_seed,
|
||||
&args.authority_owner,
|
||||
&args.new_authorized_pubkey,
|
||||
args.stake_authorize,
|
||||
)
|
||||
}
|
||||
StakeInstruction::DelegateStake => {
|
||||
let vote = next_keyed_account(keyed_accounts)?;
|
||||
|
@ -556,9 +556,17 @@ pub trait StakeAccount {
|
||||
) -> Result<(), InstructionError>;
|
||||
fn authorize(
|
||||
&self,
|
||||
authority: &Pubkey,
|
||||
stake_authorize: StakeAuthorize,
|
||||
signers: &HashSet<Pubkey>,
|
||||
new_authority: &Pubkey,
|
||||
stake_authorize: StakeAuthorize,
|
||||
) -> Result<(), InstructionError>;
|
||||
fn authorize_with_seed(
|
||||
&self,
|
||||
authority_base: &KeyedAccount,
|
||||
authority_seed: &str,
|
||||
authority_owner: &Pubkey,
|
||||
new_authority: &Pubkey,
|
||||
stake_authorize: StakeAuthorize,
|
||||
) -> Result<(), InstructionError>;
|
||||
fn delegate(
|
||||
&self,
|
||||
@ -627,24 +635,42 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
|
||||
/// staker. The default staker is the owner of the stake account's pubkey.
|
||||
fn authorize(
|
||||
&self,
|
||||
authority: &Pubkey,
|
||||
stake_authorize: StakeAuthorize,
|
||||
signers: &HashSet<Pubkey>,
|
||||
new_authority: &Pubkey,
|
||||
stake_authorize: StakeAuthorize,
|
||||
) -> Result<(), InstructionError> {
|
||||
match self.state()? {
|
||||
StakeState::Stake(mut meta, stake) => {
|
||||
meta.authorized
|
||||
.authorize(signers, authority, stake_authorize)?;
|
||||
.authorize(signers, new_authority, stake_authorize)?;
|
||||
self.set_state(&StakeState::Stake(meta, stake))
|
||||
}
|
||||
StakeState::Initialized(mut meta) => {
|
||||
meta.authorized
|
||||
.authorize(signers, authority, stake_authorize)?;
|
||||
.authorize(signers, new_authority, stake_authorize)?;
|
||||
self.set_state(&StakeState::Initialized(meta))
|
||||
}
|
||||
_ => Err(InstructionError::InvalidAccountData),
|
||||
}
|
||||
}
|
||||
fn authorize_with_seed(
|
||||
&self,
|
||||
authority_base: &KeyedAccount,
|
||||
authority_seed: &str,
|
||||
authority_owner: &Pubkey,
|
||||
new_authority: &Pubkey,
|
||||
stake_authorize: StakeAuthorize,
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut signers = HashSet::default();
|
||||
if let Some(base_pubkey) = authority_base.signer_key() {
|
||||
signers.insert(Pubkey::create_with_seed(
|
||||
base_pubkey,
|
||||
authority_seed,
|
||||
authority_owner,
|
||||
)?);
|
||||
}
|
||||
self.authorize(&signers, &new_authority, stake_authorize)
|
||||
}
|
||||
fn delegate(
|
||||
&self,
|
||||
vote_account: &KeyedAccount,
|
||||
@ -2560,7 +2586,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_authorize_uninit() {
|
||||
let stake_pubkey = Pubkey::new_rand();
|
||||
let new_authority = Pubkey::new_rand();
|
||||
let stake_lamports = 42;
|
||||
let stake_account = Account::new_ref_data_with_space(
|
||||
stake_lamports,
|
||||
@ -2570,21 +2596,21 @@ mod tests {
|
||||
)
|
||||
.expect("stake_account");
|
||||
|
||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
|
||||
let signers = vec![stake_pubkey].into_iter().collect();
|
||||
let stake_keyed_account = KeyedAccount::new(&new_authority, true, &stake_account);
|
||||
let signers = vec![new_authority].into_iter().collect();
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize(&stake_pubkey, StakeAuthorize::Staker, &signers),
|
||||
stake_keyed_account.authorize(&signers, &new_authority, StakeAuthorize::Staker),
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authorize_lockup() {
|
||||
let stake_pubkey = Pubkey::new_rand();
|
||||
let stake_authority = Pubkey::new_rand();
|
||||
let stake_lamports = 42;
|
||||
let stake_account = Account::new_ref_data_with_space(
|
||||
stake_lamports,
|
||||
&StakeState::Initialized(Meta::auto(&stake_pubkey)),
|
||||
&StakeState::Initialized(Meta::auto(&stake_authority)),
|
||||
std::mem::size_of::<StakeState>(),
|
||||
&id(),
|
||||
)
|
||||
@ -2595,16 +2621,16 @@ mod tests {
|
||||
let to_keyed_account = KeyedAccount::new(&to, false, &to_account);
|
||||
|
||||
let clock = Clock::default();
|
||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
|
||||
let stake_keyed_account = KeyedAccount::new(&stake_authority, true, &stake_account);
|
||||
|
||||
let stake_pubkey0 = Pubkey::new_rand();
|
||||
let signers = vec![stake_pubkey].into_iter().collect();
|
||||
let signers = vec![stake_authority].into_iter().collect();
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize(&stake_pubkey0, StakeAuthorize::Staker, &signers),
|
||||
stake_keyed_account.authorize(&signers, &stake_pubkey0, StakeAuthorize::Staker),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize(&stake_pubkey0, StakeAuthorize::Withdrawer, &signers),
|
||||
stake_keyed_account.authorize(&signers, &stake_pubkey0, StakeAuthorize::Withdrawer),
|
||||
Ok(())
|
||||
);
|
||||
if let StakeState::Initialized(Meta { authorized, .. }) =
|
||||
@ -2619,7 +2645,7 @@ mod tests {
|
||||
// A second authorization signed by the stake_keyed_account should fail
|
||||
let stake_pubkey1 = Pubkey::new_rand();
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize(&stake_pubkey1, StakeAuthorize::Staker, &signers),
|
||||
stake_keyed_account.authorize(&signers, &stake_pubkey1, StakeAuthorize::Staker),
|
||||
Err(InstructionError::MissingRequiredSignature)
|
||||
);
|
||||
|
||||
@ -2628,7 +2654,7 @@ mod tests {
|
||||
// Test a second authorization by the newly authorized pubkey
|
||||
let stake_pubkey2 = Pubkey::new_rand();
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize(&stake_pubkey2, StakeAuthorize::Staker, &signers0),
|
||||
stake_keyed_account.authorize(&signers0, &stake_pubkey2, StakeAuthorize::Staker),
|
||||
Ok(())
|
||||
);
|
||||
if let StakeState::Initialized(Meta { authorized, .. }) =
|
||||
@ -2638,7 +2664,7 @@ mod tests {
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize(&stake_pubkey2, StakeAuthorize::Withdrawer, &signers0),
|
||||
stake_keyed_account.authorize(&signers0, &stake_pubkey2, StakeAuthorize::Withdrawer),
|
||||
Ok(())
|
||||
);
|
||||
if let StakeState::Initialized(Meta { authorized, .. }) =
|
||||
@ -2677,6 +2703,88 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authorize_with_seed() {
|
||||
let base_pubkey = Pubkey::new_rand();
|
||||
let seed = "42";
|
||||
let withdrawer_pubkey = Pubkey::create_with_seed(&base_pubkey, &seed, &id()).unwrap();
|
||||
let stake_lamports = 42;
|
||||
let stake_account = Account::new_ref_data_with_space(
|
||||
stake_lamports,
|
||||
&StakeState::Initialized(Meta::auto(&withdrawer_pubkey)),
|
||||
std::mem::size_of::<StakeState>(),
|
||||
&id(),
|
||||
)
|
||||
.expect("stake_account");
|
||||
|
||||
let base_account = Account::new_ref(1, 0, &id());
|
||||
let base_keyed_account = KeyedAccount::new(&base_pubkey, true, &base_account);
|
||||
|
||||
let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account);
|
||||
|
||||
let new_authority = Pubkey::new_rand();
|
||||
|
||||
// Wrong seed
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize_with_seed(
|
||||
&base_keyed_account,
|
||||
&"",
|
||||
&id(),
|
||||
&new_authority,
|
||||
StakeAuthorize::Staker,
|
||||
),
|
||||
Err(InstructionError::MissingRequiredSignature)
|
||||
);
|
||||
|
||||
// Wrong base
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize_with_seed(
|
||||
&stake_keyed_account,
|
||||
&seed,
|
||||
&id(),
|
||||
&new_authority,
|
||||
StakeAuthorize::Staker,
|
||||
),
|
||||
Err(InstructionError::MissingRequiredSignature)
|
||||
);
|
||||
|
||||
// Set stake authority
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize_with_seed(
|
||||
&base_keyed_account,
|
||||
&seed,
|
||||
&id(),
|
||||
&new_authority,
|
||||
StakeAuthorize::Staker,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
// Set withdraw authority
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize_with_seed(
|
||||
&base_keyed_account,
|
||||
&seed,
|
||||
&id(),
|
||||
&new_authority,
|
||||
StakeAuthorize::Withdrawer,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
// No longer withdraw authority
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize_with_seed(
|
||||
&stake_keyed_account,
|
||||
&seed,
|
||||
&id(),
|
||||
&new_authority,
|
||||
StakeAuthorize::Withdrawer,
|
||||
),
|
||||
Err(InstructionError::MissingRequiredSignature)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authorize_override() {
|
||||
let withdrawer_pubkey = Pubkey::new_rand();
|
||||
@ -2692,39 +2800,39 @@ mod tests {
|
||||
let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account);
|
||||
|
||||
// Authorize a staker pubkey and move the withdrawer key into cold storage.
|
||||
let stake_pubkey = Pubkey::new_rand();
|
||||
let new_authority = Pubkey::new_rand();
|
||||
let signers = vec![withdrawer_pubkey].into_iter().collect();
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize(&stake_pubkey, StakeAuthorize::Staker, &signers),
|
||||
stake_keyed_account.authorize(&signers, &new_authority, StakeAuthorize::Staker),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
// Attack! The stake key (a hot key) is stolen and used to authorize a new staker.
|
||||
let mallory_pubkey = Pubkey::new_rand();
|
||||
let signers = vec![stake_pubkey].into_iter().collect();
|
||||
let signers = vec![new_authority].into_iter().collect();
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize(&mallory_pubkey, StakeAuthorize::Staker, &signers),
|
||||
stake_keyed_account.authorize(&signers, &mallory_pubkey, StakeAuthorize::Staker),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
// Verify the original staker no longer has access.
|
||||
let new_stake_pubkey = Pubkey::new_rand();
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize(&new_stake_pubkey, StakeAuthorize::Staker, &signers),
|
||||
stake_keyed_account.authorize(&signers, &new_stake_pubkey, StakeAuthorize::Staker),
|
||||
Err(InstructionError::MissingRequiredSignature)
|
||||
);
|
||||
|
||||
// Verify the withdrawer (pulled from cold storage) can save the day.
|
||||
let signers = vec![withdrawer_pubkey].into_iter().collect();
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize(&new_stake_pubkey, StakeAuthorize::Withdrawer, &signers),
|
||||
stake_keyed_account.authorize(&signers, &new_stake_pubkey, StakeAuthorize::Withdrawer),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
// Attack! Verify the staker cannot be used to authorize a withdraw.
|
||||
let signers = vec![new_stake_pubkey].into_iter().collect();
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize(&mallory_pubkey, StakeAuthorize::Withdrawer, &signers),
|
||||
stake_keyed_account.authorize(&signers, &mallory_pubkey, StakeAuthorize::Withdrawer),
|
||||
Ok(())
|
||||
);
|
||||
}
|
||||
@ -3517,7 +3625,7 @@ mod tests {
|
||||
|
||||
let new_staker_pubkey = Pubkey::new_rand();
|
||||
assert_eq!(
|
||||
stake_keyed_account.authorize(&new_staker_pubkey, StakeAuthorize::Staker, &signers),
|
||||
stake_keyed_account.authorize(&signers, &new_staker_pubkey, StakeAuthorize::Staker),
|
||||
Ok(())
|
||||
);
|
||||
let authorized =
|
||||
|
Reference in New Issue
Block a user