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