diff --git a/core/src/consensus.rs b/core/src/consensus.rs index 842e989b5b..774f272248 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -169,7 +169,7 @@ impl TowerVersions { } } -#[frozen_abi(digest = "GrkFcKqGEkJNUYoK1M8rorehi2yyLF4N3Gsj6j8f47Jn")] +#[frozen_abi(digest = "HQoLKAJEQTuVy8nMSkVWbrH3M5xKksxdMEZHGLWbnX6w")] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, AbiExample)] pub struct Tower { pub node_pubkey: Pubkey, diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index e086c04db3..b03b8114b9 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -2020,13 +2020,7 @@ impl ReplayStage { .is_active(&feature_set::compact_vote_state_updates::id()); let vote = match (should_compact, vote) { (true, VoteTransaction::VoteStateUpdate(vote_state_update)) => { - if let Some(compact_vote_state_update) = vote_state_update.compact() { - VoteTransaction::from(compact_vote_state_update) - } else { - // Compaction failed - warn!("Compaction failed when generating vote tx for vote account {}. Unable to vote", vote_account_pubkey); - return None; - } + VoteTransaction::CompactVoteStateUpdate(vote_state_update) } (_, vote) => vote, }; diff --git a/programs/vote/src/vote_processor.rs b/programs/vote/src/vote_processor.rs index a4dfcc11d5..a8b0f8e0a2 100644 --- a/programs/vote/src/vote_processor.rs +++ b/programs/vote/src/vote_processor.rs @@ -172,8 +172,8 @@ pub fn process_instruction( Err(InstructionError::InvalidInstructionData) } } - VoteInstruction::CompactUpdateVoteState(compact_vote_state_update) - | VoteInstruction::CompactUpdateVoteStateSwitch(compact_vote_state_update, _) => { + VoteInstruction::CompactUpdateVoteState(vote_state_update) + | VoteInstruction::CompactUpdateVoteStateSwitch(vote_state_update, _) => { if invoke_context .feature_set .is_active(&feature_set::allow_votes_to_directly_update_vote_state::id()) @@ -184,7 +184,6 @@ pub fn process_instruction( let sysvar_cache = invoke_context.get_sysvar_cache(); let slot_hashes = sysvar_cache.get_slot_hashes()?; let clock = sysvar_cache.get_clock()?; - let vote_state_update = compact_vote_state_update.uncompact()?; vote_state::process_vote_state_update( &mut me, slot_hashes.slot_hashes(), diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index f4da10fca3..8476a8664d 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -5,7 +5,7 @@ use { log::*, serde_derive::{Deserialize, Serialize}, solana_metrics::datapoint_debug, - solana_program::vote::{error::VoteError, program::id}, + solana_program::vote::{error::VoteError, program::id, state::serde_compact_vote_state_update}, solana_sdk::{ account::{AccountSharedData, ReadableAccount, WritableAccount}, clock::{Epoch, Slot, UnixTimestamp}, @@ -27,12 +27,13 @@ use { }, }; -#[frozen_abi(digest = "8Xa47j7LCp99Q7CQeTz4KPWU8sZgGFpAJw2K4VbPgGh8")] +#[frozen_abi(digest = "2AuJFjx7SYrJ2ugCfH1jFh3Lr9UHMEPfKwwk1NcjqND1")] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, AbiEnumVisitor, AbiExample)] pub enum VoteTransaction { Vote(Vote), VoteStateUpdate(VoteStateUpdate), - CompactVoteStateUpdate(CompactVoteStateUpdate), + #[serde(with = "serde_compact_vote_state_update")] + CompactVoteStateUpdate(VoteStateUpdate), } impl VoteTransaction { @@ -40,32 +41,26 @@ impl VoteTransaction { match self { VoteTransaction::Vote(vote) => vote.slots.clone(), VoteTransaction::VoteStateUpdate(vote_state_update) => vote_state_update.slots(), - VoteTransaction::CompactVoteStateUpdate(compact_state_update) => { - compact_state_update.slots() - } + VoteTransaction::CompactVoteStateUpdate(vote_state_update) => vote_state_update.slots(), } } pub fn slot(&self, i: usize) -> Slot { match self { VoteTransaction::Vote(vote) => vote.slots[i], - VoteTransaction::VoteStateUpdate(vote_state_update) => { + VoteTransaction::VoteStateUpdate(vote_state_update) + | VoteTransaction::CompactVoteStateUpdate(vote_state_update) => { vote_state_update.lockouts[i].slot } - VoteTransaction::CompactVoteStateUpdate(compact_state_update) => { - compact_state_update.slots()[i] - } } } pub fn len(&self) -> usize { match self { VoteTransaction::Vote(vote) => vote.slots.len(), - VoteTransaction::VoteStateUpdate(vote_state_update) => vote_state_update.lockouts.len(), - VoteTransaction::CompactVoteStateUpdate(compact_state_update) => { - 1 + compact_state_update.lockouts_32.len() - + compact_state_update.lockouts_16.len() - + compact_state_update.lockouts_8.len() + VoteTransaction::VoteStateUpdate(vote_state_update) + | VoteTransaction::CompactVoteStateUpdate(vote_state_update) => { + vote_state_update.lockouts.len() } } } @@ -73,10 +68,10 @@ impl VoteTransaction { pub fn is_empty(&self) -> bool { match self { VoteTransaction::Vote(vote) => vote.slots.is_empty(), - VoteTransaction::VoteStateUpdate(vote_state_update) => { + VoteTransaction::VoteStateUpdate(vote_state_update) + | VoteTransaction::CompactVoteStateUpdate(vote_state_update) => { vote_state_update.lockouts.is_empty() } - VoteTransaction::CompactVoteStateUpdate(_) => false, } } @@ -84,18 +79,16 @@ impl VoteTransaction { match self { VoteTransaction::Vote(vote) => vote.hash, VoteTransaction::VoteStateUpdate(vote_state_update) => vote_state_update.hash, - VoteTransaction::CompactVoteStateUpdate(compact_state_update) => { - compact_state_update.hash - } + VoteTransaction::CompactVoteStateUpdate(vote_state_update) => vote_state_update.hash, } } pub fn timestamp(&self) -> Option { match self { VoteTransaction::Vote(vote) => vote.timestamp, - VoteTransaction::VoteStateUpdate(vote_state_update) => vote_state_update.timestamp, - VoteTransaction::CompactVoteStateUpdate(compact_state_update) => { - compact_state_update.timestamp + VoteTransaction::VoteStateUpdate(vote_state_update) + | VoteTransaction::CompactVoteStateUpdate(vote_state_update) => { + vote_state_update.timestamp } } } @@ -103,9 +96,9 @@ impl VoteTransaction { pub fn set_timestamp(&mut self, ts: Option) { match self { VoteTransaction::Vote(vote) => vote.timestamp = ts, - VoteTransaction::VoteStateUpdate(vote_state_update) => vote_state_update.timestamp = ts, - VoteTransaction::CompactVoteStateUpdate(compact_state_update) => { - compact_state_update.timestamp = ts + VoteTransaction::VoteStateUpdate(vote_state_update) + | VoteTransaction::CompactVoteStateUpdate(vote_state_update) => { + vote_state_update.timestamp = ts } } } @@ -113,12 +106,10 @@ impl VoteTransaction { pub fn last_voted_slot(&self) -> Option { match self { VoteTransaction::Vote(vote) => vote.slots.last().copied(), - VoteTransaction::VoteStateUpdate(vote_state_update) => { + VoteTransaction::VoteStateUpdate(vote_state_update) + | VoteTransaction::CompactVoteStateUpdate(vote_state_update) => { Some(vote_state_update.lockouts.back()?.slot) } - VoteTransaction::CompactVoteStateUpdate(compact_state_update) => { - compact_state_update.slots().last().copied() - } } } @@ -139,12 +130,6 @@ impl From for VoteTransaction { } } -impl From for VoteTransaction { - fn from(compact_state_update: CompactVoteStateUpdate) -> Self { - VoteTransaction::CompactVoteStateUpdate(compact_state_update) - } -} - // utility function, used by Stakes, tests pub fn from(account: &T) -> Option { VoteState::deserialize(account.data()).ok() @@ -2970,103 +2955,4 @@ mod tests { Err(VoteError::SlotHashMismatch), ); } - - #[test] - fn test_compact_vote_state_update_parity() { - let mut vote_state_update = VoteStateUpdate::from(vec![(2, 4), (4, 3), (6, 2), (7, 1)]); - vote_state_update.hash = Hash::new_unique(); - vote_state_update.root = Some(1); - - let compact_vote_state_update = vote_state_update.clone().compact().unwrap(); - - assert_eq!(vote_state_update.slots(), compact_vote_state_update.slots()); - assert_eq!(vote_state_update.hash, compact_vote_state_update.hash); - assert_eq!(vote_state_update.root, compact_vote_state_update.root()); - - let vote_state_update_new = compact_vote_state_update.uncompact().unwrap(); - assert_eq!(vote_state_update, vote_state_update_new); - } - - #[test] - fn test_compact_vote_state_update_large_offsets() { - let vote_state_update = VoteStateUpdate::from(vec![ - (0, 31), - (1, 30), - (2, 29), - (3, 28), - (u64::pow(2, 28), 17), - (u64::pow(2, 28) + u64::pow(2, 16), 1), - ]); - let compact_vote_state_update = vote_state_update.clone().compact().unwrap(); - - assert_eq!(vote_state_update.slots(), compact_vote_state_update.slots()); - - let vote_state_update_new = compact_vote_state_update.uncompact().unwrap(); - assert_eq!(vote_state_update, vote_state_update_new); - } - - #[test] - fn test_compact_vote_state_update_border_conditions() { - let two_31 = u64::pow(2, 31); - let two_15 = u64::pow(2, 15); - let vote_state_update = VoteStateUpdate::from(vec![ - (0, 31), - (two_31, 16), - (two_31 + 1, 15), - (two_31 + two_15, 7), - (two_31 + two_15 + 1, 6), - (two_31 + two_15 + 1 + 64, 1), - ]); - let compact_vote_state_update = vote_state_update.clone().compact().unwrap(); - - assert_eq!(vote_state_update.slots(), compact_vote_state_update.slots()); - - let vote_state_update_new = compact_vote_state_update.uncompact().unwrap(); - assert_eq!(vote_state_update, vote_state_update_new); - } - - #[test] - fn test_compact_vote_state_update_large_root() { - let two_58 = u64::pow(2, 58); - let two_31 = u64::pow(2, 31); - let mut vote_state_update = VoteStateUpdate::from(vec![(two_58, 31), (two_58 + two_31, 1)]); - vote_state_update.root = Some(two_31); - let compact_vote_state_update = vote_state_update.clone().compact().unwrap(); - - assert_eq!(vote_state_update.slots(), compact_vote_state_update.slots()); - - let vote_state_update_new = compact_vote_state_update.uncompact().unwrap(); - assert_eq!(vote_state_update, vote_state_update_new); - } - - #[test] - fn test_compact_vote_state_update_overflow() { - let compact_vote_state_update = CompactVoteStateUpdate { - root: u64::MAX - 1, - root_to_first_vote_offset: 10, - lockouts_32: vec![], - lockouts_16: vec![], - lockouts_8: vec![CompactLockout::new(10)], - hash: Hash::new_unique(), - timestamp: None, - }; - assert_eq!( - Err(InstructionError::ArithmeticOverflow), - compact_vote_state_update.uncompact() - ); - - let compact_vote_state_update = CompactVoteStateUpdate { - root: u64::MAX - u32::MAX as u64, - root_to_first_vote_offset: 10, - lockouts_32: vec![CompactLockout::new(u32::MAX)], - lockouts_16: vec![], - lockouts_8: vec![CompactLockout::new(10)], - hash: Hash::new_unique(), - timestamp: None, - }; - assert_eq!( - Err(InstructionError::ArithmeticOverflow), - compact_vote_state_update.uncompact() - ); - } } diff --git a/runtime/src/vote_parser.rs b/runtime/src/vote_parser.rs index 660328db0a..9505345edb 100644 --- a/runtime/src/vote_parser.rs +++ b/runtime/src/vote_parser.rs @@ -82,17 +82,11 @@ fn parse_vote_instruction_data( VoteInstruction::UpdateVoteStateSwitch(vote_state_update, hash) => { Some((VoteTransaction::from(vote_state_update), Some(hash))) } - VoteInstruction::CompactUpdateVoteState(compact_vote_state_update) => { - compact_vote_state_update - .uncompact() - .ok() - .map(|vote_state_update| (VoteTransaction::from(vote_state_update), None)) + VoteInstruction::CompactUpdateVoteState(vote_state_update) => { + Some((VoteTransaction::from(vote_state_update), None)) } - VoteInstruction::CompactUpdateVoteStateSwitch(compact_vote_state_update, hash) => { - compact_vote_state_update - .uncompact() - .ok() - .map(|vote_state_update| (VoteTransaction::from(vote_state_update), Some(hash))) + VoteInstruction::CompactUpdateVoteStateSwitch(vote_state_update, hash) => { + Some((VoteTransaction::from(vote_state_update), Some(hash))) } VoteInstruction::Authorize(_, _) | VoteInstruction::AuthorizeChecked(_) diff --git a/sdk/program/src/vote/instruction.rs b/sdk/program/src/vote/instruction.rs index 1ea13738f6..8cd6b1d6ea 100644 --- a/sdk/program/src/vote/instruction.rs +++ b/sdk/program/src/vote/instruction.rs @@ -9,8 +9,9 @@ use { vote::{ program::id, state::{ - CompactVoteStateUpdate, Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, - VoteAuthorizeWithSeedArgs, VoteInit, VoteState, VoteStateUpdate, + serde_compact_vote_state_update, Vote, VoteAuthorize, + VoteAuthorizeCheckedWithSeedArgs, VoteAuthorizeWithSeedArgs, VoteInit, VoteState, + VoteStateUpdate, }, }, }, @@ -132,14 +133,18 @@ pub enum VoteInstruction { /// # Account references /// 0. `[Write]` Vote account to vote with /// 1. `[SIGNER]` Vote authority - CompactUpdateVoteState(CompactVoteStateUpdate), + #[serde(with = "serde_compact_vote_state_update")] + CompactUpdateVoteState(VoteStateUpdate), /// Update the onchain vote state for the signer along with a switching proof. /// /// # Account references /// 0. `[Write]` Vote account to vote with /// 1. `[SIGNER]` Vote authority - CompactUpdateVoteStateSwitch(CompactVoteStateUpdate, Hash), + CompactUpdateVoteStateSwitch( + #[serde(with = "serde_compact_vote_state_update")] VoteStateUpdate, + Hash, + ), } fn initialize_account(vote_pubkey: &Pubkey, vote_init: &VoteInit) -> Instruction { @@ -387,7 +392,7 @@ pub fn update_vote_state_switch( pub fn compact_update_vote_state( vote_pubkey: &Pubkey, authorized_voter_pubkey: &Pubkey, - compact_vote_state_update: CompactVoteStateUpdate, + vote_state_update: VoteStateUpdate, ) -> Instruction { let account_metas = vec![ AccountMeta::new(*vote_pubkey, false), @@ -396,7 +401,7 @@ pub fn compact_update_vote_state( Instruction::new_with_bincode( id(), - &VoteInstruction::CompactUpdateVoteState(compact_vote_state_update), + &VoteInstruction::CompactUpdateVoteState(vote_state_update), account_metas, ) } @@ -404,7 +409,7 @@ pub fn compact_update_vote_state( pub fn compact_update_vote_state_switch( vote_pubkey: &Pubkey, authorized_voter_pubkey: &Pubkey, - vote_state_update: CompactVoteStateUpdate, + vote_state_update: VoteStateUpdate, proof_hash: Hash, ) -> Instruction { let account_metas = vec![ diff --git a/sdk/program/src/vote/state/mod.rs b/sdk/program/src/vote/state/mod.rs index dc4d25edd1..2bd9481933 100644 --- a/sdk/program/src/vote/state/mod.rs +++ b/sdk/program/src/vote/state/mod.rs @@ -9,7 +9,6 @@ use { instruction::InstructionError, pubkey::Pubkey, rent::Rent, - short_vec, sysvar::clock::Clock, vote::{authorized_voters::AuthorizedVoters, error::VoteError}, }, @@ -85,28 +84,6 @@ impl Lockout { } } -#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Copy, Clone, AbiExample)] -pub struct CompactLockout { - // Offset to the next vote, 0 if this is the last vote in the tower - pub offset: T, - // Confirmation count, guarenteed to be < 32 - pub confirmation_count: u8, -} - -impl CompactLockout { - pub fn new(offset: T) -> Self { - Self { - offset, - confirmation_count: 1, - } - } - - // The number of slots for which this vote is locked - pub fn lockout(&self) -> u64 { - (INITIAL_LOCKOUT as u64).pow(self.confirmation_count.into()) - } -} - #[frozen_abi(digest = "GwJfVFsATSj7nvKwtUkHYzqPRaPY6SLxPGXApuCya3x5")] #[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] pub struct VoteStateUpdate { @@ -151,205 +128,6 @@ impl VoteStateUpdate { pub fn slots(&self) -> Vec { self.lockouts.iter().map(|lockout| lockout.slot).collect() } - - pub fn compact(self) -> Option { - CompactVoteStateUpdate::new(self.lockouts, self.root, self.hash, self.timestamp) - } -} - -/// Ignoring overhead, in a full `VoteStateUpdate` the lockouts take up -/// 31 * (64 + 32) = 2976 bits. -/// -/// In this schema we separate the votes into 3 separate lockout structures -/// and store offsets rather than slot number, allowing us to use smaller fields. -/// -/// In a full `CompactVoteStateUpdate` the lockouts take up -/// 64 + (32 + 8) * 16 + (16 + 8) * 8 + (8 + 8) * 6 = 992 bits -/// allowing us to greatly reduce block size. -#[frozen_abi(digest = "EeMnyxPUyd3hK7UQ8BcWDW8qrsdXA9F6ZUoAWAh1nDxX")] -#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] -pub struct CompactVoteStateUpdate { - /// The proposed root, u64::MAX if there is no root - pub root: Slot, - /// The offset from the root (or 0 if no root) to the first vote - pub root_to_first_vote_offset: u64, - /// Part of the proposed tower, votes with confirmation_count > 15 - #[serde(with = "short_vec")] - pub lockouts_32: Vec>, - /// Part of the proposed tower, votes with 15 >= confirmation_count > 7 - #[serde(with = "short_vec")] - pub lockouts_16: Vec>, - /// Part of the proposed tower, votes with 7 >= confirmation_count - #[serde(with = "short_vec")] - pub lockouts_8: Vec>, - - /// Signature of the bank's state at the last slot - pub hash: Hash, - /// Processing timestamp of last slot - pub timestamp: Option, -} - -impl From> for CompactVoteStateUpdate { - fn from(recent_slots: Vec<(Slot, u32)>) -> Self { - let lockouts: VecDeque = recent_slots - .into_iter() - .map(|(slot, confirmation_count)| Lockout { - slot, - confirmation_count, - }) - .collect(); - Self::new(lockouts, None, Hash::default(), None).unwrap() - } -} - -impl CompactVoteStateUpdate { - pub fn new( - mut lockouts: VecDeque, - root: Option, - hash: Hash, - timestamp: Option, - ) -> Option { - if lockouts.is_empty() { - return Some(Self::default()); - } - let mut cur_slot = root.unwrap_or(0u64); - let mut cur_confirmation_count = 0; - let offset = lockouts - .pop_front() - .map( - |Lockout { - slot, - confirmation_count, - }| { - assert!(confirmation_count < 32); - - slot.checked_sub(cur_slot).map(|offset| { - cur_slot = slot; - cur_confirmation_count = confirmation_count; - offset - }) - }, - ) - .expect("Tower should not be empty")?; - let mut lockouts_32 = Vec::new(); - let mut lockouts_16 = Vec::new(); - let mut lockouts_8 = Vec::new(); - - for Lockout { - slot, - confirmation_count, - } in lockouts - { - assert!(confirmation_count < 32); - let offset = slot.checked_sub(cur_slot)?; - if cur_confirmation_count > 15 { - lockouts_32.push(CompactLockout { - offset: offset.try_into().unwrap(), - confirmation_count: cur_confirmation_count.try_into().unwrap(), - }); - } else if cur_confirmation_count > 7 { - lockouts_16.push(CompactLockout { - offset: offset.try_into().unwrap(), - confirmation_count: cur_confirmation_count.try_into().unwrap(), - }); - } else { - lockouts_8.push(CompactLockout { - offset: offset.try_into().unwrap(), - confirmation_count: cur_confirmation_count.try_into().unwrap(), - }) - } - - cur_slot = slot; - cur_confirmation_count = confirmation_count; - } - // Last vote should be at the top of tower, so we don't have to explicitly store it - assert!(cur_confirmation_count == 1); - Some(Self { - root: root.unwrap_or(u64::MAX), - root_to_first_vote_offset: offset, - lockouts_32, - lockouts_16, - lockouts_8, - hash, - timestamp, - }) - } - - pub fn root(&self) -> Option { - if self.root == u64::MAX { - None - } else { - Some(self.root) - } - } - - pub fn slots(&self) -> Vec { - std::iter::once(self.root_to_first_vote_offset) - .chain(self.lockouts_32.iter().map(|lockout| lockout.offset.into())) - .chain(self.lockouts_16.iter().map(|lockout| lockout.offset.into())) - .chain(self.lockouts_8.iter().map(|lockout| lockout.offset.into())) - .scan(self.root().unwrap_or(0), |prev_slot, offset| { - prev_slot.checked_add(offset).map(|slot| { - *prev_slot = slot; - slot - }) - }) - .collect() - } - - pub fn uncompact(self) -> Result { - let first_slot = self - .root() - .unwrap_or(0) - .checked_add(self.root_to_first_vote_offset) - .ok_or(InstructionError::ArithmeticOverflow)?; - let mut arithmetic_overflow_occured = false; - let lockouts = self - .lockouts_32 - .iter() - .map(|lockout| (lockout.offset.into(), lockout.confirmation_count)) - .chain( - self.lockouts_16 - .iter() - .map(|lockout| (lockout.offset.into(), lockout.confirmation_count)), - ) - .chain( - self.lockouts_8 - .iter() - .map(|lockout| (lockout.offset.into(), lockout.confirmation_count)), - ) - .chain( - // To pick up the last element - std::iter::once((0, 1)), - ) - .scan( - first_slot, - |slot, (offset, confirmation_count): (u64, u8)| { - let cur_slot = *slot; - if let Some(new_slot) = slot.checked_add(offset) { - *slot = new_slot; - Some(Lockout { - slot: cur_slot, - confirmation_count: confirmation_count.into(), - }) - } else { - arithmetic_overflow_occured = true; - None - } - }, - ) - .collect(); - if arithmetic_overflow_occured { - Err(InstructionError::ArithmeticOverflow) - } else { - Ok(VoteStateUpdate { - lockouts, - root: self.root(), - hash: self.hash, - timestamp: self.timestamp, - }) - } - } } #[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] @@ -789,9 +567,109 @@ impl VoteState { } } +pub mod serde_compact_vote_state_update { + use { + super::*, + crate::{ + clock::{Slot, UnixTimestamp}, + serde_varint, short_vec, + vote::state::Lockout, + }, + serde::{Deserialize, Deserializer, Serialize, Serializer}, + }; + + #[derive(Deserialize, Serialize, AbiExample)] + struct LockoutOffset { + #[serde(with = "serde_varint")] + offset: Slot, + confirmation_count: u8, + } + + #[derive(Deserialize, Serialize)] + struct CompactVoteStateUpdate { + root: Slot, + #[serde(with = "short_vec")] + lockout_offsets: Vec, + hash: Hash, + timestamp: Option, + } + + pub fn serialize( + vote_state_update: &VoteStateUpdate, + serializer: S, + ) -> Result + where + S: Serializer, + { + let lockout_offsets = vote_state_update.lockouts.iter().scan( + vote_state_update.root.unwrap_or_default(), + |slot, lockout| { + let offset = match lockout.slot.checked_sub(*slot) { + None => return Some(Err(serde::ser::Error::custom("Invalid vote lockout"))), + Some(offset) => offset, + }; + let confirmation_count = match u8::try_from(lockout.confirmation_count) { + Ok(confirmation_count) => confirmation_count, + Err(_) => { + return Some(Err(serde::ser::Error::custom("Invalid confirmation count"))) + } + }; + let lockout_offset = LockoutOffset { + offset, + confirmation_count, + }; + *slot = lockout.slot; + Some(Ok(lockout_offset)) + }, + ); + let compact_vote_state_update = CompactVoteStateUpdate { + root: vote_state_update.root.unwrap_or(Slot::MAX), + lockout_offsets: lockout_offsets.collect::>()?, + hash: vote_state_update.hash, + timestamp: vote_state_update.timestamp, + }; + compact_vote_state_update.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let CompactVoteStateUpdate { + root, + lockout_offsets, + hash, + timestamp, + } = CompactVoteStateUpdate::deserialize(deserializer)?; + let root = (root != Slot::MAX).then_some(root); + let lockouts = + lockout_offsets + .iter() + .scan(root.unwrap_or_default(), |slot, lockout_offset| { + *slot = match slot.checked_add(lockout_offset.offset) { + None => { + return Some(Err(serde::de::Error::custom("Invalid lockout offset"))) + } + Some(slot) => slot, + }; + let lockout = Lockout { + slot: *slot, + confirmation_count: u32::from(lockout_offset.confirmation_count), + }; + Some(Ok(lockout)) + }); + Ok(VoteStateUpdate { + root, + lockouts: lockouts.collect::>()?, + hash, + timestamp, + }) + } +} + #[cfg(test)] mod tests { - use super::*; + use {super::*, itertools::Itertools, rand::Rng}; #[test] fn test_vote_serialize() { @@ -1235,4 +1113,50 @@ mod tests { // golden, may need updating when vote_state grows assert!(minimum_balance as f64 / 10f64.powf(9.0) < 0.04) } + + #[test] + fn test_serde_compact_vote_state_update() { + let mut rng = rand::thread_rng(); + for _ in 0..5000 { + run_serde_compact_vote_state_update(&mut rng); + } + } + + #[allow(clippy::integer_arithmetic)] + fn run_serde_compact_vote_state_update(rng: &mut R) { + let lockouts: VecDeque<_> = std::iter::repeat_with(|| Lockout { + slot: 149_303_885 + rng.gen_range(0, 10_000), + confirmation_count: rng.gen_range(0, 33), + }) + .take(32) + .sorted_by_key(|lockout| lockout.slot) + .collect(); + let root = rng + .gen_ratio(1, 2) + .then(|| lockouts[0].slot - rng.gen_range(0, 1_000)); + let timestamp = rng.gen_ratio(1, 2).then(|| rng.gen()); + let hash = Hash::from(rng.gen::<[u8; 32]>()); + let vote_state_update = VoteStateUpdate { + lockouts, + root, + hash, + timestamp, + }; + #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)] + enum VoteInstruction { + #[serde(with = "serde_compact_vote_state_update")] + UpdateVoteState(VoteStateUpdate), + UpdateVoteStateSwitch( + #[serde(with = "serde_compact_vote_state_update")] VoteStateUpdate, + Hash, + ), + } + let vote = VoteInstruction::UpdateVoteState(vote_state_update.clone()); + let bytes = bincode::serialize(&vote).unwrap(); + assert_eq!(vote, bincode::deserialize(&bytes).unwrap()); + let hash = Hash::from(rng.gen::<[u8; 32]>()); + let vote = VoteInstruction::UpdateVoteStateSwitch(vote_state_update, hash); + let bytes = bincode::serialize(&vote).unwrap(); + assert_eq!(vote, bincode::deserialize(&bytes).unwrap()); + } } diff --git a/transaction-status/src/parse_vote.rs b/transaction-status/src/parse_vote.rs index 96f03863c7..ec5079d7e4 100644 --- a/transaction-status/src/parse_vote.rs +++ b/transaction-status/src/parse_vote.rs @@ -135,10 +135,7 @@ pub fn parse_vote( }), }) } - VoteInstruction::CompactUpdateVoteState(compact_vote_state_update) => { - let vote_state_update = compact_vote_state_update.uncompact().map_err(|_| { - ParseInstructionError::InstructionNotParsable(ParsableProgram::Vote) - })?; + VoteInstruction::CompactUpdateVoteState(vote_state_update) => { check_num_vote_accounts(&instruction.accounts, 2)?; let vote_state_update = json!({ "lockouts": vote_state_update.lockouts, @@ -155,10 +152,7 @@ pub fn parse_vote( }), }) } - VoteInstruction::CompactUpdateVoteStateSwitch(compact_vote_state_update, hash) => { - let vote_state_update = compact_vote_state_update.uncompact().map_err(|_| { - ParseInstructionError::InstructionNotParsable(ParsableProgram::Vote) - })?; + VoteInstruction::CompactUpdateVoteStateSwitch(vote_state_update, hash) => { check_num_vote_accounts(&instruction.accounts, 2)?; let vote_state_update = json!({ "lockouts": vote_state_update.lockouts, @@ -770,7 +764,7 @@ mod test { #[test] fn test_parse_compact_vote_state_update_ix() { let vote_state_update = VoteStateUpdate::from(vec![(0, 3), (1, 2), (2, 1)]); - let compact_vote_state_update = vote_state_update.clone().compact().unwrap(); + let compact_vote_state_update = vote_state_update.clone(); let vote_pubkey = Pubkey::new_unique(); let authorized_voter_pubkey = Pubkey::new_unique(); @@ -813,7 +807,7 @@ mod test { #[test] fn test_parse_compact_vote_state_update_switch_ix() { let vote_state_update = VoteStateUpdate::from(vec![(0, 3), (1, 2), (2, 1)]); - let compact_vote_state_update = vote_state_update.clone().compact().unwrap(); + let compact_vote_state_update = vote_state_update.clone(); let vote_pubkey = Pubkey::new_unique(); let authorized_voter_pubkey = Pubkey::new_unique();