Skip to main content

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::{ReadingError, read_record_metadata, read_sector_record_chunks};
8use crate::sector::{
9    RecordMetadata, SectorContentsMap, SectorContentsMapFromBytesError, SectorMetadataChecksummed,
10};
11use crate::shard_commitment::{ShardCommitmentsRootsCache, derive_solution_shard_commitment};
12use crate::{ReadAt, ReadAtSync};
13use ab_core_primitives::hashes::Blake3Hash;
14use ab_core_primitives::pieces::{ExtendedRecord, PieceOffset, Record, RecordChunk, RecordRoot};
15use ab_core_primitives::pos::PosSeed;
16use ab_core_primitives::sectors::{SBucket, SectorId};
17use ab_core_primitives::shard::NumShards;
18use ab_core_primitives::solutions::{
19    ChunkProof, ShardMembershipEntropy, Solution, SolutionDistance,
20};
21use ab_erasure_coding::ErasureCoding;
22use ab_merkle_tree::blake3::BalancedMerkleTree;
23use ab_proof_of_space::PosProofs;
24use futures::FutureExt;
25use std::collections::VecDeque;
26use std::io;
27use thiserror::Error;
28
29/// Solutions that can be proven if necessary.
30///
31/// Solutions are generated on demand during iteration.
32pub trait ProvableSolutions: ExactSizeIterator {
33    /// Best solution distance found, `None` in case there are no solutions
34    fn best_solution_distance(&self) -> Option<SolutionDistance>;
35}
36
37/// Errors that happen during proving
38#[derive(Debug, Error)]
39pub enum ProvingError {
40    /// Failed to create polynomial for record
41    #[error("Failed to create polynomial for record at offset {piece_offset}: {error}")]
42    FailedToCreatePolynomialForRecord {
43        /// Piece offset
44        piece_offset: PieceOffset,
45        /// Lower-level error
46        error: String,
47    },
48    /// Failed to decode sector contents map
49    #[error("Failed to decode sector contents map: {0}")]
50    FailedToDecodeSectorContentsMap(#[from] SectorContentsMapFromBytesError),
51    /// Sector contents map and sector metadata disagree on the size of the audited s-bucket
52    #[error(
53        "S-bucket {s_bucket} has {actual} chunks in sector contents map, but {expected} in sector \
54        metadata"
55    )]
56    SBucketSizeMismatch {
57        /// S-bucket
58        s_bucket: SBucket,
59        /// Number of chunks according to sector metadata
60        expected: u16,
61        /// Number of chunks according to sector contents map
62        actual: usize,
63    },
64    /// I/O error occurred
65    #[error("Proving I/O error: {0}")]
66    Io(#[from] io::Error),
67    /// Record reading error
68    #[error("Record reading error: {0}")]
69    RecordReadingError(#[from] ReadingError),
70}
71
72impl ProvingError {
73    /// Whether this error is fatal and makes farm unusable
74    pub fn is_fatal(&self) -> bool {
75        #[expect(
76            clippy::rest_pattern_accessible_field,
77            reason = "Do not care about fields"
78        )]
79        match self {
80            ProvingError::FailedToCreatePolynomialForRecord { .. } => false,
81            ProvingError::FailedToDecodeSectorContentsMap(_) => false,
82            ProvingError::SBucketSizeMismatch { .. } => false,
83            ProvingError::Io(_) => true,
84            ProvingError::RecordReadingError(error) => error.is_fatal(),
85        }
86    }
87}
88
89#[derive(Debug, Clone)]
90struct WinningChunk {
91    /// Piece offset in a sector
92    piece_offset: PieceOffset,
93    /// Solution distance of this chunk
94    solution_distance: SolutionDistance,
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_hash: &'a Blake3Hash,
107    sector_id: SectorId,
108    shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
109    shard_membership_entropy: ShardMembershipEntropy,
110    num_shards: NumShards,
111    s_bucket: SBucket,
112    sector: Sector,
113    sector_metadata: &'a SectorMetadataChecksummed,
114    chunk_candidates: VecDeque<ChunkCandidate>,
115}
116
117impl<'a, Sector> Clone for SolutionCandidates<'a, Sector>
118where
119    Sector: Clone + 'a,
120{
121    fn clone(&self) -> Self {
122        Self {
123            public_key_hash: self.public_key_hash,
124            sector_id: self.sector_id,
125            shard_commitments_roots_cache: self.shard_commitments_roots_cache,
126            shard_membership_entropy: self.shard_membership_entropy,
127            num_shards: self.num_shards,
128            s_bucket: self.s_bucket,
129            sector: self.sector.clone(),
130            sector_metadata: self.sector_metadata,
131            chunk_candidates: self.chunk_candidates.clone(),
132        }
133    }
134}
135
136impl<'a, Sector> SolutionCandidates<'a, Sector>
137where
138    Sector: ReadAtSync + 'a,
139{
140    #[expect(clippy::too_many_arguments, reason = "Private API")]
141    pub(crate) fn new(
142        public_key_hash: &'a Blake3Hash,
143        sector_id: SectorId,
144        shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
145        shard_membership_entropy: ShardMembershipEntropy,
146        num_shards: NumShards,
147        s_bucket: SBucket,
148        sector: Sector,
149        sector_metadata: &'a SectorMetadataChecksummed,
150        chunk_candidates: VecDeque<ChunkCandidate>,
151    ) -> Self {
152        Self {
153            public_key_hash,
154            sector_id,
155            shard_commitments_roots_cache,
156            shard_membership_entropy,
157            num_shards,
158            s_bucket,
159            sector,
160            sector_metadata,
161            chunk_candidates,
162        }
163    }
164
165    /// Total number of candidates
166    pub fn len(&self) -> usize {
167        self.chunk_candidates.len()
168    }
169
170    /// Returns true if no candidates inside
171    pub fn is_empty(&self) -> bool {
172        self.chunk_candidates.is_empty()
173    }
174
175    /// Turn solution candidates into actual solutions
176    pub fn into_solutions<PosProofGenerator>(
177        self,
178        erasure_coding: &'a ErasureCoding,
179        table_generator: PosProofGenerator,
180    ) -> Result<impl ProvableSolutions<Item = MaybeSolution> + 'a, ProvingError>
181    where
182        PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
183    {
184        SolutionsIterator::<'a, _, _>::new(
185            self.public_key_hash,
186            self.sector_id,
187            self.shard_commitments_roots_cache,
188            self.shard_membership_entropy,
189            self.num_shards,
190            self.s_bucket,
191            self.sector,
192            self.sector_metadata,
193            erasure_coding,
194            self.chunk_candidates,
195            table_generator,
196        )
197    }
198}
199
200type MaybeSolution = Result<Solution, ProvingError>;
201
202struct SolutionsIterator<'a, PosProofGenerator, Sector>
203where
204    Sector: ReadAtSync + 'a,
205    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
206{
207    public_key_hash: &'a Blake3Hash,
208    sector_id: SectorId,
209    shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
210    shard_membership_entropy: ShardMembershipEntropy,
211    num_shards: NumShards,
212    s_bucket: SBucket,
213    sector_metadata: &'a SectorMetadataChecksummed,
214    s_bucket_offsets: Box<[u32; Record::NUM_S_BUCKETS]>,
215    erasure_coding: &'a ErasureCoding,
216    sector_contents_map: SectorContentsMap,
217    sector: ReadAt<Sector, !>,
218    winning_chunks: VecDeque<WinningChunk>,
219    count: usize,
220    best_solution_distance: Option<SolutionDistance>,
221    table_generator: PosProofGenerator,
222}
223
224impl<'a, PosProofGenerator, Sector> ExactSizeIterator
225    for SolutionsIterator<'a, PosProofGenerator, Sector>
226where
227    Sector: ReadAtSync + 'a,
228    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
229{
230}
231
232impl<'a, PosProofGenerator, Sector> Iterator for SolutionsIterator<'a, PosProofGenerator, Sector>
233where
234    Sector: ReadAtSync + 'a,
235    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
236{
237    type Item = MaybeSolution;
238
239    fn next(&mut self) -> Option<Self::Item> {
240        let WinningChunk {
241            piece_offset,
242            solution_distance: _,
243        } = self.winning_chunks.pop_front()?;
244
245        self.count -= 1;
246
247        // Derive PoSpace proofs
248        let pos_proofs =
249            (self.table_generator)(&self.sector_id.derive_evaluation_seed(piece_offset));
250
251        let maybe_solution = try {
252            let sector_record_chunks_fut = read_sector_record_chunks(
253                piece_offset,
254                self.sector_metadata.pieces_in_sector,
255                &self.s_bucket_offsets,
256                &self.sector_contents_map,
257                &pos_proofs,
258                &self.sector,
259            );
260            let mut sector_record_chunks = sector_record_chunks_fut
261                .now_or_never()
262                .expect("Sync reader; qed")
263                .map_err(ProvingError::RecordReadingError)?;
264
265            let chunk = sector_record_chunks.extended_record[self.s_bucket];
266
267            let ExtendedRecord { source, parity } = &mut *sector_record_chunks.extended_record;
268            self.erasure_coding
269                .recover_all(source, parity, &sector_record_chunks.present)
270                .map_err(|error| ReadingError::FailedToErasureDecodeRecord {
271                    piece_offset,
272                    error,
273                })
274                .map_err(ProvingError::RecordReadingError)?;
275
276            let record_merkle_tree = BalancedMerkleTree::<{ Record::NUM_S_BUCKETS }>::new_boxed(
277                RecordChunk::array_to_repr(sector_record_chunks.extended_record.as_s_buckets()),
278            );
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 RecordMetadata {
288                piece_header,
289                piece_checksum: _,
290            } = record_metadata_fut
291                .now_or_never()
292                .expect("Sync reader; qed")
293                .map_err(ProvingError::RecordReadingError)?;
294
295            let proof_of_space = pos_proofs.for_s_bucket(self.s_bucket).expect(
296                "Reading checked that the map matches the proofs, and the map has a chunk of this \
297                record in this s-bucket; qed",
298            );
299
300            let chunk_proof = record_merkle_tree
301                .all_proofs()
302                .nth(usize::from(self.s_bucket))
303                .expect("Chunk offset is valid, hence corresponding proof exists; qed");
304
305            let history_size = self.sector_metadata.history_size;
306            let shard_commitment = derive_solution_shard_commitment(
307                self.public_key_hash,
308                &self.shard_commitments_roots_cache.shard_commitments_seed(),
309                &self.shard_commitments_roots_cache.get(history_size),
310                history_size,
311                &self.shard_membership_entropy,
312                self.num_shards,
313            );
314
315            Solution {
316                public_key_hash: *self.public_key_hash,
317                shard_commitment,
318                piece_local_segment_index: piece_header.local_segment_index.as_inner(),
319                piece_super_segment_index: piece_header.super_segment_index.as_inner(),
320                segment_root: piece_header.segment_root,
321                segment_proof: piece_header.segment_proof,
322                record_root: RecordRoot::from(record_merkle_tree.root()),
323                record_proof: piece_header.record_proof,
324                chunk,
325                chunk_proof: ChunkProof::from(chunk_proof),
326                proof_of_space,
327                history_size,
328                sector_index: self.sector_metadata.sector_index,
329                piece_offset,
330                segment_position: piece_header.segment_position.as_inner(),
331                piece_shard_index: piece_header.shard_index.as_inner(),
332                padding: [0; _],
333            }
334        };
335
336        match maybe_solution {
337            Ok(solution) => Some(Ok(solution)),
338            Err(error) => Some(Err(error)),
339        }
340    }
341
342    fn size_hint(&self) -> (usize, Option<usize>) {
343        (self.count, Some(self.count))
344    }
345}
346
347impl<'a, PosProofGenerator, Sector> ProvableSolutions
348    for SolutionsIterator<'a, PosProofGenerator, Sector>
349where
350    Sector: ReadAtSync + 'a,
351    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
352{
353    fn best_solution_distance(&self) -> Option<SolutionDistance> {
354        self.best_solution_distance
355    }
356}
357
358impl<'a, PosProofGenerator, Sector> SolutionsIterator<'a, PosProofGenerator, Sector>
359where
360    Sector: ReadAtSync + 'a,
361    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
362{
363    // TODO: Struct for arguments
364    #[expect(clippy::too_many_arguments, reason = "TODO")]
365    fn new(
366        public_key_hash: &'a Blake3Hash,
367        sector_id: SectorId,
368        shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
369        shard_membership_entropy: ShardMembershipEntropy,
370        num_shards: NumShards,
371        s_bucket: SBucket,
372        sector: Sector,
373        sector_metadata: &'a SectorMetadataChecksummed,
374        erasure_coding: &'a ErasureCoding,
375        chunk_candidates: VecDeque<ChunkCandidate>,
376        table_generator: PosProofGenerator,
377    ) -> Result<Self, ProvingError> {
378        let sector_contents_map = {
379            let mut sector_contents_map_bytes =
380                vec![0; SectorContentsMap::encoded_size(sector_metadata.pieces_in_sector)];
381
382            sector.read_at(&mut sector_contents_map_bytes, 0)?;
383
384            SectorContentsMap::from_bytes(
385                &sector_contents_map_bytes,
386                sector_metadata.pieces_in_sector,
387            )?
388        };
389
390        let s_bucket_piece_offsets = sector_contents_map
391            .iter_s_bucket_piece_offsets(s_bucket)
392            .expect("S-bucket audit index is guaranteed to be in range; qed")
393            .collect::<Vec<_>>();
394        let s_bucket_size = sector_metadata
395            .s_bucket_sizes
396            .get(usize::from(s_bucket))
397            .copied()
398            .expect("S-bucket audit index is guaranteed to be in range; qed");
399        // Auditing found candidates among as many chunks as sector metadata says the s-bucket has
400        if s_bucket_piece_offsets.len() != usize::from(s_bucket_size) {
401            return Err(ProvingError::SBucketSizeMismatch {
402                s_bucket,
403                expected: s_bucket_size,
404                actual: s_bucket_piece_offsets.len(),
405            });
406        }
407        let winning_chunks = chunk_candidates
408            .into_iter()
409            .map(move |chunk_candidate| {
410                let piece_offset = s_bucket_piece_offsets
411                    .get(chunk_candidate.chunk_offset as usize)
412                    .expect(
413                        "Auditing only creates candidates within the s-bucket size from metadata, \
414                        which matches the map as checked above; qed",
415                    );
416
417                WinningChunk {
418                    piece_offset: *piece_offset,
419                    solution_distance: chunk_candidate.solution_distance,
420                }
421            })
422            .collect::<VecDeque<_>>();
423
424        let best_solution_distance = winning_chunks
425            .front()
426            .map(|winning_chunk| winning_chunk.solution_distance);
427
428        let s_bucket_offsets = sector_metadata.s_bucket_offsets();
429
430        let count = winning_chunks.len();
431
432        Ok(Self {
433            public_key_hash,
434            sector_id,
435            shard_commitments_roots_cache,
436            shard_membership_entropy,
437            num_shards,
438            s_bucket,
439            sector_metadata,
440            s_bucket_offsets,
441            erasure_coding,
442            sector_contents_map,
443            sector: ReadAt::from_sync(sector),
444            winning_chunks,
445            count,
446            best_solution_distance,
447            table_generator,
448        })
449    }
450}