|
|
@@ -1,5 +1,8 @@
|
|
|
use {
|
|
|
- crate::shred::{Shred, ShredType},
|
|
|
+ crate::{
|
|
|
+ blockstore::MAX_DATA_SHREDS_PER_SLOT,
|
|
|
+ shred::{Shred, ShredType},
|
|
|
+ },
|
|
|
bitflags::bitflags,
|
|
|
serde::{Deserialize, Deserializer, Serialize, Serializer},
|
|
|
solana_sdk::{
|
|
|
@@ -8,7 +11,7 @@ use {
|
|
|
},
|
|
|
std::{
|
|
|
collections::BTreeSet,
|
|
|
- ops::{Range, RangeBounds},
|
|
|
+ ops::{Bound, Range, RangeBounds},
|
|
|
},
|
|
|
};
|
|
|
|
|
|
@@ -112,6 +115,33 @@ pub struct Index {
|
|
|
coding: ShredIndex,
|
|
|
}
|
|
|
|
|
|
+#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
|
|
+pub struct IndexV2 {
|
|
|
+ pub slot: Slot,
|
|
|
+ data: ShredIndexV2,
|
|
|
+ coding: ShredIndexV2,
|
|
|
+}
|
|
|
+
|
|
|
+impl From<IndexV2> for Index {
|
|
|
+ fn from(index: IndexV2) -> Self {
|
|
|
+ Index {
|
|
|
+ slot: index.slot,
|
|
|
+ data: index.data.into(),
|
|
|
+ coding: index.coding.into(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl From<Index> for IndexV2 {
|
|
|
+ fn from(index: Index) -> Self {
|
|
|
+ IndexV2 {
|
|
|
+ slot: index.slot,
|
|
|
+ data: index.data.into(),
|
|
|
+ coding: index.coding.into(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
|
|
pub struct ShredIndex {
|
|
|
/// Map representing presence/absence of shreds
|
|
|
@@ -243,6 +273,10 @@ impl Index {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/// Superseded by [`ShredIndexV2`].
|
|
|
+///
|
|
|
+/// TODO: Remove this once new [`ShredIndexV2`] is fully rolled out
|
|
|
+/// and no longer relies on it for fallback.
|
|
|
impl ShredIndex {
|
|
|
pub fn num_shreds(&self) -> usize {
|
|
|
self.index.len()
|
|
|
@@ -262,6 +296,228 @@ impl ShredIndex {
|
|
|
pub(crate) fn insert(&mut self, index: u64) {
|
|
|
self.index.insert(index);
|
|
|
}
|
|
|
+
|
|
|
+ #[cfg(test)]
|
|
|
+ fn remove(&mut self, index: u64) {
|
|
|
+ self.index.remove(&index);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// A bitvec (`Vec<u8>`) of shred indices, where each u8 represents 8 shred indices.
|
|
|
+///
|
|
|
+/// The current implementation of [`ShredIndex`] utilizes a [`BTreeSet`] to store
|
|
|
+/// shred indices. While [`BTreeSet`] remains efficient as operations are amortized
|
|
|
+/// over time, the overhead of the B-tree structure becomes significant when frequently
|
|
|
+/// serialized and deserialized. In particular:
|
|
|
+/// - **Tree Traversal**: Serialization requires walking the non-contiguous tree structure.
|
|
|
+/// - **Reconstruction**: Deserialization involves rebuilding the tree in bulk,
|
|
|
+/// including dynamic memory allocations and re-balancing nodes.
|
|
|
+///
|
|
|
+/// In contrast, our bit vec implementation provides:
|
|
|
+/// - **Contiguous Memory**: All bits are stored in a contiguous array of u64 words,
|
|
|
+/// allowing direct indexing and efficient memory access patterns.
|
|
|
+/// - **Direct Range Access**: Can load only the specific words that overlap with a
|
|
|
+/// requested range, avoiding unnecessary traversal.
|
|
|
+/// - **Simplified Serialization**: The contiguous memory layout allows for efficient
|
|
|
+/// serialization/deserialization without tree reconstruction.
|
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
+pub struct ShredIndexV2 {
|
|
|
+ #[serde(with = "serde_bytes")]
|
|
|
+ index: Vec<u8>,
|
|
|
+ num_shreds: usize,
|
|
|
+}
|
|
|
+
|
|
|
+impl Default for ShredIndexV2 {
|
|
|
+ fn default() -> Self {
|
|
|
+ Self {
|
|
|
+ index: vec![0; Self::MAX_WORDS_PER_SLOT],
|
|
|
+ num_shreds: 0,
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+type ShredIndexV2Word = u8;
|
|
|
+impl ShredIndexV2 {
|
|
|
+ const SIZE_OF_WORD: usize = std::mem::size_of::<ShredIndexV2Word>();
|
|
|
+ const BITS_PER_WORD: usize = Self::SIZE_OF_WORD * 8;
|
|
|
+ const MAX_WORDS_PER_SLOT: usize = MAX_DATA_SHREDS_PER_SLOT.div_ceil(Self::BITS_PER_WORD);
|
|
|
+
|
|
|
+ pub fn num_shreds(&self) -> usize {
|
|
|
+ self.num_shreds
|
|
|
+ }
|
|
|
+
|
|
|
+ fn index_and_mask(index: u64) -> (usize, ShredIndexV2Word) {
|
|
|
+ let word_idx = index as usize / Self::BITS_PER_WORD;
|
|
|
+ let bit_idx = index as usize % Self::BITS_PER_WORD;
|
|
|
+ let mask = 1 << bit_idx;
|
|
|
+ (word_idx, mask as ShredIndexV2Word)
|
|
|
+ }
|
|
|
+
|
|
|
+ #[cfg(test)]
|
|
|
+ fn remove(&mut self, index: u64) {
|
|
|
+ assert!(
|
|
|
+ index < MAX_DATA_SHREDS_PER_SLOT as u64,
|
|
|
+ "index out of bounds. {index} >= {MAX_DATA_SHREDS_PER_SLOT}"
|
|
|
+ );
|
|
|
+
|
|
|
+ let (word_idx, mask) = Self::index_and_mask(index);
|
|
|
+
|
|
|
+ if self.index[word_idx] & mask != 0 {
|
|
|
+ self.index[word_idx] ^= mask;
|
|
|
+ self.num_shreds -= 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #[allow(unused)]
|
|
|
+ pub(crate) fn contains(&self, idx: u64) -> bool {
|
|
|
+ if idx >= MAX_DATA_SHREDS_PER_SLOT as u64 {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ let (word_idx, mask) = Self::index_and_mask(idx);
|
|
|
+ (self.index[word_idx] & mask) != 0
|
|
|
+ }
|
|
|
+
|
|
|
+ pub(crate) fn insert(&mut self, idx: u64) {
|
|
|
+ if idx >= MAX_DATA_SHREDS_PER_SLOT as u64 {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ let (word_idx, mask) = Self::index_and_mask(idx);
|
|
|
+ if self.index[word_idx] & mask == 0 {
|
|
|
+ self.index[word_idx] |= mask;
|
|
|
+ self.num_shreds += 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Provides an iterator over the set shred indices within a specified range.
|
|
|
+ ///
|
|
|
+ /// # Algorithm
|
|
|
+ /// 1. Divide the specified range into 8-bit words (u8).
|
|
|
+ /// 2. For each word:HH
|
|
|
+ /// - Calculate the base index (position of the word * 8).
|
|
|
+ /// - Process all set bits in the word.
|
|
|
+ /// - For words overlapping the range boundaries:
|
|
|
+ /// - Determine the relevant bit range using boundaries.
|
|
|
+ /// - Mask out bits outside the range.
|
|
|
+ /// - Use bit manipulation to iterate over set bits efficiently.
|
|
|
+ ///
|
|
|
+ /// ## Explanation
|
|
|
+ /// Given range `[75..205]`:
|
|
|
+ ///
|
|
|
+ /// Word layout (each word is 8 bits), where each X represents a bit candidate:
|
|
|
+ /// ```text
|
|
|
+ /// Word 9 (72-79): [..XXXXXX] ← Partial word (start)
|
|
|
+ /// Word 10 (80-87): [XXXXXXXX] ← Full word (entirely in range)
|
|
|
+ /// ...
|
|
|
+ /// Word 25 (200-207): [XXXXXX..] ← Partial word (end)
|
|
|
+ /// ```
|
|
|
+ ///
|
|
|
+ /// Partial Word 9 (contains start boundary 75):
|
|
|
+ /// - Base index = 72
|
|
|
+ /// - Lower boundary = 75 - 72 = 3
|
|
|
+ /// - Lower mask = `11111000` (right-shift)
|
|
|
+ ///
|
|
|
+ /// Partial Word 25 (contains end boundary 205):
|
|
|
+ /// - Base index = 200
|
|
|
+ /// - Upper boundary = 205 - 200 = 5
|
|
|
+ /// - Upper mask = `00111111` (left-shift)
|
|
|
+ ///
|
|
|
+ /// Final mask = `word & lower_mask & upper_mask`
|
|
|
+ ///
|
|
|
+ /// Bit iteration:
|
|
|
+ /// 1. Apply masks to restrict the bits to the range.
|
|
|
+ /// 2. While bits remain in the masked word:
|
|
|
+ /// a. Find the lowest set bit (`trailing_zeros`).
|
|
|
+ /// b. Add the bit's position to the base index.
|
|
|
+ /// c. Clear the lowest set bit (`n & (n - 1)`).
|
|
|
+ /// ```
|
|
|
+ pub(crate) fn range<R>(&self, bounds: R) -> impl Iterator<Item = u64> + '_
|
|
|
+ where
|
|
|
+ R: RangeBounds<u64>,
|
|
|
+ {
|
|
|
+ let start = match bounds.start_bound() {
|
|
|
+ Bound::Included(&n) => n as usize,
|
|
|
+ Bound::Excluded(&n) => n as usize + 1,
|
|
|
+ Bound::Unbounded => 0,
|
|
|
+ };
|
|
|
+ let end = match bounds.end_bound() {
|
|
|
+ Bound::Included(&n) => n as usize + 1,
|
|
|
+ Bound::Excluded(&n) => n as usize,
|
|
|
+ Bound::Unbounded => MAX_DATA_SHREDS_PER_SLOT,
|
|
|
+ };
|
|
|
+
|
|
|
+ let end_word = end
|
|
|
+ .div_ceil(Self::BITS_PER_WORD)
|
|
|
+ .min(Self::MAX_WORDS_PER_SLOT);
|
|
|
+ let start_word = (start / Self::BITS_PER_WORD).min(end_word);
|
|
|
+
|
|
|
+ self.index[start_word..end_word]
|
|
|
+ .iter()
|
|
|
+ .enumerate()
|
|
|
+ .flat_map(move |(word_offset, &word)| {
|
|
|
+ let base_idx = (start_word + word_offset) * Self::BITS_PER_WORD;
|
|
|
+
|
|
|
+ let lower_bound = start.saturating_sub(base_idx);
|
|
|
+ let upper_bound = if base_idx + Self::BITS_PER_WORD > end {
|
|
|
+ end - base_idx
|
|
|
+ } else {
|
|
|
+ Self::BITS_PER_WORD
|
|
|
+ };
|
|
|
+
|
|
|
+ let lower_mask = !0 << lower_bound;
|
|
|
+ let upper_mask = !0 >> (Self::BITS_PER_WORD - upper_bound);
|
|
|
+ let mask = word & lower_mask & upper_mask;
|
|
|
+
|
|
|
+ std::iter::from_fn({
|
|
|
+ let mut remaining = mask;
|
|
|
+ move || {
|
|
|
+ if remaining == 0 {
|
|
|
+ None
|
|
|
+ } else {
|
|
|
+ let bit_idx = remaining.trailing_zeros();
|
|
|
+ // Clear the lowest set bit
|
|
|
+ remaining &= remaining - 1;
|
|
|
+ Some(base_idx as u64 + bit_idx as u64)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ fn iter(&self) -> impl Iterator<Item = u64> + '_ {
|
|
|
+ self.range(0..MAX_DATA_SHREDS_PER_SLOT as u64)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl FromIterator<u64> for ShredIndexV2 {
|
|
|
+ fn from_iter<T: IntoIterator<Item = u64>>(iter: T) -> Self {
|
|
|
+ let mut index = ShredIndexV2::default();
|
|
|
+ for idx in iter {
|
|
|
+ index.insert(idx);
|
|
|
+ }
|
|
|
+ index
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl FromIterator<u64> for ShredIndex {
|
|
|
+ fn from_iter<T: IntoIterator<Item = u64>>(iter: T) -> Self {
|
|
|
+ ShredIndex {
|
|
|
+ index: iter.into_iter().collect(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl From<ShredIndex> for ShredIndexV2 {
|
|
|
+ fn from(value: ShredIndex) -> Self {
|
|
|
+ value.index.into_iter().collect()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl From<ShredIndexV2> for ShredIndex {
|
|
|
+ fn from(value: ShredIndexV2) -> Self {
|
|
|
+ ShredIndex {
|
|
|
+ index: value.iter().collect(),
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
impl SlotMeta {
|
|
|
@@ -574,10 +830,13 @@ impl OptimisticSlotMetaVersioned {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
#[cfg(test)]
|
|
|
mod test {
|
|
|
use {
|
|
|
super::*,
|
|
|
+ bincode::Options,
|
|
|
+ proptest::prelude::*,
|
|
|
rand::{seq::SliceRandom, thread_rng},
|
|
|
};
|
|
|
|
|
|
@@ -624,7 +883,7 @@ mod test {
|
|
|
.collect::<Vec<_>>()
|
|
|
.choose_multiple(&mut rng, erasure_config.num_data)
|
|
|
{
|
|
|
- index.data_mut().index.remove(&idx);
|
|
|
+ index.data_mut().remove(idx);
|
|
|
|
|
|
assert!(e_meta.should_recover_shreds(&index));
|
|
|
}
|
|
|
@@ -637,12 +896,216 @@ mod test {
|
|
|
.collect::<Vec<_>>()
|
|
|
.choose_multiple(&mut rng, erasure_config.num_coding)
|
|
|
{
|
|
|
- index.coding_mut().index.remove(&idx);
|
|
|
+ index.coding_mut().remove(idx);
|
|
|
|
|
|
assert!(!e_meta.should_recover_shreds(&index));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// Generate a random Range<u64>.
|
|
|
+ fn rand_range(range: Range<u64>) -> impl Strategy<Value = Range<u64>> {
|
|
|
+ (range.clone(), range).prop_map(
|
|
|
+ // Avoid descending (empty) ranges
|
|
|
+ |(start, end)| {
|
|
|
+ if start > end {
|
|
|
+ end..start
|
|
|
+ } else {
|
|
|
+ start..end
|
|
|
+ }
|
|
|
+ },
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ proptest! {
|
|
|
+ #[test]
|
|
|
+ fn shred_index_legacy_compat(
|
|
|
+ shreds in rand_range(0..MAX_DATA_SHREDS_PER_SLOT as u64),
|
|
|
+ range in rand_range(0..MAX_DATA_SHREDS_PER_SLOT as u64)
|
|
|
+ ) {
|
|
|
+ let mut legacy = ShredIndex::default();
|
|
|
+ let mut v2 = ShredIndexV2::default();
|
|
|
+
|
|
|
+ for i in shreds {
|
|
|
+ v2.insert(i);
|
|
|
+ legacy.insert(i);
|
|
|
+ }
|
|
|
+
|
|
|
+ for &i in legacy.index.iter() {
|
|
|
+ assert!(v2.contains(i));
|
|
|
+ }
|
|
|
+
|
|
|
+ assert_eq!(v2.num_shreds(), legacy.num_shreds());
|
|
|
+
|
|
|
+ assert_eq!(
|
|
|
+ v2.range(range.clone()).sum::<u64>(),
|
|
|
+ legacy.range(range).sum::<u64>()
|
|
|
+ );
|
|
|
+
|
|
|
+ assert_eq!(ShredIndexV2::from(legacy.clone()), v2.clone());
|
|
|
+ assert_eq!(ShredIndex::from(v2), legacy);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Property: [`Index`] cannot be deserialized from [`IndexV2`].
|
|
|
+ ///
|
|
|
+ /// # Failure cases
|
|
|
+ /// 1. Empty [`IndexV2`]
|
|
|
+ /// - [`ShredIndex`] deserialization should fail due to trailing bytes of `num_shreds`.
|
|
|
+ /// 2. Non-empty [`IndexV2`]
|
|
|
+ /// - Encoded length of [`ShredIndexV2::index`] (`Vec<u8>`) will be relative to a sequence of `u8`,
|
|
|
+ /// resulting in not enough bytes when deserialized into sequence of `u64`.
|
|
|
+ #[test]
|
|
|
+ fn test_legacy_collision(
|
|
|
+ coding_indices in rand_range(0..MAX_DATA_SHREDS_PER_SLOT as u64),
|
|
|
+ data_indices in rand_range(0..MAX_DATA_SHREDS_PER_SLOT as u64),
|
|
|
+ slot in 0..u64::MAX
|
|
|
+ ) {
|
|
|
+ let index = IndexV2 {
|
|
|
+ coding: coding_indices.into_iter().collect(),
|
|
|
+ data: data_indices.into_iter().collect(),
|
|
|
+ slot,
|
|
|
+ };
|
|
|
+ let config = bincode::DefaultOptions::new().with_fixint_encoding().reject_trailing_bytes();
|
|
|
+ let legacy = config.deserialize::<Index>(&config.serialize(&index).unwrap());
|
|
|
+ prop_assert!(legacy.is_err());
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Property: [`IndexV2`] cannot be deserialized from [`Index`].
|
|
|
+ ///
|
|
|
+ /// # Failure cases
|
|
|
+ /// 1. Empty [`Index`]
|
|
|
+ /// - [`ShredIndexV2`] deserialization should fail due to missing `num_shreds` (not enough bytes).
|
|
|
+ /// 2. Non-empty [`Index`]
|
|
|
+ /// - Encoded length of [`ShredIndex::index`] (`BTreeSet<u64>`) will be relative to a sequence of `u64`,
|
|
|
+ /// resulting in trailing bytes when deserialized into sequence of `u8`.
|
|
|
+ #[test]
|
|
|
+ fn test_legacy_collision_inverse(
|
|
|
+ coding_indices in rand_range(0..MAX_DATA_SHREDS_PER_SLOT as u64),
|
|
|
+ data_indices in rand_range(0..MAX_DATA_SHREDS_PER_SLOT as u64),
|
|
|
+ slot in 0..u64::MAX
|
|
|
+ ) {
|
|
|
+ let index = Index {
|
|
|
+ coding: coding_indices.into_iter().collect(),
|
|
|
+ data: data_indices.into_iter().collect(),
|
|
|
+ slot,
|
|
|
+ };
|
|
|
+ let config = bincode::DefaultOptions::new()
|
|
|
+ .with_fixint_encoding()
|
|
|
+ .reject_trailing_bytes();
|
|
|
+ let v2 = config.deserialize::<IndexV2>(&config.serialize(&index).unwrap());
|
|
|
+ prop_assert!(v2.is_err());
|
|
|
+ }
|
|
|
+
|
|
|
+ // Property: range queries should return correct indices
|
|
|
+ #[test]
|
|
|
+ fn range_query_correctness(
|
|
|
+ indices in rand_range(0..MAX_DATA_SHREDS_PER_SLOT as u64),
|
|
|
+ ) {
|
|
|
+ let mut index = ShredIndexV2::default();
|
|
|
+
|
|
|
+ for idx in indices.clone() {
|
|
|
+ index.insert(idx);
|
|
|
+ }
|
|
|
+
|
|
|
+ assert_eq!(
|
|
|
+ index.range(indices.clone()).collect::<Vec<_>>(),
|
|
|
+ indices.into_iter().collect::<Vec<_>>()
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn test_shred_index_v2_range_bounds() {
|
|
|
+ let mut index = ShredIndexV2::default();
|
|
|
+
|
|
|
+ index.insert(10);
|
|
|
+ index.insert(20);
|
|
|
+ index.insert(30);
|
|
|
+ index.insert(40);
|
|
|
+
|
|
|
+ use std::ops::Bound::*;
|
|
|
+
|
|
|
+ // Test all combinations of bounds
|
|
|
+ let test_cases = [
|
|
|
+ // (start_bound, end_bound, expected_result)
|
|
|
+ (Included(10), Included(30), vec![10, 20, 30]),
|
|
|
+ (Included(10), Excluded(30), vec![10, 20]),
|
|
|
+ (Excluded(10), Included(30), vec![20, 30]),
|
|
|
+ (Excluded(10), Excluded(30), vec![20]),
|
|
|
+ // Unbounded start
|
|
|
+ (Unbounded, Included(20), vec![10, 20]),
|
|
|
+ (Unbounded, Excluded(20), vec![10]),
|
|
|
+ // Unbounded end
|
|
|
+ (Included(30), Unbounded, vec![30, 40]),
|
|
|
+ (Excluded(30), Unbounded, vec![40]),
|
|
|
+ // Both Unbounded
|
|
|
+ (Unbounded, Unbounded, vec![10, 20, 30, 40]),
|
|
|
+ ];
|
|
|
+
|
|
|
+ for (start_bound, end_bound, expected) in test_cases {
|
|
|
+ let result: Vec<_> = index.range((start_bound, end_bound)).collect();
|
|
|
+ assert_eq!(
|
|
|
+ result, expected,
|
|
|
+ "Failed for bounds: start={:?}, end={:?}",
|
|
|
+ start_bound, end_bound
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn test_shred_index_v2_boundary_conditions() {
|
|
|
+ let mut index = ShredIndexV2::default();
|
|
|
+
|
|
|
+ // First possible index
|
|
|
+ index.insert(0);
|
|
|
+ // Last index in first word (bits 0-7)
|
|
|
+ index.insert(7);
|
|
|
+ // First index in second word (bits 8-15)
|
|
|
+ index.insert(8);
|
|
|
+ // Last index in second word
|
|
|
+ index.insert(15);
|
|
|
+ // Last valid index
|
|
|
+ index.insert(MAX_DATA_SHREDS_PER_SLOT as u64 - 1);
|
|
|
+ // Should be ignored (too large)
|
|
|
+ index.insert(MAX_DATA_SHREDS_PER_SLOT as u64);
|
|
|
+
|
|
|
+ // Verify contents
|
|
|
+ assert!(index.contains(0));
|
|
|
+ assert!(index.contains(7));
|
|
|
+ assert!(index.contains(8));
|
|
|
+ assert!(index.contains(15));
|
|
|
+ assert!(index.contains(MAX_DATA_SHREDS_PER_SLOT as u64 - 1));
|
|
|
+ assert!(!index.contains(MAX_DATA_SHREDS_PER_SLOT as u64));
|
|
|
+
|
|
|
+ // Cross-word boundary
|
|
|
+ assert_eq!(index.range(6..10).collect::<Vec<_>>(), vec![7, 8]);
|
|
|
+ // Full first word
|
|
|
+ assert_eq!(index.range(0..8).collect::<Vec<_>>(), vec![0, 7]);
|
|
|
+ // Full second word
|
|
|
+ assert_eq!(index.range(8..16).collect::<Vec<_>>(), vec![8, 15]);
|
|
|
+
|
|
|
+ // Empty ranges
|
|
|
+ assert_eq!(index.range(0..0).count(), 0);
|
|
|
+ assert_eq!(index.range(1..1).count(), 0);
|
|
|
+
|
|
|
+ // Test range that exceeds max
|
|
|
+ let oversized_range = index.range(0..MAX_DATA_SHREDS_PER_SLOT as u64 + 1);
|
|
|
+ assert_eq!(oversized_range.count(), 5);
|
|
|
+ assert_eq!(index.num_shreds(), 5);
|
|
|
+
|
|
|
+ index.remove(0);
|
|
|
+ assert!(!index.contains(0));
|
|
|
+ index.remove(7);
|
|
|
+ assert!(!index.contains(7));
|
|
|
+ index.remove(8);
|
|
|
+ assert!(!index.contains(8));
|
|
|
+ index.remove(15);
|
|
|
+ assert!(!index.contains(15));
|
|
|
+ index.remove(MAX_DATA_SHREDS_PER_SLOT as u64 - 1);
|
|
|
+ assert!(!index.contains(MAX_DATA_SHREDS_PER_SLOT as u64 - 1));
|
|
|
+
|
|
|
+ assert_eq!(index.num_shreds(), 0);
|
|
|
+ }
|
|
|
+
|
|
|
#[test]
|
|
|
fn test_connected_flags_compatibility() {
|
|
|
// Define a couple structs with bool and ConnectedFlags to illustrate
|