From 9a7ea1229b1d75a7d83ab164af8e2e875fd8979f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 7 Jul 2021 03:14:54 +0000 Subject: [PATCH] Add vote/stake checked instructions (backport #18345) (#18456) * Add vote/stake checked instructions (cherry picked from commit ee219ffa476c8f23f5635a62098cea60192c6869) # Conflicts: # programs/stake/src/stake_instruction.rs # sdk/program/src/stake/instruction.rs # sdk/src/feature_set.rs * Fix set-lockup custodian index (cherry picked from commit 544f62c92fc871104d76877eb69fda513db8984b) * Add parsing for new stake instructions; clean up confusing test args (cherry picked from commit 9b302ac0b5185eb90faea61e9ba905c7a18f1817) # Conflicts: # transaction-status/src/parse_stake.rs * Add parsing for new vote instructions (cherry picked from commit 39bac256ab0b5551f3e5f53e0c005d3cf9aa8fcb) * Add VoteInstruction::AuthorizeChecked test (cherry picked from commit b8ca2250fd2103959587c1da773c8cf669c04426) * Add Stake checked tests (cherry picked from commit 74e89a3e3e90992c99aad0d0b020b1b0671c4204) # Conflicts: # programs/stake/src/stake_instruction.rs * Fix conflicts and accommodate old apis in backport Co-authored-by: Michael Vines Co-authored-by: Tyera Eulberg --- programs/stake/src/stake_instruction.rs | 575 +++++++++++++++++++++++- programs/vote/src/vote_instruction.rs | 157 +++++++ sdk/src/feature_set.rs | 5 + transaction-status/src/parse_stake.rs | 321 ++++++++++++- transaction-status/src/parse_vote.rs | 32 ++ 5 files changed, 1067 insertions(+), 23 deletions(-) diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 39d6bfb6d2..ca2fd6ae19 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -169,6 +169,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)] @@ -178,6 +233,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, @@ -186,6 +247,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(), @@ -197,6 +265,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, @@ -220,6 +301,46 @@ pub fn create_account_with_seed( ] } +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), + ] +} + pub fn create_account( from_pubkey: &Pubkey, stake_pubkey: &Pubkey, @@ -385,6 +506,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, @@ -418,6 +564,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, @@ -477,6 +656,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, + ) +} + pub fn process_instruction( _program_id: &Pubkey, keyed_accounts: &[KeyedAccount], @@ -598,7 +801,6 @@ pub fn process_instruction( can_merge_expired_lockups, ) } - StakeInstruction::Withdraw(lamports) => { let to = &next_keyed_account(keyed_accounts)?; me.withdraw( @@ -615,7 +817,6 @@ pub fn process_instruction( &from_keyed_account::(next_keyed_account(keyed_accounts)?)?, &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())?) @@ -624,12 +825,107 @@ 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 rent = next_keyed_account(keyed_accounts)?; + let staker = next_keyed_account(keyed_accounts)?; + let withdrawer = next_keyed_account(keyed_accounts)?; + let authorized = Authorized { + staker: *staker.unsigned_key(), + withdrawer: *withdrawer + .signer_key() + .ok_or(InstructionError::MissingRequiredSignature)?, + }; + + me.initialize( + &authorized, + &Lockup::default(), + &from_keyed_account::(rent)?, + ) + } 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::(next_keyed_account(keyed_accounts)?)?; + let _current_authority = next_keyed_account(keyed_accounts)?; + let authorized_pubkey = &next_keyed_account(keyed_accounts)? + .signer_key() + .ok_or(InstructionError::MissingRequiredSignature)?; + let custodian = keyed_accounts.next().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 = next_keyed_account(keyed_accounts)?; + let clock = from_keyed_account::(next_keyed_account(keyed_accounts)?)?; + let authorized_pubkey = &next_keyed_account(keyed_accounts)? + .signer_key() + .ok_or(InstructionError::MissingRequiredSignature)?; + let custodian = keyed_accounts.next().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 _current_authority = next_keyed_account(keyed_accounts)?; + + let custodian = if let Some(custodian) = keyed_accounts.next() { + 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) + } + } } } #[cfg(test)] mod tests { use super::*; + use crate::stake_state::{Meta, StakeState}; use bincode::serialize; use solana_sdk::{ account::{self, Account, AccountSharedData}, @@ -637,8 +933,7 @@ mod tests { rent::Rent, sysvar::stake_history::StakeHistory, }; - use std::cell::RefCell; - use std::str::FromStr; + use std::{cell::RefCell, rc::Rc, str::FromStr}; fn create_default_account() -> RefCell { RefCell::new(AccountSharedData::default()) @@ -1173,4 +1468,276 @@ mod tests { pretty_err::(StakeError::NoCreditsToRedeem.into()) ) } + + #[test] + fn test_stake_checked_instructions() { + let stake_address = Pubkey::new_unique(); + let staker = Pubkey::new_unique(); + let withdrawer = Pubkey::new_unique(); + + // Test InitializeChecked with non-signing withdrawer + let mut instruction = + initialize_checked(&stake_address, &Authorized { staker, withdrawer }); + instruction.accounts[3] = AccountMeta::new_readonly(withdrawer, false); + assert_eq!( + process_instruction(&instruction), + Err(InstructionError::MissingRequiredSignature), + ); + + // Test InitializeChecked with withdrawer signer + let stake_account = AccountSharedData::new_ref( + 1_000_000_000, + std::mem::size_of::(), + &id(), + ); + let rent_address = sysvar::rent::id(); + let rent_account = RefCell::new(account::create_account_shared_data_for_test( + &Rent::default(), + )); + let staker_account = create_default_account(); + let withdrawer_account = create_default_account(); + + let keyed_accounts = vec![ + KeyedAccount::new(&stake_address, false, &stake_account), + KeyedAccount::new(&rent_address, false, &rent_account), + KeyedAccount::new(&staker, false, &staker_account), + KeyedAccount::new(&withdrawer, true, &withdrawer_account), + ]; + + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &keyed_accounts, + &serialize(&StakeInstruction::InitializeChecked).unwrap(), + &mut MockInvokeContext::default() + ), + Ok(()), + ); + + // Test AuthorizeChecked with non-signing authority + let authorized_address = Pubkey::new_unique(); + let mut instruction = authorize_checked( + &stake_address, + &authorized_address, + &staker, + StakeAuthorize::Staker, + None, + ); + instruction.accounts[3] = AccountMeta::new_readonly(staker, false); + assert_eq!( + process_instruction(&instruction), + Err(InstructionError::MissingRequiredSignature), + ); + + let mut instruction = authorize_checked( + &stake_address, + &authorized_address, + &withdrawer, + StakeAuthorize::Withdrawer, + None, + ); + instruction.accounts[3] = AccountMeta::new_readonly(withdrawer, false); + assert_eq!( + process_instruction(&instruction), + Err(InstructionError::MissingRequiredSignature), + ); + + // Test AuthorizeChecked with authority signer + let stake_account = AccountSharedData::new_ref_data_with_space( + 42, + &StakeState::Initialized(Meta::auto(&authorized_address)), + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let clock_address = sysvar::clock::id(); + let clock_account = RefCell::new(account::create_account_shared_data_for_test( + &Clock::default(), + )); + let authorized_account = create_default_account(); + let new_authorized_account = create_default_account(); + + let keyed_accounts = vec![ + KeyedAccount::new(&stake_address, false, &stake_account), + KeyedAccount::new(&clock_address, false, &clock_account), + KeyedAccount::new(&authorized_address, true, &authorized_account), + KeyedAccount::new(&staker, true, &new_authorized_account), + ]; + + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &keyed_accounts, + &serialize(&StakeInstruction::AuthorizeChecked(StakeAuthorize::Staker)).unwrap(), + &mut MockInvokeContext::default() + ), + Ok(()), + ); + + let keyed_accounts = vec![ + KeyedAccount::new(&stake_address, false, &stake_account), + KeyedAccount::new(&clock_address, false, &clock_account), + KeyedAccount::new(&authorized_address, true, &authorized_account), + KeyedAccount::new(&withdrawer, true, &new_authorized_account), + ]; + + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &keyed_accounts, + &serialize(&StakeInstruction::AuthorizeChecked( + StakeAuthorize::Withdrawer + )) + .unwrap(), + &mut MockInvokeContext::default() + ), + Ok(()), + ); + + // Test AuthorizeCheckedWithSeed with non-signing authority + let authorized_owner = Pubkey::new_unique(); + let seed = "test seed"; + let address_with_seed = + Pubkey::create_with_seed(&authorized_owner, seed, &authorized_owner).unwrap(); + let mut instruction = authorize_checked_with_seed( + &stake_address, + &authorized_owner, + seed.to_string(), + &authorized_owner, + &staker, + StakeAuthorize::Staker, + None, + ); + instruction.accounts[3] = AccountMeta::new_readonly(staker, false); + assert_eq!( + process_instruction(&instruction), + Err(InstructionError::MissingRequiredSignature), + ); + + let mut instruction = authorize_checked_with_seed( + &stake_address, + &authorized_owner, + seed.to_string(), + &authorized_owner, + &staker, + StakeAuthorize::Withdrawer, + None, + ); + instruction.accounts[3] = AccountMeta::new_readonly(staker, false); + assert_eq!( + process_instruction(&instruction), + Err(InstructionError::MissingRequiredSignature), + ); + + // Test AuthorizeCheckedWithSeed with authority signer + let stake_account = AccountSharedData::new_ref_data_with_space( + 42, + &StakeState::Initialized(Meta::auto(&address_with_seed)), + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let keyed_accounts = vec![ + KeyedAccount::new(&address_with_seed, false, &stake_account), + KeyedAccount::new(&authorized_owner, true, &authorized_account), + KeyedAccount::new(&clock_address, false, &clock_account), + KeyedAccount::new(&staker, true, &new_authorized_account), + ]; + + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &keyed_accounts, + &serialize(&StakeInstruction::AuthorizeCheckedWithSeed( + AuthorizeCheckedWithSeedArgs { + stake_authorize: StakeAuthorize::Staker, + authority_seed: seed.to_string(), + authority_owner: authorized_owner, + } + )) + .unwrap(), + &mut MockInvokeContext::default() + ), + Ok(()), + ); + + let keyed_accounts = vec![ + KeyedAccount::new(&address_with_seed, false, &stake_account), + KeyedAccount::new(&authorized_owner, true, &authorized_account), + KeyedAccount::new(&clock_address, false, &clock_account), + KeyedAccount::new(&withdrawer, true, &new_authorized_account), + ]; + + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &keyed_accounts, + &serialize(&StakeInstruction::AuthorizeCheckedWithSeed( + AuthorizeCheckedWithSeedArgs { + stake_authorize: StakeAuthorize::Withdrawer, + authority_seed: seed.to_string(), + authority_owner: authorized_owner, + } + )) + .unwrap(), + &mut MockInvokeContext::default() + ), + Ok(()), + ); + + // Test SetLockupChecked with non-signing lockup custodian + let custodian = Pubkey::new_unique(); + let mut instruction = set_lockup_checked( + &stake_address, + &LockupArgs { + unix_timestamp: None, + epoch: Some(1), + custodian: Some(custodian), + }, + &withdrawer, + ); + instruction.accounts[2] = AccountMeta::new_readonly(custodian, false); + assert_eq!( + process_instruction(&instruction), + Err(InstructionError::MissingRequiredSignature), + ); + + // Test SetLockupChecked with lockup custodian signer + let stake_account = AccountSharedData::new_ref_data_with_space( + 42, + &StakeState::Initialized(Meta::auto(&withdrawer)), + std::mem::size_of::(), + &id(), + ) + .unwrap(); + let custodian_account = create_default_account(); + + let keyed_accounts = vec![ + KeyedAccount::new(&stake_address, false, &stake_account), + KeyedAccount::new(&withdrawer, true, &withdrawer_account), + KeyedAccount::new(&custodian, true, &custodian_account), + ]; + + let mut invoke_context = MockInvokeContext::default(); + let clock = Clock::default(); + let mut data = vec![]; + bincode::serialize_into(&mut data, &clock).unwrap(); + invoke_context + .sysvars + .push((sysvar::clock::id(), Some(Rc::new(data)))); + + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &keyed_accounts, + &serialize(&StakeInstruction::SetLockupChecked(LockupCheckedArgs { + unix_timestamp: None, + epoch: Some(1), + })) + .unwrap(), + &mut invoke_context + ), + Ok(()), + ); + } } diff --git a/programs/vote/src/vote_instruction.rs b/programs/vote/src/vote_instruction.rs index 402a19c067..b79a14b669 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,12 +367,32 @@ pub fn process_instruction( let to = next_keyed_account(keyed_accounts)?; 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 clock = next_keyed_account(keyed_accounts)?; + let _current_authority = next_keyed_account(keyed_accounts)?; + let voter_pubkey = &next_keyed_account(keyed_accounts)? + .signer_key() + .ok_or(InstructionError::MissingRequiredSignature)?; + vote_state::authorize( + me, + voter_pubkey, + vote_authorize, + &signers, + &from_keyed_account::(clock)?, + ) + } else { + Err(InstructionError::InvalidInstructionData) + } + } } } #[cfg(test)] mod tests { use super::*; + use bincode::serialize; use solana_sdk::{ account::{self, Account, AccountSharedData}, process_instruction::MockInvokeContext, @@ -349,6 +401,10 @@ mod tests { use std::cell::RefCell; use std::str::FromStr; + fn create_default_account() -> RefCell { + RefCell::new(AccountSharedData::default()) + } + // these are for 100% coverage in this file #[test] fn test_vote_process_instruction_decode_bail() { @@ -491,6 +547,107 @@ mod tests { ); } + #[test] + fn test_vote_authorize_checked() { + let vote_pubkey = Pubkey::new_unique(); + let authorized_pubkey = Pubkey::new_unique(); + let new_authorized_pubkey = Pubkey::new_unique(); + + // Test with vanilla authorize accounts + let mut instruction = authorize_checked( + &vote_pubkey, + &authorized_pubkey, + &new_authorized_pubkey, + VoteAuthorize::Voter, + ); + instruction.accounts = instruction.accounts[0..2].to_vec(); + assert_eq!( + process_instruction(&instruction), + Err(InstructionError::NotEnoughAccountKeys), + ); + + let mut instruction = authorize_checked( + &vote_pubkey, + &authorized_pubkey, + &new_authorized_pubkey, + VoteAuthorize::Withdrawer, + ); + instruction.accounts = instruction.accounts[0..2].to_vec(); + assert_eq!( + process_instruction(&instruction), + Err(InstructionError::NotEnoughAccountKeys), + ); + + // Test with non-signing new_authorized_pubkey + let mut instruction = authorize_checked( + &vote_pubkey, + &authorized_pubkey, + &new_authorized_pubkey, + VoteAuthorize::Voter, + ); + instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false); + assert_eq!( + process_instruction(&instruction), + Err(InstructionError::MissingRequiredSignature), + ); + + let mut instruction = authorize_checked( + &vote_pubkey, + &authorized_pubkey, + &new_authorized_pubkey, + VoteAuthorize::Withdrawer, + ); + instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false); + assert_eq!( + process_instruction(&instruction), + Err(InstructionError::MissingRequiredSignature), + ); + + // Test with new_authorized_pubkey signer + let vote_account = AccountSharedData::new_ref(100, VoteState::size_of(), &id()); + let clock_address = sysvar::clock::id(); + let clock_account = RefCell::new(account::create_account_shared_data_for_test( + &Clock::default(), + )); + let default_authorized_pubkey = Pubkey::default(); + let authorized_account = create_default_account(); + let new_authorized_account = create_default_account(); + let keyed_accounts = vec![ + KeyedAccount::new(&vote_pubkey, false, &vote_account), + KeyedAccount::new(&clock_address, false, &clock_account), + KeyedAccount::new(&default_authorized_pubkey, true, &authorized_account), + KeyedAccount::new(&new_authorized_pubkey, true, &new_authorized_account), + ]; + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &keyed_accounts, + &serialize(&VoteInstruction::AuthorizeChecked(VoteAuthorize::Voter)).unwrap(), + &mut MockInvokeContext::default() + ), + Ok(()) + ); + + let keyed_accounts = vec![ + KeyedAccount::new(&vote_pubkey, false, &vote_account), + KeyedAccount::new(&clock_address, false, &clock_account), + KeyedAccount::new(&default_authorized_pubkey, true, &authorized_account), + KeyedAccount::new(&new_authorized_pubkey, true, &new_authorized_account), + ]; + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &keyed_accounts, + &serialize(&VoteInstruction::AuthorizeChecked( + VoteAuthorize::Withdrawer + )) + .unwrap(), + &mut MockInvokeContext::default() + ), + Ok(()) + ); + } + #[test] fn test_minimum_balance() { let rent = solana_sdk::rent::Rent::default(); diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 6c645a7efd..a377384f1a 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -154,6 +154,10 @@ pub mod dedupe_config_program_signers { solana_sdk::declare_id!("8kEuAshXLsgkUEdcFVLqrjCGGHVWFW99ZZpxvAzzMtBp"); } +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 = [ @@ -191,6 +195,7 @@ lazy_static! { (system_transfer_zero_check::id(), "perform all checks for transfers of 0 lamports"), (memory_ops_syscalls::id(), "add syscalls for memory operations"), (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() diff --git a/transaction-status/src/parse_stake.rs b/transaction-status/src/parse_stake.rs index 1f733459e3..b121766de1 100644 --- a/transaction-status/src/parse_stake.rs +++ b/transaction-status/src/parse_stake.rs @@ -185,6 +185,86 @@ pub fn parse_stake( info: value, }) } + StakeInstruction::InitializeChecked => { + check_num_stake_accounts(&instruction.accounts, 4)?; + Ok(ParsedInstructionEnum { + instruction_type: "initializeChecked".to_string(), + info: json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "rentSysvar": account_keys[instruction.accounts[1] as usize].to_string(), + "staker": account_keys[instruction.accounts[2] as usize].to_string(), + "withdrawer": account_keys[instruction.accounts[3] as usize].to_string(), + }), + }) + } + StakeInstruction::AuthorizeChecked(authority_type) => { + check_num_stake_accounts(&instruction.accounts, 4)?; + 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": account_keys[instruction.accounts[3] as usize].to_string(), + "authorityType": authority_type, + }); + let map = value.as_object_mut().unwrap(); + if instruction.accounts.len() >= 5 { + map.insert( + "custodian".to_string(), + json!(account_keys[instruction.accounts[4] as usize].to_string()), + ); + } + Ok(ParsedInstructionEnum { + instruction_type: "authorizeChecked".to_string(), + info: value, + }) + } + StakeInstruction::AuthorizeCheckedWithSeed(args) => { + check_num_stake_accounts(&instruction.accounts, 4)?; + let mut value = json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "authorityBase": account_keys[instruction.accounts[1] as usize].to_string(), + "clockSysvar": account_keys[instruction.accounts[2] as usize].to_string(), + "newAuthorized": account_keys[instruction.accounts[3] as usize].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() >= 5 { + map.insert( + "custodian".to_string(), + json!(account_keys[instruction.accounts[4] as usize].to_string()), + ); + } + Ok(ParsedInstructionEnum { + instruction_type: "authorizeCheckedWithSeed".to_string(), + info: value, + }) + } + StakeInstruction::SetLockupChecked(lockup_args) => { + check_num_stake_accounts(&instruction.accounts, 2)?; + let mut lockup_map = Map::new(); + if let Some(timestamp) = lockup_args.unix_timestamp { + lockup_map.insert("unixTimestamp".to_string(), json!(timestamp)); + } + if let Some(epoch) = lockup_args.epoch { + lockup_map.insert("epoch".to_string(), json!(epoch)); + } + if instruction.accounts.len() >= 3 { + lockup_map.insert( + "custodian".to_string(), + json!(account_keys[instruction.accounts[2] as usize].to_string()), + ); + } + Ok(ParsedInstructionEnum { + instruction_type: "setLockupChecked".to_string(), + info: json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "custodian": account_keys[instruction.accounts[1] as usize].to_string(), + "lockup": lockup_map, + }), + }) + } } } @@ -268,22 +348,22 @@ mod test { assert!(parse_stake(&message.instructions[0], &keys[0..2]).is_err()); let instruction = stake_instruction::authorize( - &keys[1], + &keys[2], &keys[0], - &keys[3], + &keys[4], StakeAuthorize::Withdrawer, Some(&keys[1]), ); let message = Message::new(&[instruction], None); assert_eq!( - parse_stake(&message.instructions[0], &keys[0..3]).unwrap(), + parse_stake(&message.instructions[0], &keys[0..4]).unwrap(), ParsedInstructionEnum { instruction_type: "authorize".to_string(), info: json!({ - "stakeAccount": keys[1].to_string(), - "clockSysvar": keys[2].to_string(), + "stakeAccount": keys[2].to_string(), + "clockSysvar": keys[3].to_string(), "authority": keys[0].to_string(), - "newAuthority": keys[3].to_string(), + "newAuthority": keys[4].to_string(), "authorityType": StakeAuthorize::Withdrawer, "custodian": keys[1].to_string(), }), @@ -402,7 +482,7 @@ mod test { &keys[1], &keys[0], seed.to_string(), - &keys[2], + &keys[0], &keys[3], StakeAuthorize::Staker, None, @@ -414,7 +494,7 @@ mod test { instruction_type: "authorizeWithSeed".to_string(), info: json!({ "stakeAccount": keys[1].to_string(), - "authorityOwner": keys[2].to_string(), + "authorityOwner": keys[0].to_string(), "newAuthorized": keys[3].to_string(), "authorityBase": keys[0].to_string(), "authoritySeed": seed, @@ -423,26 +503,26 @@ mod test { }), } ); - assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err()); + assert!(parse_stake(&message.instructions[0], &keys[0..2]).is_err()); let instruction = stake_instruction::authorize_with_seed( - &keys[1], + &keys[2], &keys[0], seed.to_string(), - &keys[2], - &keys[3], + &keys[0], + &keys[4], StakeAuthorize::Withdrawer, - Some(&keys[4]), + Some(&keys[1]), ); let message = Message::new(&[instruction], None); assert_eq!( - parse_stake(&message.instructions[0], &keys[0..5]).unwrap(), + parse_stake(&message.instructions[0], &keys[0..4]).unwrap(), ParsedInstructionEnum { instruction_type: "authorizeWithSeed".to_string(), info: json!({ "stakeAccount": keys[2].to_string(), - "authorityOwner": keys[2].to_string(), - "newAuthorized": keys[3].to_string(), + "authorityOwner": keys[0].to_string(), + "newAuthorized": keys[4].to_string(), "authorityBase": keys[0].to_string(), "authoritySeed": seed, "authorityType": StakeAuthorize::Withdrawer, @@ -451,14 +531,14 @@ mod test { }), } ); - assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err()); + assert!(parse_stake(&message.instructions[0], &keys[0..3]).is_err()); } #[test] #[allow(clippy::same_item_push)] - fn test_parse_set_lockup() { + fn test_parse_stake_set_lockup() { let mut keys: Vec = vec![]; - for _ in 0..2 { + for _ in 0..3 { keys.push(Pubkey::new_unique()); } let unix_timestamp = 1_234_567_890; @@ -532,5 +612,208 @@ mod test { ); assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err()); + + let lockup = LockupArgs { + unix_timestamp: Some(unix_timestamp), + epoch: None, + custodian: None, + }; + let instruction = stake_instruction::set_lockup_checked(&keys[1], &lockup, &keys[0]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "setLockupChecked".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "custodian": keys[0].to_string(), + "lockup": { + "unixTimestamp": unix_timestamp + } + }), + } + ); + + let lockup = LockupArgs { + unix_timestamp: Some(unix_timestamp), + epoch: Some(epoch), + custodian: None, + }; + let instruction = stake_instruction::set_lockup_checked(&keys[1], &lockup, &keys[0]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "setLockupChecked".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "custodian": keys[0].to_string(), + "lockup": { + "unixTimestamp": unix_timestamp, + "epoch": epoch, + } + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err()); + + let lockup = LockupArgs { + unix_timestamp: Some(unix_timestamp), + epoch: Some(epoch), + custodian: Some(keys[1]), + }; + let instruction = stake_instruction::set_lockup_checked(&keys[2], &lockup, &keys[0]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "setLockupChecked".to_string(), + info: json!({ + "stakeAccount": keys[2].to_string(), + "custodian": keys[0].to_string(), + "lockup": { + "unixTimestamp": unix_timestamp, + "epoch": epoch, + "custodian": keys[1].to_string(), + } + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..2]).is_err()); + } + + #[test] + #[allow(clippy::same_item_push)] + fn test_parse_stake_checked_instructions() { + let mut keys: Vec = vec![]; + for _ in 0..6 { + keys.push(Pubkey::new_unique()); + } + + let authorized = Authorized { + staker: keys[3], + withdrawer: keys[0], + }; + let lamports = 55; + + let instructions = + stake_instruction::create_account_checked(&keys[0], &keys[1], &authorized, lamports); + let message = Message::new(&instructions, None); + assert_eq!( + parse_stake(&message.instructions[1], &keys[0..4]).unwrap(), + ParsedInstructionEnum { + instruction_type: "initializeChecked".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "rentSysvar": keys[2].to_string(), + "staker": keys[3].to_string(), + "withdrawer": keys[0].to_string(), + }), + } + ); + assert!(parse_stake(&message.instructions[1], &keys[0..3]).is_err()); + + let instruction = stake_instruction::authorize_checked( + &keys[2], + &keys[0], + &keys[1], + StakeAuthorize::Staker, + None, + ); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..4]).unwrap(), + ParsedInstructionEnum { + instruction_type: "authorizeChecked".to_string(), + info: json!({ + "stakeAccount": keys[2].to_string(), + "clockSysvar": keys[3].to_string(), + "authority": keys[0].to_string(), + "newAuthority": keys[1].to_string(), + "authorityType": StakeAuthorize::Staker, + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..3]).is_err()); + + let instruction = stake_instruction::authorize_checked( + &keys[3], + &keys[0], + &keys[1], + StakeAuthorize::Withdrawer, + Some(&keys[2]), + ); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..5]).unwrap(), + ParsedInstructionEnum { + instruction_type: "authorizeChecked".to_string(), + info: json!({ + "stakeAccount": keys[3].to_string(), + "clockSysvar": keys[4].to_string(), + "authority": keys[0].to_string(), + "newAuthority": keys[1].to_string(), + "authorityType": StakeAuthorize::Withdrawer, + "custodian": keys[2].to_string(), + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..4]).is_err()); + + let seed = "test_seed"; + let instruction = stake_instruction::authorize_checked_with_seed( + &keys[2], + &keys[0], + seed.to_string(), + &keys[0], + &keys[1], + StakeAuthorize::Staker, + None, + ); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..4]).unwrap(), + ParsedInstructionEnum { + instruction_type: "authorizeCheckedWithSeed".to_string(), + info: json!({ + "stakeAccount": keys[2].to_string(), + "authorityOwner": keys[0].to_string(), + "newAuthorized": keys[1].to_string(), + "authorityBase": keys[0].to_string(), + "authoritySeed": seed, + "authorityType": StakeAuthorize::Staker, + "clockSysvar": keys[3].to_string(), + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..3]).is_err()); + + let instruction = stake_instruction::authorize_checked_with_seed( + &keys[3], + &keys[0], + seed.to_string(), + &keys[0], + &keys[1], + StakeAuthorize::Withdrawer, + Some(&keys[2]), + ); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..5]).unwrap(), + ParsedInstructionEnum { + instruction_type: "authorizeCheckedWithSeed".to_string(), + info: json!({ + "stakeAccount": keys[3].to_string(), + "authorityOwner": keys[0].to_string(), + "newAuthorized": keys[1].to_string(), + "authorityBase": keys[0].to_string(), + "authoritySeed": seed, + "authorityType": StakeAuthorize::Withdrawer, + "clockSysvar": keys[4].to_string(), + "custodian": keys[2].to_string(), + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..4]).is_err()); } } diff --git a/transaction-status/src/parse_vote.rs b/transaction-status/src/parse_vote.rs index 679e44c3ee..b8b11923e3 100644 --- a/transaction-status/src/parse_vote.rs +++ b/transaction-status/src/parse_vote.rs @@ -121,6 +121,19 @@ pub fn parse_vote( }), }) } + VoteInstruction::AuthorizeChecked(authority_type) => { + check_num_vote_accounts(&instruction.accounts, 4)?; + Ok(ParsedInstructionEnum { + instruction_type: "authorizeChecked".to_string(), + info: json!({ + "voteAccount": 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": account_keys[instruction.accounts[3] as usize].to_string(), + "authorityType": authority_type, + }), + }) + } } } @@ -294,5 +307,24 @@ mod test { } ); assert!(parse_vote(&message.instructions[0], &keys[0..3]).is_err()); + + let authority_type = VoteAuthorize::Voter; + let instruction = + vote_instruction::authorize_checked(&keys[1], &keys[0], &keys[3], authority_type); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_vote(&message.instructions[0], &keys[0..4]).unwrap(), + ParsedInstructionEnum { + instruction_type: "authorizeChecked".to_string(), + info: json!({ + "voteAccount": keys[2].to_string(), + "clockSysvar": keys[3].to_string(), + "authority": keys[0].to_string(), + "newAuthority": keys[1].to_string(), + "authorityType": authority_type, + }), + } + ); + assert!(parse_vote(&message.instructions[0], &keys[0..3]).is_err()); } }