Skip to main content

ab_farmer_components/
reading.rs

1//! Reading utilities
2//!
3//! This module contains utilities for extracting data from plots/sectors created by functions in
4//! [`plotting`](crate::plotting) module earlier. This is a relatively expensive operation and is
5//! only used for cold storage purposes or when there is a need to prove a solution to consensus.
6
7use crate::sector::{
8    RecordMetadata, SectorContentsMap, SectorContentsMapFromBytesError, SectorMetadataChecksummed,
9    sector_record_chunks_size,
10};
11use crate::{ReadAt, ReadAtAsync, ReadAtSync};
12use ab_core_primitives::hashes::Blake3Hash;
13use ab_core_primitives::pieces::{ExtendedRecord, Piece, PieceOffset, Record, RecordChunk};
14use ab_core_primitives::sectors::SectorId;
15use ab_erasure_coding::{ErasureCoding, ErasureCodingError, ShardsPresent};
16use ab_proof_of_space::{PosProofs, Table, TableGenerator};
17use futures::StreamExt;
18use futures::stream::FuturesUnordered;
19use parity_scale_codec::Decode;
20use rayon::prelude::*;
21use std::io;
22use std::simd::Simd;
23use thiserror::Error;
24use tracing::debug;
25
26/// Errors that happen during reading
27#[derive(Debug, Error)]
28pub enum ReadingError {
29    /// Failed to read chunk.
30    ///
31    /// This is an implementation bug, most likely due to mismatch between sector contents map and
32    /// other farming parameters.
33    #[error("Failed to read chunk at location {chunk_location}: {error}")]
34    FailedToReadChunk {
35        /// Chunk location
36        chunk_location: u64,
37        /// Low-level error
38        error: io::Error,
39    },
40    /// PoS proofs don't match the sector contents map.
41    ///
42    /// Either the sector contents map has no entry for the record, or the record's chunks in it
43    /// are not at exactly the s-buckets that have proofs. There is no entry when the piece offset
44    /// is outside of the sector. Chunks at other s-buckets usually mean that the sector doesn't
45    /// match its metadata, so proofs are created for a different sector ID than the one the sector
46    /// was plotted with.
47    #[error("PoS proofs don't match sector contents map for record at offset {piece_offset}")]
48    PosProofsMismatch {
49        /// Piece offset
50        piece_offset: PieceOffset,
51    },
52    /// Failed to erasure-decode record
53    #[error("Failed to erasure-decode record at offset {piece_offset}: {error}")]
54    FailedToErasureDecodeRecord {
55        /// Piece offset
56        piece_offset: PieceOffset,
57        /// Lower-level error
58        error: ErasureCodingError,
59    },
60    /// Wrong record size after decoding
61    #[error("Wrong record size after decoding: expected {expected}, actual {actual}")]
62    WrongRecordSizeAfterDecoding {
63        /// Expected size in bytes
64        expected: usize,
65        /// Actual size in bytes
66        actual: usize,
67    },
68    /// Failed to decode sector contents map
69    #[error("Failed to decode sector contents map: {0}")]
70    FailedToDecodeSectorContentsMap(#[from] SectorContentsMapFromBytesError),
71    /// I/O error occurred
72    #[error("Reading I/O error: {0}")]
73    Io(#[from] io::Error),
74    /// Checksum mismatch
75    #[error("Checksum mismatch")]
76    ChecksumMismatch,
77}
78
79impl ReadingError {
80    /// Whether this error is fatal and renders farm unusable
81    pub fn is_fatal(&self) -> bool {
82        #[expect(
83            clippy::rest_pattern_accessible_field,
84            reason = "Do not care about fields"
85        )]
86        match self {
87            ReadingError::FailedToReadChunk { .. } => false,
88            ReadingError::PosProofsMismatch { .. } => false,
89            ReadingError::FailedToErasureDecodeRecord { .. } => false,
90            ReadingError::WrongRecordSizeAfterDecoding { .. } => false,
91            ReadingError::FailedToDecodeSectorContentsMap(_) => false,
92            ReadingError::Io(_) => true,
93            ReadingError::ChecksumMismatch => false,
94        }
95    }
96}
97
98/// Record chunks read from a sector.
99///
100/// Contents of chunks that are not present are unspecified.
101#[derive(Debug)]
102pub struct SectorRecordChunks {
103    /// Source and parity chunks
104    pub extended_record: Box<ExtendedRecord>,
105    /// Which chunks are present
106    pub present: ShardsPresent<{ Record::NUM_CHUNKS }>,
107}
108
109/// Read sector record chunks, only plotted s-buckets are marked as present (in decoded form).
110///
111/// Returns [`ReadingError::PosProofsMismatch`] if the sector contents map has no entry for
112/// `piece_offset`, or if the record's chunks in it are not at exactly the s-buckets of
113/// `pos_proofs`. Each chunk was encoded with the proof for its s-bucket, so chunks can't be
114/// decoded correctly otherwise.
115///
116/// NOTE: This is an async function, but it also does CPU-intensive operation internally, while it
117/// is not very long, make sure it is okay to do so in your context.
118pub async fn read_sector_record_chunks<S, A>(
119    piece_offset: PieceOffset,
120    pieces_in_sector: u16,
121    s_bucket_offsets: &[u32; Record::NUM_S_BUCKETS],
122    sector_contents_map: &SectorContentsMap,
123    pos_proofs: &PosProofs,
124    sector: &ReadAt<S, A>,
125) -> Result<SectorRecordChunks, ReadingError>
126where
127    S: ReadAtSync,
128    A: ReadAtAsync,
129{
130    // Chunks are paired with proofs in s-bucket order below, which is only correct when the record
131    // has chunks at exactly the s-buckets with proofs
132    if sector_contents_map
133        .iter_record_chunks_used()
134        .get(usize::from(piece_offset))
135        != Some(&pos_proofs.found_proofs)
136    {
137        return Err(ReadingError::PosProofsMismatch { piece_offset });
138    }
139
140    let mut extended_record = ExtendedRecord::new_boxed();
141    let mut present = ShardsPresent::none();
142
143    let read_chunks_inputs = extended_record
144        .as_s_buckets_mut()
145        .par_iter_mut()
146        .zip(sector_contents_map.par_iter_record_chunk_to_plot(piece_offset))
147        .zip(s_bucket_offsets.par_iter())
148        .enumerate()
149        .map(
150            |(index, ((record_chunk, maybe_chunk_offset), &s_bucket_offset))| {
151                let chunk_offset = maybe_chunk_offset?;
152
153                let chunk_location = chunk_offset as u64 + u64::from(s_bucket_offset);
154
155                Some((index, record_chunk, chunk_location))
156            },
157        )
158        .flatten()
159        .collect::<Vec<_>>();
160
161    for &(index, _, _) in &read_chunks_inputs {
162        if let Some(parity_index) = index.checked_sub(Record::NUM_CHUNKS) {
163            present.parity.set(parity_index);
164        } else {
165            present.source.set(index);
166        }
167    }
168
169    let sector_contents_map_size = SectorContentsMap::encoded_size(pieces_in_sector) as u64;
170    match sector {
171        ReadAt::Sync(sector) => {
172            read_chunks_inputs
173                .into_par_iter()
174                .zip(&pos_proofs.proofs)
175                .try_for_each(|((_index, output_chunk, chunk_location), pos_proof)| {
176                    let mut record_chunk = [0; RecordChunk::SIZE];
177                    sector
178                        .read_at(
179                            &mut record_chunk,
180                            sector_contents_map_size + chunk_location * RecordChunk::SIZE as u64,
181                        )
182                        .map_err(|error| ReadingError::FailedToReadChunk {
183                            chunk_location,
184                            error,
185                        })?;
186
187                    // TODO: Use SIMD for hashing
188                    record_chunk =
189                        Simd::to_array(Simd::from(record_chunk) ^ Simd::from(*pos_proof.hash()));
190
191                    *output_chunk = RecordChunk::from(record_chunk);
192
193                    Ok::<_, ReadingError>(())
194                })?;
195        }
196        ReadAt::Async(sector) => {
197            let processing_chunks = read_chunks_inputs
198                .into_iter()
199                .zip(&pos_proofs.proofs)
200                .map(
201                    |((_index, output_chunk, chunk_location), pos_proof)| async move {
202                        let mut record_chunk = [0; RecordChunk::SIZE];
203                        record_chunk.copy_from_slice(
204                            &sector
205                                .read_at(
206                                    vec![0; RecordChunk::SIZE],
207                                    sector_contents_map_size
208                                        + chunk_location * RecordChunk::SIZE as u64,
209                                )
210                                .await
211                                .map_err(|error| ReadingError::FailedToReadChunk {
212                                    chunk_location,
213                                    error,
214                                })?,
215                        );
216
217                        // TODO: Use SIMD for hashing
218                        record_chunk = Simd::to_array(
219                            Simd::from(record_chunk) ^ Simd::from(*pos_proof.hash()),
220                        );
221
222                        *output_chunk = RecordChunk::from(record_chunk);
223
224                        Ok::<_, ReadingError>(())
225                    },
226                )
227                .collect::<FuturesUnordered<_>>()
228                .filter_map(|result| async move { result.err() });
229
230            std::pin::pin!(processing_chunks)
231                .next()
232                .await
233                .map_or(Ok(()), Err)?;
234        }
235    }
236
237    Ok(SectorRecordChunks {
238        extended_record,
239        present,
240    })
241}
242
243/// Given sector record chunks recover the source record in place
244pub fn recover_source_record<'a>(
245    sector_record_chunks: &'a mut SectorRecordChunks,
246    piece_offset: PieceOffset,
247    erasure_coding: &ErasureCoding,
248) -> Result<&'a Record, ReadingError> {
249    let SectorRecordChunks {
250        extended_record,
251        present,
252    } = sector_record_chunks;
253    let ExtendedRecord { source, parity } = &mut **extended_record;
254
255    erasure_coding
256        .recover_source(source, parity, present)
257        .map_err(|error| ReadingError::FailedToErasureDecodeRecord {
258            piece_offset,
259            error,
260        })?;
261
262    Ok(source)
263}
264
265/// Read metadata (roots and proof) for record
266pub(crate) async fn read_record_metadata<S, A>(
267    piece_offset: PieceOffset,
268    pieces_in_sector: u16,
269    sector: &ReadAt<S, A>,
270) -> Result<RecordMetadata, ReadingError>
271where
272    S: ReadAtSync,
273    A: ReadAtAsync,
274{
275    let sector_metadata_start = SectorContentsMap::encoded_size(pieces_in_sector) as u64
276        + sector_record_chunks_size(pieces_in_sector) as u64;
277    // Move to the beginning of the root and proof we care about
278    let record_metadata_offset =
279        sector_metadata_start + RecordMetadata::encoded_size() as u64 * u64::from(piece_offset);
280
281    let mut record_metadata_bytes = vec![0; RecordMetadata::encoded_size()];
282    match sector {
283        ReadAt::Sync(sector) => {
284            sector.read_at(&mut record_metadata_bytes, record_metadata_offset)?;
285        }
286        ReadAt::Async(sector) => {
287            record_metadata_bytes = sector
288                .read_at(record_metadata_bytes, record_metadata_offset)
289                .await?;
290        }
291    }
292    let record_metadata = RecordMetadata::decode(&mut record_metadata_bytes.as_ref())
293        .expect("Length is correct, contents doesn't have specific structure to it; qed");
294
295    Ok(record_metadata)
296}
297
298/// Read piece from sector.
299///
300/// NOTE: Even though this function is async, proof of time table generation is expensive and should
301/// be done in a dedicated thread where blocking is allowed.
302pub async fn read_piece<PosTable, S, A>(
303    piece_offset: PieceOffset,
304    sector_id: &SectorId,
305    sector_metadata: &SectorMetadataChecksummed,
306    sector: &ReadAt<S, A>,
307    erasure_coding: &ErasureCoding,
308    table_generator: &PosTable::Generator,
309) -> Result<Piece, ReadingError>
310where
311    PosTable: Table,
312    S: ReadAtSync,
313    A: ReadAtAsync,
314{
315    let pieces_in_sector = sector_metadata.pieces_in_sector;
316
317    let sector_contents_map = {
318        let mut sector_contents_map_bytes =
319            vec![0; SectorContentsMap::encoded_size(pieces_in_sector)];
320        match sector {
321            ReadAt::Sync(sector) => {
322                sector.read_at(&mut sector_contents_map_bytes, 0)?;
323            }
324            ReadAt::Async(sector) => {
325                sector_contents_map_bytes = sector.read_at(sector_contents_map_bytes, 0).await?;
326            }
327        }
328
329        SectorContentsMap::from_bytes(&sector_contents_map_bytes, pieces_in_sector)?
330    };
331
332    let mut sector_record_chunks = read_sector_record_chunks(
333        piece_offset,
334        pieces_in_sector,
335        &sector_metadata.s_bucket_offsets(),
336        &sector_contents_map,
337        &table_generator.create_proofs(&sector_id.derive_evaluation_seed(piece_offset)),
338        sector,
339    )
340    .await?;
341    // Restore source record scalars
342    let record = recover_source_record(&mut sector_record_chunks, piece_offset, erasure_coding)?;
343
344    let RecordMetadata {
345        piece_header,
346        piece_checksum,
347    } = read_record_metadata(piece_offset, pieces_in_sector, sector).await?;
348
349    let piece = Piece::from_parts(&piece_header, record);
350
351    // Verify checksum
352    let actual_checksum = Blake3Hash::from(blake3::hash(piece.as_ref()));
353    if actual_checksum != piece_checksum {
354        debug!(
355            ?sector_id,
356            %piece_offset,
357            %actual_checksum,
358            expected_checksum = %piece_checksum,
359            "Hash doesn't match, plotted piece is corrupted"
360        );
361
362        return Err(ReadingError::ChecksumMismatch);
363    }
364
365    Ok(piece)
366}