1use 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#[derive(Debug, Error)]
28pub enum ReadingError {
29 #[error("Failed to read chunk at location {chunk_location}: {error}")]
34 FailedToReadChunk {
35 chunk_location: u64,
37 error: io::Error,
39 },
40 #[error("PoS proofs don't match sector contents map for record at offset {piece_offset}")]
48 PosProofsMismatch {
49 piece_offset: PieceOffset,
51 },
52 #[error("Failed to erasure-decode record at offset {piece_offset}: {error}")]
54 FailedToErasureDecodeRecord {
55 piece_offset: PieceOffset,
57 error: ErasureCodingError,
59 },
60 #[error("Wrong record size after decoding: expected {expected}, actual {actual}")]
62 WrongRecordSizeAfterDecoding {
63 expected: usize,
65 actual: usize,
67 },
68 #[error("Failed to decode sector contents map: {0}")]
70 FailedToDecodeSectorContentsMap(#[from] SectorContentsMapFromBytesError),
71 #[error("Reading I/O error: {0}")]
73 Io(#[from] io::Error),
74 #[error("Checksum mismatch")]
76 ChecksumMismatch,
77}
78
79impl ReadingError {
80 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#[derive(Debug)]
102pub struct SectorRecordChunks {
103 pub extended_record: Box<ExtendedRecord>,
105 pub present: ShardsPresent<{ Record::NUM_CHUNKS }>,
107}
108
109pub 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 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 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 §or
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 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
243pub 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
265pub(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 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
298pub 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(§or_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 §or_metadata.s_bucket_offsets(),
336 §or_contents_map,
337 &table_generator.create_proofs(§or_id.derive_evaluation_seed(piece_offset)),
338 sector,
339 )
340 .await?;
341 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 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}