1use 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#[derive(Debug, Clone, Encode, Decode)]
43pub struct PlottedSector {
44 pub sector_id: SectorId,
46 pub sector_index: SectorIndex,
48 pub sector_metadata: SectorMetadataChecksummed,
50 pub piece_indexes: Vec<PieceIndex>,
52}
53
54#[derive(Debug, Error)]
56pub enum PlottingError {
57 #[error("Records encoder error: {error}")]
59 RecordsEncoderError {
60 error: anyhow::Error,
62 },
63 #[error(
65 "Records encoder returned sector contents map for {provided} records, expected {expected}"
66 )]
67 BadSectorContentsMapSize {
68 provided: usize,
70 expected: usize,
72 },
73 #[error(
75 "Records encoder marked {provided} chunks of record {piece_offset} as used, expected \
76 {expected}"
77 )]
78 BadRecordChunksUsed {
79 piece_offset: PieceOffset,
81 provided: u32,
83 expected: usize,
85 },
86 #[error("Bad sector output size: provided {provided}, expected {expected}")]
88 BadSectorOutputSize {
89 provided: usize,
91 expected: usize,
93 },
94 #[error("Can't recover missing piece {piece_index}: {error}")]
96 PieceRecoveryFailed {
97 piece_index: PieceIndex,
99 error: anyhow::Error,
101 },
102 #[error("Failed to retrieve pieces: {error}")]
104 FailedToRetrievePieces {
105 error: anyhow::Error,
107 },
108 #[error("Abort early")]
110 AbortEarly,
111}
112
113#[derive(Debug)]
119pub struct PlotSectorOptions<'a, RE, PG> {
120 pub public_key_hash: &'a Blake3Hash,
122 pub shard_commitments_root: &'a ShardCommitmentHash,
124 pub sector_index: SectorIndex,
126 pub piece_getter: &'a PG,
128 pub farmer_protocol_info: FarmerProtocolInfo,
130 pub erasure_coding: &'a ErasureCoding,
132 pub pieces_in_sector: u16,
134 pub sector_output: &'a mut Vec<u8>,
137 pub downloading_semaphore: Option<Arc<Semaphore>>,
140 pub encoding_semaphore: Option<&'a Semaphore>,
143 pub records_encoder: &'a mut RE,
145 pub abort_early: &'a AtomicBool,
147}
148
149pub 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#[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#[derive(Debug)]
222pub struct DownloadSectorOptions<'a, PG> {
223 pub public_key_hash: &'a Blake3Hash,
225 pub shard_commitments_root: &'a ShardCommitmentHash,
227 pub sector_index: SectorIndex,
229 pub piece_getter: &'a PG,
231 pub farmer_protocol_info: FarmerProtocolInfo,
233 pub erasure_coding: &'a ErasureCoding,
235 pub pieces_in_sector: u16,
237}
238
239pub 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 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 .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
338pub trait RecordsEncoder {
352 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#[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 let mut parity_record_chunks = Record::new_boxed();
410
411 loop {
412 global_mutex.lock_blocking();
414
415 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 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#[derive(Debug)]
470pub struct EncodeSectorOptions<'a, RE>
471where
472 RE: RecordsEncoder,
473{
474 pub sector_index: SectorIndex,
476 pub records_encoder: &'a mut RE,
478 pub abort_early: &'a AtomicBool,
480}
481
482#[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 pub plotted_sector: PlottedSector,
491 raw_sector: RawSector,
492 sector_contents_map: SectorContentsMap,
493}
494
495pub 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(§or_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 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
579pub 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 sector_output.clear();
605 sector_output.reserve_exact(sector_size);
606
607 {
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 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 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 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 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 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 unsafe {
685 sector_output.set_len(sector_size - Blake3Hash::SIZE);
686 }
687
688 let sector_checksum = {
691 let mut hasher = blake3::Hasher::new();
692 hasher.update_rayon(sector_output);
693 hasher.finalize()
694 };
695 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_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 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 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 let recovery_semaphore = &Semaphore::new(RECONSTRUCTION_CONCURRENCY_LIMIT);
754
755 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 record.copy_from_slice(&*piece.record);
870 }
871}