Skip to main content

subspace_farmer_components/
proving.rs

1//! Utilities for turning solution candidates (from auditing) into solutions (proving)
2//!
3//! Solutions generated by [`auditing`](crate::auditing) need to be converted into actual solutions
4//! before they can be sent to the node and this is exactly what this module is about.
5
6use crate::auditing::ChunkCandidate;
7use crate::reading::{
8    ReadSectorRecordChunksMode, ReadingError, read_record_metadata, read_sector_record_chunks,
9};
10use crate::sector::{
11    SectorContentsMap, SectorContentsMapFromBytesError, SectorMetadataChecksummed,
12};
13use crate::{ReadAt, ReadAtSync};
14use futures::FutureExt;
15use std::collections::VecDeque;
16use std::io;
17use subspace_core_primitives::pieces::{PieceOffset, Record};
18use subspace_core_primitives::pos::PosSeed;
19use subspace_core_primitives::sectors::{SBucket, SectorId};
20use subspace_core_primitives::segments::HistorySize;
21use subspace_core_primitives::solutions::{ChunkWitness, Solution, SolutionRange};
22use subspace_core_primitives::{PublicKey, ScalarBytes};
23use subspace_erasure_coding::ErasureCoding;
24use subspace_kzg::Kzg;
25use subspace_proof_of_space::Table;
26use thiserror::Error;
27
28/// Solutions that can be proven if necessary.
29///
30/// Solutions are generated on demand during iteration.
31pub trait ProvableSolutions: ExactSizeIterator {
32    /// Best solution distance found, `None` in case there are no solutions
33    fn best_solution_distance(&self) -> Option<SolutionRange>;
34}
35
36/// Errors that happen during proving
37#[derive(Debug, Error)]
38pub enum ProvingError {
39    /// Invalid erasure coding instance
40    #[error("Invalid erasure coding instance")]
41    InvalidErasureCodingInstance,
42    /// Failed to create polynomial for record
43    #[error("Failed to create polynomial for record at offset {piece_offset}: {error}")]
44    FailedToCreatePolynomialForRecord {
45        /// Piece offset
46        piece_offset: PieceOffset,
47        /// Lower-level error
48        error: String,
49    },
50    /// Failed to create chunk witness
51    #[error(
52        "Failed to create chunk witness for record at offset {piece_offset} chunk {chunk_offset}: \
53        {error}"
54    )]
55    FailedToCreateChunkWitness {
56        /// Piece offset
57        piece_offset: PieceOffset,
58        /// Chunk index
59        chunk_offset: u32,
60        /// Lower-level error
61        error: String,
62    },
63    /// Failed to decode sector contents map
64    #[error("Failed to decode sector contents map: {0}")]
65    FailedToDecodeSectorContentsMap(#[from] SectorContentsMapFromBytesError),
66    /// I/O error occurred
67    #[error("Proving I/O error: {0}")]
68    Io(#[from] io::Error),
69    /// Record reading error
70    #[error("Record reading error: {0}")]
71    RecordReadingError(#[from] ReadingError),
72}
73
74impl ProvingError {
75    /// Whether this error is fatal and makes farm unusable
76    pub fn is_fatal(&self) -> bool {
77        match self {
78            ProvingError::InvalidErasureCodingInstance => true,
79            ProvingError::FailedToCreatePolynomialForRecord { .. } => false,
80            ProvingError::FailedToCreateChunkWitness { .. } => false,
81            ProvingError::FailedToDecodeSectorContentsMap(_) => false,
82            ProvingError::Io(_) => true,
83            ProvingError::RecordReadingError(error) => error.is_fatal(),
84        }
85    }
86}
87
88#[derive(Debug, Clone)]
89struct WinningChunk {
90    /// Chunk offset within s-bucket
91    chunk_offset: u32,
92    /// Piece offset in a sector
93    piece_offset: PieceOffset,
94    /// Solution distance of this chunk
95    solution_distance: SolutionRange,
96}
97
98/// Container for solution candidates.
99///
100/// [`SolutionCandidates::into_solutions`] is used to get an iterator over proven solutions that are
101/// generated on demand during iteration.
102#[derive(Debug)]
103pub struct SolutionCandidates<'a, Sector>
104where
105    Sector: 'a,
106{
107    public_key: &'a PublicKey,
108    sector_id: SectorId,
109    s_bucket: SBucket,
110    sector: Sector,
111    sector_metadata: &'a SectorMetadataChecksummed,
112    chunk_candidates: VecDeque<ChunkCandidate>,
113}
114
115impl<'a, Sector> Clone for SolutionCandidates<'a, Sector>
116where
117    Sector: Clone + 'a,
118{
119    fn clone(&self) -> Self {
120        Self {
121            public_key: self.public_key,
122            sector_id: self.sector_id,
123            s_bucket: self.s_bucket,
124            sector: self.sector.clone(),
125            sector_metadata: self.sector_metadata,
126            chunk_candidates: self.chunk_candidates.clone(),
127        }
128    }
129}
130
131impl<'a, Sector> SolutionCandidates<'a, Sector>
132where
133    Sector: ReadAtSync + 'a,
134{
135    pub(crate) fn new(
136        public_key: &'a PublicKey,
137        sector_id: SectorId,
138        s_bucket: SBucket,
139        sector: Sector,
140        sector_metadata: &'a SectorMetadataChecksummed,
141        chunk_candidates: VecDeque<ChunkCandidate>,
142    ) -> Self {
143        Self {
144            public_key,
145            sector_id,
146            s_bucket,
147            sector,
148            sector_metadata,
149            chunk_candidates,
150        }
151    }
152
153    /// Total number of candidates
154    pub fn len(&self) -> usize {
155        self.chunk_candidates.len()
156    }
157
158    /// Returns true if no candidates inside
159    pub fn is_empty(&self) -> bool {
160        self.chunk_candidates.is_empty()
161    }
162
163    /// History size of the sector these candidates belong to.
164    pub fn history_size(&self) -> HistorySize {
165        self.sector_metadata.history_size
166    }
167
168    /// Turn solution candidates into actual solutions
169    pub fn into_solutions<RewardAddress, PosTable, TableGenerator>(
170        self,
171        reward_address: &'a RewardAddress,
172        kzg: &'a Kzg,
173        erasure_coding: &'a ErasureCoding,
174        mode: ReadSectorRecordChunksMode,
175        table_generator: TableGenerator,
176    ) -> Result<impl ProvableSolutions<Item = MaybeSolution<RewardAddress>> + 'a, ProvingError>
177    where
178        RewardAddress: Copy,
179        PosTable: Table,
180        TableGenerator: (FnMut(&PosSeed) -> PosTable) + 'a,
181    {
182        SolutionsIterator::<'a, _, PosTable, _, _>::new(
183            self.public_key,
184            reward_address,
185            self.sector_id,
186            self.s_bucket,
187            self.sector,
188            self.sector_metadata,
189            kzg,
190            erasure_coding,
191            self.chunk_candidates,
192            mode,
193            table_generator,
194        )
195    }
196}
197
198type MaybeSolution<RewardAddress> = Result<Solution<RewardAddress>, ProvingError>;
199
200struct SolutionsIterator<'a, RewardAddress, PosTable, TableGenerator, Sector>
201where
202    Sector: ReadAtSync + 'a,
203    PosTable: Table,
204    TableGenerator: (FnMut(&PosSeed) -> PosTable) + 'a,
205{
206    public_key: &'a PublicKey,
207    reward_address: &'a RewardAddress,
208    sector_id: SectorId,
209    s_bucket: SBucket,
210    sector_metadata: &'a SectorMetadataChecksummed,
211    s_bucket_offsets: Box<[u32; Record::NUM_S_BUCKETS]>,
212    kzg: &'a Kzg,
213    erasure_coding: &'a ErasureCoding,
214    sector_contents_map: SectorContentsMap,
215    sector: ReadAt<Sector, !>,
216    winning_chunks: VecDeque<WinningChunk>,
217    count: usize,
218    best_solution_distance: Option<SolutionRange>,
219    mode: ReadSectorRecordChunksMode,
220    table_generator: TableGenerator,
221}
222
223impl<'a, RewardAddress, PosTable, TableGenerator, Sector> ExactSizeIterator
224    for SolutionsIterator<'a, RewardAddress, PosTable, TableGenerator, Sector>
225where
226    RewardAddress: Copy,
227    Sector: ReadAtSync + 'a,
228    PosTable: Table,
229    TableGenerator: (FnMut(&PosSeed) -> PosTable) + 'a,
230{
231}
232
233impl<'a, RewardAddress, PosTable, TableGenerator, Sector> Iterator
234    for SolutionsIterator<'a, RewardAddress, PosTable, TableGenerator, Sector>
235where
236    RewardAddress: Copy,
237    Sector: ReadAtSync + 'a,
238    PosTable: Table,
239    TableGenerator: (FnMut(&PosSeed) -> PosTable) + 'a,
240{
241    type Item = MaybeSolution<RewardAddress>;
242
243    fn next(&mut self) -> Option<Self::Item> {
244        let WinningChunk {
245            chunk_offset,
246            piece_offset,
247            solution_distance: _,
248        } = self.winning_chunks.pop_front()?;
249
250        self.count -= 1;
251
252        // Derive PoSpace table
253        let pos_table =
254            (self.table_generator)(&self.sector_id.derive_evaluation_seed(piece_offset));
255
256        let maybe_solution: Result<_, ProvingError> = try {
257            let sector_record_chunks_fut = read_sector_record_chunks(
258                piece_offset,
259                self.sector_metadata.pieces_in_sector,
260                &self.s_bucket_offsets,
261                &self.sector_contents_map,
262                &pos_table,
263                &self.sector,
264                self.mode,
265            );
266            let sector_record_chunks = sector_record_chunks_fut
267                .now_or_never()
268                .expect("Sync reader; qed")
269                .map_err(ProvingError::RecordReadingError)?;
270
271            let chunk = ScalarBytes::from(
272                sector_record_chunks
273                    .get(usize::from(self.s_bucket))
274                    .expect("Within s-bucket range; qed")
275                    .expect("Winning chunk was plotted; qed"),
276            );
277
278            let source_chunks_polynomial = self
279                .erasure_coding
280                .recover_poly(sector_record_chunks.as_slice())
281                .map_err(|error| ReadingError::FailedToErasureDecodeRecord {
282                    piece_offset,
283                    error,
284                })
285                .map_err(ProvingError::RecordReadingError)?;
286            drop(sector_record_chunks);
287
288            // NOTE: We do not check plot consistency using checksum because it is more
289            // expensive and consensus will verify validity of the proof anyway
290            let record_metadata_fut = read_record_metadata(
291                piece_offset,
292                self.sector_metadata.pieces_in_sector,
293                &self.sector,
294            );
295            let record_metadata = record_metadata_fut
296                .now_or_never()
297                .expect("Sync reader; qed")
298                .map_err(ProvingError::RecordReadingError)?;
299
300            let proof_of_space = pos_table.find_proof(self.s_bucket.into()).expect(
301                "Quality exists for this s-bucket, otherwise it wouldn't be a winning chunk; qed",
302            );
303
304            let chunk_witness = self
305                .kzg
306                .create_witness(
307                    &source_chunks_polynomial,
308                    Record::NUM_S_BUCKETS,
309                    self.s_bucket.into(),
310                )
311                .map_err(|error| ProvingError::FailedToCreateChunkWitness {
312                    piece_offset,
313                    chunk_offset,
314                    error,
315                })?;
316
317            Solution {
318                public_key: *self.public_key,
319                reward_address: *self.reward_address,
320                sector_index: self.sector_metadata.sector_index,
321                history_size: self.sector_metadata.history_size,
322                piece_offset,
323                record_commitment: record_metadata.commitment,
324                record_witness: record_metadata.witness,
325                chunk,
326                chunk_witness: ChunkWitness::from(chunk_witness),
327                proof_of_space,
328            }
329        };
330
331        match maybe_solution {
332            Ok(solution) => Some(Ok(solution)),
333            Err(error) => Some(Err(error)),
334        }
335    }
336
337    fn size_hint(&self) -> (usize, Option<usize>) {
338        (self.count, Some(self.count))
339    }
340}
341
342impl<'a, RewardAddress, PosTable, TableGenerator, Sector> ProvableSolutions
343    for SolutionsIterator<'a, RewardAddress, PosTable, TableGenerator, Sector>
344where
345    RewardAddress: Copy,
346    Sector: ReadAtSync + 'a,
347    PosTable: Table,
348    TableGenerator: (FnMut(&PosSeed) -> PosTable) + 'a,
349{
350    fn best_solution_distance(&self) -> Option<SolutionRange> {
351        self.best_solution_distance
352    }
353}
354
355impl<'a, RewardAddress, PosTable, TableGenerator, Sector>
356    SolutionsIterator<'a, RewardAddress, PosTable, TableGenerator, Sector>
357where
358    RewardAddress: Copy,
359    Sector: ReadAtSync + 'a,
360    PosTable: Table,
361    TableGenerator: (FnMut(&PosSeed) -> PosTable) + 'a,
362{
363    #[allow(clippy::too_many_arguments)]
364    fn new(
365        public_key: &'a PublicKey,
366        reward_address: &'a RewardAddress,
367        sector_id: SectorId,
368        s_bucket: SBucket,
369        sector: Sector,
370        sector_metadata: &'a SectorMetadataChecksummed,
371        kzg: &'a Kzg,
372        erasure_coding: &'a ErasureCoding,
373        chunk_candidates: VecDeque<ChunkCandidate>,
374        mode: ReadSectorRecordChunksMode,
375        table_generator: TableGenerator,
376    ) -> Result<Self, ProvingError> {
377        if erasure_coding.max_shards() < Record::NUM_S_BUCKETS {
378            return Err(ProvingError::InvalidErasureCodingInstance);
379        }
380
381        let sector_contents_map = {
382            let mut sector_contents_map_bytes =
383                vec![0; SectorContentsMap::encoded_size(sector_metadata.pieces_in_sector)];
384
385            sector.read_at(&mut sector_contents_map_bytes, 0)?;
386
387            SectorContentsMap::from_bytes(
388                &sector_contents_map_bytes,
389                sector_metadata.pieces_in_sector,
390            )?
391        };
392
393        let s_bucket_records = sector_contents_map
394            .iter_s_bucket_records(s_bucket)
395            .expect("S-bucket audit index is guaranteed to be in range; qed")
396            .collect::<Vec<_>>();
397        let winning_chunks = chunk_candidates
398            .into_iter()
399            .filter_map(move |chunk_candidate| {
400                let (piece_offset, encoded_chunk_used) = s_bucket_records
401                    .get(chunk_candidate.chunk_offset as usize)
402                    .expect("Wouldn't be a candidate if wasn't within s-bucket; qed");
403
404                encoded_chunk_used.then_some(WinningChunk {
405                    chunk_offset: chunk_candidate.chunk_offset,
406                    piece_offset: *piece_offset,
407                    solution_distance: chunk_candidate.solution_distance,
408                })
409            })
410            .collect::<VecDeque<_>>();
411
412        let best_solution_distance = winning_chunks
413            .front()
414            .map(|winning_chunk| winning_chunk.solution_distance);
415
416        let s_bucket_offsets = sector_metadata.s_bucket_offsets();
417
418        let count = winning_chunks.len();
419
420        Ok(Self {
421            public_key,
422            reward_address,
423            sector_id,
424            s_bucket,
425            sector_metadata,
426            s_bucket_offsets,
427            kzg,
428            erasure_coding,
429            sector_contents_map,
430            sector: ReadAt::from_sync(sector),
431            winning_chunks,
432            count,
433            best_solution_distance,
434            mode,
435            table_generator,
436        })
437    }
438}