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::{Piece, PieceOffset, Record, RecordChunk};
14use ab_core_primitives::sectors::{SBucket, SectorId};
15use ab_erasure_coding::{ErasureCoding, ErasureCodingError, RecoveryShardState};
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    /// Missing proof of space proof.
41    ///
42    /// This is either hardware issue or if happens for everyone all the time an implementation
43    /// bug.
44    #[error("Missing PoS proof for s-bucket {s_bucket}")]
45    MissingPosProof {
46        /// S-bucket
47        s_bucket: SBucket,
48    },
49    /// Failed to erasure-decode record
50    #[error("Failed to erasure-decode record at offset {piece_offset}: {error}")]
51    FailedToErasureDecodeRecord {
52        /// Piece offset
53        piece_offset: PieceOffset,
54        /// Lower-level error
55        error: ErasureCodingError,
56    },
57    /// Wrong record size after decoding
58    #[error("Wrong record size after decoding: expected {expected}, actual {actual}")]
59    WrongRecordSizeAfterDecoding {
60        /// Expected size in bytes
61        expected: usize,
62        /// Actual size in bytes
63        actual: usize,
64    },
65    /// Failed to decode sector contents map
66    #[error("Failed to decode sector contents map: {0}")]
67    FailedToDecodeSectorContentsMap(#[from] SectorContentsMapFromBytesError),
68    /// I/O error occurred
69    #[error("Reading I/O error: {0}")]
70    Io(#[from] io::Error),
71    /// Checksum mismatch
72    #[error("Checksum mismatch")]
73    ChecksumMismatch,
74}
75
76impl ReadingError {
77    /// Whether this error is fatal and renders farm unusable
78    pub fn is_fatal(&self) -> bool {
79        match self {
80            ReadingError::FailedToReadChunk { .. } => false,
81            ReadingError::MissingPosProof { .. } => false,
82            ReadingError::FailedToErasureDecodeRecord { .. } => false,
83            ReadingError::WrongRecordSizeAfterDecoding { .. } => false,
84            ReadingError::FailedToDecodeSectorContentsMap(_) => false,
85            ReadingError::Io(_) => true,
86            ReadingError::ChecksumMismatch => false,
87        }
88    }
89}
90
91/// Read sector record chunks, only plotted s-buckets are returned (in decoded form).
92///
93/// NOTE: This is an async function, but it also does CPU-intensive operation internally, while it
94/// is not very long, make sure it is okay to do so in your context.
95pub async fn read_sector_record_chunks<S, A>(
96    piece_offset: PieceOffset,
97    pieces_in_sector: u16,
98    s_bucket_offsets: &[u32; Record::NUM_S_BUCKETS],
99    sector_contents_map: &SectorContentsMap,
100    pos_proofs: &PosProofs,
101    sector: &ReadAt<S, A>,
102) -> Result<Box<[Option<RecordChunk>; Record::NUM_S_BUCKETS]>, ReadingError>
103where
104    S: ReadAtSync,
105    A: ReadAtAsync,
106{
107    let mut record_chunks = Box::<[Option<RecordChunk>; Record::NUM_S_BUCKETS]>::try_from(
108        vec![None::<RecordChunk>; Record::NUM_S_BUCKETS].into_boxed_slice(),
109    )
110    .expect("Correct size; qed");
111
112    let read_chunks_inputs = record_chunks
113        .par_iter_mut()
114        .zip(sector_contents_map.par_iter_record_chunk_to_plot(piece_offset))
115        .zip(s_bucket_offsets.par_iter())
116        .map(
117            |((maybe_record_chunk, maybe_chunk_offset), &s_bucket_offset)| {
118                let chunk_offset = maybe_chunk_offset?;
119
120                let chunk_location = chunk_offset as u64 + u64::from(s_bucket_offset);
121
122                Some((maybe_record_chunk, chunk_location))
123            },
124        )
125        .flatten()
126        .collect::<Vec<_>>();
127
128    let sector_contents_map_size = SectorContentsMap::encoded_size(pieces_in_sector) as u64;
129    match sector {
130        ReadAt::Sync(sector) => {
131            read_chunks_inputs
132                .into_par_iter()
133                .zip(&pos_proofs.proofs)
134                .try_for_each(|((maybe_record_chunk, chunk_location), pos_proof)| {
135                    let mut record_chunk = [0; RecordChunk::SIZE];
136                    sector
137                        .read_at(
138                            &mut record_chunk,
139                            sector_contents_map_size + chunk_location * RecordChunk::SIZE as u64,
140                        )
141                        .map_err(|error| ReadingError::FailedToReadChunk {
142                            chunk_location,
143                            error,
144                        })?;
145
146                    // TODO: Use SIMD for hashing
147                    record_chunk =
148                        Simd::to_array(Simd::from(record_chunk) ^ Simd::from(*pos_proof.hash()));
149
150                    maybe_record_chunk.replace(RecordChunk::from(record_chunk));
151
152                    Ok::<_, ReadingError>(())
153                })?;
154        }
155        ReadAt::Async(sector) => {
156            let processing_chunks = read_chunks_inputs
157                .into_iter()
158                .zip(&pos_proofs.proofs)
159                .map(
160                    |((maybe_record_chunk, chunk_location), pos_proof)| async move {
161                        let mut record_chunk = [0; RecordChunk::SIZE];
162                        record_chunk.copy_from_slice(
163                            &sector
164                                .read_at(
165                                    vec![0; RecordChunk::SIZE],
166                                    sector_contents_map_size
167                                        + chunk_location * RecordChunk::SIZE as u64,
168                                )
169                                .await
170                                .map_err(|error| ReadingError::FailedToReadChunk {
171                                    chunk_location,
172                                    error,
173                                })?,
174                        );
175
176                        // TODO: Use SIMD for hashing
177                        record_chunk = Simd::to_array(
178                            Simd::from(record_chunk) ^ Simd::from(*pos_proof.hash()),
179                        );
180
181                        maybe_record_chunk.replace(RecordChunk::from(record_chunk));
182
183                        Ok::<_, ReadingError>(())
184                    },
185                )
186                .collect::<FuturesUnordered<_>>()
187                .filter_map(|result| async move { result.err() });
188
189            std::pin::pin!(processing_chunks)
190                .next()
191                .await
192                .map_or(Ok(()), Err)?;
193        }
194    }
195
196    Ok(record_chunks)
197}
198
199/// Given sector record chunks recover extended record chunks (both source and parity)
200pub fn recover_extended_record_chunks(
201    sector_record_chunks: &[Option<RecordChunk>; Record::NUM_S_BUCKETS],
202    piece_offset: PieceOffset,
203    erasure_coding: &ErasureCoding,
204) -> Result<Box<[RecordChunk; Record::NUM_S_BUCKETS]>, ReadingError> {
205    // Restore source record scalars
206
207    let mut recovered_sector_record_chunks = vec![[0u8; RecordChunk::SIZE]; Record::NUM_S_BUCKETS];
208    {
209        let (source_sector_record_chunks, parity_sector_record_chunks) =
210            sector_record_chunks.split_at(Record::NUM_CHUNKS);
211        let (source_recovered_sector_record_chunks, parity_recovered_sector_record_chunks) =
212            recovered_sector_record_chunks.split_at_mut(Record::NUM_CHUNKS);
213
214        let source = source_sector_record_chunks
215            .iter()
216            .zip(source_recovered_sector_record_chunks.iter_mut())
217            .map(
218                |(maybe_input_chunk, output_chunk)| match maybe_input_chunk {
219                    Some(input_chunk) => {
220                        output_chunk.copy_from_slice(input_chunk.as_slice());
221                        RecoveryShardState::Present(input_chunk.as_slice())
222                    }
223                    None => RecoveryShardState::MissingRecover(output_chunk.as_mut_slice()),
224                },
225            );
226        let parity = parity_sector_record_chunks
227            .iter()
228            .zip(parity_recovered_sector_record_chunks.iter_mut())
229            .map(
230                |(maybe_input_chunk, output_chunk)| match maybe_input_chunk {
231                    Some(input_chunk) => {
232                        output_chunk.copy_from_slice(input_chunk.as_slice());
233                        RecoveryShardState::Present(input_chunk.as_slice())
234                    }
235                    None => RecoveryShardState::MissingRecover(output_chunk.as_mut_slice()),
236                },
237            );
238        erasure_coding.recover(source, parity).map_err(|error| {
239            ReadingError::FailedToErasureDecodeRecord {
240                piece_offset,
241                error,
242            }
243        })?;
244    }
245
246    // Allocation in vector can be larger than contents, we need to make sure allocation is the same
247    // as the contents, this should also contain fast path if allocation matches contents
248    let record_chunks = recovered_sector_record_chunks
249        .into_iter()
250        .map(RecordChunk::from)
251        .collect::<Box<_>>();
252    // SAFETY: Size of the data is guaranteed above
253    let record_chunks = unsafe {
254        Box::from_raw(Box::into_raw(record_chunks).cast::<[RecordChunk; Record::NUM_S_BUCKETS]>())
255    };
256
257    Ok(record_chunks)
258}
259
260/// Given sector record chunks recover source record chunks in form of an iterator.
261pub fn recover_source_record(
262    sector_record_chunks: &[Option<RecordChunk>; Record::NUM_S_BUCKETS],
263    piece_offset: PieceOffset,
264    erasure_coding: &ErasureCoding,
265) -> Result<Box<Record>, ReadingError> {
266    // Restore source record scalars
267    let mut recovered_record = Record::new_boxed();
268
269    let (source_sector_record_chunks, parity_sector_record_chunks) =
270        sector_record_chunks.split_at(Record::NUM_CHUNKS);
271    let source = source_sector_record_chunks
272        .iter()
273        .zip(recovered_record.iter_mut())
274        .map(
275            |(maybe_input_chunk, output_chunk)| match maybe_input_chunk {
276                Some(input_chunk) => {
277                    output_chunk.copy_from_slice(input_chunk.as_slice());
278                    RecoveryShardState::Present(input_chunk.as_slice())
279                }
280                None => RecoveryShardState::MissingRecover(output_chunk.as_mut_slice()),
281            },
282        );
283    let parity =
284        parity_sector_record_chunks
285            .iter()
286            .map(|maybe_input_chunk| match maybe_input_chunk {
287                Some(input_chunk) => RecoveryShardState::Present(input_chunk.as_slice()),
288                None => RecoveryShardState::MissingIgnore,
289            });
290    erasure_coding.recover(source, parity).map_err(|error| {
291        ReadingError::FailedToErasureDecodeRecord {
292            piece_offset,
293            error,
294        }
295    })?;
296
297    Ok(recovered_record)
298}
299
300/// Read metadata (roots and proof) for record
301pub(crate) async fn read_record_metadata<S, A>(
302    piece_offset: PieceOffset,
303    pieces_in_sector: u16,
304    sector: &ReadAt<S, A>,
305) -> Result<RecordMetadata, ReadingError>
306where
307    S: ReadAtSync,
308    A: ReadAtAsync,
309{
310    let sector_metadata_start = SectorContentsMap::encoded_size(pieces_in_sector) as u64
311        + sector_record_chunks_size(pieces_in_sector) as u64;
312    // Move to the beginning of the root and proof we care about
313    let record_metadata_offset =
314        sector_metadata_start + RecordMetadata::encoded_size() as u64 * u64::from(piece_offset);
315
316    let mut record_metadata_bytes = vec![0; RecordMetadata::encoded_size()];
317    match sector {
318        ReadAt::Sync(sector) => {
319            sector.read_at(&mut record_metadata_bytes, record_metadata_offset)?;
320        }
321        ReadAt::Async(sector) => {
322            record_metadata_bytes = sector
323                .read_at(record_metadata_bytes, record_metadata_offset)
324                .await?;
325        }
326    }
327    let record_metadata = RecordMetadata::decode(&mut record_metadata_bytes.as_ref())
328        .expect("Length is correct, contents doesn't have specific structure to it; qed");
329
330    Ok(record_metadata)
331}
332
333/// Read piece from sector.
334///
335/// NOTE: Even though this function is async, proof of time table generation is expensive and should
336/// be done in a dedicated thread where blocking is allowed.
337pub async fn read_piece<PosTable, S, A>(
338    piece_offset: PieceOffset,
339    sector_id: &SectorId,
340    sector_metadata: &SectorMetadataChecksummed,
341    sector: &ReadAt<S, A>,
342    erasure_coding: &ErasureCoding,
343    table_generator: &PosTable::Generator,
344) -> Result<Piece, ReadingError>
345where
346    PosTable: Table,
347    S: ReadAtSync,
348    A: ReadAtAsync,
349{
350    let pieces_in_sector = sector_metadata.pieces_in_sector;
351
352    let sector_contents_map = {
353        let mut sector_contents_map_bytes =
354            vec![0; SectorContentsMap::encoded_size(pieces_in_sector)];
355        match sector {
356            ReadAt::Sync(sector) => {
357                sector.read_at(&mut sector_contents_map_bytes, 0)?;
358            }
359            ReadAt::Async(sector) => {
360                sector_contents_map_bytes = sector.read_at(sector_contents_map_bytes, 0).await?;
361            }
362        }
363
364        SectorContentsMap::from_bytes(&sector_contents_map_bytes, pieces_in_sector)?
365    };
366
367    let sector_record_chunks = read_sector_record_chunks(
368        piece_offset,
369        pieces_in_sector,
370        &sector_metadata.s_bucket_offsets(),
371        &sector_contents_map,
372        &table_generator.create_proofs(&sector_id.derive_evaluation_seed(piece_offset)),
373        sector,
374    )
375    .await?;
376    // Restore source record scalars
377    let record = recover_source_record(&sector_record_chunks, piece_offset, erasure_coding)?;
378
379    let RecordMetadata {
380        piece_header,
381        piece_checksum,
382    } = read_record_metadata(piece_offset, pieces_in_sector, sector).await?;
383
384    let mut piece = Piece::default();
385
386    piece.header = piece_header;
387    // Fancy way to insert value to avoid going through stack (if naive dereferencing is used)
388    // and potentially causing stack overflow as the result
389    piece.record.copy_from_slice(&**record);
390
391    // Verify checksum
392    let actual_checksum = Blake3Hash::from(blake3::hash(piece.as_ref()));
393    if actual_checksum != piece_checksum {
394        debug!(
395            ?sector_id,
396            %piece_offset,
397            %actual_checksum,
398            expected_checksum = %piece_checksum,
399            "Hash doesn't match, plotted piece is corrupted"
400        );
401
402        return Err(ReadingError::ChecksumMismatch);
403    }
404
405    Ok(piece.to_shared())
406}