Prechádzať zdrojové kódy

Blockstore: Migrate ShredIndex type to more efficient data structure (#3900)

Zach Brown 10 mesiacov pred
rodič
commit
f8e5b1672d

+ 1 - 0
Cargo.lock

@@ -7671,6 +7671,7 @@ dependencies = [
  "mockall",
  "num_cpus",
  "num_enum",
+ "proptest",
  "prost",
  "qualifier_attr",
  "rand 0.8.5",

+ 1 - 0
ledger/Cargo.toml

@@ -31,6 +31,7 @@ lru = { workspace = true }
 mockall = { workspace = true }
 num_cpus = { workspace = true }
 num_enum = { workspace = true }
+proptest = { workspace = true }
 prost = { workspace = true }
 qualifier_attr = { workspace = true }
 rand = { workspace = true }

+ 7 - 0
ledger/proptest-regressions/blockstore_meta.txt

@@ -0,0 +1,7 @@
+# Seeds for failure cases proptest has generated in the past. It is
+# automatically read and these particular cases re-run before any
+# novel cases are generated.
+#
+# It is recommended to check this file in to source control so that
+# everyone who runs the test benefits from these saved cases.
+cc d28b14f167a3950cfc2a5b82dff1e15c65e9ac23a5c249f812e69af96c3489ed # shrinks to coding_indices = 0..0, data_indices = 2984..15152, slot = 0

+ 37 - 8
ledger/src/blockstore_db.rs

@@ -1,8 +1,7 @@
 pub use rocksdb::Direction as IteratorDirection;
 use {
     crate::{
-        blockstore_meta,
-        blockstore_meta::MerkleRootMeta,
+        blockstore_meta::{self, MerkleRootMeta},
         blockstore_metrics::{
             maybe_enable_rocksdb_perf, report_rocksdb_read_perf, report_rocksdb_write_perf,
             BlockstoreRocksDbColumnFamilyMetrics, PerfSamplingStatus, PERF_METRIC_OP_NAME_GET,
@@ -11,7 +10,7 @@ use {
         },
         blockstore_options::{AccessType, BlockstoreOptions, LedgerColumnOptions},
     },
-    bincode::{deserialize, serialize},
+    bincode::{deserialize, Options as BincodeOptions},
     byteorder::{BigEndian, ByteOrder},
     log::*,
     prost::Message,
@@ -796,6 +795,14 @@ pub trait ColumnName {
 
 pub trait TypedColumn: Column {
     type Type: Serialize + DeserializeOwned;
+
+    fn deserialize(data: &[u8]) -> Result<Self::Type> {
+        Ok(bincode::deserialize(data)?)
+    }
+
+    fn serialize(data: &Self::Type) -> Result<Vec<u8>> {
+        Ok(bincode::serialize(data)?)
+    }
 }
 
 impl TypedColumn for columns::AddressSignatures {
@@ -1210,6 +1217,28 @@ impl ColumnName for columns::Index {
 }
 impl TypedColumn for columns::Index {
     type Type = blockstore_meta::Index;
+
+    fn deserialize(data: &[u8]) -> Result<Self::Type> {
+        let config = bincode::DefaultOptions::new()
+            // `bincode::serialize` uses fixint encoding by default, so we need to use the same here
+            .with_fixint_encoding()
+            .reject_trailing_bytes();
+
+        // Migration strategy for new column format:
+        // 1. Release 1: Add ability to read new format as fallback, keep writing old format
+        // 2. Release 2: Switch to writing new format, keep reading old format as fallback
+        // 3. Release 3: Remove old format support once stable
+        // This allows safe downgrade to Release 1 since it can read both formats
+        // https://github.com/anza-xyz/agave/issues/3570
+        let index: bincode::Result<blockstore_meta::Index> = config.deserialize(data);
+        match index {
+            Ok(index) => Ok(index),
+            Err(_) => {
+                let index: blockstore_meta::IndexV2 = config.deserialize(data)?;
+                Ok(index.into())
+            }
+        }
+    }
 }
 
 impl SlotColumn for columns::DeadSlots {}
@@ -1662,7 +1691,7 @@ where
         let result = self
             .backend
             .multi_get_cf(self.handle(), keys)
-            .map(|out| Ok(out?.as_deref().map(deserialize).transpose()?));
+            .map(|out| out?.as_deref().map(C::deserialize).transpose());
 
         if let Some(op_start_instant) = is_perf_enabled {
             // use multi-get instead
@@ -1689,7 +1718,7 @@ where
             &self.read_perf_status,
         );
         if let Some(pinnable_slice) = self.backend.get_pinned_cf(self.handle(), key)? {
-            let value = deserialize(pinnable_slice.as_ref())?;
+            let value = C::deserialize(pinnable_slice.as_ref())?;
             result = Ok(Some(value))
         }
 
@@ -1709,7 +1738,7 @@ where
             self.column_options.rocks_perf_sample_interval,
             &self.write_perf_status,
         );
-        let serialized_value = serialize(value)?;
+        let serialized_value = C::serialize(value)?;
 
         let key = Self::key_from_index(index);
         let result = self.backend.put_cf(self.handle(), &key, &serialized_value);
@@ -1732,7 +1761,7 @@ where
         value: &C::Type,
     ) -> Result<()> {
         let key = Self::key_from_index(index);
-        let serialized_value = serialize(value)?;
+        let serialized_value = C::serialize(value)?;
         batch.put_cf(self.handle(), &key, &serialized_value)
     }
 }
@@ -2254,7 +2283,7 @@ pub mod tests {
         C: ColumnIndexDeprecation + TypedColumn + ColumnName,
     {
         pub fn put_deprecated(&self, index: C::DeprecatedIndex, value: &C::Type) -> Result<()> {
-            let serialized_value = serialize(value)?;
+            let serialized_value = C::serialize(value)?;
             self.backend
                 .put_cf(self.handle(), &C::deprecated_key(index), &serialized_value)
         }

+ 467 - 4
ledger/src/blockstore_meta.rs

@@ -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

+ 78 - 0
programs/sbf/Cargo.lock

@@ -683,6 +683,21 @@ dependencies = [
  "syn 2.0.87",
 ]
 
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
 [[package]]
 name = "bitflags"
 version = "1.3.2"
@@ -3884,6 +3899,26 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "proptest"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50"
+dependencies = [
+ "bit-set",
+ "bit-vec",
+ "bitflags 2.7.0",
+ "lazy_static",
+ "num-traits",
+ "rand 0.8.5",
+ "rand_chacha 0.3.1",
+ "rand_xorshift",
+ "regex-syntax",
+ "rusty-fork",
+ "tempfile",
+ "unarray",
+]
+
 [[package]]
 name = "prost"
 version = "0.11.9"
@@ -3982,6 +4017,12 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
 [[package]]
 name = "quinn"
 version = "0.11.6"
@@ -4114,6 +4155,15 @@ dependencies = [
  "rand_core 0.5.1",
 ]
 
+[[package]]
+name = "rand_xorshift"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
 [[package]]
 name = "rand_xoshiro"
 version = "0.6.0"
@@ -4531,6 +4581,18 @@ version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
 
+[[package]]
+name = "rusty-fork"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
+dependencies = [
+ "fnv",
+ "quick-error",
+ "tempfile",
+ "wait-timeout",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.4"
@@ -6077,6 +6139,7 @@ dependencies = [
  "mockall",
  "num_cpus",
  "num_enum",
+ "proptest",
  "prost",
  "qualifier_attr",
  "rand 0.8.5",
@@ -9660,6 +9723,12 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
 
+[[package]]
+name = "unarray"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
+
 [[package]]
 name = "unicase"
 version = "2.6.0"
@@ -9831,6 +9900,15 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
 
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "walkdir"
 version = "2.3.1"

+ 78 - 0
svm/examples/Cargo.lock

@@ -603,6 +603,21 @@ dependencies = [
  "syn 2.0.87",
 ]
 
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
 [[package]]
 name = "bitflags"
 version = "1.3.2"
@@ -3775,6 +3790,26 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "proptest"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50"
+dependencies = [
+ "bit-set",
+ "bit-vec",
+ "bitflags 2.7.0",
+ "lazy_static",
+ "num-traits",
+ "rand 0.8.5",
+ "rand_chacha 0.3.1",
+ "rand_xorshift",
+ "regex-syntax",
+ "rusty-fork",
+ "tempfile",
+ "unarray",
+]
+
 [[package]]
 name = "prost"
 version = "0.11.9"
@@ -3873,6 +3908,12 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
 [[package]]
 name = "quinn"
 version = "0.11.6"
@@ -4006,6 +4047,15 @@ dependencies = [
  "rand_core 0.5.1",
 ]
 
+[[package]]
+name = "rand_xorshift"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
 [[package]]
 name = "rand_xoshiro"
 version = "0.6.0"
@@ -4403,6 +4453,18 @@ version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
 
+[[package]]
+name = "rusty-fork"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
+dependencies = [
+ "fnv",
+ "quick-error",
+ "tempfile",
+ "wait-timeout",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.18"
@@ -5907,6 +5969,7 @@ dependencies = [
  "mockall",
  "num_cpus",
  "num_enum",
+ "proptest",
  "prost",
  "qualifier_attr",
  "rand 0.8.5",
@@ -8978,6 +9041,12 @@ version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
 
+[[package]]
+name = "unarray"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
+
 [[package]]
 name = "unicase"
 version = "2.8.0"
@@ -9140,6 +9209,15 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
 
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "walkdir"
 version = "2.5.0"