diff --git a/Cargo.lock b/Cargo.lock index 19b874a11..5f44c970b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4151,12 +4151,14 @@ version = "2.2.5" dependencies = [ "arbitrary", "bincode", + "cfg_eval", "itertools 0.12.1", "num-derive", "num-traits", "rand 0.8.5", "serde", "serde_derive", + "serde_with", "solana-clock", "solana-epoch-schedule", "solana-frozen-abi", diff --git a/serialize-utils/src/cursor.rs b/serialize-utils/src/cursor.rs index e2a49576e..de8f31240 100644 --- a/serialize-utils/src/cursor.rs +++ b/serialize-utils/src/cursor.rs @@ -16,6 +16,15 @@ pub fn read_u8>(cursor: &mut Cursor) -> Result>(cursor: &mut Cursor) -> Result { + let mut buf = [0; 2]; + cursor + .read_exact(&mut buf) + .map_err(|_| InstructionError::InvalidAccountData)?; + + Ok(u16::from_le_bytes(buf)) +} + pub fn read_u32>(cursor: &mut Cursor) -> Result { let mut buf = [0; 4]; cursor diff --git a/vote-interface/Cargo.toml b/vote-interface/Cargo.toml index e6e76f324..1acb616f0 100644 --- a/vote-interface/Cargo.toml +++ b/vote-interface/Cargo.toml @@ -35,8 +35,10 @@ frozen-abi = [ "solana-short-vec/frozen-abi", ] serde = [ + "dep:cfg_eval", "dep:serde", "dep:serde_derive", + "dep:serde_with", "dep:solana-serde-varint", "dep:solana-short-vec", "solana-hash/serde", @@ -46,10 +48,12 @@ serde = [ [dependencies] arbitrary = { workspace = true, features = ["derive"], optional = true } bincode = { workspace = true, optional = true } +cfg_eval = { workspace = true, optional = true } num-derive = { workspace = true } num-traits = { workspace = true } serde = { workspace = true, optional = true } serde_derive = { workspace = true, optional = true } +serde_with = { workspace = true, features = ["macros"], optional = true } solana-clock = { workspace = true } solana-frozen-abi = { workspace = true, features = [ "frozen-abi", diff --git a/vote-interface/src/instruction.rs b/vote-interface/src/instruction.rs index fa6524528..3015711de 100644 --- a/vote-interface/src/instruction.rs +++ b/vote-interface/src/instruction.rs @@ -4,7 +4,7 @@ use { super::state::TowerSync, crate::state::{ Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, VoteAuthorizeWithSeedArgs, VoteInit, - VoteStateUpdate, VoteStateVersions, + VoteStateUpdate, VoteStateV4, }, solana_clock::{Slot, UnixTimestamp}, solana_hash::Hash, @@ -272,7 +272,7 @@ pub struct CreateVoteAccountConfig<'a> { impl Default for CreateVoteAccountConfig<'_> { fn default() -> Self { Self { - space: VoteStateVersions::vote_state_size_of(false) as u64, + space: VoteStateV4::size_of() as u64, with_seed: None, } } diff --git a/vote-interface/src/state/mod.rs b/vote-interface/src/state/mod.rs index fe6d0ccc9..d21b568aa 100644 --- a/vote-interface/src/state/mod.rs +++ b/vote-interface/src/state/mod.rs @@ -23,11 +23,18 @@ pub mod vote_state_versions; pub use vote_state_versions::*; pub mod vote_state_v3; pub use vote_state_v3::VoteStateV3; +pub mod vote_state_v4; +pub use vote_state_v4::VoteStateV4; mod vote_instruction_data; pub use vote_instruction_data::*; // The struct's name has changed. #[deprecated(since = "2.2.6", note = "Use vote_state_v3::VoteStateV3 instead")] pub use vote_state_v3::VoteStateV3 as VoteState; +#[cfg(any(target_os = "solana", feature = "bincode"))] +pub(crate) mod vote_state_deserialize; + +/// Number of bytes in a compressed BLS public key. +pub const BLS_PUBKEY_COMPRESSED_BYTES: usize = 48; // Maximum number of votes to keep around, tightly coupled with epoch_schedule::MINIMUM_SLOTS_PER_EPOCH pub const MAX_LOCKOUT_HISTORY: usize = 31; @@ -392,34 +399,58 @@ pub mod serde_tower_sync { #[cfg(test)] mod tests { use { - super::*, crate::error::VoteError, bincode::serialized_size, core::mem::MaybeUninit, - itertools::Itertools, rand::Rng, solana_clock::Clock, solana_hash::Hash, - solana_instruction_error::InstructionError, + super::*, + crate::{error::VoteError, state::vote_state_0_23_5::VoteState0_23_5}, + bincode::serialized_size, + core::mem::MaybeUninit, + itertools::Itertools, + rand::Rng, + solana_clock::Clock, + solana_hash::Hash, + solana_instruction::error::InstructionError, }; #[test] - fn test_vote_serialize() { + fn test_vote_serialize_v3() { let mut buffer: Vec = vec![0; VoteStateV3::size_of()]; let mut vote_state = VoteStateV3::default(); vote_state .votes .resize(MAX_LOCKOUT_HISTORY, LandedVote::default()); vote_state.root_slot = Some(1); - let versioned = VoteStateVersions::new_current(vote_state); + let versioned = VoteStateVersions::new_v3(vote_state); assert!(VoteStateV3::serialize(&versioned, &mut buffer[0..4]).is_err()); VoteStateV3::serialize(&versioned, &mut buffer).unwrap(); assert_eq!( VoteStateV3::deserialize(&buffer).unwrap(), - versioned.convert_to_current() + versioned.try_convert_to_v3().unwrap() ); } #[test] - fn test_vote_deserialize_into() { + fn test_vote_serialize_v4() { + let vote_pubkey = Pubkey::new_unique(); + let mut buffer: Vec = vec![0; VoteStateV4::size_of()]; + let mut vote_state = VoteStateV4::default(); + vote_state + .votes + .resize(MAX_LOCKOUT_HISTORY, LandedVote::default()); + vote_state.root_slot = Some(1); + let versioned = VoteStateVersions::new_v4(vote_state); + assert!(VoteStateV4::serialize(&versioned, &mut buffer[0..4]).is_err()); + VoteStateV4::serialize(&versioned, &mut buffer).unwrap(); + assert_eq!( + VoteStateV4::deserialize(&buffer, &vote_pubkey).unwrap(), + versioned.try_convert_to_v4(&vote_pubkey).unwrap() + ); + } + + #[test] + fn test_vote_deserialize_into_v3() { // base case let target_vote_state = VoteStateV3::default(); let vote_state_buf = - bincode::serialize(&VoteStateVersions::new_current(target_vote_state.clone())).unwrap(); + bincode::serialize(&VoteStateVersions::new_v3(target_vote_state.clone())).unwrap(); let mut test_vote_state = VoteStateV3::default(); VoteStateV3::deserialize_into(&vote_state_buf, &mut test_vote_state).unwrap(); @@ -436,20 +467,58 @@ mod tests { let target_vote_state_versions = VoteStateVersions::arbitrary(&mut unstructured).unwrap(); 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 = VoteStateV3::default(); - VoteStateV3::deserialize_into(&vote_state_buf, &mut test_vote_state).unwrap(); + // Skip any v4 since they can't convert to v3. + if let Ok(target_vote_state) = target_vote_state_versions.try_convert_to_v3() { + let mut test_vote_state = VoteStateV3::default(); + VoteStateV3::deserialize_into(&vote_state_buf, &mut test_vote_state).unwrap(); + + assert_eq!(target_vote_state, test_vote_state); + } + } + } + + #[test] + fn test_vote_deserialize_into_v4() { + let vote_pubkey = Pubkey::new_unique(); + + // base case + let target_vote_state = VoteStateV4::default(); + let vote_state_buf = + bincode::serialize(&VoteStateVersions::new_v4(target_vote_state.clone())).unwrap(); + + let mut test_vote_state = VoteStateV4::default(); + VoteStateV4::deserialize_into(&vote_state_buf, &mut test_vote_state, &vote_pubkey).unwrap(); + + assert_eq!(target_vote_state, 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::() * 4; + for _ in 0..1000 { + let raw_data: Vec = (0..struct_bytes_x4).map(|_| rand::random::()).collect(); + let mut unstructured = Unstructured::new(&raw_data); + + let target_vote_state_versions = + VoteStateVersions::arbitrary(&mut unstructured).unwrap(); + let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap(); + let target_vote_state = target_vote_state_versions + .try_convert_to_v4(&vote_pubkey) + .unwrap(); + + let mut test_vote_state = VoteStateV4::default(); + VoteStateV4::deserialize_into(&vote_state_buf, &mut test_vote_state, &vote_pubkey) + .unwrap(); assert_eq!(target_vote_state, test_vote_state); } } #[test] - fn test_vote_deserialize_into_error() { + fn test_vote_deserialize_into_error_v3() { let target_vote_state = VoteStateV3::new_rand_for_tests(Pubkey::new_unique(), 42); let mut vote_state_buf = - bincode::serialize(&VoteStateVersions::new_current(target_vote_state.clone())).unwrap(); + bincode::serialize(&VoteStateVersions::new_v3(target_vote_state.clone())).unwrap(); let len = vote_state_buf.len(); vote_state_buf.truncate(len - 1); @@ -459,11 +528,27 @@ mod tests { } #[test] - fn test_vote_deserialize_into_uninit() { + fn test_vote_deserialize_into_error_v4() { + let vote_pubkey = Pubkey::new_unique(); + + let target_vote_state = VoteStateV4::new_rand_for_tests(Pubkey::new_unique(), 42); + let mut vote_state_buf = + bincode::serialize(&VoteStateVersions::new_v4(target_vote_state.clone())).unwrap(); + let len = vote_state_buf.len(); + vote_state_buf.truncate(len - 1); + + let mut test_vote_state = VoteStateV4::default(); + VoteStateV4::deserialize_into(&vote_state_buf, &mut test_vote_state, &vote_pubkey) + .unwrap_err(); + assert_eq!(test_vote_state, VoteStateV4::default()); + } + + #[test] + fn test_vote_deserialize_into_uninit_v3() { // base case let target_vote_state = VoteStateV3::default(); let vote_state_buf = - bincode::serialize(&VoteStateVersions::new_current(target_vote_state.clone())).unwrap(); + bincode::serialize(&VoteStateVersions::new_v3(target_vote_state.clone())).unwrap(); let mut test_vote_state = MaybeUninit::uninit(); VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state).unwrap(); @@ -481,10 +566,56 @@ mod tests { let target_vote_state_versions = VoteStateVersions::arbitrary(&mut unstructured).unwrap(); let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap(); - let target_vote_state = target_vote_state_versions.convert_to_current(); + + // Skip any v4 since they can't convert to v3. + if let Ok(target_vote_state) = target_vote_state_versions.try_convert_to_v3() { + let mut test_vote_state = MaybeUninit::uninit(); + VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state) + .unwrap(); + let test_vote_state = unsafe { test_vote_state.assume_init() }; + + assert_eq!(target_vote_state, test_vote_state); + } + } + } + + #[test] + fn test_vote_deserialize_into_uninit_v4() { + let vote_pubkey = Pubkey::new_unique(); + + // base case + let target_vote_state = VoteStateV4::default(); + let vote_state_buf = + bincode::serialize(&VoteStateVersions::new_v4(target_vote_state.clone())).unwrap(); + + let mut test_vote_state = MaybeUninit::uninit(); + VoteStateV4::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state, &vote_pubkey) + .unwrap(); + let test_vote_state = unsafe { test_vote_state.assume_init() }; + + assert_eq!(target_vote_state, 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::() * 4; + for _ in 0..1000 { + let raw_data: Vec = (0..struct_bytes_x4).map(|_| rand::random::()).collect(); + let mut unstructured = Unstructured::new(&raw_data); + + let target_vote_state_versions = + VoteStateVersions::arbitrary(&mut unstructured).unwrap(); + let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap(); + let target_vote_state = target_vote_state_versions + .try_convert_to_v4(&Pubkey::default()) + .unwrap(); let mut test_vote_state = MaybeUninit::uninit(); - VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state).unwrap(); + VoteStateV4::deserialize_into_uninit( + &vote_state_buf, + &mut test_vote_state, + &Pubkey::default(), + ) + .unwrap(); let test_vote_state = unsafe { test_vote_state.assume_init() }; assert_eq!(target_vote_state, test_vote_state); @@ -492,7 +623,7 @@ mod tests { } #[test] - fn test_vote_deserialize_into_uninit_nopanic() { + fn test_vote_deserialize_into_uninit_nopanic_v3() { // base case let mut test_vote_state = MaybeUninit::uninit(); let e = VoteStateV3::deserialize_into_uninit(&[], &mut test_vote_state).unwrap_err(); @@ -507,7 +638,7 @@ mod tests { // pure random data will ~never have a valid enum tag, so lets help it out if raw_data_length >= 4 && rng.gen::() { - let tag = rng.gen::() % 3; + let tag = rng.gen::() % 4; raw_data[0] = tag; raw_data[1] = 0; raw_data[2] = 0; @@ -518,8 +649,54 @@ mod tests { // so we only check that the parser does not panic and that it succeeds or fails exactly in line with bincode let mut test_vote_state = MaybeUninit::uninit(); let test_res = VoteStateV3::deserialize_into_uninit(&raw_data, &mut test_vote_state); + + // Test with bincode for consistency. + let bincode_res = bincode::deserialize::(&raw_data) + .map_err(|_| InstructionError::InvalidAccountData) + .and_then(|versioned| versioned.try_convert_to_v3()); + + if test_res.is_err() { + assert!(bincode_res.is_err()); + } else { + let test_vote_state = unsafe { test_vote_state.assume_init() }; + assert_eq!(test_vote_state, bincode_res.unwrap()); + } + } + } + + #[test] + fn test_vote_deserialize_into_uninit_nopanic_v4() { + let vote_pubkey = Pubkey::new_unique(); + + // base case + let mut test_vote_state = MaybeUninit::uninit(); + let e = VoteStateV4::deserialize_into_uninit(&[], &mut test_vote_state, &vote_pubkey) + .unwrap_err(); + assert_eq!(e, InstructionError::InvalidAccountData); + + // variant + let serialized_len_x4 = serialized_size(&VoteStateV4::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 mut raw_data: Vec = (0..raw_data_length).map(|_| rng.gen::()).collect(); + + // pure random data will ~never have a valid enum tag, so lets help it out + if raw_data_length >= 4 && rng.gen::() { + let tag = rng.gen::() % 4; + 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 parser does not panic and that it succeeds or fails exactly in line with bincode + let mut test_vote_state = MaybeUninit::uninit(); + let test_res = + VoteStateV4::deserialize_into_uninit(&raw_data, &mut test_vote_state, &vote_pubkey); let bincode_res = bincode::deserialize::(&raw_data) - .map(|versioned| versioned.convert_to_current()); + .map(|versioned| versioned.try_convert_to_v4(&vote_pubkey).unwrap()); if test_res.is_err() { assert!(bincode_res.is_err()); @@ -531,7 +708,7 @@ mod tests { } #[test] - fn test_vote_deserialize_into_uninit_ill_sized() { + fn test_vote_deserialize_into_uninit_ill_sized_v3() { // provide 4x the minimum struct size in bytes to ensure we typically touch every field let struct_bytes_x4 = std::mem::size_of::() * 4; for _ in 0..1000 { @@ -542,6 +719,56 @@ mod tests { VoteStateVersions::arbitrary(&mut unstructured).unwrap(); let original_buf = bincode::serialize(&original_vote_state_versions).unwrap(); + // Skip any v4 since they can't convert to v3. + if !matches!(original_vote_state_versions, VoteStateVersions::V4(_)) { + let mut truncated_buf = original_buf.clone(); + let mut expanded_buf = original_buf.clone(); + + truncated_buf.resize(original_buf.len() - 8, 0); + expanded_buf.resize(original_buf.len() + 8, 0); + + // truncated fails + let mut test_vote_state = MaybeUninit::uninit(); + let test_res = + VoteStateV3::deserialize_into_uninit(&truncated_buf, &mut test_vote_state); + // `deserialize_into_uninit` will eventually call into + // `try_convert_to_v3`, so we have alignment in the following map. + let bincode_res = bincode::deserialize::(&truncated_buf) + .map_err(|_| InstructionError::InvalidAccountData) + .and_then(|versioned| versioned.try_convert_to_v3()); + + assert!(test_res.is_err()); + assert!(bincode_res.is_err()); + + // expanded succeeds + let mut test_vote_state = MaybeUninit::uninit(); + VoteStateV3::deserialize_into_uninit(&expanded_buf, &mut test_vote_state).unwrap(); + // `deserialize_into_uninit` will eventually call into + // `try_convert_to_v3`, so we have alignment in the following map. + let bincode_res = bincode::deserialize::(&expanded_buf) + .map_err(|_| InstructionError::InvalidAccountData) + .and_then(|versioned| versioned.try_convert_to_v3()); + + let test_vote_state = unsafe { test_vote_state.assume_init() }; + assert_eq!(test_vote_state, bincode_res.unwrap()); + } + } + } + + #[test] + fn test_vote_deserialize_into_uninit_ill_sized_v4() { + let vote_pubkey = Pubkey::new_unique(); + + // provide 4x the minimum struct size in bytes to ensure we typically touch every field + let struct_bytes_x4 = std::mem::size_of::() * 4; + for _ in 0..1000 { + let raw_data: Vec = (0..struct_bytes_x4).map(|_| rand::random::()).collect(); + let mut unstructured = Unstructured::new(&raw_data); + + let original_vote_state_versions = + VoteStateVersions::arbitrary(&mut unstructured).unwrap(); + let original_buf = bincode::serialize(&original_vote_state_versions).unwrap(); + let mut truncated_buf = original_buf.clone(); let mut expanded_buf = original_buf.clone(); @@ -550,19 +777,23 @@ mod tests { // truncated fails let mut test_vote_state = MaybeUninit::uninit(); - let test_res = - VoteStateV3::deserialize_into_uninit(&truncated_buf, &mut test_vote_state); + let test_res = VoteStateV4::deserialize_into_uninit( + &truncated_buf, + &mut test_vote_state, + &vote_pubkey, + ); let bincode_res = bincode::deserialize::(&truncated_buf) - .map(|versioned| versioned.convert_to_current()); + .map(|versioned| versioned.try_convert_to_v4(&vote_pubkey).unwrap()); assert!(test_res.is_err()); assert!(bincode_res.is_err()); // expanded succeeds let mut test_vote_state = MaybeUninit::uninit(); - VoteStateV3::deserialize_into_uninit(&expanded_buf, &mut test_vote_state).unwrap(); + VoteStateV4::deserialize_into_uninit(&expanded_buf, &mut test_vote_state, &vote_pubkey) + .unwrap(); let bincode_res = bincode::deserialize::(&expanded_buf) - .map(|versioned| versioned.convert_to_current()); + .map(|versioned| versioned.try_convert_to_v4(&vote_pubkey).unwrap()); let test_vote_state = unsafe { test_vote_state.assume_init() }; assert_eq!(test_vote_state, bincode_res.unwrap()); @@ -907,9 +1138,14 @@ mod tests { #[test] fn test_vote_state_size_of() { let vote_state = VoteStateV3::get_max_sized_vote_state(); - let vote_state = VoteStateVersions::new_current(vote_state); + let vote_state = VoteStateVersions::new_v3(vote_state); let size = serialized_size(&vote_state).unwrap(); assert_eq!(VoteStateV3::size_of() as u64, size); + + let vote_state = VoteStateV4::get_max_sized_vote_state(); + let vote_state = VoteStateVersions::new_v4(vote_state); + let size = serialized_size(&vote_state).unwrap(); + assert!(size < VoteStateV4::size_of() as u64); // v4 is smaller than the max size } #[test] @@ -931,9 +1167,9 @@ mod tests { ) }); - let versioned = VoteStateVersions::new_current(vote_state.take().unwrap()); + let versioned = VoteStateVersions::new_v3(vote_state.take().unwrap()); VoteStateV3::serialize(&versioned, &mut max_sized_data).unwrap(); - vote_state = Some(versioned.convert_to_current()); + vote_state = Some(versioned.try_convert_to_v3().unwrap()); } } @@ -942,19 +1178,19 @@ mod tests { // The default `VoteStateV3` is stored to de-initialize a zero-balance vote account, // so must remain such that `VoteStateVersions::is_uninitialized()` returns true // when called on a `VoteStateVersions` that stores it - assert!(VoteStateVersions::new_current(VoteStateV3::default()).is_uninitialized()); + assert!(VoteStateVersions::new_v3(VoteStateV3::default()).is_uninitialized()); } #[test] fn test_is_correct_size_and_initialized() { // Check all zeroes - let mut vote_account_data = vec![0; VoteStateVersions::vote_state_size_of(true)]; + let mut vote_account_data = vec![0; VoteStateV3::size_of()]; assert!(!VoteStateVersions::is_correct_size_and_initialized( &vote_account_data )); // Check default VoteStateV3 - let default_account_state = VoteStateVersions::new_current(VoteStateV3::default()); + let default_account_state = VoteStateVersions::new_v3(VoteStateV3::default()); VoteStateV3::serialize(&default_account_state, &mut vote_account_data).unwrap(); assert!(!VoteStateVersions::is_correct_size_and_initialized( &vote_account_data @@ -967,8 +1203,8 @@ mod tests { )); // Check non-zero large account - let mut large_vote_data = vec![1; 2 * VoteStateVersions::vote_state_size_of(true)]; - let default_account_state = VoteStateVersions::new_current(VoteStateV3::default()); + let mut large_vote_data = vec![1; 2 * VoteStateV3::size_of()]; + let default_account_state = VoteStateVersions::new_v3(VoteStateV3::default()); VoteStateV3::serialize(&default_account_state, &mut large_vote_data).unwrap(); assert!(!VoteStateVersions::is_correct_size_and_initialized( &vote_account_data @@ -984,7 +1220,7 @@ mod tests { }, &Clock::default(), ); - let account_state = VoteStateVersions::new_current(vote_state.clone()); + let account_state = VoteStateVersions::new_v3(vote_state.clone()); VoteStateV3::serialize(&account_state, &mut vote_account_data).unwrap(); assert!(VoteStateVersions::is_correct_size_and_initialized( &vote_account_data @@ -993,7 +1229,7 @@ mod tests { // Check old VoteStateV3 that hasn't been upgraded to newest version yet let old_vote_state = VoteState1_14_11::from(vote_state); let account_state = VoteStateVersions::V1_14_11(Box::new(old_vote_state)); - let mut vote_account_data = vec![0; VoteStateVersions::vote_state_size_of(false)]; + let mut vote_account_data = vec![0; VoteState1_14_11::size_of()]; VoteStateV3::serialize(&account_state, &mut vote_account_data).unwrap(); assert!(VoteStateVersions::is_correct_size_and_initialized( &vote_account_data @@ -1064,4 +1300,60 @@ mod tests { let circ_buf: CircBuf<()> = bincode::deserialize(data).unwrap(); assert_eq!(circ_buf.last(), None); } + + #[test] + fn test_vote_state_v4_bls_pubkey_compressed() { + let vote_pubkey = Pubkey::new_unique(); + + let run_test = |start, expected| { + let versioned = VoteStateVersions::new_v4(start); + let serialized = bincode::serialize(&versioned).unwrap(); + let deserialized = VoteStateV4::deserialize(&serialized, &vote_pubkey).unwrap(); + assert_eq!(deserialized.bls_pubkey_compressed, expected); + }; + + // First try `None`. + let vote_state_none = VoteStateV4::default(); + assert_eq!(vote_state_none.bls_pubkey_compressed, None); + run_test(vote_state_none, None); + + // Now try `Some`. + let test_bls_key = [42u8; BLS_PUBKEY_COMPRESSED_BYTES]; + let vote_state_some = VoteStateV4 { + bls_pubkey_compressed: Some(test_bls_key), + ..VoteStateV4::default() + }; + assert_eq!(vote_state_some.bls_pubkey_compressed, Some(test_bls_key)); + run_test(vote_state_some, Some(test_bls_key)); + } + + #[test] + fn test_vote_state_version_conversion_bls_pubkey() { + let vote_pubkey = Pubkey::new_unique(); + + // All versions before v4 should result in `None` for BLS pubkey. + let v0_23_5_state = VoteState0_23_5::default(); + let v0_23_5_versioned = VoteStateVersions::V0_23_5(Box::new(v0_23_5_state)); + + let v1_14_11_state = VoteState1_14_11::default(); + let v1_14_11_versioned = VoteStateVersions::V1_14_11(Box::new(v1_14_11_state)); + + let v3_state = VoteStateV3::default(); + let v3_versioned = VoteStateVersions::V3(Box::new(v3_state)); + + for versioned in [v0_23_5_versioned, v1_14_11_versioned, v3_versioned] { + let converted = versioned.try_convert_to_v4(&vote_pubkey).unwrap(); + assert_eq!(converted.bls_pubkey_compressed, None); + } + + // v4 to v4 conversion should preserve the BLS pubkey. + let test_bls_key = [128u8; BLS_PUBKEY_COMPRESSED_BYTES]; + let v4_state = VoteStateV4 { + bls_pubkey_compressed: Some(test_bls_key), + ..VoteStateV4::default() + }; + let v4_versioned = VoteStateVersions::V4(Box::new(v4_state)); + let converted = v4_versioned.try_convert_to_v4(&vote_pubkey).unwrap(); + assert_eq!(converted.bls_pubkey_compressed, Some(test_bls_key)); + } } diff --git a/vote-interface/src/state/vote_state_0_23_5.rs b/vote-interface/src/state/vote_state_0_23_5.rs index 20fefc7f9..a0a3e33d7 100644 --- a/vote-interface/src/state/vote_state_0_23_5.rs +++ b/vote-interface/src/state/vote_state_0_23_5.rs @@ -77,12 +77,34 @@ mod tests { 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 mut test_vote_state = MaybeUninit::uninit(); - VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state).unwrap(); - let test_vote_state = unsafe { test_vote_state.assume_init() }; + // v3 + let mut test_vote_state_v3 = MaybeUninit::uninit(); + VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state_v3).unwrap(); + let test_vote_state = unsafe { test_vote_state_v3.assume_init() }; assert_eq!( - target_vote_state_versions.convert_to_current(), + target_vote_state_versions + .clone() + .try_convert_to_v3() + .unwrap(), + test_vote_state + ); + + // v4 + let vote_pubkey = Pubkey::new_unique(); + let mut test_vote_state_v4 = MaybeUninit::uninit(); + VoteStateV4::deserialize_into_uninit( + &vote_state_buf, + &mut test_vote_state_v4, + &vote_pubkey, + ) + .unwrap(); + let test_vote_state = unsafe { test_vote_state_v4.assume_init() }; + + assert_eq!( + target_vote_state_versions + .try_convert_to_v4(&vote_pubkey) + .unwrap(), test_vote_state ); @@ -97,14 +119,35 @@ mod tests { let target_vote_state_versions = VoteStateVersions::V0_23_5(Box::new(arbitrary_vote_state)); + // v3 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 = MaybeUninit::uninit(); - VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state).unwrap(); - let test_vote_state = unsafe { test_vote_state.assume_init() }; - - assert_eq!(target_vote_state, test_vote_state); + let target_vote_state_v3 = target_vote_state_versions + .clone() + .try_convert_to_v3() + .unwrap(); + + let mut test_vote_state_v3 = MaybeUninit::uninit(); + VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state_v3).unwrap(); + let test_vote_state = unsafe { test_vote_state_v3.assume_init() }; + + assert_eq!(target_vote_state_v3, test_vote_state); + + // v4 + let vote_pubkey = Pubkey::new_unique(); + let target_vote_state_v4 = target_vote_state_versions + .try_convert_to_v4(&vote_pubkey) + .unwrap(); + + let mut test_vote_state_v4 = MaybeUninit::uninit(); + VoteStateV4::deserialize_into_uninit( + &vote_state_buf, + &mut test_vote_state_v4, + &vote_pubkey, + ) + .unwrap(); + let test_vote_state = unsafe { test_vote_state_v4.assume_init() }; + + assert_eq!(target_vote_state_v4, test_vote_state); } } } diff --git a/vote-interface/src/state/vote_state_1_14_11.rs b/vote-interface/src/state/vote_state_1_14_11.rs index 250ba8bd0..1cd72ce9b 100644 --- a/vote-interface/src/state/vote_state_1_14_11.rs +++ b/vote-interface/src/state/vote_state_1_14_11.rs @@ -95,12 +95,34 @@ mod tests { let target_vote_state_versions = VoteStateVersions::V1_14_11(Box::new(target_vote_state)); let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap(); - let mut test_vote_state = MaybeUninit::uninit(); - VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state).unwrap(); - let test_vote_state = unsafe { test_vote_state.assume_init() }; + // v3 + let mut test_vote_state_v3 = MaybeUninit::uninit(); + VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state_v3).unwrap(); + let test_vote_state = unsafe { test_vote_state_v3.assume_init() }; assert_eq!( - target_vote_state_versions.convert_to_current(), + target_vote_state_versions + .clone() + .try_convert_to_v3() + .unwrap(), + test_vote_state + ); + + // v4 + let vote_pubkey = Pubkey::new_unique(); + let mut test_vote_state_v4 = MaybeUninit::uninit(); + VoteStateV4::deserialize_into_uninit( + &vote_state_buf, + &mut test_vote_state_v4, + &vote_pubkey, + ) + .unwrap(); + let test_vote_state = unsafe { test_vote_state_v4.assume_init() }; + + assert_eq!( + target_vote_state_versions + .try_convert_to_v4(&vote_pubkey) + .unwrap(), test_vote_state ); @@ -115,14 +137,35 @@ mod tests { let target_vote_state_versions = VoteStateVersions::V1_14_11(Box::new(arbitrary_vote_state)); + // v3 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 = MaybeUninit::uninit(); - VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state).unwrap(); - let test_vote_state = unsafe { test_vote_state.assume_init() }; - - assert_eq!(target_vote_state, test_vote_state); + let target_vote_state_v3 = target_vote_state_versions + .clone() + .try_convert_to_v3() + .unwrap(); + + let mut test_vote_state_v3 = MaybeUninit::uninit(); + VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state_v3).unwrap(); + let test_vote_state = unsafe { test_vote_state_v3.assume_init() }; + + assert_eq!(target_vote_state_v3, test_vote_state); + + // v4 + let vote_pubkey = Pubkey::new_unique(); + let target_vote_state_v4 = target_vote_state_versions + .try_convert_to_v4(&vote_pubkey) + .unwrap(); + + let mut test_vote_state_v4 = MaybeUninit::uninit(); + VoteStateV4::deserialize_into_uninit( + &vote_state_buf, + &mut test_vote_state_v4, + &vote_pubkey, + ) + .unwrap(); + let test_vote_state = unsafe { test_vote_state_v4.assume_init() }; + + assert_eq!(target_vote_state_v4, test_vote_state); } } } diff --git a/vote-interface/src/state/vote_state_deserialize.rs b/vote-interface/src/state/vote_state_deserialize.rs new file mode 100644 index 000000000..068ffd5d7 --- /dev/null +++ b/vote-interface/src/state/vote_state_deserialize.rs @@ -0,0 +1,353 @@ +use { + crate::{ + authorized_voters::AuthorizedVoters, + state::{ + BlockTimestamp, LandedVote, Lockout, VoteStateV3, VoteStateV4, + BLS_PUBKEY_COMPRESSED_BYTES, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY, + }, + }, + solana_clock::Epoch, + solana_instruction::error::InstructionError, + solana_pubkey::Pubkey, + solana_serialize_utils::cursor::{ + read_bool, read_i64, read_option_u64, read_pubkey, read_pubkey_into, read_u16, read_u32, + read_u64, read_u8, + }, + std::{ + collections::VecDeque, + io::{Cursor, Read}, + ptr::addr_of_mut, + }, +}; + +const MAX_ITEMS: usize = 32; + +// This is to reset vote_state to T::default() if deserialize fails or panics. +struct DropGuard { + vote_state: *mut T, +} + +impl Drop for DropGuard { + fn drop(&mut self) { + // Safety: + // + // Deserialize failed or panicked so at this point vote_state is uninitialized. We + // must write a new _valid_ value into it or after returning (or unwinding) from + // this function the caller is left with an uninitialized `&mut T`, which is UB + // (references must always be valid). + // + // This is always safe and doesn't leak memory because deserialize_into_ptr() writes + // into the fields that heap alloc only when it returns Ok(). + unsafe { + self.vote_state.write(T::default()); + } + } +} + +fn read_votes>( + cursor: &mut Cursor, + has_latency: bool, +) -> Result, InstructionError> { + let vote_count = read_u64(cursor)? as usize; + let mut votes = VecDeque::with_capacity(vote_count.min(MAX_LOCKOUT_HISTORY)); + + for _ in 0..vote_count { + let latency = if has_latency { read_u8(cursor)? } else { 0 }; + + let slot = read_u64(cursor)?; + let confirmation_count = read_u32(cursor)?; + let lockout = Lockout::new_with_confirmation_count(slot, confirmation_count); + + votes.push_back(LandedVote { latency, lockout }); + } + + Ok(votes) +} + +fn read_authorized_voters>( + cursor: &mut Cursor, +) -> Result { + let authorized_voter_count = read_u64(cursor)?; + let mut authorized_voters = AuthorizedVoters::default(); + + for _ in 0..authorized_voter_count { + let epoch = read_u64(cursor)?; + let authorized_voter = read_pubkey(cursor)?; + authorized_voters.insert(epoch, authorized_voter); + } + + Ok(authorized_voters) +} + +fn read_prior_voters_into>( + cursor: &mut Cursor, + vote_state: *mut VoteStateV3, +) -> Result<(), InstructionError> { + // Safety: if vote_state is non-null, prior_voters is guaranteed to be valid too + unsafe { + let prior_voters = addr_of_mut!((*vote_state).prior_voters); + let prior_voters_buf = addr_of_mut!((*prior_voters).buf) as *mut (Pubkey, Epoch, Epoch); + + for i in 0..MAX_ITEMS { + let prior_voter = read_pubkey(cursor)?; + let from_epoch = read_u64(cursor)?; + let until_epoch = read_u64(cursor)?; + + prior_voters_buf + .add(i) + .write((prior_voter, from_epoch, until_epoch)); + } + + (*vote_state).prior_voters.idx = read_u64(cursor)? as usize; + (*vote_state).prior_voters.is_empty = read_bool(cursor)?; + } + Ok(()) +} + +fn read_epoch_credits>( + cursor: &mut Cursor, +) -> Result, InstructionError> { + let epoch_credit_count = read_u64(cursor)? as usize; + let mut epoch_credits = Vec::with_capacity(epoch_credit_count.min(MAX_EPOCH_CREDITS_HISTORY)); + + for _ in 0..epoch_credit_count { + let epoch = read_u64(cursor)?; + let credits = read_u64(cursor)?; + let prev_credits = read_u64(cursor)?; + epoch_credits.push((epoch, credits, prev_credits)); + } + + Ok(epoch_credits) +} + +fn read_last_timestamp>( + cursor: &mut Cursor, +) -> Result { + let slot = read_u64(cursor)?; + let timestamp = read_i64(cursor)?; + + Ok(BlockTimestamp { slot, timestamp }) +} + +fn read_option_bls_pubkey_compressed>( + cursor: &mut Cursor, +) -> Result, InstructionError> { + let variant = read_u8(cursor)?; + match variant { + 0 => Ok(None), + 1 => { + let mut buf = [0; BLS_PUBKEY_COMPRESSED_BYTES]; + cursor + .read_exact(&mut buf) + .map_err(|_| InstructionError::InvalidAccountData)?; + Ok(Some(buf)) + } + _ => Err(InstructionError::InvalidAccountData), + } +} + +fn skip_prior_voters>(cursor: &mut Cursor) -> Result<(), InstructionError> { + // Read and discard prior voters data + for _ in 0..MAX_ITEMS { + read_pubkey(cursor)?; // prior_voter + read_u64(cursor)?; // from_epoch + read_u64(cursor)?; // until_epoch + } + read_u64(cursor)?; // idx + read_bool(cursor)?; // is_empty + Ok(()) +} + +pub(crate) fn deserialize_into( + input: &[u8], + vote_state: &mut T, + deserialize_fn: impl FnOnce(&[u8], *mut T) -> Result<(), InstructionError>, +) -> Result<(), InstructionError> { + // Rebind vote_state to *mut T so that the &mut binding isn't accessible + // anymore, preventing accidental use after this point. + // + // NOTE: switch to ptr::from_mut() once platform-tools moves to rustc >= 1.76 + let vote_state = vote_state as *mut T; + + // Safety: vote_state is valid to_drop (see drop_in_place() docs). After + // dropping, the pointer is treated as uninitialized and only accessed + // through ptr::write, which is safe as per drop_in_place docs. + unsafe { + std::ptr::drop_in_place(vote_state); + } + + // This is to reset vote_state to T::default() if deserialize fails or panics. + let guard = DropGuard { vote_state }; + + let res = deserialize_fn(input, vote_state); + if res.is_ok() { + std::mem::forget(guard); + } + + res +} + +pub(crate) fn deserialize_vote_state_into_v3( + cursor: &mut Cursor<&[u8]>, + vote_state: *mut VoteStateV3, + has_latency: bool, +) -> Result<(), InstructionError> { + // General safety note: we must use addr_of_mut! to access the `vote_state` fields as the value + // is assumed to be _uninitialized_, so creating references to the state or any of its inner + // fields is UB. + + read_pubkey_into( + cursor, + // Safety: if vote_state is non-null, node_pubkey is guaranteed to be valid too + unsafe { addr_of_mut!((*vote_state).node_pubkey) }, + )?; + read_pubkey_into( + cursor, + // Safety: if vote_state is non-null, authorized_withdrawer is guaranteed to be valid too + unsafe { addr_of_mut!((*vote_state).authorized_withdrawer) }, + )?; + let commission = read_u8(cursor)?; + let votes = read_votes(cursor, has_latency)?; + let root_slot = read_option_u64(cursor)?; + let authorized_voters = read_authorized_voters(cursor)?; + read_prior_voters_into(cursor, vote_state)?; + let epoch_credits = read_epoch_credits(cursor)?; + let last_timestamp = read_last_timestamp(cursor)?; + + // Safety: if vote_state is non-null, all the fields are guaranteed to be + // valid pointers. + // + // Heap allocated collections - votes, authorized_voters and epoch_credits - + // are guaranteed not to leak after this point as the VoteStateV3 is fully + // initialized and will be regularly dropped. + unsafe { + addr_of_mut!((*vote_state).commission).write(commission); + addr_of_mut!((*vote_state).votes).write(votes); + addr_of_mut!((*vote_state).root_slot).write(root_slot); + addr_of_mut!((*vote_state).authorized_voters).write(authorized_voters); + addr_of_mut!((*vote_state).epoch_credits).write(epoch_credits); + addr_of_mut!((*vote_state).last_timestamp).write(last_timestamp); + } + + Ok(()) +} + +#[derive(PartialEq)] +pub(crate) enum SourceVersion<'a> { + V1_14_11 { vote_pubkey: &'a Pubkey }, + V3 { vote_pubkey: &'a Pubkey }, + V4, +} + +pub(crate) fn deserialize_vote_state_into_v4<'a>( + cursor: &mut Cursor<&[u8]>, + vote_state: *mut VoteStateV4, + source_version: SourceVersion<'a>, +) -> Result<(), InstructionError> { + // General safety note: we must use addr_of_mut! to access the `vote_state` fields as the value + // is assumed to be _uninitialized_, so creating references to the state or any of its inner + // fields is UB. + + // Read common fields that are in the same position for all versions. + read_pubkey_into( + cursor, + // Safety: if vote_state is non-null, node_pubkey is guaranteed to be valid too + unsafe { addr_of_mut!((*vote_state).node_pubkey) }, + )?; + read_pubkey_into( + cursor, + // Safety: if vote_state is non-null, authorized_withdrawer is guaranteed to be valid too + unsafe { addr_of_mut!((*vote_state).authorized_withdrawer) }, + )?; + + // Handle version-specific fields and conversions. + let ( + inflation_rewards_commission_bps, + block_revenue_commission_bps, + pending_delegator_rewards, + bls_pubkey_compressed, + ) = match source_version { + SourceVersion::V4 => { + // V4 has collectors and commission fields here. + read_pubkey_into(cursor, unsafe { + addr_of_mut!((*vote_state).inflation_rewards_collector) + })?; + read_pubkey_into(cursor, unsafe { + addr_of_mut!((*vote_state).block_revenue_collector) + })?; + + // Read the basis points and pending rewards directly. + let inflation = read_u16(cursor)?; + let block = read_u16(cursor)?; + let pending = read_u64(cursor)?; + + // Read the BLS pubkey. + let bls_pubkey_compressed = read_option_bls_pubkey_compressed(cursor)?; + + (inflation, block, pending, bls_pubkey_compressed) + } + SourceVersion::V1_14_11 { vote_pubkey } | SourceVersion::V3 { vote_pubkey } => { + // V1_14_11 and V3 have commission field here. + let commission = read_u8(cursor)?; + + // Set collectors based on SIMD-0185. + // Safety: if vote_state is non-null, collectors are guaranteed to be valid too + unsafe { + // We already read `node_pubkey` earlier, so we can read it + // here again. + let node_pubkey = (*vote_state).node_pubkey; + + addr_of_mut!((*vote_state).inflation_rewards_collector).write(*vote_pubkey); + addr_of_mut!((*vote_state).block_revenue_collector).write(node_pubkey); + } + + // Convert commission to basis points and set block revenue to 100%. + // No rewards tracked. No BLS pubkey. + ( + u16::from(commission).saturating_mul(100), + 10_000u16, + 0u64, + None, + ) + } + }; + + // For V3 and V4, `has_latency` is always true. + let votes = read_votes( + cursor, + !matches!(source_version, SourceVersion::V1_14_11 { .. }), + )?; + let root_slot = read_option_u64(cursor)?; + let authorized_voters = read_authorized_voters(cursor)?; + + // V1_14_11 and V3 have `prior_voters` field here. + // Skip, since V4 doesn't have this field. + if !matches!(source_version, SourceVersion::V4) { + skip_prior_voters(cursor)?; + } + + let epoch_credits = read_epoch_credits(cursor)?; + let last_timestamp = read_last_timestamp(cursor)?; + + // Safety: if vote_state is non-null, all the fields are guaranteed to be + // valid pointers. + // + // Heap allocated collections - votes, authorized_voters and epoch_credits - + // are guaranteed not to leak after this point as the VoteStateV4 is fully + // initialized and will be regularly dropped. + unsafe { + addr_of_mut!((*vote_state).inflation_rewards_commission_bps) + .write(inflation_rewards_commission_bps); + addr_of_mut!((*vote_state).block_revenue_commission_bps) + .write(block_revenue_commission_bps); + addr_of_mut!((*vote_state).pending_delegator_rewards).write(pending_delegator_rewards); + addr_of_mut!((*vote_state).bls_pubkey_compressed).write(bls_pubkey_compressed); + addr_of_mut!((*vote_state).votes).write(votes); + addr_of_mut!((*vote_state).root_slot).write(root_slot); + addr_of_mut!((*vote_state).authorized_voters).write(authorized_voters); + addr_of_mut!((*vote_state).epoch_credits).write(epoch_credits); + addr_of_mut!((*vote_state).last_timestamp).write(last_timestamp); + } + + Ok(()) +} diff --git a/vote-interface/src/state/vote_state_v3.rs b/vote-interface/src/state/vote_state_v3.rs index fcbcbc2cf..5d58bba8a 100644 --- a/vote-interface/src/state/vote_state_v3.rs +++ b/vote-interface/src/state/vote_state_v3.rs @@ -122,8 +122,8 @@ impl VoteStateV3 { #[cfg(not(target_os = "solana"))] { bincode::deserialize::(input) - .map(|versioned| versioned.convert_to_current()) .map_err(|_| InstructionError::InvalidAccountData) + .and_then(|versioned| versioned.try_convert_to_v3()) } #[cfg(target_os = "solana")] { @@ -145,49 +145,8 @@ impl VoteStateV3 { input: &[u8], vote_state: &mut VoteStateV3, ) -> Result<(), InstructionError> { - // Rebind vote_state to *mut VoteStateV3 so that the &mut binding isn't - // accessible anymore, preventing accidental use after this point. - // - // NOTE: switch to ptr::from_mut() once platform-tools moves to rustc >= 1.76 - let vote_state = vote_state as *mut VoteStateV3; - - // Safety: vote_state is valid to_drop (see drop_in_place() docs). After - // dropping, the pointer is treated as uninitialized and only accessed - // through ptr::write, which is safe as per drop_in_place docs. - unsafe { - std::ptr::drop_in_place(vote_state); - } - - // This is to reset vote_state to VoteStateV3::default() if deserialize fails or panics. - struct DropGuard { - vote_state: *mut VoteStateV3, - } - - impl Drop for DropGuard { - fn drop(&mut self) { - // Safety: - // - // Deserialize failed or panicked so at this point vote_state is uninitialized. We - // must write a new _valid_ value into it or after returning (or unwinding) from - // this function the caller is left with an uninitialized `&mut VoteStateV3`, which is - // UB (references must always be valid). - // - // This is always safe and doesn't leak memory because deserialize_into_ptr() writes - // into the fields that heap alloc only when it returns Ok(). - unsafe { - self.vote_state.write(VoteStateV3::default()); - } - } - } - - let guard = DropGuard { vote_state }; - - let res = VoteStateV3::deserialize_into_ptr(input, vote_state); - if res.is_ok() { - std::mem::forget(guard); - } - - res + use super::vote_state_deserialize; + vote_state_deserialize::deserialize_into(input, vote_state, Self::deserialize_into_ptr) } /// Deserializes the input `VoteStateVersions` buffer directly into the provided @@ -204,7 +163,7 @@ impl VoteStateV3 { input: &[u8], vote_state: &mut std::mem::MaybeUninit, ) -> Result<(), InstructionError> { - VoteStateV3::deserialize_into_ptr(input, vote_state.as_mut_ptr()) + Self::deserialize_into_ptr(input, vote_state.as_mut_ptr()) } #[cfg(any(target_os = "solana", feature = "bincode"))] @@ -212,7 +171,7 @@ impl VoteStateV3 { input: &[u8], vote_state: *mut VoteStateV3, ) -> Result<(), InstructionError> { - use vote_state_deserialize::deserialize_vote_state_into; + use super::vote_state_deserialize::deserialize_vote_state_into_v3; let mut cursor = std::io::Cursor::new(input); @@ -231,8 +190,8 @@ impl VoteStateV3 { unsafe { vote_state.write( bincode::deserialize::(input) - .map(|versioned| versioned.convert_to_current()) - .map_err(|_| InstructionError::InvalidAccountData)?, + .map_err(|_| InstructionError::InvalidAccountData) + .and_then(|versioned| versioned.try_convert_to_v3())?, ); } Ok(()) @@ -241,9 +200,9 @@ impl VoteStateV3 { 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), + 1 => deserialize_vote_state_into_v3(&mut cursor, vote_state, false), + // V3. the only difference from V1_14_11 is the addition of a slot-latency to each vote + 2 => deserialize_vote_state_into_v3(&mut cursor, vote_state, true), _ => Err(InstructionError::InvalidAccountData), }?; @@ -596,162 +555,3 @@ impl VoteStateV3 { && data[VERSION_OFFSET..DEFAULT_PRIOR_VOTERS_END] != [0; DEFAULT_PRIOR_VOTERS_OFFSET] } } - -#[cfg(any(target_os = "solana", feature = "bincode"))] -mod vote_state_deserialize { - use { - crate::{ - authorized_voters::AuthorizedVoters, - state::{ - BlockTimestamp, LandedVote, Lockout, VoteStateV3, MAX_EPOCH_CREDITS_HISTORY, - MAX_ITEMS, MAX_LOCKOUT_HISTORY, - }, - }, - solana_clock::Epoch, - solana_instruction_error::InstructionError, - solana_pubkey::Pubkey, - solana_serialize_utils::cursor::{ - read_bool, read_i64, read_option_u64, read_pubkey, read_pubkey_into, read_u32, - read_u64, read_u8, - }, - std::{collections::VecDeque, io::Cursor, ptr::addr_of_mut}, - }; - - pub(super) fn deserialize_vote_state_into( - cursor: &mut Cursor<&[u8]>, - vote_state: *mut VoteStateV3, - has_latency: bool, - ) -> Result<(), InstructionError> { - // General safety note: we must use add_or_mut! to access the `vote_state` fields as the value - // is assumed to be _uninitialized_, so creating references to the state or any of its inner - // fields is UB. - - read_pubkey_into( - cursor, - // Safety: if vote_state is non-null, node_pubkey is guaranteed to be valid too - unsafe { addr_of_mut!((*vote_state).node_pubkey) }, - )?; - read_pubkey_into( - cursor, - // Safety: if vote_state is non-null, authorized_withdrawer is guaranteed to be valid too - unsafe { addr_of_mut!((*vote_state).authorized_withdrawer) }, - )?; - let commission = read_u8(cursor)?; - let votes = read_votes(cursor, has_latency)?; - let root_slot = read_option_u64(cursor)?; - let authorized_voters = read_authorized_voters(cursor)?; - read_prior_voters_into(cursor, vote_state)?; - let epoch_credits = read_epoch_credits(cursor)?; - read_last_timestamp_into(cursor, vote_state)?; - - // Safety: if vote_state is non-null, all the fields are guaranteed to be - // valid pointers. - // - // Heap allocated collections - votes, authorized_voters and epoch_credits - - // are guaranteed not to leak after this point as the VoteStateV3 is fully - // initialized and will be regularly dropped. - unsafe { - addr_of_mut!((*vote_state).commission).write(commission); - addr_of_mut!((*vote_state).votes).write(votes); - addr_of_mut!((*vote_state).root_slot).write(root_slot); - addr_of_mut!((*vote_state).authorized_voters).write(authorized_voters); - addr_of_mut!((*vote_state).epoch_credits).write(epoch_credits); - } - - Ok(()) - } - - fn read_votes>( - cursor: &mut Cursor, - has_latency: bool, - ) -> Result, InstructionError> { - let vote_count = read_u64(cursor)? as usize; - let mut votes = VecDeque::with_capacity(vote_count.min(MAX_LOCKOUT_HISTORY)); - - for _ in 0..vote_count { - let latency = if has_latency { read_u8(cursor)? } else { 0 }; - - let slot = read_u64(cursor)?; - let confirmation_count = read_u32(cursor)?; - let lockout = Lockout::new_with_confirmation_count(slot, confirmation_count); - - votes.push_back(LandedVote { latency, lockout }); - } - - Ok(votes) - } - - fn read_authorized_voters>( - cursor: &mut Cursor, - ) -> Result { - let authorized_voter_count = read_u64(cursor)?; - let mut authorized_voters = AuthorizedVoters::default(); - - for _ in 0..authorized_voter_count { - let epoch = read_u64(cursor)?; - let authorized_voter = read_pubkey(cursor)?; - authorized_voters.insert(epoch, authorized_voter); - } - - Ok(authorized_voters) - } - - fn read_prior_voters_into>( - cursor: &mut Cursor, - vote_state: *mut VoteStateV3, - ) -> Result<(), InstructionError> { - // Safety: if vote_state is non-null, prior_voters is guaranteed to be valid too - unsafe { - let prior_voters = addr_of_mut!((*vote_state).prior_voters); - let prior_voters_buf = addr_of_mut!((*prior_voters).buf) as *mut (Pubkey, Epoch, Epoch); - - for i in 0..MAX_ITEMS { - let prior_voter = read_pubkey(cursor)?; - let from_epoch = read_u64(cursor)?; - let until_epoch = read_u64(cursor)?; - - prior_voters_buf - .add(i) - .write((prior_voter, from_epoch, until_epoch)); - } - - (*vote_state).prior_voters.idx = read_u64(cursor)? as usize; - (*vote_state).prior_voters.is_empty = read_bool(cursor)?; - } - Ok(()) - } - - fn read_epoch_credits>( - cursor: &mut Cursor, - ) -> Result, InstructionError> { - let epoch_credit_count = read_u64(cursor)? as usize; - let mut epoch_credits = - Vec::with_capacity(epoch_credit_count.min(MAX_EPOCH_CREDITS_HISTORY)); - - for _ in 0..epoch_credit_count { - let epoch = read_u64(cursor)?; - let credits = read_u64(cursor)?; - let prev_credits = read_u64(cursor)?; - epoch_credits.push((epoch, credits, prev_credits)); - } - - Ok(epoch_credits) - } - - fn read_last_timestamp_into>( - cursor: &mut Cursor, - vote_state: *mut VoteStateV3, - ) -> Result<(), InstructionError> { - let slot = read_u64(cursor)?; - let timestamp = read_i64(cursor)?; - - let last_timestamp = BlockTimestamp { slot, timestamp }; - - // Safety: if vote_state is non-null, last_timestamp is guaranteed to be valid too - unsafe { - addr_of_mut!((*vote_state).last_timestamp).write(last_timestamp); - } - - Ok(()) - } -} diff --git a/vote-interface/src/state/vote_state_v4.rs b/vote-interface/src/state/vote_state_v4.rs new file mode 100644 index 000000000..a169bc89d --- /dev/null +++ b/vote-interface/src/state/vote_state_v4.rs @@ -0,0 +1,242 @@ +#[cfg(feature = "bincode")] +use super::VoteStateVersions; +#[cfg(feature = "dev-context-only-utils")] +use arbitrary::Arbitrary; +#[cfg(feature = "serde")] +use serde_derive::{Deserialize, Serialize}; +#[cfg(feature = "serde")] +use serde_with::serde_as; +#[cfg(feature = "frozen-abi")] +use solana_frozen_abi_macro::{frozen_abi, AbiExample}; +#[cfg(any(target_os = "solana", feature = "bincode"))] +use solana_instruction::error::InstructionError; +use { + super::{BlockTimestamp, LandedVote, Lockout, BLS_PUBKEY_COMPRESSED_BYTES}, + crate::authorized_voters::AuthorizedVoters, + solana_clock::{Epoch, Slot}, + solana_pubkey::Pubkey, + std::{collections::VecDeque, fmt::Debug}, +}; + +#[cfg_attr( + feature = "frozen-abi", + frozen_abi(digest = "6wQjgsg3yTmdwi5SLBzRhGXrGnNMCD8rwxmUSR8mPEe6"), + derive(AbiExample) +)] +#[cfg_attr(feature = "serde", cfg_eval::cfg_eval, serde_as)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))] +pub struct VoteStateV4 { + /// The node that votes in this account. + pub node_pubkey: Pubkey, + /// The signer for withdrawals. + pub authorized_withdrawer: Pubkey, + + /// The collector account for inflation rewards. + pub inflation_rewards_collector: Pubkey, + /// The collector account for block revenue. + pub block_revenue_collector: Pubkey, + + /// Basis points (0-10,000) that represent how much of the inflation + /// rewards should be given to this vote account. + pub inflation_rewards_commission_bps: u16, + /// Basis points (0-10,000) that represent how much of the block revenue + /// should be given to this vote account. + pub block_revenue_commission_bps: u16, + + /// Reward amount pending distribution to stake delegators. + pub pending_delegator_rewards: u64, + + /// Compressed BLS pubkey for Alpenglow. + #[cfg_attr( + feature = "serde", + serde_as(as = "Option<[_; BLS_PUBKEY_COMPRESSED_BYTES]>") + )] + pub bls_pubkey_compressed: Option<[u8; BLS_PUBKEY_COMPRESSED_BYTES]>, + + pub votes: VecDeque, + pub root_slot: Option, + + /// The signer for vote transactions. + /// Contains entries for the current epoch and the previous epoch. + pub authorized_voters: AuthorizedVoters, + + /// History of credits earned by the end of each epoch. + /// Each tuple is (Epoch, credits, prev_credits). + pub epoch_credits: Vec<(Epoch, u64, u64)>, + + /// Most recent timestamp submitted with a vote. + pub last_timestamp: BlockTimestamp, +} + +impl VoteStateV4 { + /// Upper limit on the size of the Vote State + /// when votes.len() is MAX_LOCKOUT_HISTORY. + pub const fn size_of() -> usize { + 3762 // Same size as V3 to avoid account resizing + } + + pub fn new_rand_for_tests(node_pubkey: Pubkey, root_slot: Slot) -> Self { + let votes = (1..32) + .map(|x| LandedVote { + latency: 0, + lockout: Lockout::new_with_confirmation_count( + u64::from(x).saturating_add(root_slot), + 32_u32.saturating_sub(x), + ), + }) + .collect(); + Self { + node_pubkey, + root_slot: Some(root_slot), + votes, + ..VoteStateV4::default() + } + } + + #[cfg(any(target_os = "solana", feature = "bincode"))] + pub fn deserialize(input: &[u8], vote_pubkey: &Pubkey) -> Result { + #[cfg(not(target_os = "solana"))] + { + bincode::deserialize::(input) + .map_err(|_| InstructionError::InvalidAccountData) + .and_then(|versioned| versioned.try_convert_to_v4(vote_pubkey)) + } + #[cfg(target_os = "solana")] + { + let mut vote_state = Self::default(); + Self::deserialize_into(input, &mut vote_state, vote_pubkey)?; + Ok(vote_state) + } + } + + /// Deserializes the input `VoteStateVersions` buffer directly into the provided `VoteStateV4`. + /// + /// In a SBPF context, V0_23_5 is not supported, but in non-SBPF, all versions are supported for + /// compatibility with `bincode::deserialize`. + /// + /// On success, `vote_state` reflects the state of the input data. On failure, `vote_state` is + /// reset to `VoteStateV4::default()`. + #[cfg(any(target_os = "solana", feature = "bincode"))] + pub fn deserialize_into( + input: &[u8], + vote_state: &mut VoteStateV4, + vote_pubkey: &Pubkey, + ) -> Result<(), InstructionError> { + use super::vote_state_deserialize; + vote_state_deserialize::deserialize_into(input, vote_state, |input, vote_state| { + Self::deserialize_into_ptr(input, vote_state, vote_pubkey) + }) + } + + /// Deserializes the input `VoteStateVersions` buffer directly into the provided + /// `MaybeUninit`. + /// + /// In a SBPF context, V0_23_5 is not supported, but in non-SBPF, all versions are supported for + /// compatibility with `bincode::deserialize`. + /// + /// On success, `vote_state` is fully initialized and can be converted to `VoteStateV4` using + /// [MaybeUninit::assume_init]. On failure, `vote_state` may still be uninitialized and must not + /// be converted to `VoteStateV4`. + #[cfg(any(target_os = "solana", feature = "bincode"))] + pub fn deserialize_into_uninit( + input: &[u8], + vote_state: &mut std::mem::MaybeUninit, + vote_pubkey: &Pubkey, + ) -> Result<(), InstructionError> { + Self::deserialize_into_ptr(input, vote_state.as_mut_ptr(), vote_pubkey) + } + + #[cfg(any(target_os = "solana", feature = "bincode"))] + fn deserialize_into_ptr( + input: &[u8], + vote_state: *mut VoteStateV4, + vote_pubkey: &Pubkey, + ) -> Result<(), InstructionError> { + use super::vote_state_deserialize::{deserialize_vote_state_into_v4, SourceVersion}; + + let mut cursor = std::io::Cursor::new(input); + + let variant = solana_serialize_utils::cursor::read_u32(&mut cursor)?; + match variant { + // V0_23_5. not supported for bpf targets; these should not exist on mainnet + // supported for non-bpf targets for backwards compatibility. + // **Same pattern as v3 for this variant**. + 0 => { + #[cfg(not(target_os = "solana"))] + { + // Safety: vote_state is valid as it comes from `&mut MaybeUninit` or + // `&mut VoteStateV4`. In the first case, the value is uninitialized so we write() + // to avoid dropping invalid data; in the latter case, we `drop_in_place()` + // before writing so the value has already been dropped and we just write a new + // one in place. + unsafe { + vote_state.write( + bincode::deserialize::(input) + .map_err(|_| InstructionError::InvalidAccountData) + .and_then(|versioned| versioned.try_convert_to_v4(vote_pubkey))?, + ); + } + Ok(()) + } + #[cfg(target_os = "solana")] + Err(InstructionError::InvalidAccountData) + } + // V1_14_11 + 1 => deserialize_vote_state_into_v4( + &mut cursor, + vote_state, + SourceVersion::V1_14_11 { vote_pubkey }, + ), + // V3 + 2 => deserialize_vote_state_into_v4( + &mut cursor, + vote_state, + SourceVersion::V3 { vote_pubkey }, + ), + // V4 + 3 => deserialize_vote_state_into_v4(&mut cursor, vote_state, SourceVersion::V4), + _ => Err(InstructionError::InvalidAccountData), + }?; + + Ok(()) + } + + #[cfg(feature = "bincode")] + pub fn serialize( + versioned: &VoteStateVersions, + output: &mut [u8], + ) -> Result<(), InstructionError> { + bincode::serialize_into(output, versioned).map_err(|err| match *err { + bincode::ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall, + _ => InstructionError::GenericError, + }) + } + + pub fn is_correct_size_and_initialized(data: &[u8]) -> bool { + data.len() == VoteStateV4::size_of() && data[..4] == [3, 0, 0, 0] // little-endian 3u32 + // Always initialized + } + + #[cfg(test)] + pub(crate) fn get_max_sized_vote_state() -> Self { + use { + super::{MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY}, + solana_epoch_schedule::MAX_LEADER_SCHEDULE_EPOCH_OFFSET, + }; + + let mut authorized_voters = AuthorizedVoters::default(); + for i in 0..=MAX_LEADER_SCHEDULE_EPOCH_OFFSET { + authorized_voters.insert(i, Pubkey::new_unique()); + } + + Self { + votes: VecDeque::from(vec![LandedVote::default(); MAX_LOCKOUT_HISTORY]), + root_slot: Some(u64::MAX), + epoch_credits: vec![(0, 0, 0); MAX_EPOCH_CREDITS_HISTORY], + authorized_voters, + ..Self::default() + } + } +} diff --git a/vote-interface/src/state/vote_state_versions.rs b/vote-interface/src/state/vote_state_versions.rs index 8ffde1065..9eeab3f3b 100644 --- a/vote-interface/src/state/vote_state_versions.rs +++ b/vote-interface/src/state/vote_state_versions.rs @@ -5,9 +5,10 @@ use { authorized_voters::AuthorizedVoters, state::{ vote_state_0_23_5::VoteState0_23_5, vote_state_1_14_11::VoteState1_14_11, CircBuf, - LandedVote, Lockout, VoteStateV3, + LandedVote, Lockout, VoteStateV3, VoteStateV4, }, }, + solana_instruction::error::InstructionError, solana_pubkey::Pubkey, std::collections::VecDeque, }; @@ -20,21 +21,26 @@ use { pub enum VoteStateVersions { V0_23_5(Box), V1_14_11(Box), - Current(Box), + V3(Box), + V4(Box), } impl VoteStateVersions { - pub fn new_current(vote_state: VoteStateV3) -> Self { - Self::Current(Box::new(vote_state)) + pub fn new_v4(vote_state: VoteStateV4) -> Self { + Self::V4(Box::new(vote_state)) } - pub fn convert_to_current(self) -> VoteStateV3 { + pub fn new_v3(vote_state: VoteStateV3) -> Self { + Self::V3(Box::new(vote_state)) + } + + pub fn try_convert_to_v3(self) -> Result { match self { VoteStateVersions::V0_23_5(state) => { let authorized_voters = AuthorizedVoters::new(state.authorized_voter_epoch, state.authorized_voter); - VoteStateV3 { + Ok(VoteStateV3 { node_pubkey: state.node_pubkey, authorized_withdrawer: state.authorized_withdrawer, @@ -52,10 +58,10 @@ impl VoteStateVersions { epoch_credits: state.epoch_credits.clone(), last_timestamp: state.last_timestamp.clone(), - } + }) } - VoteStateVersions::V1_14_11(state) => VoteStateV3 { + VoteStateVersions::V1_14_11(state) => Ok(VoteStateV3 { node_pubkey: state.node_pubkey, authorized_withdrawer: state.authorized_withdrawer, commission: state.commission, @@ -71,12 +77,77 @@ impl VoteStateVersions { epoch_credits: state.epoch_credits, last_timestamp: state.last_timestamp, - }, + }), - VoteStateVersions::Current(state) => *state, + VoteStateVersions::V3(state) => Ok(*state), + VoteStateVersions::V4(_) => Err(InstructionError::InvalidArgument), } } + // Currently, all versions can be converted to v4 without data loss, so + // this function returns `Ok(..)`. However, future versions may not be + // convertible to v4 without data loss, so this function returns a `Result` + // for forward compatibility. + pub fn try_convert_to_v4(self, vote_pubkey: &Pubkey) -> Result { + Ok(match self { + VoteStateVersions::V0_23_5(state) => { + let authorized_voters = + AuthorizedVoters::new(state.authorized_voter_epoch, state.authorized_voter); + + VoteStateV4 { + node_pubkey: state.node_pubkey, + authorized_withdrawer: state.authorized_withdrawer, + inflation_rewards_collector: *vote_pubkey, + block_revenue_collector: state.node_pubkey, + inflation_rewards_commission_bps: u16::from(state.commission) + .saturating_mul(100), + block_revenue_commission_bps: 10_000u16, + pending_delegator_rewards: 0, + bls_pubkey_compressed: None, + votes: Self::landed_votes_from_lockouts(state.votes), + root_slot: state.root_slot, + authorized_voters, + epoch_credits: state.epoch_credits.clone(), + last_timestamp: state.last_timestamp.clone(), + } + } + + VoteStateVersions::V1_14_11(state) => VoteStateV4 { + node_pubkey: state.node_pubkey, + authorized_withdrawer: state.authorized_withdrawer, + inflation_rewards_collector: *vote_pubkey, + block_revenue_collector: state.node_pubkey, + inflation_rewards_commission_bps: u16::from(state.commission).saturating_mul(100), + block_revenue_commission_bps: 10_000u16, + pending_delegator_rewards: 0, + bls_pubkey_compressed: None, + votes: Self::landed_votes_from_lockouts(state.votes), + root_slot: state.root_slot, + authorized_voters: state.authorized_voters.clone(), + epoch_credits: state.epoch_credits, + last_timestamp: state.last_timestamp, + }, + + VoteStateVersions::V3(state) => VoteStateV4 { + node_pubkey: state.node_pubkey, + authorized_withdrawer: state.authorized_withdrawer, + inflation_rewards_collector: *vote_pubkey, + block_revenue_collector: state.node_pubkey, + inflation_rewards_commission_bps: u16::from(state.commission).saturating_mul(100), + block_revenue_commission_bps: 10_000u16, + pending_delegator_rewards: 0, + bls_pubkey_compressed: None, + votes: state.votes, + root_slot: state.root_slot, + authorized_voters: state.authorized_voters, + epoch_credits: state.epoch_credits, + last_timestamp: state.last_timestamp, + }, + + VoteStateVersions::V4(state) => *state, + }) + } + fn landed_votes_from_lockouts(lockouts: VecDeque) -> VecDeque { lockouts.into_iter().map(|lockout| lockout.into()).collect() } @@ -89,20 +160,16 @@ impl VoteStateVersions { VoteStateVersions::V1_14_11(vote_state) => vote_state.authorized_voters.is_empty(), - VoteStateVersions::Current(vote_state) => vote_state.authorized_voters.is_empty(), - } - } + VoteStateVersions::V3(vote_state) => vote_state.authorized_voters.is_empty(), - pub fn vote_state_size_of(is_current: bool) -> usize { - if is_current { - VoteStateV3::size_of() - } else { - VoteState1_14_11::size_of() + // As per SIMD-0185, v4 is always initialized. + VoteStateVersions::V4(_) => false, } } pub fn is_correct_size_and_initialized(data: &[u8]) -> bool { - VoteStateV3::is_correct_size_and_initialized(data) + VoteStateV4::is_correct_size_and_initialized(data) + || VoteStateV3::is_correct_size_and_initialized(data) || VoteState1_14_11::is_correct_size_and_initialized(data) } } @@ -110,10 +177,11 @@ impl VoteStateVersions { #[cfg(test)] impl Arbitrary<'_> for VoteStateVersions { fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result { - let variant = u.choose_index(2)?; + let variant = u.choose_index(3)?; match variant { - 0 => Ok(Self::Current(Box::new(VoteStateV3::arbitrary(u)?))), - 1 => Ok(Self::V1_14_11(Box::new(VoteState1_14_11::arbitrary(u)?))), + 0 => Ok(Self::V4(Box::new(VoteStateV4::arbitrary(u)?))), + 1 => Ok(Self::V3(Box::new(VoteStateV3::arbitrary(u)?))), + 2 => Ok(Self::V1_14_11(Box::new(VoteState1_14_11::arbitrary(u)?))), _ => unreachable!(), } }