diff --git a/Cargo.lock b/Cargo.lock index 61db75a2a877c3..7c9f619f8df97a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11809,9 +11809,9 @@ dependencies = [ [[package]] name = "solana-vote-interface" -version = "4.0.3" +version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6970d0b048a7b2c80203fe54e10e0cc6d52a88ef7115bb7214908415e33b930" +checksum = "db6e123e16bfdd7a81d71b4c4699e0b29580b619f4cd2ef5b6aae1eb85e8979f" dependencies = [ "arbitrary", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 706082b9559c58..4dbc63b56ffc9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -562,7 +562,7 @@ solana-unified-scheduler-pool = { path = "unified-scheduler-pool", version = "=3 solana-validator-exit = "3.0.0" solana-version = { path = "version", version = "=3.1.0" } solana-vote = { path = "vote", version = "=3.1.0" } -solana-vote-interface = "4.0.3" +solana-vote-interface = "4.0.4" solana-vote-program = { path = "programs/vote", version = "=3.1.0", default-features = false } solana-wen-restart = { path = "wen-restart", version = "=3.1.0" } solana-zk-elgamal-proof-program = { path = "programs/zk-elgamal-proof", version = "=3.1.0" } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 2ff071fa17b336..0504973a2c179b 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -9929,9 +9929,9 @@ dependencies = [ [[package]] name = "solana-vote-interface" -version = "4.0.3" +version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6970d0b048a7b2c80203fe54e10e0cc6d52a88ef7115bb7214908415e33b930" +checksum = "db6e123e16bfdd7a81d71b4c4699e0b29580b619f4cd2ef5b6aae1eb85e8979f" dependencies = [ "bincode", "cfg_eval", diff --git a/programs/vote/src/vote_state/handler.rs b/programs/vote/src/vote_state/handler.rs index 9a0eb0e666733f..74342b5c395577 100644 --- a/programs/vote/src/vote_state/handler.rs +++ b/programs/vote/src/vote_state/handler.rs @@ -30,8 +30,6 @@ use { /// Trait defining the interface for vote state operations. pub trait VoteStateHandle { - fn is_uninitialized(&self) -> bool; - fn authorized_withdrawer(&self) -> &Pubkey; fn set_authorized_withdrawer(&mut self, authorized_withdrawer: Pubkey); @@ -250,10 +248,6 @@ pub trait VoteStateHandle { } impl VoteStateHandle for VoteStateV3 { - fn is_uninitialized(&self) -> bool { - self.authorized_voters.is_empty() - } - fn authorized_withdrawer(&self) -> &Pubkey { &self.authorized_withdrawer } @@ -441,11 +435,6 @@ impl VoteStateHandle for VoteStateV3 { } impl VoteStateHandle for VoteStateV4 { - fn is_uninitialized(&self) -> bool { - // As per SIMD-0185, v4 is always initialized. - false - } - fn authorized_withdrawer(&self) -> &Pubkey { &self.authorized_withdrawer } @@ -673,13 +662,6 @@ pub struct VoteStateHandler { } impl VoteStateHandle for VoteStateHandler { - fn is_uninitialized(&self) -> bool { - match &self.target_state { - TargetVoteState::V3(v3) => v3.is_uninitialized(), - TargetVoteState::V4(v4) => v4.is_uninitialized(), - } - } - fn authorized_withdrawer(&self) -> &Pubkey { match &self.target_state { TargetVoteState::V3(v3) => v3.authorized_withdrawer(), @@ -869,26 +851,6 @@ impl VoteStateHandle for VoteStateHandler { } impl VoteStateHandler { - /// Create a new handler for the provided target version by deserializing - /// the vote state and converting it to the target. - pub fn deserialize_and_convert( - vote_account: &BorrowedInstructionAccount, - target_version: VoteStateTargetVersion, - ) -> Result { - let target_state = match target_version { - VoteStateTargetVersion::V3 => { - let vote_state = VoteStateV3::deserialize(vote_account.get_data())?; - TargetVoteState::V3(vote_state) - } - VoteStateTargetVersion::V4 => { - let vote_state = - VoteStateV4::deserialize(vote_account.get_data(), vote_account.get_key())?; - TargetVoteState::V4(vote_state) - } - }; - Ok(Self { target_state }) - } - pub fn init_vote_account_state( vote_account: &mut BorrowedInstructionAccount, vote_init: &VoteInit, @@ -938,15 +900,13 @@ impl VoteStateHandler { } } - #[cfg(test)] - pub fn new_v3(vote_state: VoteStateV3) -> Self { + pub(crate) fn new_v3(vote_state: VoteStateV3) -> Self { Self { target_state: TargetVoteState::V3(vote_state), } } - #[cfg(test)] - pub fn new_v4(vote_state: VoteStateV4) -> Self { + pub(crate) fn new_v4(vote_state: VoteStateV4) -> Self { Self { target_state: TargetVoteState::V4(vote_state), } @@ -1002,6 +962,53 @@ pub(crate) fn compute_vote_latency(voted_for_slot: Slot, current_slot: Slot) -> std::cmp::min(current_slot.saturating_sub(voted_for_slot), u8::MAX as u64) as u8 } +pub(crate) fn try_convert_to_vote_state_v4( + versioned: VoteStateVersions, + vote_pubkey: &Pubkey, +) -> Result { + match versioned { + VoteStateVersions::V0_23_5(_) => { + // V0_23_5 not supported. + Err(InstructionError::UninitializedAccount) + } + VoteStateVersions::V1_14_11(state) => Ok(VoteStateV4 { + node_pubkey: state.node_pubkey, + authorized_withdrawer: state.authorized_withdrawer, + inflation_rewards_collector: *vote_pubkey, + block_revenue_collector: state.node_pubkey, + inflation_rewards_commission_bps: u16::from(state.commission).saturating_mul(100), + block_revenue_commission_bps: 10_000u16, + pending_delegator_rewards: 0, + bls_pubkey_compressed: None, + votes: landed_votes_from_lockouts(state.votes), + root_slot: state.root_slot, + authorized_voters: state.authorized_voters.clone(), + epoch_credits: state.epoch_credits, + last_timestamp: state.last_timestamp, + }), + VoteStateVersions::V3(state) => Ok(VoteStateV4 { + node_pubkey: state.node_pubkey, + authorized_withdrawer: state.authorized_withdrawer, + inflation_rewards_collector: *vote_pubkey, + block_revenue_collector: state.node_pubkey, + inflation_rewards_commission_bps: u16::from(state.commission).saturating_mul(100), + block_revenue_commission_bps: 10_000u16, + pending_delegator_rewards: 0, + bls_pubkey_compressed: None, + votes: state.votes, + root_slot: state.root_slot, + authorized_voters: state.authorized_voters, + epoch_credits: state.epoch_credits, + last_timestamp: state.last_timestamp, + }), + VoteStateVersions::V4(state) => Ok(*state), + } +} + +fn landed_votes_from_lockouts(lockouts: VecDeque) -> VecDeque { + lockouts.into_iter().map(|lockout| lockout.into()).collect() +} + #[allow(clippy::arithmetic_side_effects)] #[allow(clippy::type_complexity)] #[cfg(test)] @@ -1994,8 +2001,6 @@ mod tests { let epoch_credits = vec![(5, 200, 100)]; let root_slot = Some(50); - let rent = Rent::default(); - // Helper to verify v4 defaults per SIMD-0185. let verify_v4_defaults = |v4_state: &VoteStateV4, expected_commission_bps: u16| { assert_eq!( @@ -2012,28 +2017,6 @@ mod tests { assert_eq!(v4_state.bls_pubkey_compressed, None); }; - // Helper to deserialize and convert vote state through handler. - let deserialize_and_convert = |versioned: VoteStateVersions, space: usize| -> VoteStateV4 { - let vote_account = - AccountSharedData::new_data(rent.minimum_balance(space), &versioned, &id()) - .unwrap(); - - let transaction_context = - mock_transaction_context(vote_pubkey, vote_account, rent.clone()); - let instruction_context = transaction_context.get_next_instruction_context().unwrap(); - let vote_account_borrowed = instruction_context - .try_borrow_instruction_account(0) - .unwrap(); - - let handler = VoteStateHandler::deserialize_and_convert( - &vote_account_borrowed, - VoteStateTargetVersion::V4, - ) - .unwrap(); - - handler.as_ref_v4().clone() - }; - // V0_23_5 // NOTE: On target_os = "solana", VoteState0_23_5::deserialize should // fail, but we can't replicate that in these unit tests. This should @@ -2053,7 +2036,7 @@ mod tests { }; let versioned = VoteStateVersions::V1_14_11(Box::new(vote_state_v2.clone())); - let vote_state_v4 = deserialize_and_convert(versioned, VoteState1_14_11::size_of()); + let vote_state_v4 = try_convert_to_vote_state_v4(versioned, &vote_pubkey).unwrap(); // Compare fields that were already present in V1_14_11. assert_eq!(vote_state_v4.node_pubkey, node_pubkey); @@ -2092,7 +2075,7 @@ mod tests { vote_state_v3.root_slot = root_slot; let versioned = VoteStateVersions::V3(Box::new(vote_state_v3.clone())); - let vote_state_v4 = deserialize_and_convert(versioned, VoteStateV3::size_of()); + let vote_state_v4 = try_convert_to_vote_state_v4(versioned, &vote_pubkey).unwrap(); // Compare fields that were already present in V3. assert_eq!(vote_state_v4.node_pubkey, node_pubkey); @@ -2134,7 +2117,7 @@ mod tests { } let versioned = VoteStateVersions::V4(Box::new(initial_vote_state_v4.clone())); - let vote_state_v4 = deserialize_and_convert(versioned, VoteStateV4::size_of()); + let vote_state_v4 = try_convert_to_vote_state_v4(versioned, &vote_pubkey).unwrap(); // Should be an exact copy. assert_eq!(vote_state_v4, initial_vote_state_v4); diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index 231131ddd2b2ff..1273755d3b3dde 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -32,6 +32,56 @@ use { // TODO: Change me once the program has full v4 feature gate support. pub(crate) const TEMP_HARDCODED_TARGET_VERSION: VoteStateTargetVersion = VoteStateTargetVersion::V3; +// Switch that preserves old behavior before vote state v4 feature gate. +// This should be cleaned up when vote state v4 is activated. +enum PreserveBehaviorInHandlerHelper { + V3 { check_initialized: bool }, + V4, +} + +impl PreserveBehaviorInHandlerHelper { + fn new(target_version: VoteStateTargetVersion, check_initialized: bool) -> Self { + match target_version { + VoteStateTargetVersion::V3 => Self::V3 { check_initialized }, + VoteStateTargetVersion::V4 => Self::V4, + } + } +} + +fn get_vote_state_handler_checked( + vote_account: &BorrowedInstructionAccount, + preserve_behavior: PreserveBehaviorInHandlerHelper, +) -> Result { + match preserve_behavior { + PreserveBehaviorInHandlerHelper::V3 { check_initialized } => { + // Existing flow before v4 feature gate activation: + // 1. Deserialize as `VoteState3`, converting during deserialization + // 2. Check for uninitialized + // + // Some callsites would deserialize without checking initialization + // status, hence the nested `check_initialized` switch. + let vote_state = VoteStateV3::deserialize(vote_account.get_data())?; + if check_initialized && vote_state.is_uninitialized() { + return Err(InstructionError::UninitializedAccount); + } + Ok(VoteStateHandler::new_v3(vote_state)) + } + PreserveBehaviorInHandlerHelper::V4 => { + // New flow after v4 feature gate activation: + // 1. Deserialize as `VoteStateVersions` + // 2. Check for uninitialized + // 3. Convert + let versioned = VoteStateVersions::deserialize(vote_account.get_data())?; + if versioned.is_uninitialized() { + return Err(InstructionError::UninitializedAccount); + } + let vote_state = + handler::try_convert_to_vote_state_v4(versioned, vote_account.get_key())?; + Ok(VoteStateHandler::new_v4(vote_state)) + } + } +} + /// Checks the proposed vote state with the current and /// slot hashes, making adjustments to the root / filtering /// votes as needed. @@ -677,7 +727,10 @@ pub fn authorize( signers: &HashSet, clock: &Clock, ) -> Result<(), InstructionError> { - let mut vote_state = VoteStateHandler::deserialize_and_convert(vote_account, target_version)?; + let mut vote_state = get_vote_state_handler_checked( + vote_account, + PreserveBehaviorInHandlerHelper::new(target_version, false), + )?; match vote_authorize { VoteAuthorize::Voter => { @@ -718,7 +771,10 @@ pub fn update_validator_identity( node_pubkey: &Pubkey, signers: &HashSet, ) -> Result<(), InstructionError> { - let mut vote_state = VoteStateHandler::deserialize_and_convert(vote_account, target_version)?; + let mut vote_state = get_vote_state_handler_checked( + vote_account, + PreserveBehaviorInHandlerHelper::new(target_version, false), + )?; // current authorized withdrawer must say "yay" verify_authorized_signer(vote_state.authorized_withdrawer(), signers)?; @@ -743,7 +799,10 @@ pub fn update_commission( epoch_schedule: &EpochSchedule, clock: &Clock, ) -> Result<(), InstructionError> { - let vote_state_result = VoteStateHandler::deserialize_and_convert(vote_account, target_version); + let vote_state_result = get_vote_state_handler_checked( + vote_account, + PreserveBehaviorInHandlerHelper::new(target_version, false), + ); let enforce_commission_update_rule = if let Ok(decoded_vote_state) = &vote_state_result { commission > decoded_vote_state.commission() } else { @@ -804,7 +863,10 @@ pub fn withdraw( ) -> Result<(), InstructionError> { let mut vote_account = instruction_context.try_borrow_instruction_account(vote_account_index)?; - let vote_state = VoteStateHandler::deserialize_and_convert(&vote_account, target_version)?; + let vote_state = get_vote_state_handler_checked( + &vote_account, + PreserveBehaviorInHandlerHelper::new(target_version, false), + )?; verify_authorized_signer(vote_state.authorized_withdrawer(), signers)?; @@ -869,24 +931,6 @@ pub fn initialize_account( VoteStateHandler::init_vote_account_state(vote_account, vote_init, clock, target_version) } -fn verify_and_get_vote_state_handler( - vote_account: &BorrowedInstructionAccount, - target_version: VoteStateTargetVersion, - clock: &Clock, - signers: &HashSet, -) -> Result { - let mut vote_state = VoteStateHandler::deserialize_and_convert(vote_account, target_version)?; - - if vote_state.is_uninitialized() { - return Err(InstructionError::UninitializedAccount); - } - - let authorized_voter = vote_state.get_and_update_authorized_voter(clock.epoch)?; - verify_authorized_signer(&authorized_voter, signers)?; - - Ok(vote_state) -} - pub fn process_vote_with_account( vote_account: &mut BorrowedInstructionAccount, target_version: VoteStateTargetVersion, @@ -895,8 +939,13 @@ pub fn process_vote_with_account( vote: &Vote, signers: &HashSet, ) -> Result<(), InstructionError> { - let mut vote_state = - verify_and_get_vote_state_handler(vote_account, target_version, clock, signers)?; + let mut vote_state = get_vote_state_handler_checked( + vote_account, + PreserveBehaviorInHandlerHelper::new(target_version, true), + )?; + + let authorized_voter = vote_state.get_and_update_authorized_voter(clock.epoch)?; + verify_authorized_signer(&authorized_voter, signers)?; process_vote(&mut vote_state, vote, slot_hashes, clock.epoch, clock.slot)?; if let Some(timestamp) = vote.timestamp { @@ -917,8 +966,14 @@ pub fn process_vote_state_update( vote_state_update: VoteStateUpdate, signers: &HashSet, ) -> Result<(), InstructionError> { - let mut vote_state = - verify_and_get_vote_state_handler(vote_account, target_version, clock, signers)?; + let mut vote_state = get_vote_state_handler_checked( + vote_account, + PreserveBehaviorInHandlerHelper::new(target_version, true), + )?; + + let authorized_voter = vote_state.get_and_update_authorized_voter(clock.epoch)?; + verify_authorized_signer(&authorized_voter, signers)?; + do_process_vote_state_update( &mut vote_state, slot_hashes, @@ -965,8 +1020,14 @@ pub fn process_tower_sync( tower_sync: TowerSync, signers: &HashSet, ) -> Result<(), InstructionError> { - let mut vote_state = - verify_and_get_vote_state_handler(vote_account, target_version, clock, signers)?; + let mut vote_state = get_vote_state_handler_checked( + vote_account, + PreserveBehaviorInHandlerHelper::new(target_version, true), + )?; + + let authorized_voter = vote_state.get_and_update_authorized_voter(clock.epoch)?; + verify_authorized_signer(&authorized_voter, signers)?; + do_process_tower_sync( &mut vote_state, slot_hashes, @@ -1210,8 +1271,11 @@ mod tests { assert_matches!(vote_state_version, VoteStateVersions::V1_14_11(_)); // Convert the vote state to current as would occur during vote instructions - let converted_vote_state = - VoteStateHandler::deserialize_and_convert(&borrowed_account, target_version).unwrap(); + let converted_vote_state = get_vote_state_handler_checked( + &borrowed_account, + PreserveBehaviorInHandlerHelper::new(target_version, true), + ) + .unwrap(); // Check to make sure that the vote_state is unchanged assert!(vote_state == converted_vote_state); @@ -1244,8 +1308,11 @@ mod tests { } // Convert the vote state to current as would occur during vote instructions - let converted_vote_state = - VoteStateHandler::deserialize_and_convert(&borrowed_account, target_version).unwrap(); + let converted_vote_state = get_vote_state_handler_checked( + &borrowed_account, + PreserveBehaviorInHandlerHelper::new(target_version, true), + ) + .unwrap(); // Check to make sure that the vote_state is unchanged assert!(vote_state == converted_vote_state); @@ -1280,8 +1347,11 @@ mod tests { } // Convert the vote state to current as would occur during vote instructions - let converted_vote_state = - VoteStateHandler::deserialize_and_convert(&borrowed_account, target_version).unwrap(); + let converted_vote_state = get_vote_state_handler_checked( + &borrowed_account, + PreserveBehaviorInHandlerHelper::new(target_version, true), + ) + .unwrap(); // Check to make sure that the vote_state is unchanged assert_eq!(vote_state, converted_vote_state); @@ -1378,9 +1448,12 @@ mod tests { // Increase commission in first half of epoch -- allowed assert_eq!( - VoteStateHandler::deserialize_and_convert(&borrowed_account, target_version) - .unwrap() - .commission(), + get_vote_state_handler_checked( + &borrowed_account, + PreserveBehaviorInHandlerHelper::new(target_version, true), + ) + .unwrap() + .commission(), 10 ); assert_matches!( @@ -1395,9 +1468,12 @@ mod tests { Ok(()) ); assert_eq!( - VoteStateHandler::deserialize_and_convert(&borrowed_account, target_version) - .unwrap() - .commission(), + get_vote_state_handler_checked( + &borrowed_account, + PreserveBehaviorInHandlerHelper::new(target_version, true), + ) + .unwrap() + .commission(), 11 ); @@ -1414,9 +1490,12 @@ mod tests { Err(_) ); assert_eq!( - VoteStateHandler::deserialize_and_convert(&borrowed_account, target_version) - .unwrap() - .commission(), + get_vote_state_handler_checked( + &borrowed_account, + PreserveBehaviorInHandlerHelper::new(target_version, true), + ) + .unwrap() + .commission(), 11 ); @@ -1433,16 +1512,22 @@ mod tests { Ok(()) ); assert_eq!( - VoteStateHandler::deserialize_and_convert(&borrowed_account, target_version) - .unwrap() - .commission(), + get_vote_state_handler_checked( + &borrowed_account, + PreserveBehaviorInHandlerHelper::new(target_version, true), + ) + .unwrap() + .commission(), 10 ); assert_eq!( - VoteStateHandler::deserialize_and_convert(&borrowed_account, target_version) - .unwrap() - .commission(), + get_vote_state_handler_checked( + &borrowed_account, + PreserveBehaviorInHandlerHelper::new(target_version, true), + ) + .unwrap() + .commission(), 10 ); @@ -1458,9 +1543,12 @@ mod tests { Ok(()) ); assert_eq!( - VoteStateHandler::deserialize_and_convert(&borrowed_account, target_version) - .unwrap() - .commission(), + get_vote_state_handler_checked( + &borrowed_account, + PreserveBehaviorInHandlerHelper::new(target_version, true), + ) + .unwrap() + .commission(), 9 ); }