Skip to main content

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