Add push and get methods for RestartLastVotedForkSlots (#33613)

* Add push and get methods for RestartLastVotedForkSlots

* Improve expression format.

* Remove fill() from RestartLastVotedForkSlots and move into constructor.

* Update ABI signature.

* Use flate2 compress directly instead of relying on CompressedSlots.

* Make constructor of RestartLastVotedForkSlots return error if necessary.

* Use minmax and remove unnecessary code.

* Replace flate2 with run-length encoding in RestartLastVotedForkSlots.

* Remove accidentally added file.

* The passed in last_voted_fork don't need to be mutable any more.

* Switch to different type of run-length encoding.

* Fix typo.

* Move constant into RestartLastVotedForkSlots.

* Use BitVec in RawOffsets.

* Remove the unnecessary clone.

* Use iter functions for RLE.

* Use take_while instead of loop.

* Change Run length encoding to iterator implementation.

* Allow one slot in RestartLastVotedForkSlots.

* Various simplifications.

* Fix various errors and use customized error type.

* Various simplifications.

* Return error from push_get_restart_last_voted_fork_slots and
remove unnecessary constraints in to_slots.

* Allow 81k slots on RestartLastVotedForkSlots.

* Limit MAX_SLOTS to 65535 so we can go back to u16.

* Use u16::MAX instead of 65535.
This commit is contained in:
Wen 2023-11-16 12:35:34 -08:00 committed by GitHub
parent 9a78924c4b
commit 3081b4378d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 350 additions and 77 deletions

View File

@ -33,7 +33,8 @@ use {
},
crds_value::{
self, AccountsHashes, CrdsData, CrdsValue, CrdsValueLabel, EpochSlotsIndex, LowestSlot,
NodeInstance, SnapshotHashes, Version, Vote, MAX_WALLCLOCK,
NodeInstance, RestartLastVotedForkSlots, RestartLastVotedForkSlotsError,
SnapshotHashes, Version, Vote, MAX_WALLCLOCK,
},
duplicate_shred::DuplicateShred,
epoch_slots::EpochSlots,
@ -267,7 +268,7 @@ pub fn make_accounts_hashes_message(
pub(crate) type Ping = ping_pong::Ping<[u8; GOSSIP_PING_TOKEN_SIZE]>;
// TODO These messages should go through the gpu pipeline for spam filtering
#[frozen_abi(digest = "CVvKB495YW6JN4w1rWwajyZmG5wvNhmD97V99rSv9fGw")]
#[frozen_abi(digest = "HvA9JnnQrJnmkcGxrp8SmTB1b4iSyQ4VK2p6LpSBaoWR")]
#[derive(Serialize, Deserialize, Debug, AbiEnumVisitor, AbiExample)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Protocol {
@ -962,6 +963,26 @@ impl ClusterInfo {
}
}
pub fn push_restart_last_voted_fork_slots(
&self,
fork: &[Slot],
last_vote_bankhash: Hash,
) -> Result<(), RestartLastVotedForkSlotsError> {
let now = timestamp();
let last_voted_fork_slots = RestartLastVotedForkSlots::new(
self.id(),
now,
fork,
last_vote_bankhash,
self.my_shred_version(),
)?;
self.push_message(CrdsValue::new_signed(
CrdsData::RestartLastVotedForkSlots(last_voted_fork_slots),
&self.keypair(),
));
Ok(())
}
fn time_gossip_read_lock<'a>(
&'a self,
label: &'static str,
@ -1214,6 +1235,24 @@ impl ClusterInfo {
.collect()
}
pub fn get_restart_last_voted_fork_slots(
&self,
cursor: &mut Cursor,
) -> Vec<RestartLastVotedForkSlots> {
let self_shred_version = self.my_shred_version();
let gossip_crds = self.gossip.crds.read().unwrap();
gossip_crds
.get_entries(cursor)
.filter_map(|entry| {
let CrdsData::RestartLastVotedForkSlots(slots) = &entry.value.data else {
return None;
};
(slots.shred_version == self_shred_version).then_some(slots)
})
.cloned()
.collect()
}
/// Returns duplicate-shreds inserted since the given cursor.
pub(crate) fn get_duplicate_shreds(&self, cursor: &mut Cursor) -> Vec<DuplicateShred> {
let gossip_crds = self.gossip.crds.read().unwrap();
@ -4487,4 +4526,73 @@ mod tests {
assert_eq!(shred_data.chunk_index() as usize, i);
}
}
#[test]
fn test_push_restart_last_voted_fork_slots() {
let keypair = Arc::new(Keypair::new());
let contact_info = ContactInfo::new_localhost(&keypair.pubkey(), 0);
let cluster_info = ClusterInfo::new(contact_info, keypair, SocketAddrSpace::Unspecified);
let slots = cluster_info.get_restart_last_voted_fork_slots(&mut Cursor::default());
assert!(slots.is_empty());
let mut update: Vec<Slot> = vec![0];
for i in 0..81 {
for j in 0..1000 {
update.push(i * 1050 + j);
}
}
assert!(cluster_info
.push_restart_last_voted_fork_slots(&update, Hash::default())
.is_ok());
cluster_info.flush_push_queue();
let mut cursor = Cursor::default();
let slots = cluster_info.get_restart_last_voted_fork_slots(&mut cursor);
assert_eq!(slots.len(), 1);
let retrieved_slots = slots[0].to_slots(0);
assert!(retrieved_slots[0] < 69000);
assert_eq!(retrieved_slots.last(), Some(84999).as_ref());
let slots = cluster_info.get_restart_last_voted_fork_slots(&mut cursor);
assert!(slots.is_empty());
// Test with different shred versions.
let mut rng = rand::thread_rng();
let node_pubkey = Pubkey::new_unique();
let mut node = LegacyContactInfo::new_rand(&mut rng, Some(node_pubkey));
node.set_shred_version(42);
let mut slots = RestartLastVotedForkSlots::new_rand(&mut rng, Some(node_pubkey));
slots.shred_version = 42;
let entries = vec![
CrdsValue::new_unsigned(CrdsData::LegacyContactInfo(node)),
CrdsValue::new_unsigned(CrdsData::RestartLastVotedForkSlots(slots)),
];
{
let mut gossip_crds = cluster_info.gossip.crds.write().unwrap();
for entry in entries {
assert!(gossip_crds
.insert(entry, /*now=*/ 0, GossipRoute::LocalMessage)
.is_ok());
}
}
// Should exclude other node's last-voted-fork-slot because of different
// shred-version.
let slots = cluster_info.get_restart_last_voted_fork_slots(&mut Cursor::default());
assert_eq!(slots.len(), 1);
assert_eq!(slots[0].from, cluster_info.id());
// Match shred versions.
{
let mut node = cluster_info.my_contact_info.write().unwrap();
node.set_shred_version(42);
}
assert!(cluster_info
.push_restart_last_voted_fork_slots(&update, Hash::default())
.is_ok());
cluster_info.flush_push_queue();
// Should now include both slots.
let slots = cluster_info.get_restart_last_voted_fork_slots(&mut Cursor::default());
assert_eq!(slots.len(), 2);
assert_eq!(slots[0].from, node_pubkey);
assert_eq!(slots[1].from, cluster_info.id());
}
}

View File

@ -1,13 +1,15 @@
use {
crate::{
cluster_info::{MAX_ACCOUNTS_HASHES, MAX_CRDS_OBJECT_SIZE},
cluster_info::MAX_ACCOUNTS_HASHES,
contact_info::ContactInfo,
deprecated,
duplicate_shred::{DuplicateShred, DuplicateShredIndex, MAX_DUPLICATE_SHREDS},
epoch_slots::{CompressedSlots, EpochSlots, MAX_SLOTS_PER_ENTRY},
epoch_slots::EpochSlots,
legacy_contact_info::LegacyContactInfo,
},
bincode::{serialize, serialized_size},
bv::BitVec,
itertools::Itertools,
rand::{CryptoRng, Rng},
serde::de::{Deserialize, Deserializer},
solana_sdk::{
@ -15,6 +17,7 @@ use {
hash::Hash,
pubkey::{self, Pubkey},
sanitize::{Sanitize, SanitizeError},
serde_varint,
signature::{Keypair, Signable, Signature, Signer},
timing::timestamp,
transaction::Transaction,
@ -26,6 +29,7 @@ use {
collections::{hash_map::Entry, BTreeSet, HashMap},
fmt,
},
thiserror::Error,
};
pub const MAX_WALLCLOCK: u64 = 1_000_000_000_000_000;
@ -490,84 +494,172 @@ impl Sanitize for NodeInstance {
}
}
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, AbiExample, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, AbiExample, AbiEnumVisitor)]
enum SlotsOffsets {
RunLengthEncoding(RunLengthEncoding),
RawOffsets(RawOffsets),
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, AbiExample)]
struct U16(#[serde(with = "serde_varint")] u16);
// The vector always starts with 1. Encode number of 1's and 0's consecutively.
// For example, 110000111 is [2, 4, 3].
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, AbiExample)]
struct RunLengthEncoding(Vec<U16>);
impl RunLengthEncoding {
fn new(bits: &BitVec<u8>) -> Self {
let encoded = (0..bits.len())
.map(|i| bits.get(i))
.dedup_with_count()
.map_while(|(count, _)| u16::try_from(count).ok())
.scan(0, |current_bytes, count| {
*current_bytes += ((u16::BITS - count.leading_zeros() + 6) / 7).max(1) as usize;
(*current_bytes <= RestartLastVotedForkSlots::MAX_BYTES).then_some(U16(count))
})
.collect();
Self(encoded)
}
fn num_encoded_slots(&self) -> usize {
self.0
.iter()
.map(|x| usize::try_from(x.0).unwrap())
.sum::<usize>()
}
fn to_slots(&self, last_slot: Slot, min_slot: Slot) -> Vec<Slot> {
let mut slots: Vec<Slot> = self
.0
.iter()
.map_while(|bit_count| usize::try_from(bit_count.0).ok())
.zip([1, 0].iter().cycle())
.flat_map(|(bit_count, bit)| std::iter::repeat(bit).take(bit_count))
.enumerate()
.filter(|(_, bit)| **bit == 1)
.map_while(|(offset, _)| {
let offset = Slot::try_from(offset).ok()?;
last_slot.checked_sub(offset)
})
.take(RestartLastVotedForkSlots::MAX_SLOTS)
.take_while(|slot| *slot >= min_slot)
.collect();
slots.reverse();
slots
}
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, AbiExample)]
struct RawOffsets(BitVec<u8>);
impl RawOffsets {
fn new(mut bits: BitVec<u8>) -> Self {
bits.truncate(RestartLastVotedForkSlots::MAX_BYTES as u64 * 8);
bits.shrink_to_fit();
Self(bits)
}
fn to_slots(&self, last_slot: Slot, min_slot: Slot) -> Vec<Slot> {
let mut slots: Vec<Slot> = (0..self.0.len())
.filter(|index| self.0.get(*index))
.map_while(|offset| last_slot.checked_sub(offset))
.take_while(|slot| *slot >= min_slot)
.collect();
slots.reverse();
slots
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, AbiExample, Debug)]
pub struct RestartLastVotedForkSlots {
pub from: Pubkey,
pub wallclock: u64,
pub slots: Vec<CompressedSlots>,
offsets: SlotsOffsets,
pub last_voted_slot: Slot,
pub last_voted_hash: Hash,
pub shred_version: u16,
}
impl Sanitize for RestartLastVotedForkSlots {
fn sanitize(&self) -> std::result::Result<(), SanitizeError> {
if self.slots.is_empty() {
return Err(SanitizeError::InvalidValue);
}
self.slots.sanitize()?;
self.last_voted_hash.sanitize()
}
}
#[derive(Debug, Error)]
pub enum RestartLastVotedForkSlotsError {
#[error("Last voted fork cannot be empty")]
LastVotedForkEmpty,
}
impl RestartLastVotedForkSlots {
pub fn new(from: Pubkey, now: u64, last_voted_hash: Hash, shred_version: u16) -> Self {
Self {
// This number is MAX_CRDS_OBJECT_SIZE - empty serialized RestartLastVotedForkSlots.
const MAX_BYTES: usize = 824;
// Per design doc, we should start wen_restart within 7 hours.
pub const MAX_SLOTS: usize = u16::MAX as usize;
pub fn new(
from: Pubkey,
now: u64,
last_voted_fork: &[Slot],
last_voted_hash: Hash,
shred_version: u16,
) -> Result<Self, RestartLastVotedForkSlotsError> {
let Some((&first_voted_slot, &last_voted_slot)) =
last_voted_fork.iter().minmax().into_option()
else {
return Err(RestartLastVotedForkSlotsError::LastVotedForkEmpty);
};
let max_size = last_voted_slot.saturating_sub(first_voted_slot) + 1;
let mut uncompressed_bitvec = BitVec::new_fill(false, max_size);
for slot in last_voted_fork {
uncompressed_bitvec.set(last_voted_slot - *slot, true);
}
let run_length_encoding = RunLengthEncoding::new(&uncompressed_bitvec);
let offsets =
if run_length_encoding.num_encoded_slots() > RestartLastVotedForkSlots::MAX_BYTES * 8 {
SlotsOffsets::RunLengthEncoding(run_length_encoding)
} else {
SlotsOffsets::RawOffsets(RawOffsets::new(uncompressed_bitvec))
};
Ok(Self {
from,
wallclock: now,
slots: Vec::new(),
offsets,
last_voted_slot,
last_voted_hash,
shred_version,
}
})
}
/// New random Version for tests and benchmarks.
pub fn new_rand<R: Rng>(rng: &mut R, pubkey: Option<Pubkey>) -> Self {
let pubkey = pubkey.unwrap_or_else(solana_sdk::pubkey::new_rand);
let mut result =
RestartLastVotedForkSlots::new(pubkey, new_rand_timestamp(rng), Hash::new_unique(), 1);
let num_slots = rng.gen_range(2..20);
let mut slots = std::iter::repeat_with(|| 47825632 + rng.gen_range(0..512))
let slots = std::iter::repeat_with(|| 47825632 + rng.gen_range(0..512))
.take(num_slots)
.collect::<Vec<Slot>>();
slots.sort();
result.fill(&slots);
result
}
pub fn fill(&mut self, slots: &[Slot]) -> usize {
let slots = &slots[slots.len().saturating_sub(MAX_SLOTS_PER_ENTRY)..];
let mut num = 0;
let space = self.max_compressed_slot_size();
if space == 0 {
return 0;
}
while num < slots.len() {
let mut cslot = CompressedSlots::new(space as usize);
num += cslot.add(&slots[num..]);
self.slots.push(cslot);
}
num
}
pub fn deflate(&mut self) {
for s in self.slots.iter_mut() {
let _ = s.deflate();
}
}
pub fn max_compressed_slot_size(&self) -> isize {
let len_header = serialized_size(self).unwrap();
let len_slot = serialized_size(&CompressedSlots::default()).unwrap();
MAX_CRDS_OBJECT_SIZE as isize - (len_header + len_slot) as isize
RestartLastVotedForkSlots::new(
pubkey,
new_rand_timestamp(rng),
&slots,
Hash::new_unique(),
1,
)
.unwrap()
}
pub fn to_slots(&self, min_slot: Slot) -> Vec<Slot> {
self.slots
.iter()
.filter(|s| min_slot < s.first_slot() + s.num_slots() as u64)
.filter_map(|s| s.to_slots(min_slot).ok())
.flatten()
.collect()
match &self.offsets {
SlotsOffsets::RunLengthEncoding(run_length_encoding) => {
run_length_encoding.to_slots(self.last_voted_slot, min_slot)
}
SlotsOffsets::RawOffsets(raw_offsets) => {
raw_offsets.to_slots(self.last_voted_slot, min_slot)
}
}
}
}
@ -797,6 +889,7 @@ pub(crate) fn sanitize_wallclock(wallclock: u64) -> Result<(), SanitizeError> {
mod test {
use {
super::*,
crate::cluster_info::MAX_CRDS_OBJECT_SIZE,
bincode::{deserialize, Options},
rand::SeedableRng,
rand_chacha::ChaChaRng,
@ -1170,20 +1263,63 @@ mod test {
assert!(!node.should_force_push(&Pubkey::new_unique()));
}
fn make_rand_slots<R: Rng>(rng: &mut R) -> impl Iterator<Item = Slot> + '_ {
repeat_with(|| rng.gen_range(1..5)).scan(0, |slot, step| {
*slot += step;
Some(*slot)
})
}
#[test]
fn test_restart_last_voted_fork_slots_max_bytes() {
let keypair = Keypair::new();
let header = RestartLastVotedForkSlots::new(
keypair.pubkey(),
timestamp(),
&[1, 2],
Hash::default(),
0,
)
.unwrap();
// If the following assert fails, please update RestartLastVotedForkSlots::MAX_BYTES
assert_eq!(
RestartLastVotedForkSlots::MAX_BYTES,
MAX_CRDS_OBJECT_SIZE - serialized_size(&header).unwrap() as usize
);
// Create large enough slots to make sure we are discarding some to make slots fit.
let mut rng = rand::thread_rng();
let large_length = 8000;
let range: Vec<Slot> = make_rand_slots(&mut rng).take(large_length).collect();
let large_slots = RestartLastVotedForkSlots::new(
keypair.pubkey(),
timestamp(),
&range,
Hash::default(),
0,
)
.unwrap();
assert!(serialized_size(&large_slots).unwrap() <= MAX_CRDS_OBJECT_SIZE as u64);
let retrieved_slots = large_slots.to_slots(0);
assert!(retrieved_slots.len() <= range.len());
assert!(retrieved_slots.last().unwrap() - retrieved_slots.first().unwrap() > 5000);
}
#[test]
fn test_restart_last_voted_fork_slots() {
let keypair = Keypair::new();
let slot = 53;
let slot_parent = slot - 5;
let shred_version = 21;
let mut slots = RestartLastVotedForkSlots::new(
let original_slots_vec = [slot_parent, slot];
let slots = RestartLastVotedForkSlots::new(
keypair.pubkey(),
timestamp(),
&original_slots_vec,
Hash::default(),
shred_version,
);
let original_slots_vec = [slot_parent, slot];
slots.fill(&original_slots_vec);
)
.unwrap();
let value =
CrdsValue::new_signed(CrdsData::RestartLastVotedForkSlots(slots.clone()), &keypair);
assert_eq!(value.sanitize(), Ok(()));
@ -1194,33 +1330,62 @@ mod test {
);
assert_eq!(label.pubkey(), keypair.pubkey());
assert_eq!(value.wallclock(), slots.wallclock);
let retrived_slots = slots.to_slots(0);
assert_eq!(retrived_slots.len(), 2);
assert_eq!(retrived_slots[0], slot_parent);
assert_eq!(retrived_slots[1], slot);
let retrieved_slots = slots.to_slots(0);
assert_eq!(retrieved_slots.len(), 2);
assert_eq!(retrieved_slots[0], slot_parent);
assert_eq!(retrieved_slots[1], slot);
let empty_slots = RestartLastVotedForkSlots::new(
let bad_value = RestartLastVotedForkSlots::new(
keypair.pubkey(),
timestamp(),
&[],
Hash::default(),
shred_version,
);
let bad_value =
CrdsValue::new_signed(CrdsData::RestartLastVotedForkSlots(empty_slots), &keypair);
assert_eq!(bad_value.sanitize(), Err(SanitizeError::InvalidValue));
assert!(bad_value.is_err());
let last_slot: Slot = (MAX_SLOTS_PER_ENTRY + 10).try_into().unwrap();
let mut large_slots = RestartLastVotedForkSlots::new(
keypair.pubkey(),
timestamp(),
Hash::default(),
shred_version,
);
let last_slot: Slot = 8000;
let large_slots_vec: Vec<Slot> = (0..last_slot + 1).collect();
large_slots.fill(&large_slots_vec);
let retrived_slots = large_slots.to_slots(0);
assert_eq!(retrived_slots.len(), MAX_SLOTS_PER_ENTRY);
assert_eq!(retrived_slots.first(), Some(&11));
assert_eq!(retrived_slots.last(), Some(&last_slot));
let large_slots = RestartLastVotedForkSlots::new(
keypair.pubkey(),
timestamp(),
&large_slots_vec,
Hash::default(),
shred_version,
)
.unwrap();
assert!(serialized_size(&large_slots).unwrap() < MAX_CRDS_OBJECT_SIZE as u64);
let retrieved_slots = large_slots.to_slots(0);
assert_eq!(retrieved_slots, large_slots_vec);
}
fn check_run_length_encoding(slots: Vec<Slot>) {
let last_voted_slot = slots[slots.len() - 1];
let mut bitvec = BitVec::new_fill(false, last_voted_slot - slots[0] + 1);
for slot in &slots {
bitvec.set(last_voted_slot - slot, true);
}
let rle = RunLengthEncoding::new(&bitvec);
let retrieved_slots = rle.to_slots(last_voted_slot, 0);
assert_eq!(retrieved_slots, slots);
}
#[test]
fn test_run_length_encoding() {
check_run_length_encoding((1000..16384 + 1000).map(|x| x as Slot).collect_vec());
check_run_length_encoding([1000 as Slot].into());
check_run_length_encoding(
[
1000 as Slot,
RestartLastVotedForkSlots::MAX_SLOTS as Slot + 999,
]
.into(),
);
check_run_length_encoding((1000..1800).step_by(2).map(|x| x as Slot).collect_vec());
let mut rng = rand::thread_rng();
let large_length = 500;
let range: Vec<Slot> = make_rand_slots(&mut rng).take(large_length).collect();
check_run_length_encoding(range);
}
}