Skip to main content

ab_farmer_components/
plotting.rs

1//! Plotting utilities
2//!
3//! This module contains functions and data structures that can be used for plotting purposes
4//! (primarily with CPU).
5//!
6//! Plotted sectors can be written to plot and later [`read`](crate::reading) and/or
7//! [`audited`](crate::auditing)/[`proven`](crate::proving) using other modules of this crate.
8
9use crate::FarmerProtocolInfo;
10use crate::sector::{
11    FoundProofs, RawSector, RecordMetadata, SectorContentsMap, SectorMetadata,
12    SectorMetadataChecksummed, sector_padding_size, sector_record_chunks_size,
13    sector_record_metadata_size, sector_size,
14};
15use crate::segment_reconstruction::recover_missing_piece;
16use ab_core_primitives::hashes::Blake3Hash;
17use ab_core_primitives::pieces::{Piece, PieceIndex, PieceOffset, Record, RecordChunk};
18use ab_core_primitives::pos::PosSeed;
19use ab_core_primitives::sectors::{SBucket, SectorId, SectorIndex};
20use ab_core_primitives::segments::HistorySize;
21use ab_core_primitives::solutions::ShardCommitmentHash;
22use ab_data_retrieval::piece_getter::PieceGetter;
23use ab_erasure_coding::ErasureCoding;
24use ab_proof_of_space::{Table, TableGenerator};
25use async_lock::{Mutex as AsyncMutex, Semaphore};
26use backon::{ExponentialBuilder, Retryable};
27use futures::stream::FuturesUnordered;
28use futures::{StreamExt, select};
29use parity_scale_codec::{Decode, Encode};
30use parking_lot::Mutex;
31use std::collections::HashMap;
32use std::simd::Simd;
33use std::sync::Arc;
34use std::sync::atomic::{AtomicBool, Ordering};
35use std::time::Duration;
36use thiserror::Error;
37use tracing::{debug, trace, warn};
38
39const RECONSTRUCTION_CONCURRENCY_LIMIT: usize = 1;
40
41/// Information about sector that was plotted
42#[derive(Debug, Clone, Encode, Decode)]
43pub struct PlottedSector {
44    /// Sector ID
45    pub sector_id: SectorId,
46    /// Sector index
47    pub sector_index: SectorIndex,
48    /// Sector metadata
49    pub sector_metadata: SectorMetadataChecksummed,
50    /// Indexes of pieces that were plotted
51    pub piece_indexes: Vec<PieceIndex>,
52}
53
54/// Plotting status
55#[derive(Debug, Error)]
56pub enum PlottingError {
57    /// Records encoder error
58    #[error("Records encoder error: {error}")]
59    RecordsEncoderError {
60        /// Lower-level error
61        error: anyhow::Error,
62    },
63    /// Records encoder returned sector contents map for a wrong number of records
64    #[error(
65        "Records encoder returned sector contents map for {provided} records, expected {expected}"
66    )]
67    BadSectorContentsMapSize {
68        /// Number of records in the returned map
69        provided: usize,
70        /// Number of records in the sector
71        expected: usize,
72    },
73    /// Records encoder marked a wrong number of chunks as used in a record
74    #[error(
75        "Records encoder marked {provided} chunks of record {piece_offset} as used, expected \
76        {expected}"
77    )]
78    BadRecordChunksUsed {
79        /// Piece offset of the record
80        piece_offset: PieceOffset,
81        /// Number of chunks marked as used
82        provided: u32,
83        /// Expected number of chunks
84        expected: usize,
85    },
86    /// Bad sector output size
87    #[error("Bad sector output size: provided {provided}, expected {expected}")]
88    BadSectorOutputSize {
89        /// Actual size
90        provided: usize,
91        /// Expected size
92        expected: usize,
93    },
94    /// Can't recover missing piece
95    #[error("Can't recover missing piece {piece_index}: {error}")]
96    PieceRecoveryFailed {
97        /// Piece index
98        piece_index: PieceIndex,
99        /// Lower-level error
100        error: anyhow::Error,
101    },
102    /// Failed to retrieve piece
103    #[error("Failed to retrieve pieces: {error}")]
104    FailedToRetrievePieces {
105        /// Lower-level error
106        error: anyhow::Error,
107    },
108    /// Abort early
109    #[error("Abort early")]
110    AbortEarly,
111}
112
113/// Options for plotting a sector.
114///
115/// Sector output and sector metadata output should be either empty (in which case they'll be
116/// resized to correct size automatically) or correctly sized from the beginning or else error will
117/// be returned.
118#[derive(Debug)]
119pub struct PlotSectorOptions<'a, RE, PG> {
120    /// Public key corresponding to sector
121    pub public_key_hash: &'a Blake3Hash,
122    /// Root of the Merkle Tree of shard commitments
123    pub shard_commitments_root: &'a ShardCommitmentHash,
124    /// Sector index
125    pub sector_index: SectorIndex,
126    /// Getter for pieces of archival history
127    pub piece_getter: &'a PG,
128    /// Farmer protocol info
129    pub farmer_protocol_info: FarmerProtocolInfo,
130    /// Erasure coding instance
131    pub erasure_coding: &'a ErasureCoding,
132    /// How many pieces should sector contain
133    pub pieces_in_sector: u16,
134    /// Where plotted sector should be written, vector must either be empty (in which case it'll be
135    /// resized to correct size automatically) or correctly sized from the beginning
136    pub sector_output: &'a mut Vec<u8>,
137    /// Semaphore for part of the plotting when farmer downloads new sector, allows to limit memory
138    /// usage of the plotting process, permit will be held until the end of the plotting process
139    pub downloading_semaphore: Option<Arc<Semaphore>>,
140    /// Semaphore for part of the plotting when farmer encodes downloaded sector, should typically
141    /// allow one permit at a time for efficient CPU utilization
142    pub encoding_semaphore: Option<&'a Semaphore>,
143    /// Proof of space table generators
144    pub records_encoder: &'a mut RE,
145    /// Whether encoding should be aborted early
146    pub abort_early: &'a AtomicBool,
147}
148
149/// Plot a single sector.
150///
151/// This is a convenient wrapper around [`download_sector`] and [`encode_sector`] functions.
152///
153/// NOTE: Even though this function is async, it has blocking code inside and must be running in a
154/// separate thread in order to prevent blocking an executor.
155pub async fn plot_sector<RE, PG>(
156    options: PlotSectorOptions<'_, RE, PG>,
157) -> Result<PlottedSector, PlottingError>
158where
159    RE: RecordsEncoder,
160    PG: PieceGetter + Send + Sync,
161{
162    let PlotSectorOptions {
163        public_key_hash,
164        shard_commitments_root,
165        sector_index,
166        piece_getter,
167        farmer_protocol_info,
168        erasure_coding,
169        pieces_in_sector,
170        sector_output,
171        downloading_semaphore,
172        encoding_semaphore,
173        records_encoder,
174        abort_early,
175    } = options;
176
177    let _downloading_permit = match downloading_semaphore {
178        Some(downloading_semaphore) => Some(downloading_semaphore.acquire_arc().await),
179        None => None,
180    };
181
182    let download_sector_fut = download_sector(DownloadSectorOptions {
183        public_key_hash,
184        shard_commitments_root,
185        sector_index,
186        piece_getter,
187        farmer_protocol_info,
188        erasure_coding,
189        pieces_in_sector,
190    });
191
192    let _encoding_permit = match encoding_semaphore {
193        Some(encoding_semaphore) => Some(encoding_semaphore.acquire().await),
194        None => None,
195    };
196
197    let encoded_sector = encode_sector(
198        download_sector_fut.await?,
199        EncodeSectorOptions::<RE> {
200            sector_index,
201            records_encoder,
202            abort_early,
203        },
204    )?;
205
206    write_sector(&encoded_sector, sector_output)?;
207
208    Ok(encoded_sector.plotted_sector)
209}
210
211/// Opaque sector downloading result and ready for writing
212#[derive(Debug)]
213pub struct DownloadedSector {
214    sector_id: SectorId,
215    piece_indices: Vec<PieceIndex>,
216    raw_sector: RawSector,
217    history_size: HistorySize,
218}
219
220/// Options for sector downloading
221#[derive(Debug)]
222pub struct DownloadSectorOptions<'a, PG> {
223    /// Public key corresponding to sector
224    pub public_key_hash: &'a Blake3Hash,
225    /// Root of the Merkle Tree of shard commitments
226    pub shard_commitments_root: &'a ShardCommitmentHash,
227    /// Sector index
228    pub sector_index: SectorIndex,
229    /// Getter for pieces of archival history
230    pub piece_getter: &'a PG,
231    /// Farmer protocol info
232    pub farmer_protocol_info: FarmerProtocolInfo,
233    /// Erasure coding instance
234    pub erasure_coding: &'a ErasureCoding,
235    /// How many pieces should sector contain
236    pub pieces_in_sector: u16,
237}
238
239/// Download sector for plotting.
240///
241/// This will identify necessary pieces and download them using provided piece getter, after which
242/// they can be encoded using [`encode_sector`] and written to the plot.
243pub async fn download_sector<PG>(
244    options: DownloadSectorOptions<'_, PG>,
245) -> Result<DownloadedSector, PlottingError>
246where
247    PG: PieceGetter + Send + Sync,
248{
249    let DownloadSectorOptions {
250        public_key_hash,
251        shard_commitments_root,
252        sector_index,
253        piece_getter,
254        farmer_protocol_info,
255        erasure_coding,
256        pieces_in_sector,
257    } = options;
258
259    let sector_id = SectorId::new(
260        public_key_hash,
261        shard_commitments_root,
262        sector_index,
263        farmer_protocol_info.history_size,
264    );
265
266    let piece_indices = (PieceOffset::ZERO..)
267        .take(pieces_in_sector.into())
268        .map(|piece_offset| {
269            sector_id.derive_piece_index(
270                piece_offset,
271                farmer_protocol_info.history_size,
272                farmer_protocol_info.max_pieces_in_sector,
273                farmer_protocol_info.recent_segments,
274                farmer_protocol_info.recent_history_fraction,
275            )
276        })
277        .collect::<Vec<_>>();
278
279    let raw_sector = {
280        let mut raw_sector = RawSector::new(pieces_in_sector);
281        let mut pieces_to_download =
282            HashMap::<PieceIndex, Vec<_>>::with_capacity(usize::from(pieces_in_sector));
283        for (piece_index, (record, metadata)) in piece_indices
284            .iter()
285            .copied()
286            .zip(raw_sector.records.iter_mut().zip(&mut raw_sector.metadata))
287        {
288            pieces_to_download
289                .entry(piece_index)
290                .or_default()
291                .push((record, metadata));
292        }
293        // This map will be mutated, removing piece indices we have already processed
294        let pieces_to_download = AsyncMutex::new(pieces_to_download);
295
296        (|| async {
297            let mut pieces_to_download = pieces_to_download.lock().await;
298
299            if let Err(error) =
300                download_sector_internal(&mut pieces_to_download, piece_getter, erasure_coding)
301                    .await
302            {
303                warn!(
304                    %sector_index,
305                    %error,
306                    %pieces_in_sector,
307                    remaining_pieces = %pieces_to_download.len(),
308                    "Sector downloading attempt failed, will retry later"
309                );
310
311                return Err(error);
312            }
313
314            debug!(%sector_index, "Sector downloaded successfully");
315
316            Ok(())
317        })
318        .retry(
319            ExponentialBuilder::default()
320                .with_min_delay(Duration::from_secs(15))
321                .with_max_delay(Duration::from_mins(10))
322                // Try until we get a valid piece
323                .without_max_times(),
324        )
325        .await?;
326
327        raw_sector
328    };
329
330    Ok(DownloadedSector {
331        sector_id,
332        piece_indices,
333        raw_sector,
334        history_size: farmer_protocol_info.history_size,
335    })
336}
337
338/// Records encoder for plotting purposes.
339///
340/// Each record is erasure coded and proofs of space are generated for it. The proofs must be the
341/// ones that [`TableGenerator::create_proofs()`] returns for
342/// `sector_id.derive_evaluation_seed(piece_offset)`, because reading and proving create them again
343/// the same way. There are exactly [`Record::NUM_CHUNKS`] of them. The record is then replaced
344/// with the chunks at the s-buckets of those proofs, each XORed with the hash of its proof, in
345/// s-bucket order. The returned sector contents map has one entry per record with a bit set for
346/// each of those s-buckets.
347///
348/// [`encode_sector()`] checks the number of entries and the number of bits in each entry, but not
349/// which bits are set. Bits that don't match the proofs result in a sector that can't be read or
350/// proven.
351pub trait RecordsEncoder {
352    /// Encode provided sector records.
353    ///
354    /// Encoding may stop early once `abort_early` is set. The returned map is not used in that
355    /// case.
356    fn encode_records(
357        &mut self,
358        sector_id: &SectorId,
359        records: &mut [Record],
360        abort_early: &AtomicBool,
361    ) -> anyhow::Result<SectorContentsMap>;
362}
363
364/// CPU implementation of [`RecordsEncoder`]
365#[derive(Debug)]
366pub struct CpuRecordsEncoder<'a, PosTable>
367where
368    PosTable: Table,
369{
370    table_generators: &'a [PosTable::Generator],
371    erasure_coding: &'a ErasureCoding,
372    global_mutex: &'a AsyncMutex<()>,
373}
374
375impl<PosTable> RecordsEncoder for CpuRecordsEncoder<'_, PosTable>
376where
377    PosTable: Table,
378{
379    fn encode_records(
380        &mut self,
381        sector_id: &SectorId,
382        records: &mut [Record],
383        abort_early: &AtomicBool,
384    ) -> anyhow::Result<SectorContentsMap> {
385        if self.table_generators.is_empty() {
386            return Err(anyhow::anyhow!("No table generators"));
387        }
388
389        let pieces_in_sector = records
390            .len()
391            .try_into()
392            .map_err(|error| anyhow::anyhow!("Failed to convert pieces in sector: {error}"))?;
393        let mut sector_contents_map = SectorContentsMap::new(pieces_in_sector);
394
395        {
396            let global_mutex = self.global_mutex;
397            let erasure_coding = self.erasure_coding;
398
399            let iter = Mutex::new(
400                (PieceOffset::ZERO..)
401                    .zip(records.iter_mut())
402                    .zip(sector_contents_map.iter_record_chunks_used_mut()),
403            );
404
405            rayon::scope(|scope| {
406                for table_generator in self.table_generators {
407                    scope.spawn(|_scope| {
408                        // Every record overwrites all parity chunks, so it is allocated just once
409                        let mut parity_record_chunks = Record::new_boxed();
410
411                        loop {
412                            // Take mutex briefly to make sure encoding is allowed right now
413                            global_mutex.lock_blocking();
414
415                            // This instead of `while` above because otherwise mutex will be held
416                            // for the duration of the loop and will limit concurrency to 1 record
417                            let Some(((piece_offset, record), record_chunks_used)) =
418                                iter.lock().next()
419                            else {
420                                return;
421                            };
422                            let pos_seed = sector_id.derive_evaluation_seed(piece_offset);
423
424                            record_encoding::<PosTable>(
425                                &pos_seed,
426                                record,
427                                &mut parity_record_chunks,
428                                record_chunks_used,
429                                table_generator,
430                                erasure_coding,
431                            );
432
433                            if abort_early.load(Ordering::Relaxed) {
434                                return;
435                            }
436                        }
437                    });
438                }
439            });
440        }
441
442        Ok(sector_contents_map)
443    }
444}
445
446impl<'a, PosTable> CpuRecordsEncoder<'a, PosTable>
447where
448    PosTable: Table,
449{
450    /// Create a new instance
451    pub fn new(
452        table_generators: &'a [PosTable::Generator],
453        erasure_coding: &'a ErasureCoding,
454        global_mutex: &'a AsyncMutex<()>,
455    ) -> Self {
456        Self {
457            table_generators,
458            erasure_coding,
459            global_mutex,
460        }
461    }
462}
463
464/// Options for encoding a sector.
465///
466/// Sector output and sector metadata output should be either empty (in which case they'll be
467/// resized to correct size automatically) or correctly sized from the beginning or else error will
468/// be returned.
469#[derive(Debug)]
470pub struct EncodeSectorOptions<'a, RE>
471where
472    RE: RecordsEncoder,
473{
474    /// Sector index
475    pub sector_index: SectorIndex,
476    /// Records encoding instance
477    pub records_encoder: &'a mut RE,
478    /// Whether encoding should be aborted early
479    pub abort_early: &'a AtomicBool,
480}
481
482/// Mostly opaque sector encoding result ready for writing
483#[derive(Debug)]
484#[expect(
485    clippy::partial_pub_fields,
486    reason = "Intentionally hiding some fields such that they can only be used by internal APIs"
487)]
488pub struct EncodedSector {
489    /// Information about sector that was plotted
490    pub plotted_sector: PlottedSector,
491    raw_sector: RawSector,
492    sector_contents_map: SectorContentsMap,
493}
494
495/// Encode downloaded sector.
496///
497/// This function encodes downloaded sector records and returns sector encoding result that can be
498/// written using [`write_sector`].
499///
500/// Returns an error if the records encoder returns a sector contents map with a wrong number of
501/// entries or bits, see [`RecordsEncoder`].
502///
503/// Returns [`PlottingError::AbortEarly`] if `abort_early` is set once all the work is done, so the
504/// caller doesn't need to check it again. It takes precedence over the map errors above, because
505/// the map may be incomplete when encoding stopped early.
506pub fn encode_sector<RE>(
507    downloaded_sector: DownloadedSector,
508    encoding_options: EncodeSectorOptions<'_, RE>,
509) -> Result<EncodedSector, PlottingError>
510where
511    RE: RecordsEncoder,
512{
513    let DownloadedSector {
514        sector_id,
515        piece_indices,
516        mut raw_sector,
517        history_size,
518    } = downloaded_sector;
519    let EncodeSectorOptions {
520        sector_index,
521        records_encoder,
522        abort_early,
523    } = encoding_options;
524
525    let pieces_in_sector = raw_sector.records.len().try_into().expect(
526        "Raw sector can only be created in this crate and it is always done correctly; qed",
527    );
528
529    let sector_contents_map = records_encoder
530        .encode_records(&sector_id, &mut raw_sector.records, abort_early)
531        .map_err(|error| PlottingError::RecordsEncoderError { error })?;
532
533    let sector_metadata = SectorMetadataChecksummed::from(SectorMetadata {
534        sector_index,
535        pieces_in_sector,
536        s_bucket_sizes: sector_contents_map.s_bucket_sizes(),
537        history_size,
538    });
539
540    // An encoder that stopped early returns an incomplete map. That must result in `AbortEarly`
541    // and not in a map error, so the map is checked after this.
542    if abort_early.load(Ordering::Acquire) {
543        return Err(PlottingError::AbortEarly);
544    }
545
546    let record_chunks_used = sector_contents_map.iter_record_chunks_used();
547    if record_chunks_used.len() != raw_sector.records.len() {
548        return Err(PlottingError::BadSectorContentsMapSize {
549            provided: record_chunks_used.len(),
550            expected: raw_sector.records.len(),
551        });
552    }
553    for (record_chunks_used, piece_offset) in record_chunks_used.iter().zip(PieceOffset::ZERO..) {
554        let chunks_used = record_chunks_used
555            .iter()
556            .map(|bits| bits.count_ones())
557            .sum::<u32>();
558        if usize::try_from(chunks_used) != Ok(Record::NUM_CHUNKS) {
559            return Err(PlottingError::BadRecordChunksUsed {
560                piece_offset,
561                provided: chunks_used,
562                expected: Record::NUM_CHUNKS,
563            });
564        }
565    }
566
567    Ok(EncodedSector {
568        plotted_sector: PlottedSector {
569            sector_id,
570            sector_index,
571            sector_metadata,
572            piece_indexes: piece_indices,
573        },
574        raw_sector,
575        sector_contents_map,
576    })
577}
578
579/// Write encoded sector into sector output
580pub fn write_sector(
581    encoded_sector: &EncodedSector,
582    sector_output: &mut Vec<u8>,
583) -> Result<(), PlottingError> {
584    let EncodedSector {
585        plotted_sector: _,
586        raw_sector,
587        sector_contents_map,
588    } = encoded_sector;
589
590    let pieces_in_sector = raw_sector.records.len().try_into().expect(
591        "Raw sector can only be created in this crate and it is always done correctly; qed",
592    );
593
594    let sector_size = sector_size(pieces_in_sector);
595
596    if !sector_output.is_empty() && sector_output.len() != sector_size {
597        return Err(PlottingError::BadSectorOutputSize {
598            provided: sector_output.len(),
599            expected: sector_size,
600        });
601    }
602
603    // Every byte of the sector is written below, there is no need to initialize memory beforehand
604    sector_output.clear();
605    sector_output.reserve_exact(sector_size);
606
607    // Write sector to disk in as the following regions:
608    // * sector contents map
609    // * record chunks as s-buckets
610    // * record metadata
611    // * zero padding
612    // * checksum
613    {
614        let (sector_contents_map_region, remaining_bytes) = sector_output
615            .spare_capacity_mut()
616            .split_at_mut(SectorContentsMap::encoded_size(pieces_in_sector));
617        // Slice remaining memory into belonging to s-buckets and metadata
618        let (s_buckets_region, remaining_bytes) =
619            remaining_bytes.split_at_mut(sector_record_chunks_size(pieces_in_sector));
620        let (metadata_region, remaining_bytes) =
621            remaining_bytes.split_at_mut(sector_record_metadata_size(pieces_in_sector));
622        let (padding_region, _remaining_bytes) =
623            remaining_bytes.split_at_mut(sector_padding_size(pieces_in_sector));
624
625        // Write sector contents map so we can decode it later
626        sector_contents_map
627            .encode_into(sector_contents_map_region)
628            .expect("Chunked into correct size above; qed");
629
630        let s_bucket_chunks = s_buckets_region.as_chunks_mut::<{ RecordChunk::SIZE }>().0;
631        let mut num_written_s_bucket_chunks = 0_usize;
632        let mut next_record_chunks_offset = vec![0usize; pieces_in_sector.into()];
633        // Write record chunks, one s-bucket at a time
634        for (piece_offset, output) in (SBucket::ZERO..=SBucket::MAX)
635            .flat_map(|s_bucket| {
636                sector_contents_map
637                    .iter_s_bucket_piece_offsets(s_bucket)
638                    .expect("S-bucket guaranteed to be in range; qed")
639            })
640            .zip(s_bucket_chunks)
641        {
642            let next_record_chunks_offset =
643                &mut next_record_chunks_offset[usize::from(piece_offset)];
644
645            let chunk_position = *next_record_chunks_offset;
646            *next_record_chunks_offset += 1;
647            output.write_copy_of_slice(
648                &raw_sector.records[usize::from(piece_offset)][chunk_position],
649            );
650            num_written_s_bucket_chunks += 1;
651        }
652        // Cheap check before exposing the memory as initialized below
653        assert_eq!(
654            num_written_s_bucket_chunks * RecordChunk::SIZE,
655            s_buckets_region.len(),
656            "Every record of an encoded sector has exactly `Record::NUM_CHUNKS` chunks used, which \
657            is checked in `encode_sector()`, so s-buckets cover all chunks of all records; qed"
658        );
659
660        let metadata_chunks = metadata_region
661            .as_chunks_mut::<{ RecordMetadata::encoded_size() }>()
662            .0;
663        let mut num_written_metadata_chunks = 0_usize;
664        for (record_metadata, output) in raw_sector.metadata.iter().zip(metadata_chunks) {
665            let mut record_metadata_bytes = [0; RecordMetadata::encoded_size()];
666            record_metadata.encode_to(&mut record_metadata_bytes.as_mut_slice());
667            output.write_copy_of_slice(&record_metadata_bytes);
668            num_written_metadata_chunks += 1;
669        }
670        // Cheap check before exposing the memory as initialized below
671        assert_eq!(
672            num_written_metadata_chunks * RecordMetadata::encoded_size(),
673            metadata_region.len(),
674            "Raw sector always has metadata for every record; qed"
675        );
676
677        padding_region.write_filled(0);
678    }
679
680    // SAFETY: The sector contents map is written by `encode_into()`, s-buckets and metadata regions
681    // are covered by contiguous chunks written from their beginning, which is checked above, the
682    // padding region is filled with zeroes, and together they are everything except the checksum.
683    // The capacity was reserved above.
684    unsafe {
685        sector_output.set_len(sector_size - Blake3Hash::SIZE);
686    }
687
688    // It would be more efficient to not re-read the whole sector again, but it makes above code
689    // significantly more convoluted and most likely not worth it
690    let sector_checksum = {
691        let mut hasher = blake3::Hasher::new();
692        hasher.update_rayon(sector_output);
693        hasher.finalize()
694    };
695    // Doesn't reallocate, the capacity was reserved above
696    sector_output.extend_from_slice(sector_checksum.as_bytes());
697
698    Ok(())
699}
700
701fn record_encoding<PosTable>(
702    pos_seed: &PosSeed,
703    record: &mut Record,
704    parity_record_chunks: &mut Record,
705    record_chunks_used: &mut FoundProofs,
706    table_generator: &PosTable::Generator,
707    erasure_coding: &ErasureCoding,
708) where
709    PosTable: Table,
710{
711    let pos_proofs = table_generator.create_proofs_parallel(pos_seed);
712
713    // Erasure code source record chunks
714    erasure_coding
715        .extend(record, parity_record_chunks)
716        .expect("Statically guaranteed valid inputs; qed");
717
718    *record_chunks_used = pos_proofs.found_proofs;
719
720    // TODO: This can probably be optimized by using SIMD
721    let mut num_found_proofs = 0_usize;
722    for (s_buckets, found_proofs) in (0..Record::NUM_S_BUCKETS)
723        .array_chunks::<{ u8::BITS as usize }>()
724        .zip(pos_proofs.found_proofs)
725    {
726        for (proof_offset, s_bucket) in s_buckets.into_iter().enumerate() {
727            if (found_proofs & (1 << proof_offset)) != 0 {
728                let record_chunk = if s_bucket < Record::NUM_CHUNKS {
729                    record[s_bucket]
730                } else {
731                    parity_record_chunks[s_bucket - Record::NUM_CHUNKS]
732                };
733                // TODO: SIMD hashing
734                record[num_found_proofs] = (Simd::from(record_chunk)
735                    ^ Simd::from(*pos_proofs.proofs[num_found_proofs].hash()))
736                .to_array();
737                num_found_proofs += 1;
738            }
739        }
740    }
741}
742
743async fn download_sector_internal<PG>(
744    pieces_to_download: &mut HashMap<PieceIndex, Vec<(&mut Record, &mut RecordMetadata)>>,
745    piece_getter: &PG,
746    erasure_coding: &ErasureCoding,
747) -> Result<(), PlottingError>
748where
749    PG: PieceGetter + Send + Sync,
750{
751    // TODO: Make configurable, likely allowing user to specify RAM usage expectations and inferring
752    //  concurrency from there
753    let recovery_semaphore = &Semaphore::new(RECONSTRUCTION_CONCURRENCY_LIMIT);
754
755    // Allocate to decouple lifetime from `pieces_to_download` that will be modified below
756    let piece_indices = pieces_to_download.keys().copied().collect::<Vec<_>>();
757    let mut downloaded_pieces = piece_getter
758        .get_pieces(piece_indices)
759        .await
760        .map_err(|error| PlottingError::FailedToRetrievePieces { error })?
761        .fuse();
762    let mut reconstructed_pieces = FuturesUnordered::new();
763
764    let mut final_result = Ok(());
765
766    loop {
767        let (piece_index, result) = select! {
768            (piece_index, result) = downloaded_pieces.select_next_some() => {
769                match result {
770                    Ok(Some(piece)) => (piece_index, Ok(piece)),
771                    Ok(None) => {
772                        trace!(%piece_index, "Piece was not found, trying reconstruction");
773
774                        reconstructed_pieces.push(reconstruct_piece(
775                            piece_index,
776                            recovery_semaphore,
777                            piece_getter,
778                            erasure_coding,
779                        ));
780                        continue;
781                    }
782                    Err(error) => {
783                        trace!(
784                            %error,
785                            %piece_index,
786                            "Failed to download piece, trying reconstruction"
787                        );
788
789                        reconstructed_pieces.push(reconstruct_piece(
790                            piece_index,
791                            recovery_semaphore,
792                            piece_getter,
793                            erasure_coding,
794                        ));
795                        continue;
796                    }
797                }
798            },
799            (piece_index, result) = reconstructed_pieces.select_next_some() => {
800                (piece_index, result)
801            },
802            complete => {
803                break;
804            }
805        };
806
807        match result {
808            Ok(piece) => {
809                process_piece(piece_index, piece, pieces_to_download);
810            }
811            Err(error) => {
812                trace!(%error, %piece_index, "Failed to download piece");
813
814                if final_result.is_ok() {
815                    final_result = Err(error);
816                }
817            }
818        }
819    }
820
821    if final_result.is_ok() && !pieces_to_download.is_empty() {
822        return Err(PlottingError::FailedToRetrievePieces {
823            error: anyhow::anyhow!(
824                "Successful result, but not all pieces were downloaded, this is likely a piece \
825                getter implementation bug"
826            ),
827        });
828    }
829
830    final_result
831}
832
833async fn reconstruct_piece<PG>(
834    piece_index: PieceIndex,
835    recovery_semaphore: &Semaphore,
836    piece_getter: &PG,
837    erasure_coding: &ErasureCoding,
838) -> (PieceIndex, Result<Piece, PlottingError>)
839where
840    PG: PieceGetter + Send + Sync,
841{
842    let _permit = recovery_semaphore.acquire().await;
843    let recovered_piece_fut =
844        recover_missing_piece(piece_getter, erasure_coding.clone(), piece_index);
845
846    (
847        piece_index,
848        recovered_piece_fut
849            .await
850            .map_err(|error| PlottingError::PieceRecoveryFailed {
851                piece_index,
852                error: error.into(),
853            }),
854    )
855}
856
857fn process_piece(
858    piece_index: PieceIndex,
859    piece: Piece,
860    pieces_to_download: &mut HashMap<PieceIndex, Vec<(&mut Record, &mut RecordMetadata)>>,
861) {
862    for (record, metadata) in pieces_to_download.remove(&piece_index).unwrap_or_default() {
863        *metadata = RecordMetadata {
864            piece_header: piece.header,
865            piece_checksum: blake3::hash(piece.as_ref()).into(),
866        };
867        // Fancy way to insert value to avoid going through stack (if naive dereferencing is used)
868        // and potentially causing stack overflow as the result
869        record.copy_from_slice(&*piece.record);
870    }
871}