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