diff --git a/core/src/shred_fetch_stage.rs b/core/src/shred_fetch_stage.rs index ec106000e6dc4d..7434c3382ee1a8 100644 --- a/core/src/shred_fetch_stage.rs +++ b/core/src/shred_fetch_stage.rs @@ -16,7 +16,7 @@ use { BytesPacket, BytesPacketBatch, PacketBatch, PacketBatchRecycler, PacketFlags, PacketRef, }, solana_pubkey::Pubkey, - solana_runtime::bank_forks::BankForks, + solana_runtime::bank_forks::{BankForks, SharableBanks}, solana_streamer::{ evicting_sender::EvictingSender, streamer::{self, ChannelSend, PacketBatchReceiver, StreamerReceiveStats}, @@ -64,7 +64,7 @@ impl ShredFetchStage { recvr: PacketBatchReceiver, recvr_stats: Option>, sendr: EvictingSender, - bank_forks: &RwLock, + sharable_banks: &SharableBanks, shred_version: u16, name: &'static str, flags: PacketFlags, @@ -82,18 +82,17 @@ impl ShredFetchStage { let ( mut last_root, mut slots_per_epoch, - mut _feature_set, - mut _epoch_schedule, + mut feature_set, + mut epoch_schedule, mut last_slot, ) = { - let bank_forks_r = bank_forks.read().unwrap(); - let root_bank = bank_forks_r.root_bank(); + let root_bank = sharable_banks.root(); ( root_bank.slot(), root_bank.get_slots_in_epoch(root_bank.epoch()), root_bank.feature_set.clone(), root_bank.epoch_schedule().clone(), - bank_forks_r.highest_slot(), + sharable_banks.working().slot(), ) }; let mut stats = ShredFetchStats::default(); @@ -101,13 +100,10 @@ impl ShredFetchStage { for mut packet_batch in recvr { if last_updated.elapsed().as_millis() as u64 > DEFAULT_MS_PER_SLOT { last_updated = Instant::now(); - let root_bank = { - let bank_forks_r = bank_forks.read().unwrap(); - last_slot = bank_forks_r.highest_slot(); - bank_forks_r.root_bank() - }; - _feature_set = root_bank.feature_set.clone(); - _epoch_schedule = root_bank.epoch_schedule().clone(); + last_slot = sharable_banks.working().slot(); + let root_bank = sharable_banks.root(); + feature_set = root_bank.feature_set.clone(); + epoch_schedule = root_bank.epoch_schedule().clone(); last_root = root_bank.slot(); slots_per_epoch = root_bank.get_slots_in_epoch(root_bank.epoch()); keypair = repair_context.as_ref().copied().map(RepairContext::keypair); @@ -149,6 +145,14 @@ impl ShredFetchStage { // Filter out shreds that are way too far in the future to avoid the // overhead of having to hold onto them. let max_slot = last_slot + MAX_SHRED_DISTANCE_MINIMUM.max(2 * slots_per_epoch); + let enforce_fixed_fec_set = |shred_slot| { + check_feature_activation( + &agave_feature_set::enforce_fixed_fec_set::id(), + shred_slot, + &feature_set, + &epoch_schedule, + ) + }; let turbine_disabled = turbine_disabled.load(Ordering::Relaxed); for mut packet in packet_batch.iter_mut().filter(|p| !p.meta().discard()) { if turbine_disabled @@ -157,6 +161,7 @@ impl ShredFetchStage { last_root, max_slot, shred_version, + enforce_fixed_fec_set, &mut stats, ) { @@ -197,6 +202,7 @@ impl ShredFetchStage { repair_context: Option, turbine_disabled: Arc, ) -> (Vec>, JoinHandle<()>) { + let sharable_banks = bank_forks.read().unwrap().sharable_banks(); let (packet_sender, packet_receiver) = EvictingSender::new_bounded(SHRED_FETCH_CHANNEL_SIZE); let receiver_stats = Arc::new(StreamerReceiveStats::new(receiver_name)); @@ -225,7 +231,7 @@ impl ShredFetchStage { packet_receiver, Some(receiver_stats), sender, - &bank_forks, + &sharable_banks, shred_version, name, flags, @@ -315,11 +321,12 @@ impl ShredFetchStage { Builder::new() .name("solTvuFetchRpr".to_string()) .spawn(move || { + let sharable_banks = bank_forks.read().unwrap().sharable_banks(); Self::modify_packets( packet_receiver, None, sender, - &bank_forks, + &sharable_banks, shred_version, "shred_fetch_repair_quic", PacketFlags::REPAIR, @@ -348,11 +355,12 @@ impl ShredFetchStage { Builder::new() .name("solTvuFetchQuic".to_string()) .spawn(move || { + let sharable_banks = bank_forks.read().unwrap().sharable_banks(); Self::modify_packets( packet_receiver, None, sender, - &bank_forks, + &sharable_banks, shred_version, "shred_fetch_quic", PacketFlags::empty(), @@ -436,7 +444,6 @@ pub(crate) fn receive_quic_datagrams( // Returns true if the feature is effective for the shred slot. #[must_use] -#[allow(dead_code)] fn check_feature_activation( feature: &Pubkey, shred_slot: Slot, diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index 5a80219d12fafc..e59b9d3fe60b65 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -1122,6 +1122,10 @@ pub mod raise_cpi_nesting_limit_to_8 { solana_pubkey::declare_id!("6TkHkRmP7JZy1fdM6fg5uXn76wChQBWGokHBJzrLB3mj"); } +pub mod enforce_fixed_fec_set { + solana_pubkey::declare_id!("fixfecLZYMfkGzwq6NJA11Yw6KYztzXiK9QcL3K78in"); +} + pub static FEATURE_NAMES: LazyLock> = LazyLock::new(|| { [ (secp256k1_program_enabled::id(), "secp256k1 program"), @@ -1363,6 +1367,7 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n (raise_block_limits_to_100m::id(), "SIMD-0286: Raise block limit to 100M"), (raise_account_cu_limit::id(), "SIMD-0306: Raise account CU limit to 40% max"), (raise_cpi_nesting_limit_to_8::id(), "SIMD-0296: Raise CPI nesting limit from 4 to 8"), + (enforce_fixed_fec_set::id(), "SIMD-0317: Enforce 32 data + 32 coding shreds"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/ledger/src/blockstore_meta.rs b/ledger/src/blockstore_meta.rs index 8b09b9d6a75f85..25de89684720a1 100644 --- a/ledger/src/blockstore_meta.rs +++ b/ledger/src/blockstore_meta.rs @@ -1,7 +1,7 @@ use { crate::{ bit_vec::BitVec, - shred::{self, Shred, ShredType, MAX_DATA_SHREDS_PER_SLOT}, + shred::{self, Shred, ShredType, DATA_SHREDS_PER_FEC_BLOCK, MAX_DATA_SHREDS_PER_SLOT}, }, bitflags::bitflags, serde::{Deserialize, Deserializer, Serialize, Serializer}, @@ -341,8 +341,14 @@ mod serde_compat_cast { #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] pub(crate) struct ErasureConfig { - num_data: usize, - num_coding: usize, + pub(crate) num_data: usize, + pub(crate) num_coding: usize, +} + +impl ErasureConfig { + pub(crate) fn is_fixed(&self) -> bool { + self.num_data == DATA_SHREDS_PER_FEC_BLOCK && self.num_coding == DATA_SHREDS_PER_FEC_BLOCK + } } #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] diff --git a/ledger/src/shred.rs b/ledger/src/shred.rs index a0e1ffa9c86f0a..6085413651a3fe 100644 --- a/ledger/src/shred.rs +++ b/ledger/src/shred.rs @@ -64,7 +64,9 @@ pub use { }; use { self::{shred_code::ShredCode, traits::Shred as _}, - crate::blockstore::{self}, + crate::{ + blockstore::{self}, + }, assert_matches::debug_assert_matches, bitflags::bitflags, num_enum::{IntoPrimitive, TryFromPrimitive}, @@ -171,6 +173,8 @@ pub enum Error { InvalidDataSize { size: u16, payload: usize }, #[error("Invalid deshred set")] InvalidDeshredSet, + #[error("Invalid erasure config")] + InvalidErasureConfig, #[error("Invalid erasure shard index: {0:?}")] InvalidErasureShardIndex(/*headers:*/ Box), #[error("Invalid merkle proof")] @@ -740,6 +744,7 @@ pub fn should_discard_shred<'a, P>( root: Slot, max_slot: Slot, shred_version: u16, + enforce_fixed_fec_set: impl Fn(Slot) -> bool, stats: &mut ShredFetchStats, ) -> bool where @@ -786,6 +791,11 @@ where stats.index_bad_deserialize += 1; return true; }; + let Some(fec_set_index) = layout::get_fec_set_index(shred) else { + stats.fec_set_index_bad_deserialize += 1; + return true; + }; + match ShredType::from(shred_variant) { ShredType::Code => { if index >= MAX_CODE_SHREDS_PER_SLOT as u32 { @@ -796,6 +806,18 @@ where stats.slot_out_of_range += 1; return true; } + + let Ok(erasure_config) = layout::get_erasure_config(shred) else { + stats.erasure_config_bad_deserialize += 1; + return true; + }; + + if !erasure_config.is_fixed() { + stats.misaligned_erasure_config += 1; + if enforce_fixed_fec_set(slot) { + return true; + } + } } ShredType::Data => { if index >= MAX_DATA_SHREDS_PER_SLOT as u32 { @@ -814,8 +836,29 @@ where stats.slot_out_of_range += 1; return true; } + + let Ok(shred_flags) = layout::get_flags(shred) else { + stats.shred_flags_bad_deserialize += 1; + return true; + }; + if shred_flags.contains(ShredFlags::LAST_SHRED_IN_SLOT) + && !check_last_data_shred_index(index) + { + stats.misaligned_last_data_index += 1; + if enforce_fixed_fec_set(slot) { + return true; + } + } } } + + if !check_fixed_fec_set(index, fec_set_index) { + stats.misaligned_fec_set += 1; + if enforce_fixed_fec_set(slot) { + return true; + } + } + match shred_variant { ShredVariant::MerkleCode { chained: false, .. } | ShredVariant::MerkleData { chained: false, .. } => { @@ -833,6 +876,27 @@ where false } +/// Returns true if `index` and `fec_set_index` are valid under the assumption that +/// all erasure sets contain exactly `DATA_SHREDS_PER_FEC_BLOCK` data and coding shreds: +/// - `index` is between `fec_set_index` and `fec_set_index + DATA_SHREDS_PER_FEC_BLOCK` +/// - `fec_set_index` is a multiple of `DATA_SHREDS_PER_FEC_BLOCK` +fn check_fixed_fec_set(index: u32, fec_set_index: u32) -> bool { + index >= fec_set_index + && index < fec_set_index + DATA_SHREDS_PER_FEC_BLOCK as u32 + && fec_set_index % DATA_SHREDS_PER_FEC_BLOCK as u32 == 0 +} + +/// Returns true if `index` of the last data shred is valid under the assumption that +/// all erasure sets contain exactly `DATA_SHREDS_PER_FEC_BLOCK` data and coding shreds: +/// - `index + 1` must be a multiple of `DATA_SHREDS_PER_FEC_BLOCK` +/// +/// Note: this check is critical to verify that the last fec set is sufficiently sized. +/// This currently is checked post insert in `Blockstore::check_last_fec_set`, but in the +/// future it can be solely checked during ingest +fn check_last_data_shred_index(index: u32) -> bool { + (index + 1) % (DATA_SHREDS_PER_FEC_BLOCK as u32) == 0 +} + pub fn max_ticks_per_n_shreds(num_shreds: u64, shred_data_size: Option) -> u64 { let ticks = create_ticks(1, 0, Hash::default()); max_entries_per_n_shred(&ticks[0], num_shreds, shred_data_size) @@ -903,10 +967,14 @@ mod tests { const SIZE_OF_SHRED_INDEX: usize = 4; const SIZE_OF_SHRED_SLOT: usize = 8; const SIZE_OF_SHRED_VARIANT: usize = 1; + const SIZE_OF_VERSION : usize = 2; + const SIZE_OF_FEC_SET_INDEX: usize = 4; + const OFFSET_OF_SHRED_VARIANT: usize = SIZE_OF_SIGNATURE; const OFFSET_OF_SHRED_SLOT: usize = SIZE_OF_SIGNATURE + SIZE_OF_SHRED_VARIANT; const OFFSET_OF_SHRED_INDEX: usize = OFFSET_OF_SHRED_SLOT + SIZE_OF_SHRED_SLOT; - const OFFSET_OF_SHRED_VARIANT: usize = SIZE_OF_SIGNATURE; + const OFFSET_OF_FEC_SET_INDEX: usize = OFFSET_OF_SHRED_INDEX + SIZE_OF_SHRED_INDEX + SIZE_OF_VERSION; + const OFFSET_OF_NUM_DATA: usize = OFFSET_OF_FEC_SET_INDEX + SIZE_OF_FEC_SET_INDEX; pub(super) fn make_merkle_shreds_for_tests( rng: &mut R, @@ -920,6 +988,7 @@ mod tests { let parent_offset = rng.gen_range(1..=u16::try_from(slot).unwrap_or(u16::MAX)); let parent_slot = slot.checked_sub(u64::from(parent_offset)).unwrap(); let mut data = vec![0u8; data_size]; + let fec_set_index = rng.gen_range(0..21) * DATA_SHREDS_PER_FEC_BLOCK as u32; rng.fill(&mut data[..]); merkle::make_shreds_from_data( &thread_pool, @@ -931,8 +1000,8 @@ mod tests { rng.gen(), // shred_version rng.gen_range(1..64), // reference_tick is_last_in_slot, - rng.gen_range(0..671), // next_shred_index - rng.gen_range(0..781), // next_code_index + fec_set_index, // next_shred_index + fec_set_index, // next_code_index &ReedSolomonCache::default(), &mut ProcessShredsStats::default(), ) @@ -1091,6 +1160,7 @@ mod tests { root, max_slot, shred_version, + |_| true, &mut stats )); } @@ -1103,6 +1173,7 @@ mod tests { root, max_slot, shred_version, + |_| true, &mut stats )); assert_eq!(stats.index_overrun, 1); @@ -1113,6 +1184,7 @@ mod tests { root, max_slot, shred_version, + |_| true, &mut stats )); assert_eq!(stats.index_overrun, 2); @@ -1123,6 +1195,7 @@ mod tests { root, max_slot, shred_version, + |_| true, &mut stats )); assert_eq!(stats.index_overrun, 3); @@ -1133,6 +1206,7 @@ mod tests { root, max_slot, shred_version, + |_| true, &mut stats )); assert_eq!(stats.index_overrun, 4); @@ -1143,6 +1217,7 @@ mod tests { root, max_slot, shred_version, + |_| true, &mut stats )); assert_eq!(stats.index_overrun, 5); @@ -1154,6 +1229,7 @@ mod tests { root, max_slot, shred_version.wrapping_add(1), + |_| true, &mut stats )); assert_eq!(stats.shred_version_mismatch, 1); @@ -1165,6 +1241,7 @@ mod tests { parent_slot + 1, // root max_slot, shred_version, + |_| true, &mut stats )); assert_eq!(stats.slot_out_of_range, 1); @@ -1186,6 +1263,7 @@ mod tests { root, max_slot, shred_version, + |_| true, &mut stats )); assert_eq!(stats.slot_out_of_range, 1); @@ -1207,6 +1285,7 @@ mod tests { root, max_slot, shred_version, + |_| true, &mut stats )); assert_eq!(stats.bad_parent_offset, 1); @@ -1227,6 +1306,7 @@ mod tests { root, max_slot, shred_version, + |_| true, &mut stats )); assert_eq!(stats.index_out_of_bounds, 1); @@ -1243,6 +1323,7 @@ mod tests { root, max_slot, shred_version, + |_| true, &mut stats )); } @@ -1253,6 +1334,7 @@ mod tests { root, max_slot, shred_version.wrapping_add(1), + |_| true, &mut stats )); assert_eq!(stats.shred_version_mismatch, 1); @@ -1264,6 +1346,7 @@ mod tests { slot, // root max_slot, shred_version, + |_| true, &mut stats )); assert_eq!(stats.slot_out_of_range, 1); @@ -1284,12 +1367,165 @@ mod tests { root, max_slot, shred_version, + |_| true, &mut stats )); assert_eq!(stats.index_out_of_bounds, 1); } } + #[test_case(true; "enforce_fixed_fec_set")] + #[test_case(false ; "do_not_enforce_fixed_fec_set")] + fn test_should_discard_shred_fec_set_checks(enforce_fixed_fec_set: bool) { + solana_logger::setup(); + let mut rng = rand::thread_rng(); + let slot = 18_291; + let shreds = make_merkle_shreds_for_tests( + &mut rng, + slot, + 1200 * 5, // data_size + true, // chained + false, // is_last_in_slot + ) + .unwrap(); + let shreds: Vec<_> = shreds.into_iter().map(Shred::from).collect(); + assert_eq!(shreds.iter().map(Shred::fec_set_index).dedup().count(), 1); + + assert_matches!(shreds[0].shred_type(), ShredType::Data); + let parent_slot = shreds[0].parent().unwrap(); + let shred_version = shreds[0].common_header().version; + let root = rng.gen_range(0..parent_slot); + let max_slot = slot + rng.gen_range(1..65536); + + // fec_set_index not multiple of 32 + { + let mut packet = Packet::default(); + shreds[0].copy_to_packet(&mut packet); + + let bad_fec_set_index = 5u32; + { + let mut cursor = Cursor::new(packet.buffer_mut()); + cursor.seek(SeekFrom::Start(OFFSET_OF_FEC_SET_INDEX as u64)).unwrap(); + cursor.write_all(&bad_fec_set_index.to_le_bytes()).unwrap(); + } + + let mut stats = ShredFetchStats::default(); + let should_discard = should_discard_shred( + &packet, + root, + max_slot, + shred_version, + |_| enforce_fixed_fec_set, + &mut stats, + ); + assert_eq!(should_discard, enforce_fixed_fec_set); + assert_eq!(stats.misaligned_fec_set, 1); + } + + // index not in range [fec_set_index, fec_set_index + 32) + { + let mut packet = Packet::default(); + shreds[0].copy_to_packet(&mut packet); + + let fec_set_index = 64u32; // Multiple of 32 + let bad_index = 100u32; // Outside [64, 96) + { + let mut cursor = Cursor::new(packet.buffer_mut()); + cursor + .seek(SeekFrom::Start(OFFSET_OF_SHRED_INDEX as u64)) + .unwrap(); + cursor.write_all(&bad_index.to_le_bytes()).unwrap(); + cursor.seek(SeekFrom::Start(OFFSET_OF_FEC_SET_INDEX as u64)).unwrap(); + cursor.write_all(&fec_set_index.to_le_bytes()).unwrap(); + } + + let mut stats = ShredFetchStats::default(); + let should_discard = should_discard_shred( + &packet, + root, + max_slot, + shred_version, + |_| enforce_fixed_fec_set, + &mut stats, + ); + assert_eq!(should_discard, enforce_fixed_fec_set); + assert_eq!(stats.misaligned_fec_set, 1); + } + + // bad erasure config 16:32 + { + let code_shred = shreds + .iter() + .find(|s| s.shred_type() == ShredType::Code) + .unwrap(); + let mut packet = Packet::default(); + code_shred.copy_to_packet(&mut packet); + + let bad_num_data = 16u16; + { + let mut cursor = Cursor::new(packet.buffer_mut()); + cursor.seek(SeekFrom::Start(OFFSET_OF_NUM_DATA as u64)).unwrap(); + cursor.write_all(&bad_num_data.to_le_bytes()).unwrap(); + } + + let mut stats = ShredFetchStats::default(); + let should_discard = should_discard_shred( + &packet, + root, + max_slot, + shred_version, + |_| enforce_fixed_fec_set, + &mut stats, + ); + assert_eq!(should_discard, enforce_fixed_fec_set); + assert_eq!(stats.misaligned_erasure_config, 1); + } + + // data shred with LAST_SHRED_IN_SLOT flag on shred 30 + let shreds = make_merkle_shreds_for_tests( + &mut rng, + slot, + 1200 * 5, // data_size + true, // chained + true, // is_last_in_slot + ) + .unwrap(); + let shreds: Vec<_> = shreds.into_iter().map(Shred::from).collect(); + let parent_slot = shreds[0].parent().unwrap(); + let shred_version = shreds[0].common_header().version; + let root = rng.gen_range(0..parent_slot); + let data_shreds: Vec<_> = shreds + .iter() + .filter(|s| s.shred_type() == ShredType::Data) + .collect(); + let last_data_shred = data_shreds.last().unwrap(); + assert!(last_data_shred.last_in_slot()); + let mut packet = Packet::default(); + last_data_shred.copy_to_packet(&mut packet); + + let bad_last_index = 30u32; + let fec_set_index = 0u32; + { + let mut cursor = Cursor::new(packet.buffer_mut()); + cursor.seek(SeekFrom::Start(OFFSET_OF_SHRED_INDEX as u64)).unwrap(); + cursor.write_all(&bad_last_index.to_le_bytes()).unwrap(); + cursor.seek(SeekFrom::Start(OFFSET_OF_FEC_SET_INDEX as u64)).unwrap(); + cursor.write_all(&fec_set_index.to_le_bytes()).unwrap(); + } + + let mut stats = ShredFetchStats::default(); + let should_discard = should_discard_shred( + &packet, + root, + max_slot, + shred_version, + |_| enforce_fixed_fec_set, + &mut stats, + ); + assert_eq!(should_discard, enforce_fixed_fec_set); + assert_eq!(stats.misaligned_last_data_index, 1); + } + // Asserts that ShredType is backward compatible with u8. #[test] fn test_shred_type_compat() { diff --git a/ledger/src/shred/merkle.rs b/ledger/src/shred/merkle.rs index af7039f0953bcc..21ac79c649b41a 100644 --- a/ledger/src/shred/merkle.rs +++ b/ledger/src/shred/merkle.rs @@ -196,9 +196,7 @@ impl ShredData { ); // Shred index in the erasure batch. let index = { - let fec_set_index = <[u8; 4]>::try_from(shred.get(79..83)?) - .map(u32::from_le_bytes) - .ok()?; + let fec_set_index = shred::layout::get_fec_set_index(shred)?; shred::layout::get_index(shred)? .checked_sub(fec_set_index) .map(usize::try_from)? diff --git a/ledger/src/shred/stats.rs b/ledger/src/shred/stats.rs index fe8a3f9047c77e..40a864d50b8610 100644 --- a/ledger/src/shred/stats.rs +++ b/ledger/src/shred/stats.rs @@ -58,6 +58,12 @@ pub struct ShredFetchStats { pub(super) bad_shred_type: usize, pub(super) shred_version_mismatch: usize, pub(super) bad_parent_offset: usize, + pub(super) fec_set_index_bad_deserialize: usize, + pub(super) misaligned_fec_set: usize, + pub(super) erasure_config_bad_deserialize: usize, + pub(super) misaligned_erasure_config: usize, + pub(super) shred_flags_bad_deserialize: usize, + pub(super) misaligned_last_data_index: usize, since: Option, pub overflow_shreds: usize, } @@ -182,6 +188,24 @@ impl ShredFetchStats { ("bad_shred_type", self.bad_shred_type, i64), ("shred_version_mismatch", self.shred_version_mismatch, i64), ("bad_parent_offset", self.bad_parent_offset, i64), + ( + "fec_set_index_bad_deserialize", + self.fec_set_index_bad_deserialize, + i64 + ), + ("misaligned_fec_set_size", self.misaligned_fec_set, i64), + ( + "erasure_config_bad_deserialize", + self.erasure_config_bad_deserialize, + i64 + ), + ("misaligned_erasure_config", self.misaligned_erasure_config, i64), + ( + "shred_flags_bad_deserialize", + self.shred_flags_bad_deserialize, + i64 + ), + ("misaligned_last_data_index", self.misaligned_last_data_index, i64), ("overflow_shreds", self.overflow_shreds, i64), ); *self = Self { diff --git a/ledger/src/shred/wire.rs b/ledger/src/shred/wire.rs index f9109ea2e420ee..a71673da166816 100644 --- a/ledger/src/shred/wire.rs +++ b/ledger/src/shred/wire.rs @@ -2,9 +2,12 @@ // deserializing the entire payload. #![deny(clippy::indexing_slicing)] use { - crate::shred::{ - self, merkle_tree::SIZE_OF_MERKLE_ROOT, traits::Shred, Error, Nonce, ShredFlags, ShredId, - ShredType, ShredVariant, SIZE_OF_COMMON_SHRED_HEADER, + crate::{ + blockstore_meta::ErasureConfig, + shred::{ + self, merkle_tree::SIZE_OF_MERKLE_ROOT, traits::Shred, Error, Nonce, ShredFlags, + ShredId, ShredType, ShredVariant, SIZE_OF_COMMON_SHRED_HEADER, + }, }, solana_clock::Slot, solana_hash::Hash, @@ -102,6 +105,12 @@ pub(super) fn get_version(shred: &[u8]) -> Option { Some(u16::from_le_bytes(bytes)) } +#[inline] +pub fn get_fec_set_index(shred: &[u8]) -> Option { + let bytes = <[u8; 4]>::try_from(shred.get(79..79 + 4)?).unwrap(); + Some(u32::from_le_bytes(bytes)) +} + // The caller should verify first that the shred is data and not code! #[inline] pub(super) fn get_parent_offset(shred: &[u8]) -> Option { @@ -163,6 +172,34 @@ pub(crate) fn get_data(shred: &[u8]) -> Result<&[u8], Error> { } } +/// Returns the ErasureConfig specified by the coding shred, or an Error if +/// the shred is a data shred +#[inline] +pub(crate) fn get_erasure_config(shred: &[u8]) -> Result { + if !matches!(get_shred_type(shred).unwrap(), ShredType::Code) { + return Err(Error::InvalidShredType); + } + let Some(num_data_bytes) = shred.get(83..83 + 2) else { + return Err(Error::InvalidPayloadSize(shred.len())); + }; + let Some(num_coding_bytes) = shred.get(85..85 + 2) else { + return Err(Error::InvalidPayloadSize(shred.len())); + }; + let num_data = <[u8; 2]>::try_from(num_data_bytes) + .map(u16::from_le_bytes) + .map(usize::from) + .map_err(|_| Error::InvalidErasureConfig)?; + let num_coding = <[u8; 2]>::try_from(num_coding_bytes) + .map(u16::from_le_bytes) + .map(usize::from) + .map_err(|_| Error::InvalidErasureConfig)?; + + Ok(ErasureConfig { + num_data, + num_coding, + }) +} + #[inline] pub fn get_shred_id(shred: &[u8]) -> Option { Some(ShredId(