Skip to main content

ab_farmer_components/
sector.rs

1//! Sector-related data structures
2//!
3//! Sectors and corresponding metadata created by functions in [`plotting`](crate::plotting) module
4//! have a specific structure, represented by data structured in this module.
5//!
6//! It is typically not needed to construct these data structures explicitly outside of this crate,
7//! instead they will be returned as a result of certain operations (like plotting).
8
9#[cfg(test)]
10mod tests;
11
12use ab_core_primitives::checksum::Blake3Checksummed;
13use ab_core_primitives::hashes::Blake3Hash;
14use ab_core_primitives::pieces::{PieceHeader, PieceOffset, Record};
15use ab_core_primitives::sectors::{SBucket, SectorIndex};
16use ab_core_primitives::segments::HistorySize;
17use ab_direct_io_file::AlignedPage;
18use ab_io_type::trivial_type::TrivialType;
19use parity_scale_codec::{Decode, Encode};
20use rayon::prelude::*;
21use std::mem::MaybeUninit;
22use std::ops::{Deref, DerefMut};
23use thiserror::Error;
24use tracing::debug;
25
26/// Size of the part of the plot containing record chunks (s-buckets).
27///
28/// Total size of the plot can be computed with [`sector_size()`].
29#[inline]
30pub const fn sector_record_chunks_size(pieces_in_sector: u16) -> usize {
31    pieces_in_sector as usize * Record::SIZE
32}
33
34/// Size of the part of the plot containing record metadata.
35///
36/// Total size of the plot can be computed with [`sector_size()`].
37#[inline]
38pub const fn sector_record_metadata_size(pieces_in_sector: u16) -> usize {
39    pieces_in_sector as usize * RecordMetadata::encoded_size()
40}
41
42/// Size of the zero padding between record metadata and sector checksum.
43///
44/// Padding makes [`sector_size()`] a multiple of [`AlignedPage::SIZE`].
45#[inline]
46pub const fn sector_padding_size(pieces_in_sector: u16) -> usize {
47    let unpadded_size = sector_record_chunks_size(pieces_in_sector)
48        + sector_record_metadata_size(pieces_in_sector)
49        + SectorContentsMap::encoded_size(pieces_in_sector)
50        + Blake3Hash::SIZE;
51
52    unpadded_size.next_multiple_of(AlignedPage::SIZE) - unpadded_size
53}
54
55/// Exact sector plot size (sector contents map, record chunks, record metadata, padding and
56/// checksum).
57///
58/// NOTE: Each sector also has corresponding fixed size metadata whose size can be obtained with
59/// [`SectorMetadataChecksummed::disk_size()`], size of the record chunks (s-buckets) with
60/// [`sector_record_chunks_size()`], size of record roots and proofs with
61/// [`sector_record_metadata_size()`] and size of padding with [`sector_padding_size()`]. This
62/// function just combines those together for convenience.
63#[inline]
64pub const fn sector_size(pieces_in_sector: u16) -> usize {
65    sector_record_chunks_size(pieces_in_sector)
66        + sector_record_metadata_size(pieces_in_sector)
67        + SectorContentsMap::encoded_size(pieces_in_sector)
68        + sector_padding_size(pieces_in_sector)
69        + Blake3Hash::SIZE
70}
71
72/// Metadata of the plotted sector
73#[derive(Debug, Encode, Decode, Clone)]
74pub struct SectorMetadata {
75    /// Sector index
76    pub sector_index: SectorIndex,
77    /// Number of pieces stored in this sector
78    pub pieces_in_sector: u16,
79    /// S-bucket sizes in a sector
80    pub s_bucket_sizes: Box<[u16; Record::NUM_S_BUCKETS]>,
81    // TODO: Should this be constrained to the last segment of the super segment?
82    /// Size of the blockchain history at the time of sector creation
83    pub history_size: HistorySize,
84}
85
86impl SectorMetadata {
87    /// Returns offsets of each s-bucket relatively to the beginning of the sector (in chunks)
88    pub fn s_bucket_offsets(&self) -> Box<[u32; Record::NUM_S_BUCKETS]> {
89        let s_bucket_offsets = self
90            .s_bucket_sizes
91            .iter()
92            .map({
93                let mut base_offset = 0;
94
95                move |s_bucket_size| {
96                    let offset = base_offset;
97                    base_offset += u32::from(*s_bucket_size);
98                    offset
99                }
100            })
101            .collect::<Box<_>>();
102
103        assert_eq!(s_bucket_offsets.len(), Record::NUM_S_BUCKETS);
104        // SAFETY: Number of elements checked above
105        unsafe {
106            Box::from_raw(Box::into_raw(s_bucket_offsets).cast::<[u32; Record::NUM_S_BUCKETS]>())
107        }
108    }
109}
110
111/// Same as [`SectorMetadata`], but with checksums verified during SCALE encoding/decoding
112#[derive(Debug, Clone, Encode, Decode)]
113pub struct SectorMetadataChecksummed(Blake3Checksummed<SectorMetadata>);
114
115impl From<SectorMetadata> for SectorMetadataChecksummed {
116    #[inline]
117    fn from(value: SectorMetadata) -> Self {
118        Self(Blake3Checksummed(value))
119    }
120}
121
122impl Deref for SectorMetadataChecksummed {
123    type Target = SectorMetadata;
124
125    #[inline]
126    fn deref(&self) -> &Self::Target {
127        &self.0.0
128    }
129}
130
131impl DerefMut for SectorMetadataChecksummed {
132    #[inline]
133    fn deref_mut(&mut self) -> &mut Self::Target {
134        &mut self.0.0
135    }
136}
137
138impl SectorMetadataChecksummed {
139    /// Size of checksummed sector metadata on disk.
140    ///
141    /// Sector metadata is padded with zeroes to a multiple of [`AlignedPage::SIZE`], such that it
142    /// can be written with direct I/O without read-modify-write, see [`Self::encode_for_disk()`].
143    ///
144    /// For sector plot size use [`sector_size()`].
145    #[inline]
146    pub const fn disk_size() -> usize {
147        Self::encoded_size().next_multiple_of(AlignedPage::SIZE)
148    }
149
150    /// Encode checksummed sector metadata for storing on disk, padded with zeroes to
151    /// [`Self::disk_size()`]
152    pub fn encode_for_disk(&self) -> Box<[u8; Self::disk_size()]> {
153        // SAFETY: Bytes filled with zeroes are valid
154        let mut bytes = unsafe { Box::<[u8; Self::disk_size()]>::new_zeroed().assume_init() };
155        self.encode_to(&mut bytes.as_mut_slice());
156        bytes
157    }
158
159    /// Decode checksummed sector metadata stored on disk, the inverse of
160    /// [`Self::encode_for_disk()`]
161    pub fn decode_from_disk(
162        bytes: &[u8; Self::disk_size()],
163    ) -> Result<Self, parity_scale_codec::Error> {
164        Self::decode(&mut bytes.as_slice())
165    }
166
167    /// Size of SCALE-encoded checksummed sector metadata
168    const fn encoded_size() -> usize {
169        // Fields of `SectorMetadata` followed by the checksum
170        SectorIndex::SIZE
171            + size_of::<u16>()
172            + size_of::<[u16; Record::NUM_S_BUCKETS]>()
173            + size_of::<HistorySize>()
174            + Blake3Hash::SIZE
175    }
176}
177
178/// Root and proof corresponding to the same record
179#[derive(Debug, Default, Copy, Clone, Encode, Decode)]
180#[repr(C)]
181pub(crate) struct RecordMetadata {
182    /// Piece header
183    pub(crate) piece_header: PieceHeader,
184    /// Checksum (hash) of the whole piece
185    pub(crate) piece_checksum: Blake3Hash,
186}
187
188const {
189    assert!(align_of::<RecordMetadata>() == 1);
190}
191
192impl RecordMetadata {
193    pub(crate) const fn encoded_size() -> usize {
194        size_of::<Self>()
195    }
196}
197
198/// Raw sector before it is transformed and written to plot, used during plotting
199#[derive(Debug, Clone)]
200pub(crate) struct RawSector {
201    /// List of records, likely downloaded from the network
202    pub(crate) records: Vec<Record>,
203    /// Metadata (root and proof) corresponding to the same record
204    pub(crate) metadata: Vec<RecordMetadata>,
205}
206
207impl RawSector {
208    /// Create new raw sector with internal vectors being allocated and filled with default values
209    pub(crate) fn new(pieces_in_sector: u16) -> Self {
210        Self {
211            records: Record::new_zero_vec(usize::from(pieces_in_sector)),
212            metadata: vec![RecordMetadata::default(); usize::from(pieces_in_sector)],
213        }
214    }
215}
216
217/// S-buckets at which proofs were found.
218///
219/// S-buckets are grouped by 8, within each `u8` bits right to left (LSB) indicate the presence
220/// of a proof for corresponding s-bucket, so that the whole array of bytes can be thought as a
221/// large set of bits.
222///
223/// There are always exactly [`Record::NUM_CHUNKS`] proofs produced/bits set to `1`.
224pub type FoundProofs = [u8; Record::NUM_S_BUCKETS / u8::BITS as usize];
225
226/// Error happening when trying to create [`SectorContentsMap`] from bytes
227#[derive(Debug, Error, Copy, Clone, Eq, PartialEq)]
228pub enum SectorContentsMapFromBytesError {
229    /// Invalid bytes length
230    #[error("Invalid bytes length, expected {expected}, actual {actual}")]
231    InvalidBytesLength {
232        /// Expected length
233        expected: usize,
234        /// Actual length
235        actual: usize,
236    },
237    /// Checksum mismatch
238    #[error("Checksum mismatch")]
239    ChecksumMismatch,
240}
241
242/// Error happening when trying to encode [`SectorContentsMap`] into bytes
243#[derive(Debug, Error, Copy, Clone, Eq, PartialEq)]
244pub enum SectorContentsMapEncodeIntoError {
245    /// Invalid bytes length
246    #[error("Invalid bytes length, expected {expected}, actual {actual}")]
247    InvalidBytesLength {
248        /// Expected length
249        expected: usize,
250        /// Actual length
251        actual: usize,
252    },
253}
254
255/// Error happening when trying to create [`SectorContentsMap`] from bytes
256#[derive(Debug, Error, Copy, Clone, Eq, PartialEq)]
257pub enum SectorContentsMapIterationError {
258    /// S-bucket provided is out of range
259    #[error("S-bucket provided {provided} is out of range, max {max}")]
260    SBucketOutOfRange {
261        /// Provided s-bucket
262        provided: usize,
263        /// Max s-bucket
264        max: usize,
265    },
266}
267
268/// Map of sector contents.
269///
270/// Abstraction on top of bitfields that allow making sense of sector contents that contain
271/// encoded (meaning erasure coded and encoded with existing PoSpace proof) chunks used at the same
272/// time both in records (before writing to plot) and s-buckets (written into the plot) format
273#[derive(Debug, Clone, Eq, PartialEq)]
274pub struct SectorContentsMap {
275    /// Bitfields for each record, each bit is `true` if record chunk at corresponding position was
276    /// used
277    record_chunks_used: Vec<FoundProofs>,
278}
279
280impl SectorContentsMap {
281    /// Create new sector contents map initialized with zeroes to store data for `pieces_in_sector`
282    /// records
283    pub fn new(pieces_in_sector: u16) -> Self {
284        Self {
285            record_chunks_used: vec![[0; _]; usize::from(pieces_in_sector)],
286        }
287    }
288
289    /// Reconstruct sector contents map from bytes.
290    ///
291    /// Returns error if length of the vector doesn't match [`Self::encoded_size()`] for
292    /// `pieces_in_sector`.
293    pub fn from_bytes(
294        bytes: &[u8],
295        pieces_in_sector: u16,
296    ) -> Result<Self, SectorContentsMapFromBytesError> {
297        if bytes.len() != Self::encoded_size(pieces_in_sector) {
298            return Err(SectorContentsMapFromBytesError::InvalidBytesLength {
299                expected: Self::encoded_size(pieces_in_sector),
300                actual: bytes.len(),
301            });
302        }
303
304        let (single_records_bit_arrays, expected_checksum) =
305            bytes.split_at(bytes.len() - Blake3Hash::SIZE);
306        let expected_checksum = Blake3Hash::from_bytes_exact(expected_checksum)
307            .expect("Correct size, no alignment requirements; qed");
308        // Verify checksum
309        let actual_checksum = Blake3Hash::from(blake3::hash(single_records_bit_arrays));
310        if &actual_checksum != expected_checksum {
311            debug!(
312                %actual_checksum,
313                %expected_checksum,
314                "Hash doesn't match, corrupted bytes"
315            );
316
317            return Err(SectorContentsMapFromBytesError::ChecksumMismatch);
318        }
319
320        // Length was checked above, so there is no remainder
321        let record_chunks_used = single_records_bit_arrays.as_chunks().0.to_vec();
322
323        Ok(Self { record_chunks_used })
324    }
325
326    /// Size of sector contents map when encoded and stored in the plot for specified number of
327    /// pieces in sector
328    pub const fn encoded_size(pieces_in_sector: u16) -> usize {
329        size_of::<FoundProofs>() * pieces_in_sector as usize + Blake3Hash::SIZE
330    }
331
332    /// Encode internal contents into `output`, which doesn't need to be initialized.
333    ///
334    /// Returns `output` that is now fully initialized.
335    pub fn encode_into<'a>(
336        &self,
337        output: &'a mut [MaybeUninit<u8>],
338    ) -> Result<&'a mut [u8], SectorContentsMapEncodeIntoError> {
339        if output.len() != Self::encoded_size(self.record_chunks_used.len() as u16) {
340            return Err(SectorContentsMapEncodeIntoError::InvalidBytesLength {
341                expected: Self::encoded_size(self.record_chunks_used.len() as u16),
342                actual: output.len(),
343            });
344        }
345
346        let slice = self.record_chunks_used.as_flattened();
347        // Write data and checksum
348        let (data_output, checksum_output) = output.split_at_mut(slice.len());
349        data_output.write_copy_of_slice(slice);
350        checksum_output.write_copy_of_slice(blake3::hash(slice).as_bytes());
351
352        // SAFETY: Data and checksum together cover the whole `output`, both were initialized above
353        Ok(unsafe { output.assume_init_mut() })
354    }
355
356    /// Iterate over individual record chunks (s-buckets) that were used
357    pub fn iter_record_chunks_used(&self) -> &[FoundProofs] {
358        &self.record_chunks_used
359    }
360
361    /// Iterate mutably over individual record chunks (s-buckets) that were used
362    pub fn iter_record_chunks_used_mut(&mut self) -> &mut [FoundProofs] {
363        &mut self.record_chunks_used
364    }
365
366    /// Returns sizes of each s-bucket
367    pub fn s_bucket_sizes(&self) -> Box<[u16; Record::NUM_S_BUCKETS]> {
368        // Rayon doesn't support iteration over custom types yet
369        let s_bucket_sizes = (u16::from(SBucket::ZERO)..=u16::from(SBucket::MAX))
370            .into_par_iter()
371            .map(SBucket::from)
372            .map(|s_bucket| {
373                self.iter_s_bucket_piece_offsets(s_bucket)
374                    .expect("S-bucket guaranteed to be in range; qed")
375                    .count() as u16
376            })
377            .collect::<Box<_>>();
378
379        assert_eq!(s_bucket_sizes.len(), Record::NUM_S_BUCKETS);
380
381        // SAFETY: Number of elements checked above
382        unsafe {
383            Box::from_raw(Box::into_raw(s_bucket_sizes).cast::<[u16; Record::NUM_S_BUCKETS]>())
384        }
385    }
386
387    /// Creates an iterator of `(s_bucket, chunk_location)`, where `s_bucket` is the position of the
388    /// chunk in the erasure coded record and `chunk_location` is the offset of the chunk in the
389    /// plot (across all s-buckets).
390    pub fn iter_record_chunk_to_plot(
391        &self,
392        piece_offset: PieceOffset,
393    ) -> impl Iterator<Item = (SBucket, usize)> + '_ {
394        // Iterate over all s-buckets
395        (SBucket::ZERO..=SBucket::MAX)
396            // In each s-bucket map all records used
397            .flat_map(|s_bucket| {
398                self.iter_s_bucket_piece_offsets(s_bucket)
399                    .expect("S-bucket guaranteed to be in range; qed")
400                    .map(move |current_piece_offset| (s_bucket, current_piece_offset))
401            })
402            // We've got contents of all s-buckets in a flat iterator, enumerating them so it is
403            // possible to find in the plot later if desired
404            .enumerate()
405            // Everything about the piece offset we care about
406            .filter_map(move |(chunk_location, (s_bucket, current_piece_offset))| {
407                // In case record for `piece_offset` is found, return necessary information
408                (current_piece_offset == piece_offset).then_some((s_bucket, chunk_location))
409            })
410            // Tiny optimization in case we have found chunks for all records already
411            .take(Record::NUM_CHUNKS)
412    }
413
414    /// Creates an iterator of `Option<chunk_offset>`, where each entry corresponds
415    /// s-bucket/position of the chunk in the erasure coded record, `chunk_offset` is the offset of
416    /// the chunk in the corresponding s-bucket.
417    ///
418    /// Similar to `Self::iter_record_chunk_to_plot()`, but runs in parallel, returns entries for
419    /// all s-buckets and offsets are within corresponding s-buckets rather than the whole plot.
420    pub fn par_iter_record_chunk_to_plot(
421        &self,
422        piece_offset: PieceOffset,
423    ) -> impl IndexedParallelIterator<Item = Option<usize>> + '_ {
424        let piece_offset = usize::from(piece_offset);
425        (u16::from(SBucket::ZERO)..=u16::from(SBucket::MAX))
426            .into_par_iter()
427            .map(SBucket::from)
428            // In each s-bucket map all records used
429            .map(move |s_bucket| {
430                let byte_offset = usize::from(s_bucket) / u8::BITS as usize;
431                let bit_mask = 1 << (usize::from(s_bucket) % u8::BITS as usize);
432
433                if self.record_chunks_used[piece_offset][byte_offset] & bit_mask == 0 {
434                    return None;
435                }
436
437                // How many other record chunks we have in s-bucket before piece offset we care
438                // about
439                let chunk_offset = self
440                    .record_chunks_used
441                    .iter()
442                    .take(piece_offset)
443                    .filter(move |record_chunks_used| {
444                        record_chunks_used[byte_offset] & bit_mask != 0
445                    })
446                    .count();
447
448                Some(chunk_offset)
449            })
450    }
451
452    /// Creates an iterator of piece offsets to which corresponding chunks belong.
453    ///
454    /// Returns error if `s_bucket` is outside of [`Record::NUM_S_BUCKETS`] range.
455    pub fn iter_s_bucket_piece_offsets(
456        &self,
457        s_bucket: SBucket,
458    ) -> Result<impl Iterator<Item = PieceOffset> + '_, SectorContentsMapIterationError> {
459        let s_bucket = usize::from(s_bucket);
460
461        if s_bucket >= Record::NUM_S_BUCKETS {
462            return Err(SectorContentsMapIterationError::SBucketOutOfRange {
463                provided: s_bucket,
464                max: Record::NUM_S_BUCKETS,
465            });
466        }
467
468        Ok((PieceOffset::ZERO..)
469            .zip(&self.record_chunks_used)
470            .filter_map(move |(piece_offset, record_chunks_used)| {
471                let byte_offset = s_bucket / u8::BITS as usize;
472                let bit_mask = 1 << (s_bucket % u8::BITS as usize);
473
474                (record_chunks_used[byte_offset] & bit_mask != 0).then_some(piece_offset)
475            }))
476    }
477
478    /// Iterate over chunks of s-bucket indicating if record chunk is used at corresponding
479    /// position.
480    ///
481    /// ## Panics
482    /// Panics if `s_bucket` is outside of [`Record::NUM_S_BUCKETS`] range.
483    pub fn iter_s_bucket_used_record_chunks_used(
484        &self,
485        s_bucket: SBucket,
486    ) -> Result<impl Iterator<Item = bool> + '_, SectorContentsMapIterationError> {
487        let s_bucket = usize::from(s_bucket);
488
489        if s_bucket >= Record::NUM_S_BUCKETS {
490            return Err(SectorContentsMapIterationError::SBucketOutOfRange {
491                provided: s_bucket,
492                max: Record::NUM_S_BUCKETS,
493            });
494        }
495
496        Ok(self
497            .record_chunks_used
498            .iter()
499            .map(move |record_chunks_used| {
500                let byte_offset = s_bucket / u8::BITS as usize;
501                let bit_mask = 1 << (s_bucket % u8::BITS as usize);
502
503                record_chunks_used[byte_offset] & bit_mask != 0
504            }))
505    }
506}