diff --git a/docs/src/cluster/stake-delegation-and-rewards.md b/docs/src/cluster/stake-delegation-and-rewards.md index ad63ae15d91159..344774c9209789 100644 --- a/docs/src/cluster/stake-delegation-and-rewards.md +++ b/docs/src/cluster/stake-delegation-and-rewards.md @@ -45,6 +45,13 @@ VoteState is the current state of all the votes the validator has submitted to t Updates the account with a new authorized voter or withdrawer, according to the VoteAuthorize parameter \(`Voter` or `Withdrawer`\). The transaction must be signed by the Vote account's current `authorized_voter` or `authorized_withdrawer`. +- `account[0]` - RW - The VoteState. + `VoteState::authorized_voter` or `authorized_withdrawer` is set to `Pubkey`. + +### VoteInstruction::AuthorizeWithSeed\(VoteAuthorizeWithSeedArgs\) + +Updates the account with a new authorized voter or withdrawer, according to the VoteAuthorize parameter \(`Voter` or `Withdrawer`\). Unlike `VoteInstruction::Authorize` this instruction is for use when the Vote account's current `authorized_voter` or `authorized_withdrawer` is a derived key. The transaction must be signed by someone who can sign for the base key of that derived key. + - `account[0]` - RW - The VoteState. `VoteState::authorized_voter` or `authorized_withdrawer` is set to `Pubkey`. diff --git a/programs/vote/src/vote_instruction.rs b/programs/vote/src/vote_instruction.rs index 90d973ed8b55f0..e6b965de3591b6 100644 --- a/programs/vote/src/vote_instruction.rs +++ b/programs/vote/src/vote_instruction.rs @@ -3,7 +3,10 @@ use { crate::{ id, - vote_state::{Vote, VoteAuthorize, VoteInit, VoteState, VoteStateUpdate}, + vote_state::{ + Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, VoteAuthorizeWithSeedArgs, + VoteInit, VoteState, VoteStateUpdate, + }, }, serde_derive::{Deserialize, Serialize}, solana_sdk::{ @@ -99,6 +102,30 @@ pub enum VoteInstruction { /// 0. `[Write]` Vote account to vote with /// 1. `[SIGNER]` Vote authority UpdateVoteStateSwitch(VoteStateUpdate, Hash), + + /// Given that the current Voter or Withdrawer authority is a derived key, + /// this instruction allows someone who can sign for that derived key's + /// base key to authorize a new Voter or Withdrawer for a vote account. + /// + /// # Account references + /// 0. `[Write]` Vote account to be updated + /// 1. `[]` Clock sysvar + /// 2. `[SIGNER]` Base key of current Voter or Withdrawer authority's derived key + AuthorizeWithSeed(VoteAuthorizeWithSeedArgs), + + /// Given that the current Voter or Withdrawer authority is a derived key, + /// this instruction allows someone who can sign for that derived key's + /// base key to authorize a new Voter or Withdrawer for a vote account. + /// + /// This instruction behaves like `AuthorizeWithSeed` 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 + /// 1. `[]` Clock sysvar + /// 2. `[SIGNER]` Base key of current Voter or Withdrawer authority's derived key + /// 3. `[SIGNER]` New vote or withdraw authority + AuthorizeCheckedWithSeed(VoteAuthorizeCheckedWithSeedArgs), } fn initialize_account(vote_pubkey: &Pubkey, vote_init: &VoteInit) -> Instruction { @@ -190,6 +217,58 @@ pub fn authorize_checked( ) } +pub fn authorize_with_seed( + vote_pubkey: &Pubkey, + current_authority_base_key: &Pubkey, + current_authority_derived_key_owner: &Pubkey, + current_authority_derived_key_seed: &str, + new_authority: &Pubkey, + authorization_type: VoteAuthorize, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*vote_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*current_authority_base_key, true), + ]; + + Instruction::new_with_bincode( + id(), + &VoteInstruction::AuthorizeWithSeed(VoteAuthorizeWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: *current_authority_derived_key_owner, + current_authority_derived_key_seed: current_authority_derived_key_seed.to_string(), + new_authority: *new_authority, + }), + account_metas, + ) +} + +pub fn authorize_checked_with_seed( + vote_pubkey: &Pubkey, + current_authority_base_key: &Pubkey, + current_authority_derived_key_owner: &Pubkey, + current_authority_derived_key_seed: &str, + new_authority: &Pubkey, + authorization_type: VoteAuthorize, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*vote_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*current_authority_base_key, true), + AccountMeta::new_readonly(*new_authority, true), + ]; + + Instruction::new_with_bincode( + id(), + &VoteInstruction::AuthorizeCheckedWithSeed(VoteAuthorizeCheckedWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: *current_authority_derived_key_owner, + current_authority_derived_key_seed: current_authority_derived_key_seed.to_string(), + }), + account_metas, + ) +} + pub fn update_validator_identity( vote_pubkey: &Pubkey, authorized_withdrawer_pubkey: &Pubkey, diff --git a/programs/vote/src/vote_processor.rs b/programs/vote/src/vote_processor.rs index 482c32c3dcef86..437587d1196c0d 100644 --- a/programs/vote/src/vote_processor.rs +++ b/programs/vote/src/vote_processor.rs @@ -1,14 +1,65 @@ //! Vote program processor use { - crate::{id, vote_instruction::VoteInstruction, vote_state}, + crate::{ + id, + vote_instruction::VoteInstruction, + vote_state::{self, VoteAuthorize}, + }, log::*, solana_program_runtime::{ invoke_context::InvokeContext, sysvar_cache::get_sysvar_with_account_check, }, - solana_sdk::{feature_set, instruction::InstructionError, program_utils::limited_deserialize}, + solana_sdk::{ + feature_set, + instruction::InstructionError, + program_utils::limited_deserialize, + pubkey::Pubkey, + transaction_context::{BorrowedAccount, InstructionContext, TransactionContext}, + }, + std::collections::HashSet, }; +fn process_authorize_with_seed_instruction( + invoke_context: &InvokeContext, + instruction_context: &InstructionContext, + transaction_context: &TransactionContext, + first_instruction_account: usize, + vote_account: &mut BorrowedAccount, + new_authority: &Pubkey, + authorization_type: VoteAuthorize, + current_authority_derived_key_owner: &Pubkey, + current_authority_derived_key_seed: &str, +) -> Result<(), InstructionError> { + if !invoke_context + .feature_set + .is_active(&feature_set::vote_authorize_with_seed::id()) + { + return Err(InstructionError::InvalidInstructionData); + } + let clock = get_sysvar_with_account_check::clock(invoke_context, instruction_context, 1)?; + let mut expected_authority_keys: HashSet = HashSet::default(); + let authority_base_key_index = first_instruction_account + 2; + if instruction_context.is_signer(authority_base_key_index)? { + let base_pubkey = transaction_context.get_key_of_account_at_index( + instruction_context.get_index_in_transaction(authority_base_key_index)?, + )?; + expected_authority_keys.insert(Pubkey::create_with_seed( + base_pubkey, + current_authority_derived_key_seed, + current_authority_derived_key_owner, + )?); + }; + vote_state::authorize( + vote_account, + new_authority, + authorization_type, + &expected_authority_keys, + &clock, + &invoke_context.feature_set, + ) +} + pub fn process_instruction( first_instruction_account: usize, invoke_context: &mut InvokeContext, @@ -48,6 +99,41 @@ pub fn process_instruction( &invoke_context.feature_set, ) } + VoteInstruction::AuthorizeWithSeed(args) => { + instruction_context.check_number_of_instruction_accounts(3)?; + process_authorize_with_seed_instruction( + invoke_context, + instruction_context, + transaction_context, + first_instruction_account, + &mut me, + &args.new_authority, + args.authorization_type, + &args.current_authority_derived_key_owner, + args.current_authority_derived_key_seed.as_str(), + ) + } + VoteInstruction::AuthorizeCheckedWithSeed(args) => { + instruction_context.check_number_of_instruction_accounts(4)?; + let new_authority_index = first_instruction_account + 3; + let new_authority = transaction_context.get_key_of_account_at_index( + instruction_context.get_index_in_transaction(new_authority_index)?, + )?; + if !instruction_context.is_signer(new_authority_index)? { + return Err(InstructionError::MissingRequiredSignature); + } + process_authorize_with_seed_instruction( + invoke_context, + instruction_context, + transaction_context, + first_instruction_account, + &mut me, + new_authority, + args.authorization_type, + &args.current_authority_derived_key_owner, + args.current_authority_derived_key_seed.as_str(), + ) + } VoteInstruction::UpdateValidatorIdentity => { instruction_context.check_number_of_instruction_accounts(2)?; let node_pubkey = transaction_context.get_key_of_account_at_index( @@ -166,8 +252,8 @@ mod tests { vote_switch, withdraw, VoteInstruction, }, vote_state::{ - Lockout, Vote, VoteAuthorize, VoteInit, VoteState, VoteStateUpdate, - VoteStateVersions, + Lockout, Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, + VoteAuthorizeWithSeedArgs, VoteInit, VoteState, VoteStateUpdate, VoteStateVersions, }, }, bincode::serialize, @@ -184,6 +270,17 @@ mod tests { std::{collections::HashSet, str::FromStr}, }; + struct VoteAccountTestFixtureWithAuthorities { + vote_account: AccountSharedData, + vote_pubkey: Pubkey, + voter_base_key: Pubkey, + voter_owner: Pubkey, + voter_seed: String, + withdrawer_base_key: Pubkey, + withdrawer_owner: Pubkey, + withdrawer_seed: String, + } + fn create_default_account() -> AccountSharedData { AccountSharedData::new(0, 0, &Pubkey::new_unique()) } @@ -312,6 +409,41 @@ mod tests { ) } + fn create_test_account_with_authorized_from_seed() -> VoteAccountTestFixtureWithAuthorities { + let vote_pubkey = Pubkey::new_unique(); + let voter_base_key = Pubkey::new_unique(); + let voter_owner = Pubkey::new_unique(); + let voter_seed = String::from("VOTER_SEED"); + let withdrawer_base_key = Pubkey::new_unique(); + let withdrawer_owner = Pubkey::new_unique(); + let withdrawer_seed = String::from("WITHDRAWER_SEED"); + let authorized_voter = + Pubkey::create_with_seed(&voter_base_key, voter_seed.as_str(), &voter_owner).unwrap(); + let authorized_withdrawer = Pubkey::create_with_seed( + &withdrawer_base_key, + withdrawer_seed.as_str(), + &withdrawer_owner, + ) + .unwrap(); + + VoteAccountTestFixtureWithAuthorities { + vote_account: vote_state::create_account_with_authorized( + &Pubkey::new_unique(), + &authorized_voter, + &authorized_withdrawer, + 0, + 100, + ), + vote_pubkey, + voter_base_key, + voter_owner, + voter_seed, + withdrawer_base_key, + withdrawer_owner, + withdrawer_seed, + } + } + fn create_test_account_with_epoch_credits( credits_to_append: &[u64], ) -> (Pubkey, AccountSharedData) { @@ -1096,6 +1228,515 @@ mod tests { ); } + fn perform_authorize_with_seed_test( + authorization_type: VoteAuthorize, + vote_pubkey: Pubkey, + vote_account: AccountSharedData, + current_authority_base_key: Pubkey, + current_authority_seed: String, + current_authority_owner: Pubkey, + new_authority_pubkey: Pubkey, + ) { + let clock = Clock { + epoch: 1, + leader_schedule_epoch: 2, + ..Clock::default() + }; + let clock_account = account::create_account_shared_data_for_test(&clock); + let transaction_accounts = vec![ + (vote_pubkey, vote_account), + (sysvar::clock::id(), clock_account), + (current_authority_base_key, AccountSharedData::default()), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: vote_pubkey, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: current_authority_base_key, + is_signer: true, + is_writable: false, + }, + ]; + + // Can't change authority unless base key signs. + instruction_accounts[2].is_signer = false; + process_instruction( + &serialize(&VoteInstruction::AuthorizeWithSeed( + VoteAuthorizeWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: current_authority_owner, + current_authority_derived_key_seed: current_authority_seed.clone(), + new_authority: new_authority_pubkey, + }, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::MissingRequiredSignature), + ); + instruction_accounts[2].is_signer = true; + + // Can't change authority if seed doesn't match. + process_instruction( + &serialize(&VoteInstruction::AuthorizeWithSeed( + VoteAuthorizeWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: current_authority_owner, + current_authority_derived_key_seed: String::from("WRONG_SEED"), + new_authority: new_authority_pubkey, + }, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::MissingRequiredSignature), + ); + + // Can't change authority if owner doesn't match. + process_instruction( + &serialize(&VoteInstruction::AuthorizeWithSeed( + VoteAuthorizeWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: Pubkey::new_unique(), // Wrong owner. + current_authority_derived_key_seed: current_authority_seed.clone(), + new_authority: new_authority_pubkey, + }, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::MissingRequiredSignature), + ); + + // Can change authority when base key signs for related derived key. + process_instruction( + &serialize(&VoteInstruction::AuthorizeWithSeed( + VoteAuthorizeWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: current_authority_owner, + current_authority_derived_key_seed: current_authority_seed.clone(), + new_authority: new_authority_pubkey, + }, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // Should fail when the `vote_authorize_with_seed` feature is disabled + process_instruction_disabled_features( + &serialize(&VoteInstruction::AuthorizeWithSeed( + VoteAuthorizeWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: current_authority_owner, + current_authority_derived_key_seed: current_authority_seed, + new_authority: new_authority_pubkey, + }, + )) + .unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::InvalidInstructionData), + ); + } + + fn perform_authorize_checked_with_seed_test( + authorization_type: VoteAuthorize, + vote_pubkey: Pubkey, + vote_account: AccountSharedData, + current_authority_base_key: Pubkey, + current_authority_seed: String, + current_authority_owner: Pubkey, + new_authority_pubkey: Pubkey, + ) { + let clock = Clock { + epoch: 1, + leader_schedule_epoch: 2, + ..Clock::default() + }; + let clock_account = account::create_account_shared_data_for_test(&clock); + let transaction_accounts = vec![ + (vote_pubkey, vote_account), + (sysvar::clock::id(), clock_account), + (current_authority_base_key, AccountSharedData::default()), + (new_authority_pubkey, AccountSharedData::default()), + ]; + let mut instruction_accounts = vec![ + AccountMeta { + pubkey: vote_pubkey, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: current_authority_base_key, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: new_authority_pubkey, + is_signer: true, + is_writable: false, + }, + ]; + + // Can't change authority unless base key signs. + instruction_accounts[2].is_signer = false; + process_instruction( + &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( + VoteAuthorizeCheckedWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: current_authority_owner, + current_authority_derived_key_seed: current_authority_seed.clone(), + }, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::MissingRequiredSignature), + ); + instruction_accounts[2].is_signer = true; + + // Can't change authority unless new authority signs. + instruction_accounts[3].is_signer = false; + process_instruction( + &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( + VoteAuthorizeCheckedWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: current_authority_owner, + current_authority_derived_key_seed: current_authority_seed.clone(), + }, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::MissingRequiredSignature), + ); + instruction_accounts[3].is_signer = true; + + // Can't change authority if seed doesn't match. + process_instruction( + &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( + VoteAuthorizeCheckedWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: current_authority_owner, + current_authority_derived_key_seed: String::from("WRONG_SEED"), + }, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::MissingRequiredSignature), + ); + + // Can't change authority if owner doesn't match. + process_instruction( + &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( + VoteAuthorizeCheckedWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: Pubkey::new_unique(), // Wrong owner. + current_authority_derived_key_seed: current_authority_seed.clone(), + }, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Err(InstructionError::MissingRequiredSignature), + ); + + // Can change authority when base key signs for related derived key and new authority signs. + process_instruction( + &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( + VoteAuthorizeCheckedWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: current_authority_owner, + current_authority_derived_key_seed: current_authority_seed.clone(), + }, + )) + .unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + + // Should fail when the `vote_authorize_with_seed` feature is disabled + process_instruction_disabled_features( + &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( + VoteAuthorizeCheckedWithSeedArgs { + authorization_type, + current_authority_derived_key_owner: current_authority_owner, + current_authority_derived_key_seed: current_authority_seed, + }, + )) + .unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::InvalidInstructionData), + ); + } + + #[test] + fn test_voter_base_key_can_authorize_new_voter() { + let VoteAccountTestFixtureWithAuthorities { + vote_pubkey, + voter_base_key, + voter_owner, + voter_seed, + vote_account, + .. + } = create_test_account_with_authorized_from_seed(); + let new_voter_pubkey = Pubkey::new_unique(); + perform_authorize_with_seed_test( + VoteAuthorize::Voter, + vote_pubkey, + vote_account, + voter_base_key, + voter_seed, + voter_owner, + new_voter_pubkey, + ); + } + + #[test] + fn test_withdrawer_base_key_can_authorize_new_voter() { + let VoteAccountTestFixtureWithAuthorities { + vote_pubkey, + withdrawer_base_key, + withdrawer_owner, + withdrawer_seed, + vote_account, + .. + } = create_test_account_with_authorized_from_seed(); + let new_voter_pubkey = Pubkey::new_unique(); + perform_authorize_with_seed_test( + VoteAuthorize::Voter, + vote_pubkey, + vote_account, + withdrawer_base_key, + withdrawer_seed, + withdrawer_owner, + new_voter_pubkey, + ); + } + + #[test] + fn test_voter_base_key_can_not_authorize_new_withdrawer() { + let VoteAccountTestFixtureWithAuthorities { + vote_pubkey, + voter_base_key, + voter_owner, + voter_seed, + vote_account, + .. + } = create_test_account_with_authorized_from_seed(); + let new_withdrawer_pubkey = Pubkey::new_unique(); + let clock = Clock { + epoch: 1, + leader_schedule_epoch: 2, + ..Clock::default() + }; + let clock_account = account::create_account_shared_data_for_test(&clock); + let transaction_accounts = vec![ + (vote_pubkey, vote_account), + (sysvar::clock::id(), clock_account), + (voter_base_key, AccountSharedData::default()), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: vote_pubkey, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: voter_base_key, + is_signer: true, + is_writable: false, + }, + ]; + // Despite having Voter authority, you may not change the Withdrawer authority. + process_instruction( + &serialize(&VoteInstruction::AuthorizeWithSeed( + VoteAuthorizeWithSeedArgs { + authorization_type: VoteAuthorize::Withdrawer, + current_authority_derived_key_owner: voter_owner, + current_authority_derived_key_seed: voter_seed, + new_authority: new_withdrawer_pubkey, + }, + )) + .unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::MissingRequiredSignature), + ); + } + + #[test] + fn test_withdrawer_base_key_can_authorize_new_withdrawer() { + let VoteAccountTestFixtureWithAuthorities { + vote_pubkey, + withdrawer_base_key, + withdrawer_owner, + withdrawer_seed, + vote_account, + .. + } = create_test_account_with_authorized_from_seed(); + let new_withdrawer_pubkey = Pubkey::new_unique(); + perform_authorize_with_seed_test( + VoteAuthorize::Withdrawer, + vote_pubkey, + vote_account, + withdrawer_base_key, + withdrawer_seed, + withdrawer_owner, + new_withdrawer_pubkey, + ); + } + + #[test] + fn test_voter_base_key_can_authorize_new_voter_checked() { + let VoteAccountTestFixtureWithAuthorities { + vote_pubkey, + voter_base_key, + voter_owner, + voter_seed, + vote_account, + .. + } = create_test_account_with_authorized_from_seed(); + let new_voter_pubkey = Pubkey::new_unique(); + perform_authorize_checked_with_seed_test( + VoteAuthorize::Voter, + vote_pubkey, + vote_account, + voter_base_key, + voter_seed, + voter_owner, + new_voter_pubkey, + ); + } + + #[test] + fn test_withdrawer_base_key_can_authorize_new_voter_checked() { + let VoteAccountTestFixtureWithAuthorities { + vote_pubkey, + withdrawer_base_key, + withdrawer_owner, + withdrawer_seed, + vote_account, + .. + } = create_test_account_with_authorized_from_seed(); + let new_voter_pubkey = Pubkey::new_unique(); + perform_authorize_checked_with_seed_test( + VoteAuthorize::Voter, + vote_pubkey, + vote_account, + withdrawer_base_key, + withdrawer_seed, + withdrawer_owner, + new_voter_pubkey, + ); + } + + #[test] + fn test_voter_base_key_can_not_authorize_new_withdrawer_checked() { + let VoteAccountTestFixtureWithAuthorities { + vote_pubkey, + voter_base_key, + voter_owner, + voter_seed, + vote_account, + .. + } = create_test_account_with_authorized_from_seed(); + let new_withdrawer_pubkey = Pubkey::new_unique(); + let clock = Clock { + epoch: 1, + leader_schedule_epoch: 2, + ..Clock::default() + }; + let clock_account = account::create_account_shared_data_for_test(&clock); + let transaction_accounts = vec![ + (vote_pubkey, vote_account), + (sysvar::clock::id(), clock_account), + (voter_base_key, AccountSharedData::default()), + (new_withdrawer_pubkey, AccountSharedData::default()), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: vote_pubkey, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: sysvar::clock::id(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: voter_base_key, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: new_withdrawer_pubkey, + is_signer: true, + is_writable: false, + }, + ]; + // Despite having Voter authority, you may not change the Withdrawer authority. + process_instruction( + &serialize(&VoteInstruction::AuthorizeCheckedWithSeed( + VoteAuthorizeCheckedWithSeedArgs { + authorization_type: VoteAuthorize::Withdrawer, + current_authority_derived_key_owner: voter_owner, + current_authority_derived_key_seed: voter_seed, + }, + )) + .unwrap(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::MissingRequiredSignature), + ); + } + + #[test] + fn test_withdrawer_base_key_can_authorize_new_withdrawer_checked() { + let VoteAccountTestFixtureWithAuthorities { + vote_pubkey, + withdrawer_base_key, + withdrawer_owner, + withdrawer_seed, + vote_account, + .. + } = create_test_account_with_authorized_from_seed(); + let new_withdrawer_pubkey = Pubkey::new_unique(); + perform_authorize_checked_with_seed_test( + VoteAuthorize::Withdrawer, + vote_pubkey, + vote_account, + withdrawer_base_key, + withdrawer_seed, + withdrawer_owner, + new_withdrawer_pubkey, + ); + } + #[test] fn test_spoofed_vote() { process_instruction_as_one_arg( diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index 5a7d03bf176f9f..7907435180471d 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -240,6 +240,21 @@ pub enum VoteAuthorize { Withdrawer, } +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct VoteAuthorizeWithSeedArgs { + pub authorization_type: VoteAuthorize, + pub current_authority_derived_key_owner: Pubkey, + pub current_authority_derived_key_seed: String, + pub new_authority: Pubkey, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct VoteAuthorizeCheckedWithSeedArgs { + pub authorization_type: VoteAuthorize, + pub current_authority_derived_key_owner: Pubkey, + pub current_authority_derived_key_seed: String, +} + #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone, AbiExample)] pub struct BlockTimestamp { pub slot: Slot, diff --git a/runtime/src/vote_parser.rs b/runtime/src/vote_parser.rs index c7ae7e246f29fa..06c6abdbff9531 100644 --- a/runtime/src/vote_parser.rs +++ b/runtime/src/vote_parser.rs @@ -82,6 +82,8 @@ fn parse_vote_instruction_data( } VoteInstruction::Authorize(_, _) | VoteInstruction::AuthorizeChecked(_) + | VoteInstruction::AuthorizeWithSeed(_) + | VoteInstruction::AuthorizeCheckedWithSeed(_) | VoteInstruction::InitializeAccount(_) | VoteInstruction::UpdateCommission(_) | VoteInstruction::UpdateValidatorIdentity diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 000c7e5d512753..443ac67744ad68 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -440,6 +440,10 @@ pub mod nonce_must_be_advanceable { solana_sdk::declare_id!("3u3Er5Vc2jVcwz4xr2GJeSAXT3fAj6ADHZ4BJMZiScFd"); } +pub mod vote_authorize_with_seed { + solana_sdk::declare_id!("6tRxEYKuy2L5nnv5bgn7iT28MxUbYxp5h7F3Ncf1exrT"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -544,6 +548,7 @@ lazy_static! { (quick_bail_on_panic::id(), "quick bail on panic"), (nonce_must_be_authorized::id(), "nonce must be authorized"), (nonce_must_be_advanceable::id(), "durable nonces must be advanceable"), + (vote_authorize_with_seed::id(), "An instruction you can use to change a vote accounts authority when the current authority is a derived key #25860"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/transaction-status/src/parse_vote.rs b/transaction-status/src/parse_vote.rs index b8978d8dca25f0..0ae00c06c5116a 100644 --- a/transaction-status/src/parse_vote.rs +++ b/transaction-status/src/parse_vote.rs @@ -52,6 +52,36 @@ pub fn parse_vote( }), }) } + VoteInstruction::AuthorizeWithSeed(args) => { + check_num_vote_accounts(&instruction.accounts, 3)?; + Ok(ParsedInstructionEnum { + instruction_type: "authorizeWithSeed".to_string(), + info: json!({ + "voteAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "clockSysvar": account_keys[instruction.accounts[1] as usize].to_string(), + "authorityBaseKey": account_keys[instruction.accounts[2] as usize].to_string(), + "authorityOwner": args.current_authority_derived_key_owner.to_string(), + "authoritySeed": args.current_authority_derived_key_seed, + "newAuthority": args.new_authority.to_string(), + "authorityType": args.authorization_type, + }), + }) + } + VoteInstruction::AuthorizeCheckedWithSeed(args) => { + check_num_vote_accounts(&instruction.accounts, 4)?; + Ok(ParsedInstructionEnum { + instruction_type: "authorizeCheckedWithSeed".to_string(), + info: json!({ + "voteAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "clockSysvar": account_keys[instruction.accounts[1] as usize].to_string(), + "authorityBaseKey": account_keys[instruction.accounts[2] as usize].to_string(), + "authorityOwner": args.current_authority_derived_key_owner.to_string(), + "authoritySeed": args.current_authority_derived_key_seed, + "newAuthority": account_keys[instruction.accounts[3] as usize].to_string(), + "authorityType": args.authorization_type, + }), + }) + } VoteInstruction::Vote(vote) => { check_num_vote_accounts(&instruction.accounts, 4)?; let vote = json!({ @@ -285,6 +315,92 @@ mod test { assert!(parse_vote(&message.instructions[0], &AccountKeys::new(&keys, None)).is_err()); } + #[test] + fn test_parse_vote_authorize_with_seed_ix() { + let vote_pubkey = Pubkey::new_unique(); + let authorized_base_key = Pubkey::new_unique(); + let new_authorized_pubkey = Pubkey::new_unique(); + let authority_type = VoteAuthorize::Voter; + let current_authority_owner = Pubkey::new_unique(); + let current_authority_seed = "AUTHORITY_SEED"; + let instruction = vote_instruction::authorize_with_seed( + &vote_pubkey, + &authorized_base_key, + ¤t_authority_owner, + current_authority_seed, + &new_authorized_pubkey, + authority_type, + ); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_vote( + &message.instructions[0], + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "authorizeWithSeed".to_string(), + info: json!({ + "voteAccount": vote_pubkey.to_string(), + "clockSysvar": sysvar::clock::ID.to_string(), + "authorityBaseKey": authorized_base_key.to_string(), + "authorityOwner": current_authority_owner.to_string(), + "authoritySeed": current_authority_seed, + "newAuthority": new_authorized_pubkey.to_string(), + "authorityType": authority_type, + }), + } + ); + assert!(parse_vote( + &message.instructions[0], + &AccountKeys::new(&message.account_keys[0..2], None) + ) + .is_err()); + } + + #[test] + fn test_parse_vote_authorize_with_seed_checked_ix() { + let vote_pubkey = Pubkey::new_unique(); + let authorized_base_key = Pubkey::new_unique(); + let new_authorized_pubkey = Pubkey::new_unique(); + let authority_type = VoteAuthorize::Voter; + let current_authority_owner = Pubkey::new_unique(); + let current_authority_seed = "AUTHORITY_SEED"; + let instruction = vote_instruction::authorize_checked_with_seed( + &vote_pubkey, + &authorized_base_key, + ¤t_authority_owner, + current_authority_seed, + &new_authorized_pubkey, + authority_type, + ); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_vote( + &message.instructions[0], + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "authorizeCheckedWithSeed".to_string(), + info: json!({ + "voteAccount": vote_pubkey.to_string(), + "clockSysvar": sysvar::clock::ID.to_string(), + "authorityBaseKey": authorized_base_key.to_string(), + "authorityOwner": current_authority_owner.to_string(), + "authoritySeed": current_authority_seed, + "newAuthority": new_authorized_pubkey.to_string(), + "authorityType": authority_type, + }), + } + ); + assert!(parse_vote( + &message.instructions[0], + &AccountKeys::new(&message.account_keys[0..3], None) + ) + .is_err()); + } + #[test] fn test_parse_vote_ix() { let hash = Hash::new_from_array([1; 32]);