#[cfg(feature = "dev-context-only-utils")]
pub mod handler;
#[cfg(not(feature = "dev-context-only-utils"))]
pub(crate) mod handler;
pub use solana_vote_interface::state::{vote_state_versions::*, *};
use {
handler::{VoteStateHandle, VoteStateHandler, VoteStateTargetVersion},
log::*,
solana_account::{AccountSharedData, WritableAccount},
solana_bls_signatures::{VerifiableProofOfPossession, keypair::Keypair as BLSKeypair},
solana_clock::{Clock, Epoch, Slot},
solana_epoch_schedule::EpochSchedule,
solana_hash::Hash,
solana_instruction::error::InstructionError,
solana_program_runtime::invoke_context::InvokeContext,
solana_pubkey::Pubkey,
solana_rent::Rent,
solana_sdk_ids::system_program,
solana_slot_hashes::SlotHash,
solana_system_interface::instruction as system_instruction,
solana_transaction_context::{
IndexOfAccount, instruction::InstructionContext,
instruction_accounts::BorrowedInstructionAccount,
},
solana_vote_interface::{error::VoteError, instruction::CommissionKind, program::id},
std::{
cmp::Ordering,
collections::{HashSet, VecDeque},
},
};
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<VoteStateHandler, InstructionError> {
match preserve_behavior {
PreserveBehaviorInHandlerHelper::V3 { check_initialized } => {
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 => {
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))
}
}
}
fn check_and_filter_proposed_vote_state(
vote_state: &VoteStateHandler,
proposed_lockouts: &mut VecDeque<Lockout>,
proposed_root: &mut Option<Slot>,
proposed_hash: Hash,
slot_hashes: &[(Slot, Hash)],
) -> Result<(), VoteError> {
if proposed_lockouts.is_empty() {
return Err(VoteError::EmptySlots);
}
let last_proposed_slot = proposed_lockouts
.back()
.expect("must be nonempty, checked above")
.slot();
if let Some(last_vote_slot) = vote_state.votes().back().map(|lockout| lockout.slot()) {
if last_proposed_slot <= last_vote_slot {
return Err(VoteError::VoteTooOld);
}
}
if slot_hashes.is_empty() {
return Err(VoteError::SlotsMismatch);
}
let earliest_slot_hash_in_history = slot_hashes.last().unwrap().0;
if last_proposed_slot < earliest_slot_hash_in_history {
return Err(VoteError::VoteTooOld);
}
if let Some(root) = *proposed_root {
if root < earliest_slot_hash_in_history {
*proposed_root = vote_state.root_slot();
for vote in vote_state.votes().iter().rev() {
if vote.slot() <= root {
*proposed_root = Some(vote.slot());
break;
}
}
}
}
let mut root_to_check = *proposed_root;
let mut proposed_lockouts_index = 0;
let mut slot_hashes_index = slot_hashes.len();
let mut proposed_lockouts_indices_to_filter = vec![];
while proposed_lockouts_index < proposed_lockouts.len() && slot_hashes_index > 0 {
let proposed_vote_slot = if let Some(root) = root_to_check {
root
} else {
proposed_lockouts[proposed_lockouts_index].slot()
};
if root_to_check.is_none()
&& proposed_lockouts_index > 0
&& proposed_vote_slot
<= proposed_lockouts[proposed_lockouts_index.checked_sub(1).expect(
"`proposed_lockouts_index` is positive when checking `SlotsNotOrdered`",
)]
.slot()
{
return Err(VoteError::SlotsNotOrdered);
}
let ancestor_slot = slot_hashes[slot_hashes_index
.checked_sub(1)
.expect("`slot_hashes_index` is positive when computing `ancestor_slot`")]
.0;
match proposed_vote_slot.cmp(&ancestor_slot) {
Ordering::Less => {
if slot_hashes_index == slot_hashes.len() {
if proposed_vote_slot >= earliest_slot_hash_in_history {
return Err(VoteError::AssertionFailed);
}
if !vote_state.contains_slot(proposed_vote_slot) && root_to_check.is_none() {
proposed_lockouts_indices_to_filter.push(proposed_lockouts_index);
}
if let Some(new_proposed_root) = root_to_check {
assert_eq!(new_proposed_root, proposed_vote_slot);
if new_proposed_root >= earliest_slot_hash_in_history {
return Err(VoteError::AssertionFailed);
}
root_to_check = None;
} else {
proposed_lockouts_index = proposed_lockouts_index.checked_add(1).expect(
"`proposed_lockouts_index` is bounded by `MAX_LOCKOUT_HISTORY` when \
`proposed_vote_slot` is too old to be in SlotHashes history",
);
}
continue;
} else {
if root_to_check.is_some() {
return Err(VoteError::RootOnDifferentFork);
} else {
return Err(VoteError::SlotsMismatch);
}
}
}
Ordering::Greater => {
slot_hashes_index = slot_hashes_index.checked_sub(1).expect(
"`slot_hashes_index` is positive when finding newer slots in SlotHashes \
history",
);
continue;
}
Ordering::Equal => {
if root_to_check.is_some() {
root_to_check = None;
} else {
proposed_lockouts_index = proposed_lockouts_index.checked_add(1).expect(
"`proposed_lockouts_index` is bounded by `MAX_LOCKOUT_HISTORY` when match \
is found in SlotHashes history",
);
slot_hashes_index = slot_hashes_index.checked_sub(1).expect(
"`slot_hashes_index` is positive when match is found in SlotHashes history",
);
}
}
}
}
if proposed_lockouts_index != proposed_lockouts.len() {
return Err(VoteError::SlotsMismatch);
}
assert_eq!(last_proposed_slot, slot_hashes[slot_hashes_index].0);
if slot_hashes[slot_hashes_index].1 != proposed_hash {
warn!(
"{} dropped vote {:?} root {:?} failed to match hash {} {}",
vote_state.node_pubkey(),
proposed_lockouts,
proposed_root,
proposed_hash,
slot_hashes[slot_hashes_index].1
);
return Err(VoteError::SlotHashMismatch);
}
let mut proposed_lockouts_index = 0;
let mut filter_votes_index = 0;
proposed_lockouts.retain(|_lockout| {
let should_retain = if filter_votes_index == proposed_lockouts_indices_to_filter.len() {
true
} else if proposed_lockouts_index == proposed_lockouts_indices_to_filter[filter_votes_index]
{
filter_votes_index = filter_votes_index.checked_add(1).unwrap();
false
} else {
true
};
proposed_lockouts_index = proposed_lockouts_index.checked_add(1).expect(
"`proposed_lockouts_index` is bounded by `MAX_LOCKOUT_HISTORY` when filtering out \
irrelevant votes",
);
should_retain
});
Ok(())
}
fn check_slots_are_valid<T: VoteStateHandle>(
vote_state: &T,
vote_slots: &[Slot],
vote_hash: &Hash,
slot_hashes: &[(Slot, Hash)],
) -> Result<(), VoteError> {
let mut i = 0;
let mut j = slot_hashes.len();
while i < vote_slots.len() && j > 0 {
if vote_state
.last_voted_slot()
.is_some_and(|last_voted_slot| vote_slots[i] <= last_voted_slot)
{
i = i
.checked_add(1)
.expect("`i` is bounded by `MAX_LOCKOUT_HISTORY` when finding larger slots");
continue;
}
if vote_slots[i] != slot_hashes[j.checked_sub(1).expect("`j` is positive")].0 {
j = j
.checked_sub(1)
.expect("`j` is positive when finding newer slots");
continue;
}
i = i
.checked_add(1)
.expect("`i` is bounded by `MAX_LOCKOUT_HISTORY` when hash is found");
j = j
.checked_sub(1)
.expect("`j` is positive when hash is found");
}
if j == slot_hashes.len() {
debug!(
"{} dropped vote slots {:?}, vote hash: {:?} slot hashes:SlotHash {:?}, too old ",
vote_state.node_pubkey(),
vote_slots,
vote_hash,
slot_hashes
);
return Err(VoteError::VoteTooOld);
}
if i != vote_slots.len() {
info!(
"{} dropped vote slots {:?} failed to match slot hashes: {:?}",
vote_state.node_pubkey(),
vote_slots,
slot_hashes,
);
return Err(VoteError::SlotsMismatch);
}
if &slot_hashes[j].1 != vote_hash {
warn!(
"{} dropped vote slots {:?} failed to match hash {} {}",
vote_state.node_pubkey(),
vote_slots,
vote_hash,
slot_hashes[j].1
);
return Err(VoteError::SlotHashMismatch);
}
Ok(())
}
pub fn process_new_vote_state(
vote_state: &mut VoteStateHandler,
mut new_state: VecDeque<LandedVote>,
new_root: Option<Slot>,
timestamp: Option<i64>,
epoch: Epoch,
current_slot: Slot,
) -> Result<(), VoteError> {
assert!(!new_state.is_empty());
if new_state.len() > MAX_LOCKOUT_HISTORY {
return Err(VoteError::TooManyVotes);
}
match (new_root, vote_state.root_slot()) {
(Some(new_root), Some(current_root)) => {
if new_root < current_root {
return Err(VoteError::RootRollBack);
}
}
(None, Some(_)) => {
return Err(VoteError::RootRollBack);
}
_ => (),
}
let mut previous_vote: Option<&LandedVote> = None;
for vote in &new_state {
if vote.confirmation_count() == 0 {
return Err(VoteError::ZeroConfirmations);
} else if vote.confirmation_count() > MAX_LOCKOUT_HISTORY as u32 {
return Err(VoteError::ConfirmationTooLarge);
} else if let Some(new_root) = new_root {
if vote.slot() <= new_root
&&
new_root != Slot::default()
{
return Err(VoteError::SlotSmallerThanRoot);
}
}
if let Some(previous_vote) = previous_vote {
if previous_vote.slot() >= vote.slot() {
return Err(VoteError::SlotsNotOrdered);
} else if previous_vote.confirmation_count() <= vote.confirmation_count() {
return Err(VoteError::ConfirmationsNotOrdered);
} else if vote.slot() > previous_vote.lockout.last_locked_out_slot() {
return Err(VoteError::NewVoteStateLockoutMismatch);
}
}
previous_vote = Some(vote);
}
let mut current_vote_state_index: usize = 0;
let mut new_vote_state_index = 0;
let mut earned_credits = 0_u64;
if let Some(new_root) = new_root {
for current_vote in vote_state.votes() {
if current_vote.slot() <= new_root {
earned_credits = earned_credits
.checked_add(vote_state.credits_for_vote_at_index(current_vote_state_index))
.expect("`earned_credits` does not overflow");
current_vote_state_index = current_vote_state_index.checked_add(1).expect(
"`current_vote_state_index` is bounded by `MAX_LOCKOUT_HISTORY` when \
processing new root",
);
continue;
}
break;
}
}
while current_vote_state_index < vote_state.votes().len()
&& new_vote_state_index < new_state.len()
{
let current_vote = &vote_state.votes()[current_vote_state_index];
let new_vote = &mut new_state[new_vote_state_index];
match current_vote.slot().cmp(&new_vote.slot()) {
Ordering::Less => {
if current_vote.lockout.last_locked_out_slot() >= new_vote.slot() {
return Err(VoteError::LockoutConflict);
}
current_vote_state_index = current_vote_state_index.checked_add(1).expect(
"`current_vote_state_index` is bounded by `MAX_LOCKOUT_HISTORY` when slot is \
less than proposed",
);
}
Ordering::Equal => {
if new_vote.confirmation_count() < current_vote.confirmation_count() {
return Err(VoteError::ConfirmationRollBack);
}
new_vote.latency = vote_state.votes()[current_vote_state_index].latency;
current_vote_state_index = current_vote_state_index.checked_add(1).expect(
"`current_vote_state_index` is bounded by `MAX_LOCKOUT_HISTORY` when slot is \
equal to proposed",
);
new_vote_state_index = new_vote_state_index.checked_add(1).expect(
"`new_vote_state_index` is bounded by `MAX_LOCKOUT_HISTORY` when slot is \
equal to proposed",
);
}
Ordering::Greater => {
new_vote_state_index = new_vote_state_index.checked_add(1).expect(
"`new_vote_state_index` is bounded by `MAX_LOCKOUT_HISTORY` when slot is \
greater than proposed",
);
}
}
}
for new_vote in new_state.iter_mut() {
if new_vote.latency == 0 {
new_vote.latency = handler::compute_vote_latency(new_vote.slot(), current_slot);
}
}
if vote_state.root_slot() != new_root {
vote_state.increment_credits(epoch, earned_credits);
}
if let Some(timestamp) = timestamp {
let last_slot = new_state.back().unwrap().slot();
vote_state.process_timestamp(last_slot, timestamp)?;
}
vote_state.set_root_slot(new_root);
vote_state.set_votes(new_state);
Ok(())
}
pub fn process_vote_unfiltered<T: VoteStateHandle>(
vote_state: &mut T,
vote_slots: &[Slot],
vote: &Vote,
slot_hashes: &[SlotHash],
epoch: Epoch,
current_slot: Slot,
) -> Result<(), VoteError> {
check_slots_are_valid(vote_state, vote_slots, &vote.hash, slot_hashes)?;
vote_slots
.iter()
.for_each(|s| vote_state.process_next_vote_slot(*s, epoch, current_slot));
Ok(())
}
pub fn process_vote(
vote_state: &mut VoteStateHandler,
vote: &Vote,
slot_hashes: &[SlotHash],
epoch: Epoch,
current_slot: Slot,
) -> Result<(), VoteError> {
if vote.slots.is_empty() {
return Err(VoteError::EmptySlots);
}
let earliest_slot_in_history = slot_hashes.last().map(|(slot, _hash)| *slot).unwrap_or(0);
let vote_slots = vote
.slots
.iter()
.filter(|slot| **slot >= earliest_slot_in_history)
.cloned()
.collect::<Vec<Slot>>();
if vote_slots.is_empty() {
return Err(VoteError::VotesTooOldAllFiltered);
}
process_vote_unfiltered(
vote_state,
&vote_slots,
vote,
slot_hashes,
epoch,
current_slot,
)
}
pub fn process_vote_unchecked<T: VoteStateHandle>(
vote_state: &mut T,
vote: Vote,
) -> Result<(), VoteError> {
if vote.slots.is_empty() {
return Err(VoteError::EmptySlots);
}
let slot_hashes: Vec<_> = vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect();
process_vote_unfiltered(
vote_state,
&vote.slots,
&vote,
&slot_hashes,
vote_state.current_epoch(),
0,
)
}
#[cfg(test)]
pub fn process_slot_votes_unchecked<T: VoteStateHandle>(vote_state: &mut T, slots: &[Slot]) {
for slot in slots {
process_slot_vote_unchecked(vote_state, *slot);
}
}
pub fn process_slot_vote_unchecked<T: VoteStateHandle>(vote_state: &mut T, slot: Slot) {
let _ = process_vote_unchecked(vote_state, Vote::new(vec![slot], Hash::default()));
}
pub fn authorize<S: std::hash::BuildHasher, F>(
vote_account: &mut BorrowedInstructionAccount,
target_version: VoteStateTargetVersion,
authorized: &Pubkey,
vote_authorize: VoteAuthorize,
signers: &HashSet<Pubkey, S>,
clock: &Clock,
is_vote_authorize_with_bls_enabled: bool,
consume_pop_compute_units: F,
) -> Result<(), InstructionError>
where
F: FnOnce() -> Result<(), InstructionError>,
{
let mut vote_state = get_vote_state_handler_checked(
vote_account,
PreserveBehaviorInHandlerHelper::new(target_version, false),
)?;
match vote_authorize {
VoteAuthorize::Voter => {
if is_vote_authorize_with_bls_enabled && vote_state.has_bls_pubkey() {
return Err(InstructionError::InvalidInstructionData);
}
let authorized_withdrawer_signer =
verify_authorized_signer(vote_state.authorized_withdrawer(), signers).is_ok();
vote_state.set_new_authorized_voter(
authorized,
clock.epoch,
clock
.leader_schedule_epoch
.checked_add(1)
.ok_or(InstructionError::InvalidAccountData)?,
None,
|epoch_authorized_voter| {
if authorized_withdrawer_signer {
Ok(())
} else {
verify_authorized_signer(&epoch_authorized_voter, signers)
}
},
)?;
}
VoteAuthorize::Withdrawer => {
verify_authorized_signer(vote_state.authorized_withdrawer(), signers)?;
vote_state.set_authorized_withdrawer(*authorized);
}
VoteAuthorize::VoterWithBLS(args) => {
if !is_vote_authorize_with_bls_enabled {
return Err(InstructionError::InvalidInstructionData);
}
let authorized_withdrawer_signer =
verify_authorized_signer(vote_state.authorized_withdrawer(), signers).is_ok();
verify_bls_proof_of_possession(
vote_account.get_key(),
&args.bls_pubkey,
&args.bls_proof_of_possession,
consume_pop_compute_units,
)?;
vote_state.set_new_authorized_voter(
authorized,
clock.epoch,
clock
.leader_schedule_epoch
.checked_add(1)
.ok_or(InstructionError::InvalidAccountData)?,
Some(&args.bls_pubkey),
|epoch_authorized_voter| {
if authorized_withdrawer_signer {
Ok(())
} else {
verify_authorized_signer(&epoch_authorized_voter, signers)
}
},
)?;
}
}
vote_state.set_vote_account_state(vote_account)
}
pub fn update_validator_identity<S: std::hash::BuildHasher>(
vote_account: &mut BorrowedInstructionAccount,
target_version: VoteStateTargetVersion,
node_pubkey: &Pubkey,
signers: &HashSet<Pubkey, S>,
custom_commission_collector_enabled: bool,
) -> Result<(), InstructionError> {
let mut vote_state = get_vote_state_handler_checked(
vote_account,
PreserveBehaviorInHandlerHelper::new(target_version, false),
)?;
verify_authorized_signer(vote_state.authorized_withdrawer(), signers)?;
verify_authorized_signer(node_pubkey, signers)?;
vote_state.set_node_pubkey(*node_pubkey);
if !custom_commission_collector_enabled {
vote_state.set_block_revenue_collector(*node_pubkey);
}
vote_state.set_vote_account_state(vote_account)
}
pub fn update_commission<S: std::hash::BuildHasher>(
vote_account: &mut BorrowedInstructionAccount,
target_version: VoteStateTargetVersion,
commission: u8,
signers: &HashSet<Pubkey, S>,
epoch_schedule: &EpochSchedule,
clock: &Clock,
disable_commission_update_rule: bool,
) -> Result<(), InstructionError> {
let vote_state_result = get_vote_state_handler_checked(
vote_account,
PreserveBehaviorInHandlerHelper::new(target_version, false),
);
let enforce_commission_update_rule = !disable_commission_update_rule
&& match vote_state_result.as_ref() {
Ok(decoded_vote_state) => commission > decoded_vote_state.commission(),
Err(_) => true,
};
if enforce_commission_update_rule && !is_commission_update_allowed(clock.slot, epoch_schedule) {
return Err(VoteError::CommissionUpdateTooLate.into());
}
let mut vote_state = vote_state_result?;
verify_authorized_signer(vote_state.authorized_withdrawer(), signers)?;
vote_state.set_commission(commission);
vote_state.set_vote_account_state(vote_account)
}
pub fn update_commission_bps<S: std::hash::BuildHasher>(
vote_account: &mut BorrowedInstructionAccount,
target_version: VoteStateTargetVersion,
commission_bps: u16,
kind: CommissionKind,
signers: &HashSet<Pubkey, S>,
block_revenue_sharing_enabled: bool,
) -> Result<(), InstructionError> {
if matches!(kind, CommissionKind::BlockRevenue) && !block_revenue_sharing_enabled {
return Err(InstructionError::InvalidInstructionData);
}
let mut vote_state = get_vote_state_handler_checked(
vote_account,
PreserveBehaviorInHandlerHelper::new(target_version, false),
)?;
verify_authorized_signer(vote_state.authorized_withdrawer(), signers)?;
match kind {
CommissionKind::InflationRewards => {
vote_state.set_inflation_rewards_commission_bps(commission_bps);
}
CommissionKind::BlockRevenue => {
vote_state.set_block_revenue_commission_bps(commission_bps);
}
}
vote_state.set_vote_account_state(vote_account)
}
pub enum NewCommissionCollector<'a, 'b> {
VoteAccount,
NewAccount(BorrowedInstructionAccount<'a, 'b>),
}
pub fn update_commission_collector<S: std::hash::BuildHasher>(
vote_account: &mut BorrowedInstructionAccount,
target_version: VoteStateTargetVersion,
new_collector: NewCommissionCollector,
kind: CommissionKind,
signers: &HashSet<Pubkey, S>,
rent: &Rent,
) -> Result<(), InstructionError> {
let mut vote_state = get_vote_state_handler_checked(
vote_account,
PreserveBehaviorInHandlerHelper::new(target_version, true),
)?;
verify_authorized_signer(vote_state.authorized_withdrawer(), signers)?;
let new_collector_key = match new_collector {
NewCommissionCollector::VoteAccount => *vote_account.get_key(),
NewCommissionCollector::NewAccount(collector_account) => {
if collector_account.get_owner() != &system_program::id() {
return Err(InstructionError::InvalidAccountOwner);
}
if !rent.is_exempt(
collector_account.get_lamports(),
collector_account.get_data().len(),
) {
return Err(InstructionError::InsufficientFunds);
}
if !collector_account.is_writable() {
return Err(InstructionError::InvalidArgument);
}
*collector_account.get_key()
}
};
match kind {
CommissionKind::InflationRewards => {
vote_state.set_inflation_rewards_collector(new_collector_key);
}
CommissionKind::BlockRevenue => {
vote_state.set_block_revenue_collector(new_collector_key);
}
}
vote_state.set_vote_account_state(vote_account)
}
pub fn deposit_delegator_rewards(
invoke_context: &mut InvokeContext,
deposit: u64,
) -> Result<(), InstructionError> {
const VOTE_ACCOUNT_INDEX: IndexOfAccount = 0;
const SENDER_ACCOUNT_INDEX: IndexOfAccount = 1;
let transaction_context = &invoke_context.transaction_context;
let instruction_context = transaction_context.get_current_instruction_context()?;
if !instruction_context.is_instruction_account_signer(SENDER_ACCOUNT_INDEX)? {
return Err(InstructionError::MissingRequiredSignature);
}
let vote_address = *instruction_context.get_key_of_instruction_account(VOTE_ACCOUNT_INDEX)?;
let source_address =
*instruction_context.get_key_of_instruction_account(SENDER_ACCOUNT_INDEX)?;
let mut vote_state = {
let vote_account =
instruction_context.try_borrow_instruction_account(VOTE_ACCOUNT_INDEX)?;
let versioned = VoteStateVersions::deserialize(vote_account.get_data())?;
if let VoteStateVersions::V4(vote_state_v4) = versioned {
Ok(VoteStateHandler::new_v4(*vote_state_v4))
} else {
Err(InstructionError::InvalidAccountData)
}
}?;
invoke_context.native_invoke_signed(
system_instruction::transfer(&source_address, &vote_address, deposit),
&[],
)?;
let transaction_context = &invoke_context.transaction_context;
let instruction_context = transaction_context.get_current_instruction_context()?;
let mut vote_account =
instruction_context.try_borrow_instruction_account(VOTE_ACCOUNT_INDEX)?;
vote_state.add_pending_delegator_rewards(deposit)?;
vote_state.set_vote_account_state(&mut vote_account)
}
pub fn is_commission_update_allowed(slot: Slot, epoch_schedule: &EpochSchedule) -> bool {
if let Some(relative_slot) = slot
.saturating_sub(epoch_schedule.first_normal_slot)
.checked_rem(epoch_schedule.slots_per_epoch)
{
relative_slot.saturating_mul(2) <= epoch_schedule.slots_per_epoch
} else {
true
}
}
fn verify_authorized_signer<S: std::hash::BuildHasher>(
authorized: &Pubkey,
signers: &HashSet<Pubkey, S>,
) -> Result<(), InstructionError> {
if signers.contains(authorized) {
Ok(())
} else {
Err(InstructionError::MissingRequiredSignature)
}
}
const POP_MESSAGE_SIZE: usize = 9 + size_of::<Pubkey>() + BLS_PUBLIC_KEY_COMPRESSED_SIZE;
pub(crate) fn generate_pop_message(
vote_account_pubkey: &Pubkey,
bls_pubkey_bytes: &[u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE],
) -> [u8; POP_MESSAGE_SIZE] {
const LABEL_LEN: usize = 9;
const PUBKEY_LEN: usize = size_of::<Pubkey>();
const BLS_LEN: usize = BLS_PUBLIC_KEY_COMPRESSED_SIZE;
const LABEL_START: usize = 0;
const LABEL_END: usize = LABEL_START + LABEL_LEN;
const PUBKEY_START: usize = LABEL_END;
const PUBKEY_END: usize = PUBKEY_START + PUBKEY_LEN;
const BLS_START: usize = PUBKEY_END;
const BLS_END: usize = BLS_START + BLS_LEN;
const _: () = assert!(BLS_END == POP_MESSAGE_SIZE);
let mut message = [0u8; POP_MESSAGE_SIZE];
message[LABEL_START..LABEL_END].copy_from_slice(b"ALPENGLOW");
message[PUBKEY_START..PUBKEY_END].copy_from_slice(vote_account_pubkey.as_ref());
message[BLS_START..BLS_END].copy_from_slice(bls_pubkey_bytes);
message
}
pub fn verify_bls_proof_of_possession<F>(
vote_account_pubkey: &Pubkey,
bls_pubkey_compressed_bytes: &[u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE],
bls_proof_of_possession_compressed_bytes: &[u8; BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE],
consume_pop_compute_units: F,
) -> Result<(), InstructionError>
where
F: FnOnce() -> Result<(), InstructionError>,
{
consume_pop_compute_units()?;
let message = generate_pop_message(vote_account_pubkey, bls_pubkey_compressed_bytes);
bls_proof_of_possession_compressed_bytes
.verify(bls_pubkey_compressed_bytes, Some(&message))
.map_err(|_| InstructionError::InvalidArgument)
}
pub fn withdraw<S: std::hash::BuildHasher>(
instruction_context: &InstructionContext,
vote_account_index: IndexOfAccount,
target_version: VoteStateTargetVersion,
lamports: u64,
to_account_index: IndexOfAccount,
signers: &HashSet<Pubkey, S>,
rent_sysvar: &Rent,
clock: &Clock,
) -> Result<(), InstructionError> {
let mut vote_account =
instruction_context.try_borrow_instruction_account(vote_account_index)?;
let vote_state = get_vote_state_handler_checked(
&vote_account,
PreserveBehaviorInHandlerHelper::new(target_version, false),
)?;
verify_authorized_signer(vote_state.authorized_withdrawer(), signers)?;
let remaining_balance = vote_account
.get_lamports()
.checked_sub(lamports)
.ok_or(InstructionError::InsufficientFunds)?;
let pending_delegator_rewards = vote_state.pending_delegator_rewards();
if remaining_balance == 0 {
if pending_delegator_rewards > 0 {
return Err(InstructionError::InsufficientFunds);
}
let reject_active_vote_account_close = vote_state
.epoch_credits()
.last()
.map(|(last_epoch_with_credits, _, _)| {
let current_epoch = clock.epoch;
current_epoch.saturating_sub(*last_epoch_with_credits) < 2
})
.unwrap_or(false);
if reject_active_vote_account_close {
return Err(VoteError::ActiveVoteAccountClose.into());
} else {
VoteStateHandler::deinitialize_vote_account_state(&mut vote_account, target_version)?;
}
} else {
let min_rent_exempt_balance = rent_sysvar.minimum_balance(vote_account.get_data().len());
let min_balance = min_rent_exempt_balance
.checked_add(pending_delegator_rewards)
.ok_or(InstructionError::ArithmeticOverflow)?;
if remaining_balance < min_balance {
return Err(InstructionError::InsufficientFunds);
}
}
vote_account.checked_sub_lamports(lamports)?;
drop(vote_account);
let mut to_account = instruction_context.try_borrow_instruction_account(to_account_index)?;
to_account.checked_add_lamports(lamports)?;
Ok(())
}
pub fn initialize_account_v2<S: std::hash::BuildHasher, F>(
vote_account: &mut BorrowedInstructionAccount,
target_version: VoteStateTargetVersion,
vote_init: &VoteInitV2,
signers: &HashSet<Pubkey, S>,
clock: &Clock,
consume_pop_compute_units: F,
) -> Result<(), InstructionError>
where
F: FnOnce() -> Result<(), InstructionError>,
{
VoteStateHandler::check_vote_account_length(vote_account, target_version)?;
let versioned = vote_account.get_state::<VoteStateVersions>()?;
if !versioned.is_uninitialized() {
return Err(InstructionError::AccountAlreadyInitialized);
}
verify_authorized_signer(&vote_init.node_pubkey, signers)?;
verify_bls_proof_of_possession(
vote_account.get_key(),
&vote_init.authorized_voter_bls_pubkey,
&vote_init.authorized_voter_bls_proof_of_possession,
consume_pop_compute_units,
)?;
VoteStateHandler::init_vote_account_state_v2(vote_account, vote_init, clock, target_version)
}
pub fn initialize_account<S: std::hash::BuildHasher>(
vote_account: &mut BorrowedInstructionAccount,
target_version: VoteStateTargetVersion,
vote_init: &VoteInit,
signers: &HashSet<Pubkey, S>,
clock: &Clock,
) -> Result<(), InstructionError> {
VoteStateHandler::check_vote_account_length(vote_account, target_version)?;
let versioned = vote_account.get_state::<VoteStateVersions>()?;
if !versioned.is_uninitialized() {
return Err(InstructionError::AccountAlreadyInitialized);
}
verify_authorized_signer(&vote_init.node_pubkey, signers)?;
VoteStateHandler::init_vote_account_state(vote_account, vote_init, clock, target_version)
}
pub fn process_vote_with_account<S: std::hash::BuildHasher>(
vote_account: &mut BorrowedInstructionAccount,
target_version: VoteStateTargetVersion,
slot_hashes: &[SlotHash],
clock: &Clock,
vote: &Vote,
signers: &HashSet<Pubkey, S>,
) -> Result<(), InstructionError> {
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 {
vote.slots
.iter()
.max()
.ok_or(VoteError::EmptySlots)
.and_then(|slot| vote_state.process_timestamp(*slot, timestamp))?;
}
vote_state.set_vote_account_state(vote_account)
}
pub fn process_vote_state_update<S: std::hash::BuildHasher>(
vote_account: &mut BorrowedInstructionAccount,
target_version: VoteStateTargetVersion,
slot_hashes: &[SlotHash],
clock: &Clock,
vote_state_update: VoteStateUpdate,
signers: &HashSet<Pubkey, S>,
) -> Result<(), InstructionError> {
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,
clock.epoch,
clock.slot,
vote_state_update,
)?;
vote_state.set_vote_account_state(vote_account)
}
pub fn do_process_vote_state_update(
vote_state: &mut VoteStateHandler,
slot_hashes: &[SlotHash],
epoch: u64,
slot: u64,
mut vote_state_update: VoteStateUpdate,
) -> Result<(), VoteError> {
check_and_filter_proposed_vote_state(
vote_state,
&mut vote_state_update.lockouts,
&mut vote_state_update.root,
vote_state_update.hash,
slot_hashes,
)?;
process_new_vote_state(
vote_state,
vote_state_update
.lockouts
.iter()
.map(|lockout| LandedVote::from(*lockout))
.collect(),
vote_state_update.root,
vote_state_update.timestamp,
epoch,
slot,
)
}
pub fn process_tower_sync<S: std::hash::BuildHasher>(
vote_account: &mut BorrowedInstructionAccount,
target_version: VoteStateTargetVersion,
slot_hashes: &[SlotHash],
clock: &Clock,
tower_sync: TowerSync,
signers: &HashSet<Pubkey, S>,
) -> Result<(), InstructionError> {
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,
clock.epoch,
clock.slot,
tower_sync,
)?;
vote_state.set_vote_account_state(vote_account)
}
fn do_process_tower_sync(
vote_state: &mut VoteStateHandler,
slot_hashes: &[SlotHash],
epoch: u64,
slot: u64,
mut tower_sync: TowerSync,
) -> Result<(), VoteError> {
check_and_filter_proposed_vote_state(
vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
slot_hashes,
)?;
process_new_vote_state(
vote_state,
tower_sync
.lockouts
.iter()
.map(|lockout| LandedVote::from(*lockout))
.collect(),
tower_sync.root,
tower_sync.timestamp,
epoch,
slot,
)
}
pub fn create_v3_account_with_authorized(
node_pubkey: &Pubkey,
authorized_voter: &Pubkey,
authorized_withdrawer: &Pubkey,
commission: u8,
lamports: u64,
) -> AccountSharedData {
let mut vote_account = AccountSharedData::new(lamports, VoteStateV3::size_of(), &id());
let vote_state = VoteStateV3::new(
&VoteInit {
node_pubkey: *node_pubkey,
authorized_voter: *authorized_voter,
authorized_withdrawer: *authorized_withdrawer,
commission,
},
&Clock::default(),
);
VoteStateV3::serialize(
&VoteStateVersions::V3(Box::new(vote_state)),
vote_account.data_as_mut_slice(),
)
.unwrap();
vote_account
}
pub fn create_v4_account_with_authorized(
node_pubkey: &Pubkey,
authorized_voter: &Pubkey,
authorized_voter_bls_pubkey: [u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE],
authorized_withdrawer: &Pubkey,
inflation_rewards_commission_bps: u16,
inflation_rewards_collector: &Pubkey,
block_revenue_commission_bps: u16,
block_revenue_collector: &Pubkey,
lamports: u64,
) -> AccountSharedData {
let mut vote_account = AccountSharedData::new(lamports, VoteStateV4::size_of(), &id());
let authorized_voter_bls_proof_of_possession = [0; BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE];
let vote_state = VoteStateV4::new(
&VoteInitV2 {
node_pubkey: *node_pubkey,
authorized_voter: *authorized_voter,
authorized_voter_bls_pubkey,
authorized_voter_bls_proof_of_possession,
authorized_withdrawer: *authorized_withdrawer,
inflation_rewards_commission_bps,
inflation_rewards_collector: *inflation_rewards_collector,
block_revenue_commission_bps,
block_revenue_collector: *block_revenue_collector,
},
&Clock::default(),
);
VoteStateV4::serialize(
&VoteStateVersions::V4(Box::new(vote_state)),
vote_account.data_as_mut_slice(),
)
.unwrap();
vote_account
}
pub fn create_bls_pubkey_and_proof_of_possession(
vote_account_pubkey: &Pubkey,
) -> (
[u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE],
[u8; BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE],
) {
let bls_keypair = BLSKeypair::new();
create_bls_proof_of_possession(vote_account_pubkey, &bls_keypair)
}
pub fn create_bls_proof_of_possession(
vote_account_pubkey: &Pubkey,
bls_keypair: &BLSKeypair,
) -> (
[u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE],
[u8; BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE],
) {
let bls_pubkey_bytes = bls_keypair.public.to_bytes_compressed();
let message = generate_pop_message(vote_account_pubkey, &bls_pubkey_bytes);
let proof_of_possession = bls_keypair.proof_of_possession(Some(&message));
let proof_of_possession_bytes = proof_of_possession.to_bytes_compressed();
(bls_pubkey_bytes, proof_of_possession_bytes)
}
#[allow(clippy::arithmetic_side_effects)]
#[cfg(test)]
mod tests {
use {
super::*,
assert_matches::assert_matches,
solana_account::{AccountSharedData, ReadableAccount},
solana_clock::DEFAULT_SLOTS_PER_EPOCH,
solana_sha256_hasher::hash,
solana_transaction_context::{
instruction_accounts::InstructionAccount, transaction::TransactionContext,
},
solana_vote_interface::authorized_voters::AuthorizedVoters,
test_case::{test_case, test_matrix},
};
const MAX_RECENT_VOTES: usize = 16;
fn vote_state_new_for_test(
vote_pubkey: &Pubkey,
target_version: VoteStateTargetVersion,
) -> VoteStateHandler {
let auth_pubkey = solana_pubkey::new_rand();
let vote_init = VoteInit {
node_pubkey: solana_pubkey::new_rand(),
authorized_voter: auth_pubkey,
authorized_withdrawer: auth_pubkey,
commission: 0,
};
let clock = Clock::default();
match target_version {
VoteStateTargetVersion::V3 => {
VoteStateHandler::new_v3(VoteStateV3::new(&vote_init, &clock))
}
VoteStateTargetVersion::V4 => VoteStateHandler::new_v4(VoteStateV4::new_with_defaults(
vote_pubkey,
&vote_init,
&clock,
)),
}
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_vote_state_upgrade_from_1_14_11(target_version: VoteStateTargetVersion) {
let vote_pubkey = solana_pubkey::new_rand();
let mut vote_state = vote_state_new_for_test(&vote_pubkey, target_version);
vote_state.increment_credits(0, 100);
assert_eq!(
vote_state.set_new_authorized_voter(
&solana_pubkey::new_rand(),
0,
1,
None,
|_pubkey| Ok(())
),
Ok(())
);
vote_state.increment_credits(1, 200);
assert_eq!(
vote_state.set_new_authorized_voter(
&solana_pubkey::new_rand(),
1,
2,
None,
|_pubkey| Ok(())
),
Ok(())
);
vote_state.increment_credits(2, 300);
assert_eq!(
vote_state.set_new_authorized_voter(
&solana_pubkey::new_rand(),
2,
3,
None,
|_pubkey| Ok(())
),
Ok(())
);
vec![
100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133,
134, 135,
]
.into_iter()
.for_each(|v| vote_state.process_next_vote_slot(v, 4, 0));
let vote_state_v1_14_11 = match target_version {
VoteStateTargetVersion::V3 => {
VoteState1_14_11::from(vote_state.as_ref_v3().clone())
}
VoteStateTargetVersion::V4 => {
VoteState1_14_11 {
node_pubkey: *vote_state.node_pubkey(),
authorized_withdrawer: *vote_state.authorized_withdrawer(),
commission: vote_state.commission(),
votes: vote_state
.votes()
.iter()
.map(|landed_vote| (*landed_vote).into())
.collect(),
root_slot: vote_state.root_slot(),
authorized_voters: vote_state.authorized_voters().clone(),
epoch_credits: vote_state.epoch_credits().clone(),
last_timestamp: vote_state.last_timestamp().clone(),
prior_voters: CircBuf::default(), }
}
};
let version1_14_11_serialized =
bincode::serialize(&VoteStateVersions::V1_14_11(Box::new(vote_state_v1_14_11)))
.unwrap();
let version1_14_11_serialized_len = version1_14_11_serialized.len();
let rent = Rent::default();
let lamports = rent.minimum_balance(version1_14_11_serialized_len);
let mut vote_account =
AccountSharedData::new(lamports, version1_14_11_serialized_len, &id());
vote_account.set_data_from_slice(&version1_14_11_serialized);
let processor_account = AccountSharedData::new(0, 0, &solana_sdk_ids::native_loader::id());
let mut transaction_context = TransactionContext::new(
vec![(id(), processor_account), (vote_pubkey, vote_account)],
rent.clone(),
0,
0,
1,
);
transaction_context
.configure_top_level_instruction_for_tests(
0,
vec![InstructionAccount::new(1, false, true)],
vec![],
)
.unwrap();
let instruction_context = transaction_context.get_next_instruction_context().unwrap();
let mut borrowed_account = instruction_context
.try_borrow_instruction_account(0)
.unwrap();
let vote_state_version = borrowed_account.get_state::<VoteStateVersions>().unwrap();
assert_matches!(vote_state_version, VoteStateVersions::V1_14_11(_));
let converted_vote_state = get_vote_state_handler_checked(
&borrowed_account,
PreserveBehaviorInHandlerHelper::new(target_version, true),
)
.unwrap();
assert!(vote_state == converted_vote_state);
let vote_state = converted_vote_state;
match target_version {
VoteStateTargetVersion::V3 => {
assert_eq!(
vote_state
.clone()
.set_vote_account_state(&mut borrowed_account),
Ok(())
);
let vote_state_version = borrowed_account.get_state::<VoteStateVersions>().unwrap();
assert_matches!(vote_state_version, VoteStateVersions::V1_14_11(_));
}
VoteStateTargetVersion::V4 => {
assert_eq!(
vote_state
.clone()
.set_vote_account_state(&mut borrowed_account),
Err(InstructionError::AccountNotRentExempt)
);
}
}
let converted_vote_state = get_vote_state_handler_checked(
&borrowed_account,
PreserveBehaviorInHandlerHelper::new(target_version, true),
)
.unwrap();
assert!(vote_state == converted_vote_state);
let vote_state = converted_vote_state;
let space = match target_version {
VoteStateTargetVersion::V3 => VoteStateV3::size_of(),
VoteStateTargetVersion::V4 => VoteStateV4::size_of(), };
assert_eq!(
borrowed_account.set_lamports(rent.minimum_balance(space)),
Ok(())
);
assert_eq!(
vote_state
.clone()
.set_vote_account_state(&mut borrowed_account),
Ok(())
);
let vote_state_version = borrowed_account.get_state::<VoteStateVersions>().unwrap();
match target_version {
VoteStateTargetVersion::V3 => {
assert_matches!(vote_state_version, VoteStateVersions::V3(_));
}
VoteStateTargetVersion::V4 => {
assert_matches!(vote_state_version, VoteStateVersions::V4(_));
}
}
let converted_vote_state = get_vote_state_handler_checked(
&borrowed_account,
PreserveBehaviorInHandlerHelper::new(target_version, true),
)
.unwrap();
assert_eq!(vote_state, converted_vote_state);
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_vote_lockout(target_version: VoteStateTargetVersion) {
let mut vote_state = vote_state_new_for_test(&solana_pubkey::new_rand(), target_version);
for i in 0..(MAX_LOCKOUT_HISTORY + 1) {
process_slot_vote_unchecked(&mut vote_state, (INITIAL_LOCKOUT * i) as u64);
}
assert_eq!(vote_state.votes().len(), MAX_LOCKOUT_HISTORY);
assert_eq!(vote_state.root_slot(), Some(0));
check_lockouts(&vote_state);
let top_vote = vote_state.votes().front().unwrap().slot();
let slot = vote_state.last_lockout().unwrap().last_locked_out_slot();
process_slot_vote_unchecked(&mut vote_state, slot);
assert_eq!(Some(top_vote), vote_state.root_slot());
let slot = vote_state
.votes()
.front()
.unwrap()
.lockout
.last_locked_out_slot();
process_slot_vote_unchecked(&mut vote_state, slot);
assert_eq!(vote_state.votes().len(), 2);
}
#[test_matrix(
[VoteStateTargetVersion::V3, VoteStateTargetVersion::V4],
[true, false]
)]
fn test_update_commission(
target_version: VoteStateTargetVersion,
disable_commission_update_rule: bool,
) {
let mut vote_state = vote_state_new_for_test(&solana_pubkey::new_rand(), target_version);
let node_pubkey = *vote_state.node_pubkey();
let withdrawer_pubkey = *vote_state.authorized_withdrawer();
vote_state.set_commission(10);
let serialized = vote_state.serialize();
let serialized_len = serialized.len();
let rent = Rent::default();
let lamports = rent.minimum_balance(serialized_len);
let mut vote_account = AccountSharedData::new(lamports, serialized_len, &id());
vote_account.set_data_from_slice(&serialized);
let processor_account = AccountSharedData::new(0, 0, &solana_sdk_ids::native_loader::id());
let mut transaction_context = TransactionContext::new(
vec![(id(), processor_account), (node_pubkey, vote_account)],
rent,
0,
0,
1,
);
transaction_context
.configure_top_level_instruction_for_tests(
0,
vec![InstructionAccount::new(1, false, true)],
vec![],
)
.unwrap();
let instruction_context = transaction_context.get_next_instruction_context().unwrap();
let mut borrowed_account = instruction_context
.try_borrow_instruction_account(0)
.unwrap();
let epoch_schedule = std::sync::Arc::new(EpochSchedule::without_warmup());
let first_half_clock = std::sync::Arc::new(Clock {
slot: epoch_schedule.slots_per_epoch / 4,
..Clock::default()
});
let second_half_clock = std::sync::Arc::new(Clock {
slot: (epoch_schedule.slots_per_epoch * 3) / 4,
..Clock::default()
});
let signers: HashSet<Pubkey> = vec![withdrawer_pubkey].into_iter().collect();
assert_eq!(
get_vote_state_handler_checked(
&borrowed_account,
PreserveBehaviorInHandlerHelper::new(target_version, true),
)
.unwrap()
.commission(),
10
);
assert_matches!(
update_commission(
&mut borrowed_account,
target_version,
11,
&signers,
&epoch_schedule,
&first_half_clock,
disable_commission_update_rule,
),
Ok(())
);
assert_eq!(
get_vote_state_handler_checked(
&borrowed_account,
PreserveBehaviorInHandlerHelper::new(target_version, true),
)
.unwrap()
.commission(),
11
);
let result = update_commission(
&mut borrowed_account,
target_version,
12,
&signers,
&epoch_schedule,
&second_half_clock,
disable_commission_update_rule,
);
let state_commission = get_vote_state_handler_checked(
&borrowed_account,
PreserveBehaviorInHandlerHelper::new(target_version, true),
)
.unwrap()
.commission();
if disable_commission_update_rule {
assert_matches!(result, Ok(()));
assert_eq!(state_commission, 12);
} else {
assert_matches!(result, Err(_));
assert_eq!(state_commission, 11);
}
assert_matches!(
update_commission(
&mut borrowed_account,
target_version,
10,
&signers,
&epoch_schedule,
&first_half_clock,
disable_commission_update_rule,
),
Ok(())
);
assert_eq!(
get_vote_state_handler_checked(
&borrowed_account,
PreserveBehaviorInHandlerHelper::new(target_version, true),
)
.unwrap()
.commission(),
10
);
assert_eq!(
get_vote_state_handler_checked(
&borrowed_account,
PreserveBehaviorInHandlerHelper::new(target_version, true),
)
.unwrap()
.commission(),
10
);
assert_matches!(
update_commission(
&mut borrowed_account,
target_version,
9,
&signers,
&epoch_schedule,
&second_half_clock,
disable_commission_update_rule,
),
Ok(())
);
assert_eq!(
get_vote_state_handler_checked(
&borrowed_account,
PreserveBehaviorInHandlerHelper::new(target_version, true),
)
.unwrap()
.commission(),
9
);
}
#[test]
fn test_update_commission_bps() {
let target_version = VoteStateTargetVersion::V4;
let mut vote_state = vote_state_new_for_test(&solana_pubkey::new_rand(), target_version);
let withdrawer_pubkey = *vote_state.authorized_withdrawer();
let node_pubkey = *vote_state.node_pubkey();
vote_state.set_commission(10);
let serialized = vote_state.serialize();
let serialized_len = serialized.len();
let rent = Rent::default();
let lamports = rent.minimum_balance(serialized_len);
let mut vote_account = AccountSharedData::new(lamports, serialized_len, &id());
vote_account.set_data_from_slice(&serialized);
let processor_account = AccountSharedData::new(0, 0, &solana_sdk_ids::native_loader::id());
let mut transaction_context = TransactionContext::new(
vec![(id(), processor_account), (node_pubkey, vote_account)],
rent,
0,
0,
1,
);
transaction_context
.configure_top_level_instruction_for_tests(
0,
vec![InstructionAccount::new(1, false, true)],
vec![],
)
.unwrap();
let instruction_context = transaction_context.get_next_instruction_context().unwrap();
let mut borrowed_account = instruction_context
.try_borrow_instruction_account(0)
.unwrap();
let signers: HashSet<Pubkey> = vec![withdrawer_pubkey].into_iter().collect();
let non_signers: HashSet<Pubkey> = HashSet::new();
assert_eq!(
update_commission_bps(
&mut borrowed_account,
target_version,
500,
CommissionKind::BlockRevenue,
&signers,
false, ),
Err(InstructionError::InvalidInstructionData)
);
assert_eq!(
update_commission_bps(
&mut borrowed_account,
target_version,
500,
CommissionKind::InflationRewards,
&non_signers,
false,
),
Err(InstructionError::MissingRequiredSignature)
);
let wrong_signers: HashSet<Pubkey> = vec![Pubkey::new_unique()].into_iter().collect();
assert_eq!(
update_commission_bps(
&mut borrowed_account,
target_version,
500,
CommissionKind::InflationRewards,
&wrong_signers,
false,
),
Err(InstructionError::MissingRequiredSignature)
);
let mut commission_bps_roundtrip = |new_commission_bps: u16| {
update_commission_bps(
&mut borrowed_account,
target_version,
new_commission_bps,
CommissionKind::InflationRewards,
&signers,
false,
)
.unwrap();
update_commission_bps(
&mut borrowed_account,
target_version,
new_commission_bps,
CommissionKind::BlockRevenue,
&signers,
true,
)
.unwrap();
let handler = get_vote_state_handler_checked(
&borrowed_account,
PreserveBehaviorInHandlerHelper::new(target_version, true),
)
.unwrap();
assert_eq!(
handler.as_ref_v4().inflation_rewards_commission_bps,
new_commission_bps
);
assert_eq!(
handler.as_ref_v4().block_revenue_commission_bps,
new_commission_bps
);
};
commission_bps_roundtrip(1_100); commission_bps_roundtrip(5_000); commission_bps_roundtrip(4_400); commission_bps_roundtrip(4_600);
commission_bps_roundtrip(15_000); commission_bps_roundtrip(50_000); }
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_vote_double_lockout_after_expiration(target_version: VoteStateTargetVersion) {
let mut vote_state = vote_state_new_for_test(&solana_pubkey::new_rand(), target_version);
for i in 0..3 {
process_slot_vote_unchecked(&mut vote_state, i as u64);
}
check_lockouts(&vote_state);
process_slot_vote_unchecked(&mut vote_state, (2 + INITIAL_LOCKOUT + 1) as u64);
check_lockouts(&vote_state);
process_slot_vote_unchecked(&mut vote_state, (2 + INITIAL_LOCKOUT + 2) as u64);
check_lockouts(&vote_state);
process_slot_vote_unchecked(&mut vote_state, (2 + INITIAL_LOCKOUT + 3) as u64);
check_lockouts(&vote_state);
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_expire_multiple_votes(target_version: VoteStateTargetVersion) {
let mut vote_state = vote_state_new_for_test(&solana_pubkey::new_rand(), target_version);
for i in 0..3 {
process_slot_vote_unchecked(&mut vote_state, i as u64);
}
assert_eq!(vote_state.votes()[0].confirmation_count(), 3);
let expire_slot =
vote_state.votes()[1].slot() + vote_state.votes()[1].lockout.lockout() + 1;
process_slot_vote_unchecked(&mut vote_state, expire_slot);
assert_eq!(vote_state.votes().len(), 2);
assert_eq!(vote_state.votes()[0].slot(), 0);
assert_eq!(vote_state.votes()[1].slot(), expire_slot);
process_slot_vote_unchecked(&mut vote_state, expire_slot + 1);
assert_eq!(vote_state.votes()[0].confirmation_count(), 3);
assert_eq!(vote_state.votes()[1].confirmation_count(), 2);
assert_eq!(vote_state.votes()[2].confirmation_count(), 1);
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_vote_credits(target_version: VoteStateTargetVersion) {
let mut vote_state = vote_state_new_for_test(&solana_pubkey::new_rand(), target_version);
for i in 0..MAX_LOCKOUT_HISTORY {
process_slot_vote_unchecked(&mut vote_state, i as u64);
}
assert_eq!(vote_state.credits(), 0);
process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 1);
assert_eq!(vote_state.credits(), 1);
process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 2);
assert_eq!(vote_state.credits(), 2);
process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 3);
assert_eq!(vote_state.credits(), 3);
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_duplicate_vote(target_version: VoteStateTargetVersion) {
let mut vote_state = vote_state_new_for_test(&solana_pubkey::new_rand(), target_version);
process_slot_vote_unchecked(&mut vote_state, 0);
process_slot_vote_unchecked(&mut vote_state, 1);
process_slot_vote_unchecked(&mut vote_state, 0);
assert_eq!(vote_state.nth_recent_lockout(0).unwrap().slot(), 1);
assert_eq!(vote_state.nth_recent_lockout(1).unwrap().slot(), 0);
assert!(vote_state.nth_recent_lockout(2).is_none());
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_nth_recent_lockout(target_version: VoteStateTargetVersion) {
let mut vote_state = vote_state_new_for_test(&solana_pubkey::new_rand(), target_version);
for i in 0..MAX_LOCKOUT_HISTORY {
process_slot_vote_unchecked(&mut vote_state, i as u64);
}
for i in 0..(MAX_LOCKOUT_HISTORY - 1) {
assert_eq!(
vote_state.nth_recent_lockout(i).unwrap().slot() as usize,
MAX_LOCKOUT_HISTORY - i - 1,
);
}
assert!(vote_state.nth_recent_lockout(MAX_LOCKOUT_HISTORY).is_none());
}
fn check_lockouts(vote_state: &VoteStateHandler) {
let votes = vote_state.votes();
for (i, vote) in votes.iter().enumerate() {
let num_votes = votes
.len()
.checked_sub(i)
.expect("`i` is less than `vote_state.votes().len()`");
assert_eq!(
vote.lockout.lockout(),
INITIAL_LOCKOUT.pow(num_votes as u32) as u64
);
}
}
fn recent_votes(vote_state: &VoteStateHandler) -> Vec<Vote> {
let votes = vote_state.votes();
let start = votes.len().saturating_sub(MAX_RECENT_VOTES);
(start..votes.len())
.map(|i| Vote::new(vec![votes.get(i).unwrap().slot()], Hash::default()))
.collect()
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_process_missed_votes(target_version: VoteStateTargetVersion) {
let mut vote_state_a = vote_state_new_for_test(&solana_pubkey::new_rand(), target_version);
let mut vote_state_b = vote_state_new_for_test(&solana_pubkey::new_rand(), target_version);
(0..5).for_each(|i| process_slot_vote_unchecked(&mut vote_state_a, i as u64));
assert_ne!(recent_votes(&vote_state_a), recent_votes(&vote_state_b));
let slots = (0u64..MAX_RECENT_VOTES as u64).collect();
let vote = Vote::new(slots, Hash::default());
let slot_hashes: Vec<_> = vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect();
assert_eq!(
process_vote(&mut vote_state_a, &vote, &slot_hashes, 0, 0),
Ok(())
);
assert_eq!(
process_vote(&mut vote_state_b, &vote, &slot_hashes, 0, 0),
Ok(())
);
assert_eq!(recent_votes(&vote_state_a), recent_votes(&vote_state_b));
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_vote_skips_old_vote(mut vote_state: VoteStateHandler) {
let vote = Vote::new(vec![0], Hash::default());
let slot_hashes: Vec<_> = vec![(0, vote.hash)];
assert_eq!(
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0),
Ok(())
);
let recent = recent_votes(&vote_state);
assert_eq!(
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0),
Err(VoteError::VoteTooOld)
);
assert_eq!(recent, recent_votes(&vote_state));
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_check_slots_are_valid_vote_empty_slot_hashes(vote_state: VoteStateHandler) {
let vote = Vote::new(vec![0], Hash::default());
assert_eq!(
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &[]),
Err(VoteError::VoteTooOld)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_check_slots_are_valid_new_vote(vote_state: VoteStateHandler) {
let vote = Vote::new(vec![0], Hash::default());
let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)];
assert_eq!(
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes),
Ok(())
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_check_slots_are_valid_bad_hash(vote_state: VoteStateHandler) {
let vote = Vote::new(vec![0], Hash::default());
let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), hash(vote.hash.as_ref()))];
assert_eq!(
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes),
Err(VoteError::SlotHashMismatch)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_check_slots_are_valid_bad_slot(vote_state: VoteStateHandler) {
let vote = Vote::new(vec![1], Hash::default());
let slot_hashes: Vec<_> = vec![(0, vote.hash)];
assert_eq!(
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes),
Err(VoteError::SlotsMismatch)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_check_slots_are_valid_duplicate_vote(mut vote_state: VoteStateHandler) {
let vote = Vote::new(vec![0], Hash::default());
let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)];
assert_eq!(
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0),
Ok(())
);
assert_eq!(
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes),
Err(VoteError::VoteTooOld)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_check_slots_are_valid_next_vote(mut vote_state: VoteStateHandler) {
let vote = Vote::new(vec![0], Hash::default());
let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)];
assert_eq!(
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0),
Ok(())
);
let vote = Vote::new(vec![0, 1], Hash::default());
let slot_hashes: Vec<_> = vec![(1, vote.hash), (0, vote.hash)];
assert_eq!(
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes),
Ok(())
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_check_slots_are_valid_next_vote_only(mut vote_state: VoteStateHandler) {
let vote = Vote::new(vec![0], Hash::default());
let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)];
assert_eq!(
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0),
Ok(())
);
let vote = Vote::new(vec![1], Hash::default());
let slot_hashes: Vec<_> = vec![(1, vote.hash), (0, vote.hash)];
assert_eq!(
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes),
Ok(())
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_vote_empty_slots(mut vote_state: VoteStateHandler) {
let vote = Vote::new(vec![], Hash::default());
assert_eq!(
process_vote(&mut vote_state, &vote, &[], 0, 0),
Err(VoteError::EmptySlots)
);
}
pub fn process_new_vote_state_from_lockouts(
vote_state: &mut VoteStateHandler,
new_state: VecDeque<Lockout>,
new_root: Option<Slot>,
timestamp: Option<i64>,
epoch: Epoch,
) -> Result<(), VoteError> {
process_new_vote_state(
vote_state,
new_state.into_iter().map(LandedVote::from).collect(),
new_root,
timestamp,
epoch,
0,
)
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_vote_state_update_increment_credits(mut vote_state: VoteStateHandler) {
let test_vote_groups: Vec<Vec<Slot>> = vec![
vec![1, 2, 3, 4, 5, 6, 7, 8],
vec![
9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
30, 31,
],
vec![32],
vec![33],
vec![34, 35],
vec![36, 37, 38],
vec![
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68,
],
vec![
69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89,
90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
],
vec![100, 101, 106, 107, 112, 116, 120, 121, 122, 124],
vec![200, 201],
vec![
202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,
218, 219, 220, 221, 222, 223, 224, 225, 226,
],
vec![227, 228, 229, 230, 231, 232, 233, 234, 235, 236],
];
for vote_group in test_vote_groups {
let mut vote_state_after_vote = vote_state.clone();
process_vote_unchecked(
&mut vote_state_after_vote,
Vote {
slots: vote_group.clone(),
hash: Hash::new_unique(),
timestamp: None,
},
)
.unwrap();
assert_eq!(
process_new_vote_state(
&mut vote_state,
vote_state_after_vote.votes().clone(),
vote_state_after_vote.root_slot(),
None,
0,
0,
),
Ok(())
);
assert_eq!(
vote_state.epoch_credits(),
vote_state_after_vote.epoch_credits()
);
}
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_timely_credits(target_version: VoteStateTargetVersion) {
let test_vote_groups: Vec<(Vec<Slot>, Slot, u32)> = vec![
(
vec![1, 2, 3, 4, 5, 6, 7, 8],
9,
0,
),
(
vec![
9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
29, 30, 31,
],
34,
0,
),
(
vec![32],
35,
10,
),
(
vec![33],
36,
10 + 11, ),
(
vec![34, 35],
37,
21 + 12 + 13, ),
(
vec![36, 37, 38],
39,
46 + 14 + 15 + 16, ),
(
vec![
39, 40, 41, 42, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68,
],
69,
91 + 16
+ 9 + 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 15
+ 15
+ 15
+ 16
+ 15
+ 16, ),
(
vec![
69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88,
89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
],
100,
327 + 16
+ 14 + 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 16, ),
(
vec![115, 116, 117, 118, 119, 120, 121, 122, 123, 124],
130,
508 + ((74 - 69) + 1), ),
(
vec![200, 201],
202,
514,
),
(
vec![
202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,
218, 219, 220, 221, 222, 223, 224, 225, 226,
],
227,
514 + 9 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13, ),
(
vec![227, 228, 229, 230, 231, 232, 233, 234, 235, 236],
237,
613 + 3 + 4 + 5 + 6 + 16 + 16 + 1 + 1 + 1 + 1, ),
];
let new_vote_state = || match target_version {
VoteStateTargetVersion::V3 => VoteStateHandler::default_v3(),
VoteStateTargetVersion::V4 => VoteStateHandler::default_v4(),
};
for i in 0..test_vote_groups.len() {
let mut vote_state_1 = new_vote_state();
let mut vote_state_2 = new_vote_state();
test_vote_groups.iter().take(i + 1).for_each(|vote_group| {
let vote = Vote {
slots: vote_group.0.clone(), hash: Hash::new_unique(),
timestamp: None,
};
let slot_hashes: Vec<_> =
vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect();
assert_eq!(
process_vote(
&mut vote_state_1,
&vote,
&slot_hashes,
0,
vote_group.1, ),
Ok(())
);
assert_eq!(
process_new_vote_state(
&mut vote_state_2,
vote_state_1.votes().clone(),
vote_state_1.root_slot(),
None,
0,
vote_group.1, ),
Ok(())
);
});
let vote_group = &test_vote_groups[i];
assert_eq!(vote_state_1.credits(), vote_group.2 as u64); assert_eq!(vote_state_2.credits(), vote_group.2 as u64); }
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_retroactive_voting_timely_credits(mut vote_state: VoteStateHandler) {
#[allow(clippy::type_complexity)]
let test_vote_state_updates: Vec<(Vec<(Slot, u32)>, Slot, Option<Slot>, u32)> = vec![
(
vec![(7, 4), (8, 3), (9, 2), (10, 1)],
11,
None,
0,
),
(
vec![
(1, 10),
(2, 9),
(3, 8),
(4, 7),
(5, 6),
(6, 5),
(7, 4),
(8, 3),
(9, 2),
(10, 1),
],
12,
None,
0,
),
(
vec![
(11, 31),
(12, 30),
(13, 29),
(14, 28),
(15, 27),
(16, 26),
(17, 25),
(18, 24),
(19, 23),
(20, 22),
(21, 21),
(22, 20),
(23, 19),
(24, 18),
(25, 17),
(26, 16),
(27, 15),
(28, 14),
(29, 13),
(30, 12),
(31, 11),
(32, 10),
(33, 9),
(34, 8),
(35, 7),
(36, 6),
(37, 5),
(38, 4),
(39, 3),
(40, 2),
(41, 1),
],
42,
Some(10),
7 + 8 + 9 + 10 + 11 + 12 + 14 + 15 + 16 + 16,
),
];
test_vote_state_updates
.iter()
.for_each(|proposed_vote_state| {
let new_state = proposed_vote_state
.0 .iter()
.map(|(slot, confirmation_count)| LandedVote {
latency: 0,
lockout: Lockout::new_with_confirmation_count(*slot, *confirmation_count),
})
.collect::<VecDeque<LandedVote>>();
assert_eq!(
process_new_vote_state(
&mut vote_state,
new_state,
proposed_vote_state.2, None,
0,
proposed_vote_state.1, ),
Ok(())
);
assert_eq!(vote_state.credits(), proposed_vote_state.3 as u64);
});
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_too_many_votes(mut vote_state1: VoteStateHandler) {
let bad_votes: VecDeque<Lockout> = (0..=MAX_LOCKOUT_HISTORY)
.map(|slot| {
Lockout::new_with_confirmation_count(
slot as Slot,
(MAX_LOCKOUT_HISTORY - slot + 1) as u32,
)
})
.collect();
let current_epoch = vote_state1.current_epoch();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
bad_votes,
None,
None,
current_epoch,
),
Err(VoteError::TooManyVotes)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_root_rollback(mut vote_state1: VoteStateHandler) {
for i in 0..MAX_LOCKOUT_HISTORY + 2 {
process_slot_vote_unchecked(&mut vote_state1, i as Slot);
}
assert_eq!(vote_state1.root_slot().unwrap(), 1);
let mut vote_state2 = vote_state1.clone();
process_slot_vote_unchecked(&mut vote_state2, MAX_LOCKOUT_HISTORY as Slot + 3);
let lesser_root = Some(0);
let current_epoch = vote_state2.current_epoch();
assert_eq!(
process_new_vote_state(
&mut vote_state1,
vote_state2.votes().clone(),
lesser_root,
None,
current_epoch,
0,
),
Err(VoteError::RootRollBack)
);
let none_root = None;
assert_eq!(
process_new_vote_state(
&mut vote_state1,
vote_state2.votes().clone(),
none_root,
None,
current_epoch,
0,
),
Err(VoteError::RootRollBack)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_zero_confirmations(mut vote_state1: VoteStateHandler) {
let current_epoch = vote_state1.current_epoch();
let bad_votes: VecDeque<Lockout> = vec![
Lockout::new_with_confirmation_count(0, 0),
Lockout::new_with_confirmation_count(1, 1),
]
.into_iter()
.collect();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
bad_votes,
None,
None,
current_epoch,
),
Err(VoteError::ZeroConfirmations)
);
let bad_votes: VecDeque<Lockout> = vec![
Lockout::new_with_confirmation_count(0, 2),
Lockout::new_with_confirmation_count(1, 0),
]
.into_iter()
.collect();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
bad_votes,
None,
None,
current_epoch,
),
Err(VoteError::ZeroConfirmations)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_confirmations_too_large(initial_vote_state: VoteStateHandler) {
let mut vote_state1 = initial_vote_state.clone();
let current_epoch = vote_state1.current_epoch();
let good_votes: VecDeque<Lockout> = vec![Lockout::new_with_confirmation_count(
0,
MAX_LOCKOUT_HISTORY as u32,
)]
.into_iter()
.collect();
process_new_vote_state_from_lockouts(
&mut vote_state1,
good_votes,
None,
None,
current_epoch,
)
.unwrap();
let mut vote_state1 = initial_vote_state;
let bad_votes: VecDeque<Lockout> = vec![Lockout::new_with_confirmation_count(
0,
MAX_LOCKOUT_HISTORY as u32 + 1,
)]
.into_iter()
.collect();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
bad_votes,
None,
None,
current_epoch,
),
Err(VoteError::ConfirmationTooLarge)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_slot_smaller_than_root(mut vote_state1: VoteStateHandler) {
let current_epoch = vote_state1.current_epoch();
let root_slot = 5;
let bad_votes: VecDeque<Lockout> = vec![
Lockout::new_with_confirmation_count(root_slot, 2),
Lockout::new_with_confirmation_count(root_slot + 1, 1),
]
.into_iter()
.collect();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
bad_votes,
Some(root_slot),
None,
current_epoch,
),
Err(VoteError::SlotSmallerThanRoot)
);
let bad_votes: VecDeque<Lockout> = vec![
Lockout::new_with_confirmation_count(root_slot - 1, 2),
Lockout::new_with_confirmation_count(root_slot + 1, 1),
]
.into_iter()
.collect();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
bad_votes,
Some(root_slot),
None,
current_epoch,
),
Err(VoteError::SlotSmallerThanRoot)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_slots_not_ordered(mut vote_state1: VoteStateHandler) {
let current_epoch = vote_state1.current_epoch();
let bad_votes: VecDeque<Lockout> = vec![
Lockout::new_with_confirmation_count(1, 2),
Lockout::new_with_confirmation_count(0, 1),
]
.into_iter()
.collect();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
bad_votes,
None,
None,
current_epoch,
),
Err(VoteError::SlotsNotOrdered)
);
let bad_votes: VecDeque<Lockout> = vec![
Lockout::new_with_confirmation_count(1, 2),
Lockout::new_with_confirmation_count(1, 1),
]
.into_iter()
.collect();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
bad_votes,
None,
None,
current_epoch,
),
Err(VoteError::SlotsNotOrdered)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_confirmations_not_ordered(mut vote_state1: VoteStateHandler) {
let current_epoch = vote_state1.current_epoch();
let bad_votes: VecDeque<Lockout> = vec![
Lockout::new_with_confirmation_count(0, 1),
Lockout::new_with_confirmation_count(1, 2),
]
.into_iter()
.collect();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
bad_votes,
None,
None,
current_epoch,
),
Err(VoteError::ConfirmationsNotOrdered)
);
let bad_votes: VecDeque<Lockout> = vec![
Lockout::new_with_confirmation_count(0, 1),
Lockout::new_with_confirmation_count(1, 1),
]
.into_iter()
.collect();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
bad_votes,
None,
None,
current_epoch,
),
Err(VoteError::ConfirmationsNotOrdered)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_new_vote_state_lockout_mismatch(
mut vote_state1: VoteStateHandler,
) {
let current_epoch = vote_state1.current_epoch();
let bad_votes: VecDeque<Lockout> = vec![
Lockout::new_with_confirmation_count(0, 2),
Lockout::new_with_confirmation_count(7, 1),
]
.into_iter()
.collect();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
bad_votes,
None,
None,
current_epoch,
),
Err(VoteError::NewVoteStateLockoutMismatch)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_confirmation_rollback(mut vote_state1: VoteStateHandler) {
let current_epoch = vote_state1.current_epoch();
let votes: VecDeque<Lockout> = vec![
Lockout::new_with_confirmation_count(0, 4),
Lockout::new_with_confirmation_count(1, 3),
]
.into_iter()
.collect();
process_new_vote_state_from_lockouts(&mut vote_state1, votes, None, None, current_epoch)
.unwrap();
let votes: VecDeque<Lockout> = vec![
Lockout::new_with_confirmation_count(0, 4),
Lockout::new_with_confirmation_count(1, 2),
Lockout::new_with_confirmation_count(2, 1),
]
.into_iter()
.collect();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
votes,
None,
None,
current_epoch,
),
Err(VoteError::ConfirmationRollBack)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_root_progress(mut vote_state1: VoteStateHandler) {
for i in 0..MAX_LOCKOUT_HISTORY {
process_slot_vote_unchecked(&mut vote_state1, i as u64);
}
assert!(vote_state1.root_slot().is_none());
let mut vote_state2 = vote_state1.clone();
for new_vote in MAX_LOCKOUT_HISTORY + 1..=MAX_LOCKOUT_HISTORY + 2 {
process_slot_vote_unchecked(&mut vote_state2, new_vote as Slot);
assert_ne!(vote_state1.root_slot(), vote_state2.root_slot());
process_new_vote_state(
&mut vote_state1,
vote_state2.votes().clone(),
vote_state2.root_slot(),
None,
vote_state2.current_epoch(),
0,
)
.unwrap();
assert_eq!(vote_state1, vote_state2);
}
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_same_slot_but_not_common_ancestor(
initial_vote_state: VoteStateHandler,
) {
let mut vote_state1 = initial_vote_state.clone();
process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 5]);
assert_eq!(
vote_state1
.votes()
.iter()
.map(|vote| vote.slot())
.collect::<Vec<Slot>>(),
vec![1, 5]
);
let mut vote_state2 = initial_vote_state;
process_slot_votes_unchecked(&mut vote_state2, &[1, 2, 3, 5, 7]);
assert_eq!(
vote_state2
.votes()
.iter()
.map(|vote| vote.slot())
.collect::<Vec<Slot>>(),
vec![1, 2, 3, 5, 7]
);
process_new_vote_state(
&mut vote_state1,
vote_state2.votes().clone(),
vote_state2.root_slot(),
None,
vote_state2.current_epoch(),
0,
)
.unwrap();
assert_eq!(vote_state1, vote_state2);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_lockout_violation(initial_vote_state: VoteStateHandler) {
let mut vote_state1 = initial_vote_state.clone();
process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 4, 5]);
assert_eq!(
vote_state1
.votes()
.iter()
.map(|vote| vote.slot())
.collect::<Vec<Slot>>(),
vec![1, 2, 4, 5]
);
let mut vote_state2 = initial_vote_state;
process_slot_votes_unchecked(&mut vote_state2, &[1, 2, 3, 5, 7]);
assert_eq!(
vote_state2
.votes()
.iter()
.map(|vote| vote.slot())
.collect::<Vec<Slot>>(),
vec![1, 2, 3, 5, 7]
);
assert_eq!(
process_new_vote_state(
&mut vote_state1,
vote_state2.votes().clone(),
vote_state2.root_slot(),
None,
vote_state2.current_epoch(),
0,
),
Err(VoteError::LockoutConflict)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_lockout_violation2(initial_vote_state: VoteStateHandler) {
let mut vote_state1 = initial_vote_state.clone();
process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 5, 6, 7]);
assert_eq!(
vote_state1
.votes()
.iter()
.map(|vote| vote.slot())
.collect::<Vec<Slot>>(),
vec![1, 5, 6, 7]
);
let mut vote_state2 = initial_vote_state;
process_slot_votes_unchecked(&mut vote_state2, &[1, 2, 3, 5, 6, 8]);
assert_eq!(
vote_state2
.votes()
.iter()
.map(|vote| vote.slot())
.collect::<Vec<Slot>>(),
vec![1, 2, 3, 5, 6, 8]
);
assert_eq!(
process_new_vote_state(
&mut vote_state1,
vote_state2.votes().clone(),
vote_state2.root_slot(),
None,
vote_state2.current_epoch(),
0,
),
Err(VoteError::LockoutConflict)
);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_state_expired_ancestor_not_removed(mut vote_state1: VoteStateHandler) {
process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 3, 9]);
assert_eq!(
vote_state1
.votes()
.iter()
.map(|vote| vote.slot())
.collect::<Vec<Slot>>(),
vec![1, 9]
);
let mut vote_state2 = vote_state1.clone();
process_slot_vote_unchecked(&mut vote_state2, 10);
assert_eq!(vote_state2.votes()[0].slot(), 1);
assert_eq!(vote_state2.votes()[0].lockout.last_locked_out_slot(), 9);
assert_eq!(
vote_state2
.votes()
.iter()
.map(|vote| vote.slot())
.collect::<Vec<Slot>>(),
vec![1, 9, 10]
);
process_new_vote_state(
&mut vote_state1,
vote_state2.votes().clone(),
vote_state2.root_slot(),
None,
vote_state2.current_epoch(),
0,
)
.unwrap();
assert_eq!(vote_state1, vote_state2,);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_process_new_vote_current_state_contains_bigger_slots(
mut vote_state1: VoteStateHandler,
) {
process_slot_votes_unchecked(&mut vote_state1, &[6, 7, 8]);
assert_eq!(
vote_state1
.votes()
.iter()
.map(|vote| vote.slot())
.collect::<Vec<Slot>>(),
vec![6, 7, 8]
);
let bad_votes: VecDeque<Lockout> = vec![
Lockout::new_with_confirmation_count(2, 5),
Lockout::new_with_confirmation_count(14, 1),
]
.into_iter()
.collect();
let root = Some(1);
let current_epoch = vote_state1.current_epoch();
assert_eq!(
process_new_vote_state_from_lockouts(
&mut vote_state1,
bad_votes,
root,
None,
current_epoch,
),
Err(VoteError::LockoutConflict)
);
let good_votes: VecDeque<LandedVote> = vec![
Lockout::new_with_confirmation_count(2, 5).into(),
Lockout::new_with_confirmation_count(15, 1).into(),
]
.into_iter()
.collect();
let current_epoch = vote_state1.current_epoch();
process_new_vote_state(
&mut vote_state1,
good_votes.clone(),
root,
None,
current_epoch,
0,
)
.unwrap();
assert_eq!(*vote_state1.votes(), good_votes);
}
#[test_case(VoteStateHandler::default_v3() ; "VoteStateV3")]
#[test_case(VoteStateHandler::default_v4() ; "VoteStateV4")]
fn test_filter_old_votes(mut vote_state: VoteStateHandler) {
let old_vote_slot = 1;
let vote = Vote::new(vec![old_vote_slot], Hash::default());
let slot_hashes = vec![(3, Hash::new_unique()), (2, Hash::new_unique())];
assert_eq!(
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0),
Err(VoteError::VotesTooOldAllFiltered)
);
let vote_slot = 2;
let vote_slot_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == vote_slot)
.unwrap()
.1;
let vote = Vote::new(vec![old_vote_slot, vote_slot], vote_slot_hash);
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0).unwrap();
assert_eq!(
vote_state
.votes()
.iter()
.map(|vote| vote.lockout)
.collect::<Vec<Lockout>>(),
vec![Lockout::new_with_confirmation_count(vote_slot, 1)]
);
}
fn build_slot_hashes(slots: Vec<Slot>) -> Vec<(Slot, Hash)> {
slots
.iter()
.rev()
.map(|x| (*x, Hash::new_unique()))
.collect()
}
fn build_vote_state(
target_version: VoteStateTargetVersion,
vote_slots: Vec<Slot>,
slot_hashes: &[(Slot, Hash)],
) -> VoteStateHandler {
let mut vote_state = match target_version {
VoteStateTargetVersion::V3 => VoteStateHandler::default_v3(),
VoteStateTargetVersion::V4 => VoteStateHandler::default_v4(),
};
if !vote_slots.is_empty() {
let vote_hash = slot_hashes
.iter()
.find(|(slot, _hash)| slot == vote_slots.last().unwrap())
.unwrap()
.1;
let vote = Vote::new(vote_slots, vote_hash);
process_vote_unfiltered(&mut vote_state, &vote.slots, &vote, slot_hashes, 0, 0)
.unwrap();
}
vote_state
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_empty(target_version: VoteStateTargetVersion) {
let empty_slot_hashes = build_slot_hashes(vec![]);
let empty_vote_state = build_vote_state(target_version, vec![], &empty_slot_hashes);
let mut tower_sync = TowerSync::from(vec![]);
assert_eq!(
check_and_filter_proposed_vote_state(
&empty_vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&empty_slot_hashes
),
Err(VoteError::EmptySlots),
);
let mut tower_sync = TowerSync::from(vec![(0, 1)]);
assert_eq!(
check_and_filter_proposed_vote_state(
&empty_vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&empty_slot_hashes
),
Err(VoteError::SlotsMismatch),
);
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_too_old(target_version: VoteStateTargetVersion) {
let slot_hashes = build_slot_hashes(vec![1, 2, 3, 4]);
let latest_vote = 4;
let vote_state = build_vote_state(target_version, vec![1, 2, 3, latest_vote], &slot_hashes);
let mut tower_sync = TowerSync::from(vec![(latest_vote, 1)]);
assert_eq!(
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes
),
Err(VoteError::VoteTooOld),
);
let earliest_slot_in_history = latest_vote + 2;
let slot_hashes = build_slot_hashes(vec![earliest_slot_in_history]);
let mut tower_sync = TowerSync::from(vec![(earliest_slot_in_history - 1, 1)]);
assert_eq!(
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes
),
Err(VoteError::VoteTooOld),
);
}
fn run_test_check_and_filter_proposed_vote_state_older_than_history_root(
target_version: VoteStateTargetVersion,
earliest_slot_in_history: Slot,
current_vote_state_slots: Vec<Slot>,
current_vote_state_root: Option<Slot>,
proposed_slots_and_lockouts: Vec<(Slot, u32)>,
proposed_root: Slot,
expected_root: Option<Slot>,
expected_vote_state: Vec<Lockout>,
) {
assert!(proposed_root < earliest_slot_in_history);
assert_eq!(
expected_root,
current_vote_state_slots
.iter()
.rev()
.find(|slot| **slot <= proposed_root)
.cloned()
);
let latest_slot_in_history = proposed_slots_and_lockouts
.last()
.unwrap()
.0
.max(earliest_slot_in_history);
let mut slot_hashes = build_slot_hashes(
(current_vote_state_slots.first().copied().unwrap_or(0)..=latest_slot_in_history)
.collect::<Vec<Slot>>(),
);
let mut vote_state =
build_vote_state(target_version, current_vote_state_slots, &slot_hashes);
vote_state.set_root_slot(current_vote_state_root);
slot_hashes.retain(|slot| slot.0 >= earliest_slot_in_history);
assert!(!proposed_slots_and_lockouts.is_empty());
let proposed_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == proposed_slots_and_lockouts.last().unwrap().0)
.unwrap()
.1;
let mut tower_sync = TowerSync::from(proposed_slots_and_lockouts);
tower_sync.hash = proposed_hash;
tower_sync.root = Some(proposed_root);
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes,
)
.unwrap();
assert_eq!(tower_sync.root, expected_root);
assert!(
do_process_tower_sync(&mut vote_state, &slot_hashes, 0, 0, tower_sync.clone(),).is_ok()
);
assert_eq!(vote_state.root_slot(), expected_root);
assert_eq!(
vote_state
.votes()
.iter()
.map(|vote| vote.lockout)
.collect::<Vec<Lockout>>(),
expected_vote_state,
);
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_older_than_history_root(
target_version: VoteStateTargetVersion,
) {
let earliest_slot_in_history = 5;
let current_vote_state_slots: Vec<Slot> = vec![1, 2, 3, 4];
let current_vote_state_root = None;
let proposed_slots_and_lockouts = vec![(5, 1)];
let proposed_root = 4;
let expected_root = Some(4);
let expected_vote_state = vec![Lockout::new_with_confirmation_count(5, 1)];
run_test_check_and_filter_proposed_vote_state_older_than_history_root(
target_version,
earliest_slot_in_history,
current_vote_state_slots,
current_vote_state_root,
proposed_slots_and_lockouts,
proposed_root,
expected_root,
expected_vote_state,
);
let earliest_slot_in_history = 5;
let current_vote_state_slots: Vec<Slot> = vec![1, 2, 3, 4];
let current_vote_state_root = Some(0);
let proposed_slots_and_lockouts = vec![(5, 1)];
let proposed_root = 4;
let expected_root = Some(4);
let expected_vote_state = vec![Lockout::new_with_confirmation_count(5, 1)];
run_test_check_and_filter_proposed_vote_state_older_than_history_root(
target_version,
earliest_slot_in_history,
current_vote_state_slots,
current_vote_state_root,
proposed_slots_and_lockouts,
proposed_root,
expected_root,
expected_vote_state,
);
let earliest_slot_in_history = 5;
let current_vote_state_slots: Vec<Slot> = vec![1, 2, 3, 4];
let current_vote_state_root = Some(0);
let proposed_slots_and_lockouts = vec![(4, 2), (5, 1)];
let proposed_root = 3;
let expected_root = Some(3);
let expected_vote_state = vec![
Lockout::new_with_confirmation_count(4, 2),
Lockout::new_with_confirmation_count(5, 1),
];
run_test_check_and_filter_proposed_vote_state_older_than_history_root(
target_version,
earliest_slot_in_history,
current_vote_state_slots,
current_vote_state_root,
proposed_slots_and_lockouts,
proposed_root,
expected_root,
expected_vote_state,
);
let earliest_slot_in_history = 5;
let current_vote_state_slots: Vec<Slot> = vec![1, 2, 4];
let current_vote_state_root = Some(0);
let proposed_slots_and_lockouts = vec![(4, 2), (5, 1)];
let proposed_root = 3;
let expected_root = Some(2);
let expected_vote_state = vec![
Lockout::new_with_confirmation_count(4, 2),
Lockout::new_with_confirmation_count(5, 1),
];
run_test_check_and_filter_proposed_vote_state_older_than_history_root(
target_version,
earliest_slot_in_history,
current_vote_state_slots,
current_vote_state_root,
proposed_slots_and_lockouts,
proposed_root,
expected_root,
expected_vote_state,
);
let earliest_slot_in_history = 4;
let current_vote_state_slots: Vec<Slot> = vec![3, 4];
let current_vote_state_root = None;
let proposed_slots_and_lockouts = vec![(3, 3), (4, 2), (5, 1)];
let proposed_root = 2;
let expected_root = None;
let expected_vote_state = vec![
Lockout::new_with_confirmation_count(3, 3),
Lockout::new_with_confirmation_count(4, 2),
Lockout::new_with_confirmation_count(5, 1),
];
run_test_check_and_filter_proposed_vote_state_older_than_history_root(
target_version,
earliest_slot_in_history,
current_vote_state_slots,
current_vote_state_root,
proposed_slots_and_lockouts,
proposed_root,
expected_root,
expected_vote_state,
);
let earliest_slot_in_history = 4;
let current_vote_state_slots: Vec<Slot> = vec![];
let current_vote_state_root = None;
let proposed_slots_and_lockouts = vec![(5, 1)];
let proposed_root = 2;
let expected_root = None;
let expected_vote_state = vec![Lockout::new_with_confirmation_count(5, 1)];
run_test_check_and_filter_proposed_vote_state_older_than_history_root(
target_version,
earliest_slot_in_history,
current_vote_state_slots,
current_vote_state_root,
proposed_slots_and_lockouts,
proposed_root,
expected_root,
expected_vote_state,
);
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_slots_not_ordered(
target_version: VoteStateTargetVersion,
) {
let slot_hashes = build_slot_hashes(vec![1, 2, 3, 4]);
let vote_state = build_vote_state(target_version, vec![1], &slot_hashes);
let vote_slot = 3;
let vote_slot_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == vote_slot)
.unwrap()
.1;
let mut tower_sync = TowerSync::from(vec![(2, 2), (1, 3), (vote_slot, 1)]);
tower_sync.hash = vote_slot_hash;
assert_eq!(
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes
),
Err(VoteError::SlotsNotOrdered),
);
let mut tower_sync = TowerSync::from(vec![(2, 2), (2, 2), (vote_slot, 1)]);
tower_sync.hash = vote_slot_hash;
assert_eq!(
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes
),
Err(VoteError::SlotsNotOrdered),
);
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_older_than_history_slots_filtered(
target_version: VoteStateTargetVersion,
) {
let slot_hashes = build_slot_hashes(vec![1, 2, 3, 4]);
let mut vote_state = build_vote_state(target_version, vec![1, 2, 3, 4], &slot_hashes);
let earliest_slot_in_history = 11;
let slot_hashes = build_slot_hashes(vec![earliest_slot_in_history, 12, 13, 14]);
let vote_slot = 12;
let vote_slot_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == vote_slot)
.unwrap()
.1;
let missing_older_than_history_slot = earliest_slot_in_history - 1;
let mut tower_sync = TowerSync::from(vec![
(1, 4),
(missing_older_than_history_slot, 2),
(vote_slot, 3),
]);
tower_sync.hash = vote_slot_hash;
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes,
)
.unwrap();
assert_eq!(
tower_sync
.clone()
.lockouts
.into_iter()
.collect::<Vec<Lockout>>(),
vec![
Lockout::new_with_confirmation_count(1, 4),
Lockout::new_with_confirmation_count(vote_slot, 3)
]
);
assert!(do_process_tower_sync(&mut vote_state, &slot_hashes, 0, 0, tower_sync,).is_ok());
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_older_than_history_slots_not_filtered(
target_version: VoteStateTargetVersion,
) {
let slot_hashes = build_slot_hashes(vec![4]);
let mut vote_state = build_vote_state(target_version, vec![4], &slot_hashes);
let earliest_slot_in_history = 11;
let slot_hashes = build_slot_hashes(vec![earliest_slot_in_history, 12, 13, 14]);
let vote_slot = 12;
let vote_slot_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == vote_slot)
.unwrap()
.1;
let existing_older_than_history_slot = 4;
let mut tower_sync =
TowerSync::from(vec![(existing_older_than_history_slot, 3), (vote_slot, 2)]);
tower_sync.hash = vote_slot_hash;
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes,
)
.unwrap();
assert_eq!(tower_sync.lockouts.len(), 2);
assert_eq!(
tower_sync
.clone()
.lockouts
.into_iter()
.collect::<Vec<Lockout>>(),
vec![
Lockout::new_with_confirmation_count(existing_older_than_history_slot, 3),
Lockout::new_with_confirmation_count(vote_slot, 2)
]
);
assert!(do_process_tower_sync(&mut vote_state, &slot_hashes, 0, 0, tower_sync,).is_ok());
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_older_than_history_slots_filtered_and_not_filtered(
target_version: VoteStateTargetVersion,
) {
let slot_hashes = build_slot_hashes(vec![6]);
let mut vote_state = build_vote_state(target_version, vec![6], &slot_hashes);
let earliest_slot_in_history = 11;
let slot_hashes = build_slot_hashes(vec![earliest_slot_in_history, 12, 13, 14]);
let vote_slot = 14;
let vote_slot_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == vote_slot)
.unwrap()
.1;
let missing_older_than_history_slot = 4;
let existing_older_than_history_slot = 6;
let mut tower_sync = TowerSync::from(vec![
(missing_older_than_history_slot, 4),
(existing_older_than_history_slot, 3),
(12, 2),
(vote_slot, 1),
]);
tower_sync.hash = vote_slot_hash;
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes,
)
.unwrap();
assert_eq!(tower_sync.lockouts.len(), 3);
assert_eq!(
tower_sync
.clone()
.lockouts
.into_iter()
.collect::<Vec<Lockout>>(),
vec![
Lockout::new_with_confirmation_count(existing_older_than_history_slot, 3),
Lockout::new_with_confirmation_count(12, 2),
Lockout::new_with_confirmation_count(vote_slot, 1)
]
);
assert!(do_process_tower_sync(&mut vote_state, &slot_hashes, 0, 0, tower_sync,).is_ok());
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_slot_not_on_fork(
target_version: VoteStateTargetVersion,
) {
let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8]);
let vote_state = build_vote_state(target_version, vec![2, 4, 6], &slot_hashes);
let missing_vote_slot = 3;
let vote_slot = vote_state.votes().back().unwrap().slot() + 2;
let vote_slot_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == vote_slot)
.unwrap()
.1;
let mut tower_sync = TowerSync::from(vec![(missing_vote_slot, 2), (vote_slot, 3)]);
tower_sync.hash = vote_slot_hash;
assert_eq!(
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes
),
Err(VoteError::SlotsMismatch),
);
let missing_vote_slot = 7;
let mut tower_sync = TowerSync::from(vec![
(2, 5),
(4, 4),
(6, 3),
(missing_vote_slot, 2),
(vote_slot, 1),
]);
tower_sync.hash = vote_slot_hash;
assert_eq!(
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes
),
Err(VoteError::SlotsMismatch),
);
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_root_on_different_fork(
target_version: VoteStateTargetVersion,
) {
let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8]);
let vote_state = build_vote_state(target_version, vec![6], &slot_hashes);
let new_root = 3;
let vote_slot = 8;
assert_eq!(vote_slot, slot_hashes.first().unwrap().0);
let vote_slot_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == vote_slot)
.unwrap()
.1;
let mut tower_sync = TowerSync::from(vec![(vote_slot, 1)]);
tower_sync.hash = vote_slot_hash;
tower_sync.root = Some(new_root);
assert_eq!(
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes
),
Err(VoteError::RootOnDifferentFork),
);
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_slot_newer_than_slot_history(
target_version: VoteStateTargetVersion,
) {
let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8, 10]);
let vote_state = build_vote_state(target_version, vec![2, 4, 6], &slot_hashes);
let missing_vote_slot = slot_hashes.first().unwrap().0 + 1;
let vote_slot_hash = Hash::new_unique();
let mut tower_sync = TowerSync::from(vec![(8, 2), (missing_vote_slot, 3)]);
tower_sync.hash = vote_slot_hash;
assert_eq!(
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes
),
Err(VoteError::SlotsMismatch),
);
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_slot_all_slot_hashes_in_update_ok(
target_version: VoteStateTargetVersion,
) {
let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8]);
let mut vote_state = build_vote_state(target_version, vec![2, 4, 6], &slot_hashes);
let vote_slot = vote_state.votes().back().unwrap().slot() + 2;
let vote_slot_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == vote_slot)
.unwrap()
.1;
let mut tower_sync = TowerSync::from(vec![(2, 4), (4, 3), (6, 2), (vote_slot, 1)]);
tower_sync.hash = vote_slot_hash;
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes,
)
.unwrap();
assert_eq!(
tower_sync
.clone()
.lockouts
.into_iter()
.collect::<Vec<Lockout>>(),
vec![
Lockout::new_with_confirmation_count(2, 4),
Lockout::new_with_confirmation_count(4, 3),
Lockout::new_with_confirmation_count(6, 2),
Lockout::new_with_confirmation_count(vote_slot, 1)
]
);
assert!(do_process_tower_sync(&mut vote_state, &slot_hashes, 0, 0, tower_sync,).is_ok());
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_slot_some_slot_hashes_in_update_ok(
target_version: VoteStateTargetVersion,
) {
let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8, 10]);
let mut vote_state = build_vote_state(target_version, vec![6], &slot_hashes);
let vote_slot = vote_state.votes().back().unwrap().slot() + 2;
let vote_slot_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == vote_slot)
.unwrap()
.1;
let mut tower_sync = TowerSync::from(vec![(4, 2), (vote_slot, 1)]);
tower_sync.hash = vote_slot_hash;
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes,
)
.unwrap();
assert_eq!(
tower_sync
.clone()
.lockouts
.into_iter()
.collect::<Vec<Lockout>>(),
vec![
Lockout::new_with_confirmation_count(4, 2),
Lockout::new_with_confirmation_count(vote_slot, 1)
]
);
assert_eq!(
do_process_tower_sync(&mut vote_state, &slot_hashes, 0, 0, tower_sync,),
Err(VoteError::LockoutConflict)
);
}
#[test_case(VoteStateTargetVersion::V3 ; "VoteStateV3")]
#[test_case(VoteStateTargetVersion::V4 ; "VoteStateV4")]
fn test_check_and_filter_proposed_vote_state_slot_hash_mismatch(
target_version: VoteStateTargetVersion,
) {
let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8]);
let vote_state = build_vote_state(target_version, vec![2, 4, 6], &slot_hashes);
let vote_slot = vote_state.votes().back().unwrap().slot() + 2;
let vote_slot_hash = Hash::new_unique();
let mut tower_sync = TowerSync::from(vec![(2, 4), (4, 3), (6, 2), (vote_slot, 1)]);
tower_sync.hash = vote_slot_hash;
assert_eq!(
check_and_filter_proposed_vote_state(
&vote_state,
&mut tower_sync.lockouts,
&mut tower_sync.root,
tower_sync.hash,
&slot_hashes,
),
Err(VoteError::SlotHashMismatch),
);
}
#[test_case(0, true; "first slot")]
#[test_case(DEFAULT_SLOTS_PER_EPOCH / 2, true; "halfway through epoch")]
#[test_case((DEFAULT_SLOTS_PER_EPOCH / 2).saturating_add(1), false; "halfway through epoch plus one")]
#[test_case(DEFAULT_SLOTS_PER_EPOCH.saturating_sub(1), false; "last slot in epoch")]
#[test_case(DEFAULT_SLOTS_PER_EPOCH, true; "first slot in second epoch")]
fn test_epoch_half_check(slot: Slot, expected_allowed: bool) {
let epoch_schedule = EpochSchedule::without_warmup();
assert_eq!(
is_commission_update_allowed(slot, &epoch_schedule),
expected_allowed
);
}
#[test]
fn test_warmup_epoch_half_check_with_warmup() {
let epoch_schedule = EpochSchedule::default();
let first_normal_slot = epoch_schedule.first_normal_slot;
assert!(is_commission_update_allowed(0, &epoch_schedule));
assert!(is_commission_update_allowed(
first_normal_slot - 1,
&epoch_schedule
));
}
#[test_case(0, true; "first slot")]
#[test_case(DEFAULT_SLOTS_PER_EPOCH / 2, true; "halfway through epoch")]
#[test_case((DEFAULT_SLOTS_PER_EPOCH / 2).saturating_add(1), false; "halfway through epoch plus one")]
#[test_case(DEFAULT_SLOTS_PER_EPOCH.saturating_sub(1), false; "last slot in epoch")]
#[test_case(DEFAULT_SLOTS_PER_EPOCH, true; "first slot in second epoch")]
fn test_epoch_half_check_with_warmup(slot: Slot, expected_allowed: bool) {
let epoch_schedule = EpochSchedule::default();
let first_normal_slot = epoch_schedule.first_normal_slot;
assert_eq!(
is_commission_update_allowed(first_normal_slot.saturating_add(slot), &epoch_schedule),
expected_allowed
);
}
#[test]
fn test_create_v4_account_with_authorized() {
let node_pubkey = Pubkey::new_unique();
let authorized_voter = Pubkey::new_unique();
let authorized_withdrawer = Pubkey::new_unique();
let bls_pubkey_compressed = [42; 48];
let inflation_rewards_commission_bps = 10000;
let lamports = 100;
let vote_account = create_v4_account_with_authorized(
&node_pubkey,
&authorized_voter,
bls_pubkey_compressed,
&authorized_withdrawer,
inflation_rewards_commission_bps,
&authorized_withdrawer,
0,
&authorized_withdrawer,
lamports,
);
assert_eq!(vote_account.lamports(), lamports);
assert_eq!(vote_account.owner(), &id());
assert_eq!(vote_account.data().len(), VoteStateV4::size_of());
let vote_state_v4 = VoteStateV4::deserialize(vote_account.data(), &node_pubkey).unwrap();
assert_eq!(vote_state_v4.node_pubkey, node_pubkey);
assert_eq!(
vote_state_v4.authorized_voters,
AuthorizedVoters::new(0, authorized_voter)
);
assert_eq!(vote_state_v4.authorized_withdrawer, authorized_withdrawer);
assert_eq!(
vote_state_v4.bls_pubkey_compressed,
Some(bls_pubkey_compressed)
);
assert_eq!(
vote_state_v4.inflation_rewards_commission_bps,
inflation_rewards_commission_bps
);
}
#[test]
fn test_update_validator_identity_syncs_block_revenue_collector() {
let custom_commission_collector_enabled = false;
let vote_state =
vote_state_new_for_test(&solana_pubkey::new_rand(), VoteStateTargetVersion::V4);
let node_pubkey = *vote_state.node_pubkey();
let withdrawer_pubkey = *vote_state.authorized_withdrawer();
let serialized = vote_state.serialize();
let serialized_len = serialized.len();
let rent = Rent::default();
let lamports = rent.minimum_balance(serialized_len);
let mut vote_account = AccountSharedData::new(lamports, serialized_len, &id());
vote_account.set_data_from_slice(&serialized);
let processor_account = AccountSharedData::new(0, 0, &solana_sdk_ids::native_loader::id());
let mut transaction_context = TransactionContext::new(
vec![(id(), processor_account), (node_pubkey, vote_account)],
rent,
0,
0,
1,
);
transaction_context
.configure_top_level_instruction_for_tests(
0,
vec![InstructionAccount::new(1, false, true)],
vec![],
)
.unwrap();
let instruction_context = transaction_context.get_next_instruction_context().unwrap();
let mut borrowed_account = instruction_context
.try_borrow_instruction_account(0)
.unwrap();
let new_node_pubkey = solana_pubkey::new_rand();
let signers: HashSet<Pubkey> = vec![withdrawer_pubkey, new_node_pubkey]
.into_iter()
.collect();
update_validator_identity(
&mut borrowed_account,
VoteStateTargetVersion::V4,
&new_node_pubkey,
&signers,
custom_commission_collector_enabled,
)
.unwrap();
let vote_state =
VoteStateV4::deserialize(borrowed_account.get_data(), &new_node_pubkey).unwrap();
assert_eq!(vote_state.node_pubkey, new_node_pubkey);
assert_eq!(vote_state.block_revenue_collector, new_node_pubkey);
let new_node_pubkey = solana_pubkey::new_rand();
let signers: HashSet<Pubkey> = vec![withdrawer_pubkey, new_node_pubkey]
.into_iter()
.collect();
update_validator_identity(
&mut borrowed_account,
VoteStateTargetVersion::V4,
&new_node_pubkey,
&signers,
custom_commission_collector_enabled,
)
.unwrap();
let vote_state =
VoteStateV4::deserialize(borrowed_account.get_data(), &new_node_pubkey).unwrap();
assert_eq!(vote_state.node_pubkey, new_node_pubkey);
assert_eq!(vote_state.block_revenue_collector, new_node_pubkey);
}
#[test]
fn test_get_and_update_authorized_voter_v4_with_bls() {
let vote_account_pubkey = Pubkey::new_unique();
let (bls_pubkey, bls_proof_of_possession) =
create_bls_pubkey_and_proof_of_possession(&vote_account_pubkey);
let node_pubkey = Pubkey::new_unique();
let authorized_voter = Pubkey::new_unique();
let authorized_withdrawer = Pubkey::new_unique();
let inflation_rewards_commission_bps = 10000;
let rent = Rent::default();
let lamports = rent.minimum_balance(VoteStateV4::size_of());
let vote_account = create_v4_account_with_authorized(
&node_pubkey,
&authorized_voter,
[0u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE],
&authorized_withdrawer,
inflation_rewards_commission_bps,
&authorized_withdrawer,
0,
&authorized_withdrawer,
lamports,
);
assert_eq!(vote_account.lamports(), lamports);
assert_eq!(vote_account.owner(), &id());
assert_eq!(vote_account.data().len(), VoteStateV4::size_of());
let processor_account = AccountSharedData::new(0, 0, &solana_sdk_ids::native_loader::id());
let mut transaction_context = TransactionContext::new(
vec![
(id(), processor_account),
(vote_account_pubkey, vote_account),
],
rent,
0,
0,
1,
);
transaction_context
.configure_top_level_instruction_for_tests(
0,
vec![InstructionAccount::new(1, false, true)],
vec![],
)
.unwrap();
let instruction_context = transaction_context.get_next_instruction_context().unwrap();
let mut borrowed_account = instruction_context
.try_borrow_instruction_account(0)
.unwrap();
let new_node_pubkey = solana_pubkey::new_rand();
let signers: HashSet<Pubkey> = vec![authorized_withdrawer, new_node_pubkey]
.into_iter()
.collect();
let clock = Clock::default();
assert!(
authorize(
&mut borrowed_account,
VoteStateTargetVersion::V4,
&new_node_pubkey,
VoteAuthorize::VoterWithBLS(VoterWithBLSArgs {
bls_pubkey,
bls_proof_of_possession
}),
&signers,
&clock,
true,
|| Ok(()),
)
.is_ok()
);
let vote_state =
VoteStateV4::deserialize(borrowed_account.get_data(), &new_node_pubkey).unwrap();
assert_eq!(vote_state.bls_pubkey_compressed, Some(bls_pubkey));
assert!(vote_state.has_bls_pubkey());
let clock = Clock {
epoch: 3,
..Clock::default()
};
let (others_bls_pubkey, others_bls_proof_of_possession) =
create_bls_pubkey_and_proof_of_possession(&Pubkey::new_unique());
let new_node_pubkey = solana_pubkey::new_rand();
let signers: HashSet<Pubkey> = vec![authorized_withdrawer, new_node_pubkey]
.into_iter()
.collect();
assert_eq!(
authorize(
&mut borrowed_account,
VoteStateTargetVersion::V4,
&new_node_pubkey,
VoteAuthorize::VoterWithBLS(VoterWithBLSArgs {
bls_pubkey: others_bls_pubkey,
bls_proof_of_possession: others_bls_proof_of_possession
}),
&signers,
&clock,
true,
|| Ok(()),
),
Err(InstructionError::InvalidArgument),
);
let clock = Clock {
epoch: 5,
..Clock::default()
};
let (new_bls_pubkey, new_bls_proof_of_possession) =
create_bls_pubkey_and_proof_of_possession(&vote_account_pubkey);
let new_authorized_voter = solana_pubkey::new_rand();
let signers: HashSet<Pubkey> = vec![authorized_withdrawer, new_authorized_voter]
.into_iter()
.collect();
assert_eq!(
authorize(
&mut borrowed_account,
VoteStateTargetVersion::V4,
&new_authorized_voter,
VoteAuthorize::VoterWithBLS(VoterWithBLSArgs {
bls_pubkey: new_bls_pubkey,
bls_proof_of_possession: new_bls_proof_of_possession
}),
&signers,
&clock,
true,
|| Ok(()),
),
Ok(())
);
let vote_state =
VoteStateV4::deserialize(borrowed_account.get_data(), &new_authorized_voter).unwrap();
assert_eq!(vote_state.bls_pubkey_compressed, Some(new_bls_pubkey));
assert!(vote_state.has_bls_pubkey());
}
fn new_transaction_context(
accounts: Vec<(Pubkey, AccountSharedData)>,
instruction_accounts: Vec<InstructionAccount>,
rent: &Rent,
) -> TransactionContext<'_> {
let mut transaction_context = TransactionContext::new(accounts, rent.clone(), 0, 0, 1);
transaction_context
.configure_top_level_instruction_for_tests(0, instruction_accounts, vec![])
.unwrap();
transaction_context
}
#[test]
fn test_update_commission_collector() {
let target_version = VoteStateTargetVersion::V4;
let vote_pubkey = solana_pubkey::new_rand();
let vote_state = vote_state_new_for_test(&vote_pubkey, target_version);
let withdrawer_pubkey = *vote_state.authorized_withdrawer();
let node_pubkey = *vote_state.node_pubkey();
let signers: HashSet<Pubkey> = vec![withdrawer_pubkey].into_iter().collect();
let serialized = vote_state.serialize();
let serialized_len = serialized.len();
let rent = Rent::default();
let lamports = rent.minimum_balance(serialized_len);
let mut vote_account = AccountSharedData::new(lamports, serialized_len, &id());
vote_account.set_data_from_slice(&serialized);
let get_commission_collector =
|vote_account: &BorrowedInstructionAccount, kind: CommissionKind| {
let handler = get_vote_state_handler_checked(
vote_account,
PreserveBehaviorInHandlerHelper::new(target_version, true),
)
.unwrap();
let vote_state = handler.as_ref_v4();
match kind {
CommissionKind::InflationRewards => vote_state.inflation_rewards_collector,
CommissionKind::BlockRevenue => vote_state.block_revenue_collector,
}
};
let processor_account = AccountSharedData::new(0, 0, &solana_sdk_ids::native_loader::id());
let new_collector = solana_pubkey::new_rand();
let collector_lamports = rent.minimum_balance(0);
let collector_account =
AccountSharedData::new(collector_lamports, 0, &system_program::id());
let original_inflation_collector = vote_pubkey;
let original_block_revenue_collector = node_pubkey;
{
let transaction_context = new_transaction_context(
vec![
(id(), processor_account.clone()),
(vote_pubkey, vote_account.clone()),
(new_collector, collector_account.clone()),
],
vec![
InstructionAccount::new(1, false, true),
InstructionAccount::new(2, false, true),
],
&rent,
);
let instruction_context = transaction_context.get_next_instruction_context().unwrap();
let mut borrowed_vote_account = instruction_context
.try_borrow_instruction_account(0)
.unwrap();
update_commission_collector(
&mut borrowed_vote_account,
target_version,
NewCommissionCollector::NewAccount(
instruction_context
.try_borrow_instruction_account(1)
.unwrap(),
),
CommissionKind::InflationRewards,
&signers,
&rent,
)
.unwrap();
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::InflationRewards),
new_collector,
);
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::BlockRevenue),
original_block_revenue_collector, );
update_commission_collector(
&mut borrowed_vote_account,
target_version,
NewCommissionCollector::NewAccount(
instruction_context
.try_borrow_instruction_account(1)
.unwrap(),
),
CommissionKind::BlockRevenue,
&signers,
&rent,
)
.unwrap();
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::InflationRewards),
new_collector,
);
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::BlockRevenue),
new_collector,
);
}
{
let transaction_context = new_transaction_context(
vec![
(id(), processor_account.clone()),
(vote_pubkey, vote_account.clone()),
],
vec![
InstructionAccount::new(1, false, true),
InstructionAccount::new(1, false, true), ],
&rent,
);
let instruction_context = transaction_context.get_next_instruction_context().unwrap();
let mut borrowed_vote_account = instruction_context
.try_borrow_instruction_account(0)
.unwrap();
update_commission_collector(
&mut borrowed_vote_account,
target_version,
NewCommissionCollector::VoteAccount,
CommissionKind::InflationRewards,
&signers,
&rent,
)
.unwrap();
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::InflationRewards),
vote_pubkey,
);
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::BlockRevenue),
original_block_revenue_collector, );
update_commission_collector(
&mut borrowed_vote_account,
target_version,
NewCommissionCollector::VoteAccount,
CommissionKind::BlockRevenue,
&signers,
&rent,
)
.unwrap();
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::InflationRewards),
vote_pubkey,
);
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::BlockRevenue),
vote_pubkey,
);
}
{
let non_signers: HashSet<Pubkey> = HashSet::new();
let transaction_context = new_transaction_context(
vec![
(id(), processor_account.clone()),
(vote_pubkey, vote_account.clone()),
(new_collector, collector_account.clone()),
],
vec![
InstructionAccount::new(1, false, true),
InstructionAccount::new(2, false, true),
],
&rent,
);
let instruction_context = transaction_context.get_next_instruction_context().unwrap();
let mut borrowed_vote_account = instruction_context
.try_borrow_instruction_account(0)
.unwrap();
assert_eq!(
update_commission_collector(
&mut borrowed_vote_account,
target_version,
NewCommissionCollector::NewAccount(
instruction_context
.try_borrow_instruction_account(1)
.unwrap()
),
CommissionKind::InflationRewards,
&non_signers,
&rent,
),
Err(InstructionError::MissingRequiredSignature)
);
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::InflationRewards),
original_inflation_collector, );
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::BlockRevenue),
original_block_revenue_collector, );
}
{
let wrong_signers: HashSet<Pubkey> = vec![Pubkey::new_unique()].into_iter().collect();
let transaction_context = new_transaction_context(
vec![
(id(), processor_account.clone()),
(vote_pubkey, vote_account.clone()),
(new_collector, collector_account),
],
vec![
InstructionAccount::new(1, false, true),
InstructionAccount::new(2, false, true),
],
&rent,
);
let instruction_context = transaction_context.get_next_instruction_context().unwrap();
let mut borrowed_vote_account = instruction_context
.try_borrow_instruction_account(0)
.unwrap();
assert_eq!(
update_commission_collector(
&mut borrowed_vote_account,
target_version,
NewCommissionCollector::NewAccount(
instruction_context
.try_borrow_instruction_account(1)
.unwrap()
),
CommissionKind::InflationRewards,
&wrong_signers,
&rent,
),
Err(InstructionError::MissingRequiredSignature)
);
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::InflationRewards),
original_inflation_collector, );
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::BlockRevenue),
original_block_revenue_collector, );
}
{
let bad_collector = solana_pubkey::new_rand();
let non_system_owner = solana_pubkey::new_rand();
let bad_collector_account =
AccountSharedData::new(collector_lamports, 0, &non_system_owner);
let transaction_context = new_transaction_context(
vec![
(id(), processor_account.clone()),
(vote_pubkey, vote_account.clone()),
(bad_collector, bad_collector_account),
],
vec![
InstructionAccount::new(1, false, true),
InstructionAccount::new(2, false, true),
],
&rent,
);
let instruction_context = transaction_context.get_next_instruction_context().unwrap();
let mut borrowed_vote_account = instruction_context
.try_borrow_instruction_account(0)
.unwrap();
assert_eq!(
update_commission_collector(
&mut borrowed_vote_account,
target_version,
NewCommissionCollector::NewAccount(
instruction_context
.try_borrow_instruction_account(1)
.unwrap()
),
CommissionKind::InflationRewards,
&signers,
&rent,
),
Err(InstructionError::InvalidAccountOwner)
);
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::InflationRewards),
original_inflation_collector, );
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::BlockRevenue),
original_block_revenue_collector, );
}
{
let bad_collector = solana_pubkey::new_rand();
let bad_collector_account = AccountSharedData::new(0, 0, &system_program::id());
let transaction_context = new_transaction_context(
vec![
(id(), processor_account.clone()),
(vote_pubkey, vote_account.clone()),
(bad_collector, bad_collector_account),
],
vec![
InstructionAccount::new(1, false, true),
InstructionAccount::new(2, false, true),
],
&rent,
);
let instruction_context = transaction_context.get_next_instruction_context().unwrap();
let mut borrowed_vote_account = instruction_context
.try_borrow_instruction_account(0)
.unwrap();
assert_eq!(
update_commission_collector(
&mut borrowed_vote_account,
target_version,
NewCommissionCollector::NewAccount(
instruction_context
.try_borrow_instruction_account(1)
.unwrap()
),
CommissionKind::InflationRewards,
&signers,
&rent,
),
Err(InstructionError::InsufficientFunds)
);
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::InflationRewards),
original_inflation_collector, );
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::BlockRevenue),
original_block_revenue_collector, );
}
{
let bad_collector = solana_pubkey::new_rand();
let bad_collector_account =
AccountSharedData::new(collector_lamports, 0, &system_program::id());
let transaction_context = new_transaction_context(
vec![
(id(), processor_account),
(vote_pubkey, vote_account.clone()),
(bad_collector, bad_collector_account),
],
vec![
InstructionAccount::new(1, false, true),
InstructionAccount::new(2, false, false), ],
&rent,
);
let instruction_context = transaction_context.get_next_instruction_context().unwrap();
let mut borrowed_vote_account = instruction_context
.try_borrow_instruction_account(0)
.unwrap();
assert_eq!(
update_commission_collector(
&mut borrowed_vote_account,
target_version,
NewCommissionCollector::NewAccount(
instruction_context
.try_borrow_instruction_account(1)
.unwrap()
),
CommissionKind::InflationRewards,
&signers,
&rent,
),
Err(InstructionError::InvalidArgument)
);
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::InflationRewards),
original_inflation_collector, );
assert_eq!(
get_commission_collector(&borrowed_vote_account, CommissionKind::BlockRevenue),
original_block_revenue_collector, );
}
}
}