diff --git a/programs/vote/src/vote_instruction.rs b/programs/vote/src/vote_instruction.rs index c133dd0dfb..e5e3ce51d2 100644 --- a/programs/vote/src/vote_instruction.rs +++ b/programs/vote/src/vote_instruction.rs @@ -409,7 +409,14 @@ pub fn process_instruction( keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?, invoke_context, )?; - vote_state::authorize(me, &voter_pubkey, vote_authorize, &signers, &clock) + vote_state::authorize( + me, + &voter_pubkey, + vote_authorize, + &signers, + &clock, + &invoke_context.feature_set, + ) } VoteInstruction::UpdateValidatorIdentity => vote_state::update_validator_identity( me, @@ -461,6 +468,7 @@ pub fn process_instruction( keyed_accounts, first_instruction_account + 1, )?)?, + &invoke_context.feature_set, ) } else { Err(InstructionError::InvalidInstructionData) diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index ae67c8543e..9bd05ecb35 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -10,6 +10,7 @@ use { account_utils::State, clock::{Epoch, Slot, UnixTimestamp}, epoch_schedule::MAX_LEADER_SCHEDULE_EPOCH_OFFSET, + feature_set::{self, FeatureSet}, hash::Hash, instruction::InstructionError, keyed_account::KeyedAccount, @@ -813,21 +814,37 @@ pub fn authorize( vote_authorize: VoteAuthorize, signers: &HashSet, clock: &Clock, + feature_set: &FeatureSet, ) -> Result<(), InstructionError> { let mut vote_state: VoteState = State::::state(vote_account)?.convert_to_current(); - // current authorized signer must say "yay" match vote_authorize { VoteAuthorize::Voter => { + let authorized_withdrawer_signer = if feature_set + .is_active(&feature_set::vote_withdraw_authority_may_change_authorized_voter::id()) + { + verify_authorized_signer(&vote_state.authorized_withdrawer, signers).is_ok() + } else { + false + }; + vote_state.set_new_authorized_voter( authorized, clock.epoch, clock.leader_schedule_epoch + 1, - |epoch_authorized_voter| verify_authorized_signer(&epoch_authorized_voter, signers), + |epoch_authorized_voter| { + // current authorized withdrawer or authorized voter must say "yay" + if authorized_withdrawer_signer { + Ok(()) + } else { + verify_authorized_signer(&epoch_authorized_voter, signers) + } + }, )?; } VoteAuthorize::Withdrawer => { + // current authorized withdrawer must say "yay" verify_authorized_signer(&vote_state.authorized_withdrawer, signers)?; vote_state.authorized_withdrawer = *authorized; } @@ -1408,6 +1425,7 @@ mod tests { leader_schedule_epoch: 2, ..Clock::default() }, + &FeatureSet::default(), ); assert_eq!(res, Err(InstructionError::MissingRequiredSignature)); @@ -1423,6 +1441,7 @@ mod tests { leader_schedule_epoch: 2, ..Clock::default() }, + &FeatureSet::default(), ); assert_eq!(res, Ok(())); @@ -1438,6 +1457,7 @@ mod tests { leader_schedule_epoch: 2, ..Clock::default() }, + &FeatureSet::default(), ); assert_eq!(res, Err(VoteError::TooSoonToReauthorize.into())); @@ -1460,6 +1480,7 @@ mod tests { leader_schedule_epoch: 4, ..Clock::default() }, + &FeatureSet::default(), ); assert_eq!(res, Ok(())); @@ -1478,6 +1499,7 @@ mod tests { leader_schedule_epoch: 4, ..Clock::default() }, + &FeatureSet::default(), ); assert_eq!(res, Ok(())); @@ -1498,6 +1520,7 @@ mod tests { leader_schedule_epoch: 4, ..Clock::default() }, + &FeatureSet::default(), ); assert_eq!(res, Ok(())); @@ -1538,6 +1561,39 @@ mod tests { &signers, ); assert_eq!(res, Ok(())); + + // verify authorized_withdrawer can authorize a new authorized_voter when + // `feature_set::vote_withdraw_authority_may_change_authorized_voter` is enabled + let keyed_accounts = &[ + KeyedAccount::new(&vote_pubkey, false, &vote_account), + KeyedAccount::new(&authorized_withdrawer_pubkey, true, &withdrawer_account), + ]; + let another_authorized_voter_pubkey = solana_sdk::pubkey::new_rand(); + let signers: HashSet = get_signers(keyed_accounts); + + for (feature_set, expected_res) in [ + ( + FeatureSet::default(), + Err(InstructionError::MissingRequiredSignature), + ), + (FeatureSet::all_enabled(), Ok(())), + ] + .into_iter() + { + let res = authorize( + &keyed_accounts[0], + &another_authorized_voter_pubkey, + VoteAuthorize::Voter, + &signers, + &Clock { + epoch: 4, + leader_schedule_epoch: 5, + ..Clock::default() + }, + &feature_set, + ); + assert_eq!(res, expected_res) + } } #[test] @@ -2019,6 +2075,7 @@ mod tests { VoteAuthorize::Withdrawer, &signers, &Clock::default(), + &FeatureSet::default(), ); assert_eq!(res, Ok(())); diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index a86504506d..a5e6698b62 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -287,6 +287,10 @@ pub mod require_rent_exempt_accounts { solana_sdk::declare_id!("BkFDxiJQWZXGTZaJQxH7wVEHkAmwCgSEVkrvswFfRJPD"); } +pub mod vote_withdraw_authority_may_change_authorized_voter { + solana_sdk::declare_id!("AVZS3ZsN4gi6Rkx2QUibYuSJG3S6QHib7xCYhG6vGJxU"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -353,6 +357,7 @@ lazy_static! { (cap_accounts_data_len::id(), "cap the accounts data len"), (max_tx_account_locks::id(), "enforce max number of locked accounts per transaction"), (require_rent_exempt_accounts::id(), "require all new transaction accounts with data to be rent-exempt"), + (vote_withdraw_authority_may_change_authorized_voter::id(), "vote account withdraw authority may change the authorized voter #22521"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter()