From ee219ffa476c8f23f5635a62098cea60192c6869 Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Wed, 30 Jun 2021 17:34:50 -0700 Subject: [PATCH] Add vote/stake checked instructions --- programs/stake/src/stake_instruction.rs | 117 ++++++++++++-- programs/vote/src/vote_instruction.rs | 49 ++++++ sdk/program/src/stake/instruction.rs | 203 ++++++++++++++++++++++++ sdk/src/feature_set.rs | 5 + 4 files changed, 365 insertions(+), 9 deletions(-) diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 011732913..daa78f1c4 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -8,7 +8,11 @@ use { process_instruction::{get_sysvar, InvokeContext}, program_utils::limited_deserialize, pubkey::Pubkey, - stake::{instruction::StakeInstruction, program::id}, + stake::{ + instruction::StakeInstruction, + program::id, + state::{Authorized, Lockup}, + }, sysvar::{self, clock::Clock, rent::Rent, stake_history::StakeHistory}, }, }; @@ -56,8 +60,9 @@ pub fn process_instruction( let clock = from_keyed_account::(keyed_account_at_index(keyed_accounts, 1)?)?; let _current_authority = keyed_account_at_index(keyed_accounts, 2)?; - let custodian = - keyed_account_at_index(keyed_accounts, 3).map(|ka| ka.unsigned_key()); + let custodian = keyed_account_at_index(keyed_accounts, 3) + .ok() + .map(|ka| ka.unsigned_key()); me.authorize( &signers, @@ -65,7 +70,7 @@ pub fn process_instruction( stake_authorize, require_custodian_for_locked_stake_authorize, &clock, - custodian.ok(), + custodian, ) } else { me.authorize( @@ -87,8 +92,9 @@ pub fn process_instruction( if require_custodian_for_locked_stake_authorize { let clock = from_keyed_account::(keyed_account_at_index(keyed_accounts, 2)?)?; - let custodian = - keyed_account_at_index(keyed_accounts, 3).map(|ka| ka.unsigned_key()); + let custodian = keyed_account_at_index(keyed_accounts, 3) + .ok() + .map(|ka| ka.unsigned_key()); me.authorize_with_seed( authority_base, @@ -98,7 +104,7 @@ pub fn process_instruction( args.stake_authorize, require_custodian_for_locked_stake_authorize, &clock, - custodian.ok(), + custodian, ) } else { me.authorize_with_seed( @@ -144,7 +150,6 @@ pub fn process_instruction( can_merge_expired_lockups, ) } - StakeInstruction::Withdraw(lamports) => { let to = &keyed_account_at_index(keyed_accounts, 1)?; me.withdraw( @@ -161,7 +166,6 @@ pub fn process_instruction( &from_keyed_account::(keyed_account_at_index(keyed_accounts, 1)?)?, &signers, ), - StakeInstruction::SetLockup(lockup) => { let clock = if invoke_context.is_feature_active(&feature_set::stake_program_v4::id()) { Some(get_sysvar::(invoke_context, &sysvar::clock::id())?) @@ -170,6 +174,101 @@ pub fn process_instruction( }; me.set_lockup(&lockup, &signers, clock.as_ref()) } + StakeInstruction::InitializeChecked => { + if invoke_context.is_feature_active(&feature_set::vote_stake_checked_instructions::id()) + { + let authorized = Authorized { + staker: *keyed_account_at_index(keyed_accounts, 2)?.unsigned_key(), + withdrawer: *keyed_account_at_index(keyed_accounts, 3)? + .signer_key() + .ok_or(InstructionError::MissingRequiredSignature)?, + }; + + me.initialize( + &authorized, + &Lockup::default(), + &from_keyed_account::(keyed_account_at_index(keyed_accounts, 1)?)?, + ) + } else { + Err(InstructionError::InvalidInstructionData) + } + } + StakeInstruction::AuthorizeChecked(stake_authorize) => { + if invoke_context.is_feature_active(&feature_set::vote_stake_checked_instructions::id()) + { + let clock = + from_keyed_account::(keyed_account_at_index(keyed_accounts, 1)?)?; + let _current_authority = keyed_account_at_index(keyed_accounts, 2)?; + let authorized_pubkey = &keyed_account_at_index(keyed_accounts, 3)? + .signer_key() + .ok_or(InstructionError::MissingRequiredSignature)?; + let custodian = keyed_account_at_index(keyed_accounts, 4) + .ok() + .map(|ka| ka.unsigned_key()); + + me.authorize( + &signers, + authorized_pubkey, + stake_authorize, + true, + &clock, + custodian, + ) + } else { + Err(InstructionError::InvalidInstructionData) + } + } + StakeInstruction::AuthorizeCheckedWithSeed(args) => { + if invoke_context.is_feature_active(&feature_set::vote_stake_checked_instructions::id()) + { + let authority_base = keyed_account_at_index(keyed_accounts, 1)?; + let clock = + from_keyed_account::(keyed_account_at_index(keyed_accounts, 2)?)?; + let authorized_pubkey = &keyed_account_at_index(keyed_accounts, 3)? + .signer_key() + .ok_or(InstructionError::MissingRequiredSignature)?; + let custodian = keyed_account_at_index(keyed_accounts, 4) + .ok() + .map(|ka| ka.unsigned_key()); + + me.authorize_with_seed( + authority_base, + &args.authority_seed, + &args.authority_owner, + authorized_pubkey, + args.stake_authorize, + true, + &clock, + custodian, + ) + } else { + Err(InstructionError::InvalidInstructionData) + } + } + StakeInstruction::SetLockupChecked(lockup_checked) => { + if invoke_context.is_feature_active(&feature_set::vote_stake_checked_instructions::id()) + { + let custodian = if let Ok(custodian) = keyed_account_at_index(keyed_accounts, 4) { + Some( + *custodian + .signer_key() + .ok_or(InstructionError::MissingRequiredSignature)?, + ) + } else { + None + }; + + let lockup = LockupArgs { + unix_timestamp: lockup_checked.unix_timestamp, + epoch: lockup_checked.epoch, + custodian, + }; + let clock = Some(get_sysvar::(invoke_context, &sysvar::clock::id())?); + me.set_lockup(&lockup, &signers, clock.as_ref()) + } else { + Err(InstructionError::InvalidInstructionData) + } + } } } diff --git a/programs/vote/src/vote_instruction.rs b/programs/vote/src/vote_instruction.rs index 92ce7d981..5a34b0a6d 100644 --- a/programs/vote/src/vote_instruction.rs +++ b/programs/vote/src/vote_instruction.rs @@ -111,6 +111,18 @@ pub enum VoteInstruction { /// 2. [] Clock sysvar /// 3. [SIGNER] Vote authority VoteSwitch(Vote, Hash), + + /// Authorize a key to send votes or issue a withdrawal + /// + /// This instruction behaves like `Authorize` with the additional requirement that the new vote + /// or withdraw authority must also be a signer. + /// + /// # Account references + /// 0. [WRITE] Vote account to be updated with the Pubkey for authorization + /// 1. [] Clock sysvar + /// 2. [SIGNER] Vote or withdraw authority + /// 3. [SIGNER] New vote or withdraw authority + AuthorizeChecked(VoteAuthorize), } fn initialize_account(vote_pubkey: &Pubkey, vote_init: &VoteInit) -> Instruction { @@ -182,6 +194,26 @@ pub fn authorize( ) } +pub fn authorize_checked( + vote_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, // currently authorized + new_authorized_pubkey: &Pubkey, + vote_authorize: VoteAuthorize, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*vote_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*authorized_pubkey, true), + AccountMeta::new_readonly(*new_authorized_pubkey, true), + ]; + + Instruction::new_with_bincode( + id(), + &VoteInstruction::AuthorizeChecked(vote_authorize), + account_metas, + ) +} + pub fn update_validator_identity( vote_pubkey: &Pubkey, authorized_withdrawer_pubkey: &Pubkey, @@ -335,6 +367,23 @@ pub fn process_instruction( let to = keyed_account_at_index(keyed_accounts, 1)?; vote_state::withdraw(me, lamports, to, &signers) } + VoteInstruction::AuthorizeChecked(vote_authorize) => { + if invoke_context.is_feature_active(&feature_set::vote_stake_checked_instructions::id()) + { + let voter_pubkey = &keyed_account_at_index(keyed_accounts, 3)? + .signer_key() + .ok_or(InstructionError::MissingRequiredSignature)?; + vote_state::authorize( + me, + voter_pubkey, + vote_authorize, + &signers, + &from_keyed_account::(keyed_account_at_index(keyed_accounts, 1)?)?, + ) + } else { + Err(InstructionError::InvalidInstructionData) + } + } } } diff --git a/sdk/program/src/stake/instruction.rs b/sdk/program/src/stake/instruction.rs index 3b4782144..6fadfd7a0 100644 --- a/sdk/program/src/stake/instruction.rs +++ b/sdk/program/src/stake/instruction.rs @@ -167,6 +167,61 @@ pub enum StakeInstruction { /// 3. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before /// lockup expiration AuthorizeWithSeed(AuthorizeWithSeedArgs), + + /// Initialize a stake with authorization information + /// + /// This instruction is similar to `Initialize` except that the withdraw authority + /// must be a signer, and no lockup is applied to the account. + /// + /// # Account references + /// 0. [WRITE] Uninitialized stake account + /// 1. [] Rent sysvar + /// 2. [] The stake authority + /// 3. [SIGNER] The withdraw authority + /// + InitializeChecked, + + /// Authorize a key to manage stake or withdrawal + /// + /// This instruction behaves like `Authorize` with the additional requirement that the new + /// stake or withdraw authority must also be a signer. + /// + /// # Account references + /// 0. [WRITE] Stake account to be updated + /// 1. [] Clock sysvar + /// 2. [SIGNER] The stake or withdraw authority + /// 3. [SIGNER] The new stake or withdraw authority + /// 4. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before + /// lockup expiration + AuthorizeChecked(StakeAuthorize), + + /// Authorize a key to manage stake or withdrawal with a derived key + /// + /// This instruction behaves like `AuthorizeWithSeed` with the additional requirement that + /// the new stake or withdraw authority must also be a signer. + /// + /// # Account references + /// 0. [WRITE] Stake account to be updated + /// 1. [SIGNER] Base key of stake or withdraw authority + /// 2. [] Clock sysvar + /// 3. [SIGNER] The new stake or withdraw authority + /// 4. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before + /// lockup expiration + AuthorizeCheckedWithSeed(AuthorizeCheckedWithSeedArgs), + + /// Set stake lockup + /// + /// This instruction behaves like `SetLockup` with the additional requirement that + /// the new lockup authority also be a signer. + /// + /// If a lockup is not active, the withdraw authority may set a new lockup + /// If a lockup is active, the lockup custodian may update the lockup parameters + /// + /// # Account references + /// 0. [WRITE] Initialized stake account + /// 1. [SIGNER] Lockup authority or withdraw authority + /// 2. Optional: [SIGNER] New lockup authority + SetLockupChecked(LockupCheckedArgs), } #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] @@ -176,6 +231,12 @@ pub struct LockupArgs { pub custodian: Option, } +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] +pub struct LockupCheckedArgs { + pub unix_timestamp: Option, + pub epoch: Option, +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct AuthorizeWithSeedArgs { pub new_authorized_pubkey: Pubkey, @@ -184,6 +245,13 @@ pub struct AuthorizeWithSeedArgs { pub authority_owner: Pubkey, } +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct AuthorizeCheckedWithSeedArgs { + pub stake_authorize: StakeAuthorize, + pub authority_seed: String, + pub authority_owner: Pubkey, +} + pub fn initialize(stake_pubkey: &Pubkey, authorized: &Authorized, lockup: &Lockup) -> Instruction { Instruction::new_with_bincode( id(), @@ -195,6 +263,19 @@ pub fn initialize(stake_pubkey: &Pubkey, authorized: &Authorized, lockup: &Locku ) } +pub fn initialize_checked(stake_pubkey: &Pubkey, authorized: &Authorized) -> Instruction { + Instruction::new_with_bincode( + id(), + &StakeInstruction::InitializeChecked, + vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(authorized.staker, false), + AccountMeta::new_readonly(authorized.withdrawer, true), + ], + ) +} + pub fn create_account_with_seed( from_pubkey: &Pubkey, stake_pubkey: &Pubkey, @@ -237,6 +318,46 @@ pub fn create_account( ] } +pub fn create_account_with_seed_checked( + from_pubkey: &Pubkey, + stake_pubkey: &Pubkey, + base: &Pubkey, + seed: &str, + authorized: &Authorized, + lamports: u64, +) -> Vec { + vec![ + system_instruction::create_account_with_seed( + from_pubkey, + stake_pubkey, + base, + seed, + lamports, + std::mem::size_of::() as u64, + &id(), + ), + initialize_checked(stake_pubkey, authorized), + ] +} + +pub fn create_account_checked( + from_pubkey: &Pubkey, + stake_pubkey: &Pubkey, + authorized: &Authorized, + lamports: u64, +) -> Vec { + vec![ + system_instruction::create_account( + from_pubkey, + stake_pubkey, + lamports, + std::mem::size_of::() as u64, + &id(), + ), + initialize_checked(stake_pubkey, authorized), + ] +} + fn _split( stake_pubkey: &Pubkey, authorized_pubkey: &Pubkey, @@ -383,6 +504,31 @@ pub fn authorize( ) } +pub fn authorize_checked( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + new_authorized_pubkey: &Pubkey, + stake_authorize: StakeAuthorize, + custodian_pubkey: Option<&Pubkey>, +) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*authorized_pubkey, true), + AccountMeta::new_readonly(*new_authorized_pubkey, true), + ]; + + if let Some(custodian_pubkey) = custodian_pubkey { + account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true)); + } + + Instruction::new_with_bincode( + id(), + &StakeInstruction::AuthorizeChecked(stake_authorize), + account_metas, + ) +} + pub fn authorize_with_seed( stake_pubkey: &Pubkey, authority_base: &Pubkey, @@ -416,6 +562,39 @@ pub fn authorize_with_seed( ) } +pub fn authorize_checked_with_seed( + stake_pubkey: &Pubkey, + authority_base: &Pubkey, + authority_seed: String, + authority_owner: &Pubkey, + new_authorized_pubkey: &Pubkey, + stake_authorize: StakeAuthorize, + custodian_pubkey: Option<&Pubkey>, +) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(*authority_base, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*new_authorized_pubkey, true), + ]; + + if let Some(custodian_pubkey) = custodian_pubkey { + account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true)); + } + + let args = AuthorizeCheckedWithSeedArgs { + stake_authorize, + authority_seed, + authority_owner: *authority_owner, + }; + + Instruction::new_with_bincode( + id(), + &StakeInstruction::AuthorizeCheckedWithSeed(args), + account_metas, + ) +} + pub fn delegate_stake( stake_pubkey: &Pubkey, authorized_pubkey: &Pubkey, @@ -475,6 +654,30 @@ pub fn set_lockup( Instruction::new_with_bincode(id(), &StakeInstruction::SetLockup(*lockup), account_metas) } +pub fn set_lockup_checked( + stake_pubkey: &Pubkey, + lockup: &LockupArgs, + custodian_pubkey: &Pubkey, +) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(*custodian_pubkey, true), + ]; + + let lockup_checked = LockupCheckedArgs { + unix_timestamp: lockup.unix_timestamp, + epoch: lockup.epoch, + }; + if let Some(new_custodian) = lockup.custodian { + account_metas.push(AccountMeta::new_readonly(new_custodian, true)); + } + Instruction::new_with_bincode( + id(), + &StakeInstruction::SetLockupChecked(lockup_checked), + account_metas, + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index cff5fe5b2..2c9262a42 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -147,6 +147,10 @@ pub mod verify_tx_signatures_len { solana_sdk::declare_id!("EVW9B5xD9FFK7vw1SBARwMA4s5eRo5eKJdKpsBikzKBz"); } +pub mod vote_stake_checked_instructions { + solana_sdk::declare_id!("BcWknVcgvonN8sL4HE4XFuEVgfcee5MwxWPAgP6ZV89X"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -183,6 +187,7 @@ lazy_static! { (system_transfer_zero_check::id(), "perform all checks for transfers of 0 lamports"), (blake3_syscall_enabled::id(), "blake3 syscall"), (dedupe_config_program_signers::id(), "dedupe config program signers"), + (vote_stake_checked_instructions::id(), "vote/state program checked instructions #18345"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter()