ab_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    ReadingError, read_record_metadata, read_sector_record_chunks, recover_extended_record_chunks,
9};
10use crate::sector::{
11    SectorContentsMap, SectorContentsMapFromBytesError, SectorMetadataChecksummed,
12};
13use crate::{ReadAt, ReadAtSync};
14use ab_core_primitives::hashes::Blake3Hash;
15use ab_core_primitives::pieces::{PieceOffset, Record, RecordChunk};
16use ab_core_primitives::pos::PosSeed;
17use ab_core_primitives::sectors::{SBucket, SectorId};
18use ab_core_primitives::solutions::{ChunkProof, Solution, SolutionDistance};
19use ab_erasure_coding::ErasureCoding;
20use ab_merkle_tree::balanced::BalancedMerkleTree;
21use ab_proof_of_space::PosProofs;
22use futures::FutureExt;
23use std::collections::VecDeque;
24use std::io;
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<SolutionDistance>;
33}
34
35/// Errors that happen during proving
36#[derive(Debug, Error)]
37pub enum ProvingError {
38    /// Failed to create polynomial for record
39    #[error("Failed to create polynomial for record at offset {piece_offset}: {error}")]
40    FailedToCreatePolynomialForRecord {
41        /// Piece offset
42        piece_offset: PieceOffset,
43        /// Lower-level error
44        error: String,
45    },
46    /// Failed to decode sector contents map
47    #[error("Failed to decode sector contents map: {0}")]
48    FailedToDecodeSectorContentsMap(#[from] SectorContentsMapFromBytesError),
49    /// I/O error occurred
50    #[error("Proving I/O error: {0}")]
51    Io(#[from] io::Error),
52    /// Record reading error
53    #[error("Record reading error: {0}")]
54    RecordReadingError(#[from] ReadingError),
55}
56
57impl ProvingError {
58    /// Whether this error is fatal and makes farm unusable
59    pub fn is_fatal(&self) -> bool {
60        match self {
61            ProvingError::FailedToCreatePolynomialForRecord { .. } => false,
62            ProvingError::FailedToDecodeSectorContentsMap(_) => false,
63            ProvingError::Io(_) => true,
64            ProvingError::RecordReadingError(error) => error.is_fatal(),
65        }
66    }
67}
68
69#[derive(Debug, Clone)]
70struct WinningChunk {
71    /// Piece offset in a sector
72    piece_offset: PieceOffset,
73    /// Solution distance of this chunk
74    solution_distance: SolutionDistance,
75}
76
77/// Container for solution candidates.
78///
79/// [`SolutionCandidates::into_solutions`] is used to get an iterator over proven solutions that are
80/// generated on demand during iteration.
81#[derive(Debug)]
82pub struct SolutionCandidates<'a, Sector>
83where
84    Sector: 'a,
85{
86    public_key_hash: &'a Blake3Hash,
87    sector_id: SectorId,
88    s_bucket: SBucket,
89    sector: Sector,
90    sector_metadata: &'a SectorMetadataChecksummed,
91    chunk_candidates: VecDeque<ChunkCandidate>,
92}
93
94impl<'a, Sector> Clone for SolutionCandidates<'a, Sector>
95where
96    Sector: Clone + 'a,
97{
98    fn clone(&self) -> Self {
99        Self {
100            public_key_hash: self.public_key_hash,
101            sector_id: self.sector_id,
102            s_bucket: self.s_bucket,
103            sector: self.sector.clone(),
104            sector_metadata: self.sector_metadata,
105            chunk_candidates: self.chunk_candidates.clone(),
106        }
107    }
108}
109
110impl<'a, Sector> SolutionCandidates<'a, Sector>
111where
112    Sector: ReadAtSync + 'a,
113{
114    pub(crate) fn new(
115        public_key_hash: &'a Blake3Hash,
116        sector_id: SectorId,
117        s_bucket: SBucket,
118        sector: Sector,
119        sector_metadata: &'a SectorMetadataChecksummed,
120        chunk_candidates: VecDeque<ChunkCandidate>,
121    ) -> Self {
122        Self {
123            public_key_hash,
124            sector_id,
125            s_bucket,
126            sector,
127            sector_metadata,
128            chunk_candidates,
129        }
130    }
131
132    /// Total number of candidates
133    pub fn len(&self) -> usize {
134        self.chunk_candidates.len()
135    }
136
137    /// Returns true if no candidates inside
138    pub fn is_empty(&self) -> bool {
139        self.chunk_candidates.is_empty()
140    }
141
142    /// Turn solution candidates into actual solutions
143    pub fn into_solutions<PosProofGenerator>(
144        self,
145        erasure_coding: &'a ErasureCoding,
146        table_generator: PosProofGenerator,
147    ) -> Result<impl ProvableSolutions<Item = MaybeSolution> + 'a, ProvingError>
148    where
149        PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
150    {
151        SolutionsIterator::<'a, _, _>::new(
152            self.public_key_hash,
153            self.sector_id,
154            self.s_bucket,
155            self.sector,
156            self.sector_metadata,
157            erasure_coding,
158            self.chunk_candidates,
159            table_generator,
160        )
161    }
162}
163
164type MaybeSolution = Result<Solution, ProvingError>;
165
166struct SolutionsIterator<'a, PosProofGenerator, Sector>
167where
168    Sector: ReadAtSync + 'a,
169    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
170{
171    public_key_hash: &'a Blake3Hash,
172    sector_id: SectorId,
173    s_bucket: SBucket,
174    sector_metadata: &'a SectorMetadataChecksummed,
175    s_bucket_offsets: Box<[u32; Record::NUM_S_BUCKETS]>,
176    erasure_coding: &'a ErasureCoding,
177    sector_contents_map: SectorContentsMap,
178    sector: ReadAt<Sector, !>,
179    winning_chunks: VecDeque<WinningChunk>,
180    count: usize,
181    best_solution_distance: Option<SolutionDistance>,
182    table_generator: PosProofGenerator,
183}
184
185impl<'a, PosProofGenerator, Sector> ExactSizeIterator
186    for SolutionsIterator<'a, PosProofGenerator, Sector>
187where
188    Sector: ReadAtSync + 'a,
189    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
190{
191}
192
193impl<'a, PosProofGenerator, Sector> Iterator for SolutionsIterator<'a, PosProofGenerator, Sector>
194where
195    Sector: ReadAtSync + 'a,
196    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
197{
198    type Item = MaybeSolution;
199
200    fn next(&mut self) -> Option<Self::Item> {
201        let WinningChunk {
202            piece_offset,
203            solution_distance: _,
204        } = self.winning_chunks.pop_front()?;
205
206        self.count -= 1;
207
208        // Derive PoSpace proofs
209        let pos_proofs =
210            (self.table_generator)(&self.sector_id.derive_evaluation_seed(piece_offset));
211
212        let maybe_solution: Result<_, ProvingError> = try {
213            let sector_record_chunks_fut = read_sector_record_chunks(
214                piece_offset,
215                self.sector_metadata.pieces_in_sector,
216                &self.s_bucket_offsets,
217                &self.sector_contents_map,
218                &pos_proofs,
219                &self.sector,
220            );
221            let sector_record_chunks = sector_record_chunks_fut
222                .now_or_never()
223                .expect("Sync reader; qed")?;
224
225            let chunk = sector_record_chunks
226                .get(usize::from(self.s_bucket))
227                .expect("Within s-bucket range; qed")
228                .expect("Winning chunk was plotted; qed");
229
230            let chunks = recover_extended_record_chunks(
231                &sector_record_chunks,
232                piece_offset,
233                self.erasure_coding,
234            )?;
235            drop(sector_record_chunks);
236
237            // TODO: This is a workaround for https://github.com/rust-lang/rust/issues/139866 that
238            //  allows the code to compile. Constant 65536 is hardcoded here and below for
239            //  compilation to succeed.
240            const _: () = {
241                assert!(Record::NUM_S_BUCKETS == 65536);
242            };
243            let record_merkle_tree = BalancedMerkleTree::<65536>::new_boxed(
244                RecordChunk::slice_to_repr(chunks.as_slice())
245                    .try_into()
246                    .expect("Statically guaranteed to have correct length; qed"),
247            );
248
249            // NOTE: We do not check plot consistency using checksum because it is more
250            // expensive and consensus will verify validity of the proof anyway
251            let record_metadata_fut = read_record_metadata(
252                piece_offset,
253                self.sector_metadata.pieces_in_sector,
254                &self.sector,
255            );
256            let record_metadata = record_metadata_fut
257                .now_or_never()
258                .expect("Sync reader; qed")?;
259
260            let proof_of_space = pos_proofs.for_s_bucket(self.s_bucket).expect(
261                "Proof exists for this s-bucket, otherwise it wouldn't be a winning chunk; qed",
262            );
263
264            let chunk_proof = record_merkle_tree
265                .all_proofs()
266                .nth(usize::from(self.s_bucket))
267                .expect("Chunk offset is valid, hence corresponding proof exists; qed");
268
269            Solution {
270                public_key_hash: *self.public_key_hash,
271                record_root: record_metadata.root,
272                record_proof: record_metadata.proof,
273                chunk,
274                chunk_proof: ChunkProof::from(chunk_proof),
275                proof_of_space,
276                history_size: self.sector_metadata.history_size,
277                sector_index: self.sector_metadata.sector_index,
278                piece_offset,
279                padding: [0; _],
280            }
281        };
282
283        match maybe_solution {
284            Ok(solution) => Some(Ok(solution)),
285            Err(error) => Some(Err(error)),
286        }
287    }
288
289    fn size_hint(&self) -> (usize, Option<usize>) {
290        (self.count, Some(self.count))
291    }
292}
293
294impl<'a, PosProofGenerator, Sector> ProvableSolutions
295    for SolutionsIterator<'a, PosProofGenerator, Sector>
296where
297    Sector: ReadAtSync + 'a,
298    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
299{
300    fn best_solution_distance(&self) -> Option<SolutionDistance> {
301        self.best_solution_distance
302    }
303}
304
305impl<'a, PosProofGenerator, Sector> SolutionsIterator<'a, PosProofGenerator, Sector>
306where
307    Sector: ReadAtSync + 'a,
308    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
309{
310    #[allow(clippy::too_many_arguments)]
311    fn new(
312        public_key_hash: &'a Blake3Hash,
313        sector_id: SectorId,
314        s_bucket: SBucket,
315        sector: Sector,
316        sector_metadata: &'a SectorMetadataChecksummed,
317        erasure_coding: &'a ErasureCoding,
318        chunk_candidates: VecDeque<ChunkCandidate>,
319        table_generator: PosProofGenerator,
320    ) -> Result<Self, ProvingError> {
321        let sector_contents_map = {
322            let mut sector_contents_map_bytes =
323                vec![0; SectorContentsMap::encoded_size(sector_metadata.pieces_in_sector)];
324
325            sector.read_at(&mut sector_contents_map_bytes, 0)?;
326
327            SectorContentsMap::from_bytes(
328                &sector_contents_map_bytes,
329                sector_metadata.pieces_in_sector,
330            )?
331        };
332
333        let s_bucket_piece_offsets = sector_contents_map
334            .iter_s_bucket_piece_offsets(s_bucket)
335            .expect("S-bucket audit index is guaranteed to be in range; qed")
336            .collect::<Vec<_>>();
337        let winning_chunks = chunk_candidates
338            .into_iter()
339            .map(move |chunk_candidate| {
340                let piece_offset = s_bucket_piece_offsets
341                    .get(chunk_candidate.chunk_offset as usize)
342                    .expect("Wouldn't be a candidate if wasn't within s-bucket; qed");
343
344                WinningChunk {
345                    piece_offset: *piece_offset,
346                    solution_distance: chunk_candidate.solution_distance,
347                }
348            })
349            .collect::<VecDeque<_>>();
350
351        let best_solution_distance = winning_chunks
352            .front()
353            .map(|winning_chunk| winning_chunk.solution_distance);
354
355        let s_bucket_offsets = sector_metadata.s_bucket_offsets();
356
357        let count = winning_chunks.len();
358
359        Ok(Self {
360            public_key_hash,
361            sector_id,
362            s_bucket,
363            sector_metadata,
364            s_bucket_offsets,
365            erasure_coding,
366            sector_contents_map,
367            sector: ReadAt::from_sync(sector),
368            winning_chunks,
369            count,
370            best_solution_distance,
371            table_generator,
372        })
373    }
374}