Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use custom VoteState parser everywhere #1738

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 10 additions & 20 deletions programs/vote/src/vote_state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,9 +854,7 @@ pub fn authorize<S: std::hash::BuildHasher>(
clock: &Clock,
feature_set: &FeatureSet,
) -> Result<(), InstructionError> {
let mut vote_state: VoteState = vote_account
.get_state::<VoteStateVersions>()?
.convert_to_current();
let mut vote_state = VoteState::deserialize(vote_account.get_data())?;

match vote_authorize {
VoteAuthorize::Voter => {
Expand Down Expand Up @@ -897,9 +895,7 @@ pub fn update_validator_identity<S: std::hash::BuildHasher>(
signers: &HashSet<Pubkey, S>,
feature_set: &FeatureSet,
) -> Result<(), InstructionError> {
let mut vote_state: VoteState = vote_account
.get_state::<VoteStateVersions>()?
.convert_to_current();
let mut vote_state = VoteState::deserialize(vote_account.get_data())?;

// current authorized withdrawer must say "yay"
verify_authorized_signer(&vote_state.authorized_withdrawer, signers)?;
Expand All @@ -926,8 +922,8 @@ pub fn update_commission<S: std::hash::BuildHasher>(

let enforce_commission_update_rule =
if feature_set.is_active(&feature_set::allow_commission_decrease_at_any_time::id()) {
if let Ok(decoded_vote_state) = vote_account.get_state::<VoteStateVersions>() {
vote_state = Some(decoded_vote_state.convert_to_current());
if let Ok(decoded_vote_state) = VoteState::deserialize(vote_account.get_data()) {
vote_state = Some(decoded_vote_state);
is_commission_increase(vote_state.as_ref().unwrap(), commission)
} else {
true
Expand All @@ -948,9 +944,7 @@ pub fn update_commission<S: std::hash::BuildHasher>(

let mut vote_state = match vote_state {
Some(vote_state) => vote_state,
None => vote_account
.get_state::<VoteStateVersions>()?
.convert_to_current(),
None => VoteState::deserialize(vote_account.get_data())?,
};

// current authorized withdrawer must say "yay"
Expand Down Expand Up @@ -1007,9 +1001,7 @@ pub fn withdraw<S: std::hash::BuildHasher>(
) -> Result<(), InstructionError> {
let mut vote_account = instruction_context
.try_borrow_instruction_account(transaction_context, vote_account_index)?;
let vote_state: VoteState = vote_account
.get_state::<VoteStateVersions>()?
.convert_to_current();
let vote_state = VoteState::deserialize(vote_account.get_data())?;

verify_authorized_signer(&vote_state.authorized_withdrawer, signers)?;

Expand Down Expand Up @@ -1071,9 +1063,9 @@ pub fn initialize_account<S: std::hash::BuildHasher>(
{
return Err(InstructionError::InvalidAccountData);
}
let versioned = vote_account.get_state::<VoteStateVersions>()?;

if !versioned.is_uninitialized() {
let vote_state = VoteState::deserialize(vote_account.get_data())?;
if !vote_state.is_uninitialized() {
return Err(InstructionError::AccountAlreadyInitialized);
}

Expand All @@ -1088,13 +1080,11 @@ fn verify_and_get_vote_state<S: std::hash::BuildHasher>(
clock: &Clock,
signers: &HashSet<Pubkey, S>,
) -> Result<VoteState, InstructionError> {
let versioned = vote_account.get_state::<VoteStateVersions>()?;

if versioned.is_uninitialized() {
let mut vote_state = VoteState::deserialize(vote_account.get_data())?;
if vote_state.is_uninitialized() {
return Err(InstructionError::UninitializedAccount);
}

let mut vote_state = versioned.convert_to_current();
let authorized_voter = vote_state.get_and_update_authorized_voter(clock.epoch)?;
verify_authorized_signer(&authorized_voter, signers)?;

Expand Down
5 changes: 5 additions & 0 deletions sdk/program/src/vote/authorized_voters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ impl AuthorizedVoters {
self.authorized_voters.is_empty()
}

// when an uninitialized V0_23_5 account is converted to current, it inserts a null voter
pub fn is_uninitialized(&self) -> bool {
self.is_empty() || (self.len() == 1 && self.first() == Some((&0, &Pubkey::default())))
}

pub fn first(&self) -> Option<(&u64, &Pubkey)> {
self.authorized_voters.iter().next()
}
Expand Down
92 changes: 51 additions & 41 deletions sdk/program/src/vote/state/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
//! Vote state

#[cfg(not(target_os = "solana"))]
use bincode::deserialize;
#[cfg(test)]
use {
crate::epoch_schedule::MAX_LEADER_SCHEDULE_EPOCH_OFFSET,
Expand All @@ -18,7 +16,7 @@ use {
sysvar::clock::Clock,
vote::{authorized_voters::AuthorizedVoters, error::VoteError},
},
bincode::{serialize_into, serialized_size, ErrorKind},
bincode::{serialize_into, ErrorKind},
serde_derive::{Deserialize, Serialize},
std::{collections::VecDeque, fmt::Debug, io::Cursor},
};
Expand Down Expand Up @@ -475,50 +473,50 @@ impl VoteState {
3762 // see test_vote_state_size_of.
}

// we retain bincode deserialize for not(target_os = "solana")
// because the hand-written parser does not support V0_23_5
/// Deserializes the input buffer into a newly allocated `VoteState`
///
/// This function is intended as a drop-in replacement for `bincode::deserialize()`.
/// V0_23_5 is not supported in a BPF context, but all versions are supported on non-BPF.
pub fn deserialize(input: &[u8]) -> Result<Self, InstructionError> {
#[cfg(not(target_os = "solana"))]
{
deserialize::<VoteStateVersions>(input)
.map(|versioned| versioned.convert_to_current())
.map_err(|_| InstructionError::InvalidAccountData)
}
#[cfg(target_os = "solana")]
{
let mut vote_state = Self::default();
Self::deserialize_into(input, &mut vote_state)?;
Ok(vote_state)
}
let mut vote_state = Self::default();
Self::deserialize_into(input, &mut vote_state)?;
Ok(vote_state)
}

/// Deserializes the input buffer into the provided `VoteState`
///
/// This function exists to deserialize `VoteState` in a BPF context without going above
/// the compute limit, and must be kept up to date with `bincode::deserialize`.
/// This function is exposed to allow deserialization in a BPF context directly into boxed memory.
pub fn deserialize_into(
input: &[u8],
vote_state: &mut VoteState,
) -> Result<(), InstructionError> {
let minimum_size =
serialized_size(vote_state).map_err(|_| InstructionError::InvalidAccountData)?;
if (input.len() as u64) < minimum_size {
return Err(InstructionError::InvalidAccountData);
}

let mut cursor = Cursor::new(input);

let variant = read_u32(&mut cursor)?;
match variant {
// V0_23_5. not supported; these should not exist on mainnet
0 => Err(InstructionError::InvalidAccountData),
// V0_23_5. not supported for bpf targets; these should not exist on mainnet
// supported for non-bpf targets for backwards compatibility
0 => {
#[cfg(not(target_os = "solana"))]
{
*vote_state = bincode::deserialize::<VoteStateVersions>(input)
.map(|versioned| versioned.convert_to_current())
.map_err(|_| InstructionError::InvalidAccountData)?;

Ok(())
}
#[cfg(target_os = "solana")]
Err(InstructionError::InvalidAccountData)
}
// V1_14_11. substantially different layout and data from V0_23_5
1 => deserialize_vote_state_into(&mut cursor, vote_state, false),
// Current. the only difference from V1_14_11 is the addition of a slot-latency to each vote
2 => deserialize_vote_state_into(&mut cursor, vote_state, true),
_ => Err(InstructionError::InvalidAccountData),
}?;

// if cursor overruns the input, it produces 0 values and continues to advance `position`
// this check ensures we do not accept such a malformed input erroneously
if cursor.position() > input.len() as u64 {
return Err(InstructionError::InvalidAccountData);
}
Expand Down Expand Up @@ -888,6 +886,10 @@ impl VoteState {
data.len() == VoteState::size_of()
&& data[VERSION_OFFSET..DEFAULT_PRIOR_VOTERS_END] != [0; DEFAULT_PRIOR_VOTERS_OFFSET]
}

pub fn is_uninitialized(&self) -> bool {
self.authorized_voters.is_uninitialized()
}
}

pub mod serde_compact_vote_state_update {
Expand Down Expand Up @@ -1087,7 +1089,7 @@ pub mod serde_tower_sync {

#[cfg(test)]
mod tests {
use {super::*, itertools::Itertools, rand::Rng};
use {super::*, bincode::serialized_size, itertools::Itertools, rand::Rng};

#[test]
fn test_vote_serialize() {
Expand All @@ -1107,14 +1109,13 @@ mod tests {
}

#[test]
fn test_vote_deserialize_into() {
fn test_vote_deserialize() {
// base case
let target_vote_state = VoteState::default();
let vote_state_buf =
bincode::serialize(&VoteStateVersions::new_current(target_vote_state.clone())).unwrap();

let mut test_vote_state = VoteState::default();
VoteState::deserialize_into(&vote_state_buf, &mut test_vote_state).unwrap();
let test_vote_state = VoteState::deserialize(&vote_state_buf).unwrap();

assert_eq!(target_vote_state, test_vote_state);

Expand All @@ -1130,31 +1131,40 @@ mod tests {
let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap();
let target_vote_state = target_vote_state_versions.convert_to_current();

let mut test_vote_state = VoteState::default();
VoteState::deserialize_into(&vote_state_buf, &mut test_vote_state).unwrap();
let test_vote_state = VoteState::deserialize(&vote_state_buf).unwrap();

assert_eq!(target_vote_state, test_vote_state);
}
}

#[test]
fn test_vote_deserialize_into_nopanic() {
fn test_vote_deserialize_nopanic() {
// base case
let mut test_vote_state = VoteState::default();
let e = VoteState::deserialize_into(&[], &mut test_vote_state).unwrap_err();
let e = VoteState::deserialize(&[]).unwrap_err();
assert_eq!(e, InstructionError::InvalidAccountData);

// variant
let serialized_len_x4 = serialized_size(&test_vote_state).unwrap() * 4;
let serialized_len_x4 = serialized_size(&VoteState::default()).unwrap() * 4;
let mut rng = rand::thread_rng();
for _ in 0..1000 {
let raw_data_length = rng.gen_range(1..serialized_len_x4);
let raw_data: Vec<u8> = (0..raw_data_length).map(|_| rng.gen::<u8>()).collect();
let mut raw_data: Vec<u8> = (0..raw_data_length).map(|_| rng.gen::<u8>()).collect();

// pure random data will ~never have a valid enum tag, so lets help it out
if raw_data_length >= 4 && rng.gen::<bool>() {
let tag = rng.gen::<u8>() % 3;
raw_data[0] = tag;
raw_data[1] = 0;
raw_data[2] = 0;
raw_data[3] = 0;
}

// it is extremely improbable, though theoretically possible, for random bytes to be syntactically valid
// so we only check that the deserialize function does not panic
let mut test_vote_state = VoteState::default();
let _ = VoteState::deserialize_into(&raw_data, &mut test_vote_state);
// so we only check that the parser does not panic and that it succeeds or fails exactly in line with bincode
let test_res = VoteState::deserialize(&raw_data);
let bincode_res = bincode::deserialize::<VoteStateVersions>(&raw_data);

assert_eq!(test_res.is_ok(), bincode_res.is_ok());
}
}

Expand Down
59 changes: 59 additions & 0 deletions sdk/program/src/vote/state/vote_state_0_23_5.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#![allow(clippy::arithmetic_side_effects)]
use super::*;
#[cfg(test)]
use arbitrary::{Arbitrary, Unstructured};

const MAX_ITEMS: usize = 32;

#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[cfg_attr(test, derive(Arbitrary))]
pub struct VoteState0_23_5 {
/// the node that votes in this account
pub node_pubkey: Pubkey,
Expand Down Expand Up @@ -59,3 +62,59 @@ impl<I> CircBuf<I> {
self.buf[self.idx] = item;
}
}

#[cfg(test)]
impl<'a, I: Default + Copy> Arbitrary<'a> for CircBuf<I>
where
I: Arbitrary<'a>,
{
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let mut circbuf = Self::default();

let len = u.arbitrary_len::<I>()?;
for _ in 0..len {
circbuf.append(I::arbitrary(u)?);
}

Ok(circbuf)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_vote_deserialize_0_23_5() {
// base case
let target_vote_state = VoteState0_23_5::default();
let target_vote_state_versions = VoteStateVersions::V0_23_5(Box::new(target_vote_state));
let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap();

let test_vote_state = VoteState::deserialize(&vote_state_buf).unwrap();

assert_eq!(
target_vote_state_versions.convert_to_current(),
test_vote_state
);

// variant
// provide 4x the minimum struct size in bytes to ensure we typically touch every field
let struct_bytes_x4 = std::mem::size_of::<VoteState0_23_5>() * 4;
for _ in 0..100 {
let raw_data: Vec<u8> = (0..struct_bytes_x4).map(|_| rand::random::<u8>()).collect();
let mut unstructured = Unstructured::new(&raw_data);

let arbitrary_vote_state = VoteState0_23_5::arbitrary(&mut unstructured).unwrap();
let target_vote_state_versions =
VoteStateVersions::V0_23_5(Box::new(arbitrary_vote_state));

let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap();
let target_vote_state = target_vote_state_versions.convert_to_current();

let test_vote_state = VoteState::deserialize(&vote_state_buf).unwrap();

assert_eq!(target_vote_state, test_vote_state);
}
}
}
4 changes: 4 additions & 0 deletions sdk/program/src/vote/state/vote_state_1_14_11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ impl VoteState1_14_11 {
data.len() == VoteState1_14_11::size_of()
&& data[VERSION_OFFSET..DEFAULT_PRIOR_VOTERS_END] != [0; DEFAULT_PRIOR_VOTERS_OFFSET]
}

pub fn is_uninitialized(&self) -> bool {
self.authorized_voters.is_uninitialized()
}
}

impl From<VoteState> for VoteState1_14_11 {
Expand Down
Loading