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::{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::balanced::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    /// I/O error occurred
52    #[error("Proving I/O error: {0}")]
53    Io(#[from] io::Error),
54    /// Record reading error
55    #[error("Record reading error: {0}")]
56    RecordReadingError(#[from] ReadingError),
57}
58
59impl ProvingError {
60    /// Whether this error is fatal and makes farm unusable
61    pub fn is_fatal(&self) -> bool {
62        #[expect(
63            clippy::rest_pattern_accessible_field,
64            reason = "Do not care about fields"
65        )]
66        match self {
67            ProvingError::FailedToCreatePolynomialForRecord { .. } => false,
68            ProvingError::FailedToDecodeSectorContentsMap(_) => false,
69            ProvingError::Io(_) => true,
70            ProvingError::RecordReadingError(error) => error.is_fatal(),
71        }
72    }
73}
74
75#[derive(Debug, Clone)]
76struct WinningChunk {
77    /// Piece offset in a sector
78    piece_offset: PieceOffset,
79    /// Solution distance of this chunk
80    solution_distance: SolutionDistance,
81}
82
83/// Container for solution candidates.
84///
85/// [`SolutionCandidates::into_solutions`] is used to get an iterator over proven solutions that are
86/// generated on demand during iteration.
87#[derive(Debug)]
88pub struct SolutionCandidates<'a, Sector>
89where
90    Sector: 'a,
91{
92    public_key_hash: &'a Blake3Hash,
93    sector_id: SectorId,
94    shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
95    shard_membership_entropy: ShardMembershipEntropy,
96    num_shards: NumShards,
97    s_bucket: SBucket,
98    sector: Sector,
99    sector_metadata: &'a SectorMetadataChecksummed,
100    chunk_candidates: VecDeque<ChunkCandidate>,
101}
102
103impl<'a, Sector> Clone for SolutionCandidates<'a, Sector>
104where
105    Sector: Clone + 'a,
106{
107    fn clone(&self) -> Self {
108        Self {
109            public_key_hash: self.public_key_hash,
110            sector_id: self.sector_id,
111            shard_commitments_roots_cache: self.shard_commitments_roots_cache,
112            shard_membership_entropy: self.shard_membership_entropy,
113            num_shards: self.num_shards,
114            s_bucket: self.s_bucket,
115            sector: self.sector.clone(),
116            sector_metadata: self.sector_metadata,
117            chunk_candidates: self.chunk_candidates.clone(),
118        }
119    }
120}
121
122impl<'a, Sector> SolutionCandidates<'a, Sector>
123where
124    Sector: ReadAtSync + 'a,
125{
126    #[expect(clippy::too_many_arguments, reason = "Private API")]
127    pub(crate) fn new(
128        public_key_hash: &'a Blake3Hash,
129        sector_id: SectorId,
130        shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
131        shard_membership_entropy: ShardMembershipEntropy,
132        num_shards: NumShards,
133        s_bucket: SBucket,
134        sector: Sector,
135        sector_metadata: &'a SectorMetadataChecksummed,
136        chunk_candidates: VecDeque<ChunkCandidate>,
137    ) -> Self {
138        Self {
139            public_key_hash,
140            sector_id,
141            shard_commitments_roots_cache,
142            shard_membership_entropy,
143            num_shards,
144            s_bucket,
145            sector,
146            sector_metadata,
147            chunk_candidates,
148        }
149    }
150
151    /// Total number of candidates
152    pub fn len(&self) -> usize {
153        self.chunk_candidates.len()
154    }
155
156    /// Returns true if no candidates inside
157    pub fn is_empty(&self) -> bool {
158        self.chunk_candidates.is_empty()
159    }
160
161    /// Turn solution candidates into actual solutions
162    pub fn into_solutions<PosProofGenerator>(
163        self,
164        erasure_coding: &'a ErasureCoding,
165        table_generator: PosProofGenerator,
166    ) -> Result<impl ProvableSolutions<Item = MaybeSolution> + 'a, ProvingError>
167    where
168        PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
169    {
170        SolutionsIterator::<'a, _, _>::new(
171            self.public_key_hash,
172            self.sector_id,
173            self.shard_commitments_roots_cache,
174            self.shard_membership_entropy,
175            self.num_shards,
176            self.s_bucket,
177            self.sector,
178            self.sector_metadata,
179            erasure_coding,
180            self.chunk_candidates,
181            table_generator,
182        )
183    }
184}
185
186type MaybeSolution = Result<Solution, ProvingError>;
187
188struct SolutionsIterator<'a, PosProofGenerator, Sector>
189where
190    Sector: ReadAtSync + 'a,
191    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
192{
193    public_key_hash: &'a Blake3Hash,
194    sector_id: SectorId,
195    shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
196    shard_membership_entropy: ShardMembershipEntropy,
197    num_shards: NumShards,
198    s_bucket: SBucket,
199    sector_metadata: &'a SectorMetadataChecksummed,
200    s_bucket_offsets: Box<[u32; Record::NUM_S_BUCKETS]>,
201    erasure_coding: &'a ErasureCoding,
202    sector_contents_map: SectorContentsMap,
203    sector: ReadAt<Sector, !>,
204    winning_chunks: VecDeque<WinningChunk>,
205    count: usize,
206    best_solution_distance: Option<SolutionDistance>,
207    table_generator: PosProofGenerator,
208}
209
210impl<'a, PosProofGenerator, Sector> ExactSizeIterator
211    for SolutionsIterator<'a, PosProofGenerator, Sector>
212where
213    Sector: ReadAtSync + 'a,
214    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
215{
216}
217
218impl<'a, PosProofGenerator, Sector> Iterator for SolutionsIterator<'a, PosProofGenerator, Sector>
219where
220    Sector: ReadAtSync + 'a,
221    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
222{
223    type Item = MaybeSolution;
224
225    fn next(&mut self) -> Option<Self::Item> {
226        let WinningChunk {
227            piece_offset,
228            solution_distance: _,
229        } = self.winning_chunks.pop_front()?;
230
231        self.count -= 1;
232
233        // Derive PoSpace proofs
234        let pos_proofs =
235            (self.table_generator)(&self.sector_id.derive_evaluation_seed(piece_offset));
236
237        let maybe_solution = try {
238            let sector_record_chunks_fut = read_sector_record_chunks(
239                piece_offset,
240                self.sector_metadata.pieces_in_sector,
241                &self.s_bucket_offsets,
242                &self.sector_contents_map,
243                &pos_proofs,
244                &self.sector,
245            );
246            let mut sector_record_chunks = sector_record_chunks_fut
247                .now_or_never()
248                .expect("Sync reader; qed")
249                .map_err(ProvingError::RecordReadingError)?;
250
251            let chunk = sector_record_chunks.get_chunk(self.s_bucket);
252
253            self.erasure_coding
254                .recover_all(
255                    &mut sector_record_chunks.source,
256                    &mut sector_record_chunks.parity,
257                    &sector_record_chunks.present,
258                )
259                .map_err(|error| ReadingError::FailedToErasureDecodeRecord {
260                    piece_offset,
261                    error,
262                })
263                .map_err(ProvingError::RecordReadingError)?;
264
265            // TODO: Remove this extra allocation
266            // SAFETY: Data structure filled with zeroes is a valid invariant
267            let mut record_chunks = unsafe {
268                Box::<[[u8; RecordChunk::SIZE]; Record::NUM_S_BUCKETS]>::new_zeroed().assume_init()
269            };
270
271            for (target, source) in record_chunks.iter_mut().zip(
272                sector_record_chunks
273                    .source
274                    .iter()
275                    .chain(sector_record_chunks.parity.iter()),
276            ) {
277                *target = *source;
278            }
279
280            let record_merkle_tree =
281                BalancedMerkleTree::<{ Record::NUM_S_BUCKETS }>::new_boxed(&record_chunks);
282            drop(sector_record_chunks);
283
284            // NOTE: We do not check plot consistency using checksum because it is more
285            // expensive and consensus will verify validity of the proof anyway
286            let record_metadata_fut = read_record_metadata(
287                piece_offset,
288                self.sector_metadata.pieces_in_sector,
289                &self.sector,
290            );
291            let RecordMetadata {
292                piece_header,
293                piece_checksum: _,
294            } = record_metadata_fut
295                .now_or_never()
296                .expect("Sync reader; qed")
297                .map_err(ProvingError::RecordReadingError)?;
298
299            let proof_of_space = pos_proofs.for_s_bucket(self.s_bucket).expect(
300                "Proof exists for this s-bucket, otherwise it wouldn't be a winning chunk; qed",
301            );
302
303            let chunk_proof = record_merkle_tree
304                .all_proofs()
305                .nth(usize::from(self.s_bucket))
306                .expect("Chunk offset is valid, hence corresponding proof exists; qed");
307
308            let history_size = self.sector_metadata.history_size;
309            let shard_commitment = derive_solution_shard_commitment(
310                self.public_key_hash,
311                &self.shard_commitments_roots_cache.shard_commitments_seed(),
312                &self.shard_commitments_roots_cache.get(history_size),
313                history_size,
314                &self.shard_membership_entropy,
315                self.num_shards,
316            );
317
318            Solution {
319                public_key_hash: *self.public_key_hash,
320                shard_commitment,
321                piece_local_segment_index: piece_header.local_segment_index.as_inner(),
322                piece_super_segment_index: piece_header.super_segment_index.as_inner(),
323                segment_root: piece_header.segment_root,
324                segment_proof: piece_header.segment_proof,
325                record_root: RecordRoot::from(record_merkle_tree.root()),
326                record_proof: piece_header.record_proof,
327                chunk,
328                chunk_proof: ChunkProof::from(chunk_proof),
329                proof_of_space,
330                history_size,
331                sector_index: self.sector_metadata.sector_index,
332                piece_offset,
333                segment_position: piece_header.segment_position.as_inner(),
334                piece_shard_index: piece_header.shard_index.as_inner(),
335                padding: [0; _],
336            }
337        };
338
339        match maybe_solution {
340            Ok(solution) => Some(Ok(solution)),
341            Err(error) => Some(Err(error)),
342        }
343    }
344
345    fn size_hint(&self) -> (usize, Option<usize>) {
346        (self.count, Some(self.count))
347    }
348}
349
350impl<'a, PosProofGenerator, Sector> ProvableSolutions
351    for SolutionsIterator<'a, PosProofGenerator, Sector>
352where
353    Sector: ReadAtSync + 'a,
354    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
355{
356    fn best_solution_distance(&self) -> Option<SolutionDistance> {
357        self.best_solution_distance
358    }
359}
360
361impl<'a, PosProofGenerator, Sector> SolutionsIterator<'a, PosProofGenerator, Sector>
362where
363    Sector: ReadAtSync + 'a,
364    PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
365{
366    // TODO: Struct for arguments
367    #[expect(clippy::too_many_arguments, reason = "TODO")]
368    fn new(
369        public_key_hash: &'a Blake3Hash,
370        sector_id: SectorId,
371        shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
372        shard_membership_entropy: ShardMembershipEntropy,
373        num_shards: NumShards,
374        s_bucket: SBucket,
375        sector: Sector,
376        sector_metadata: &'a SectorMetadataChecksummed,
377        erasure_coding: &'a ErasureCoding,
378        chunk_candidates: VecDeque<ChunkCandidate>,
379        table_generator: PosProofGenerator,
380    ) -> Result<Self, ProvingError> {
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_piece_offsets = sector_contents_map
394            .iter_s_bucket_piece_offsets(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            .map(move |chunk_candidate| {
400                let piece_offset = s_bucket_piece_offsets
401                    .get(chunk_candidate.chunk_offset as usize)
402                    .expect("Wouldn't be a candidate if wasn't within s-bucket; qed");
403
404                WinningChunk {
405                    piece_offset: *piece_offset,
406                    solution_distance: chunk_candidate.solution_distance,
407                }
408            })
409            .collect::<VecDeque<_>>();
410
411        let best_solution_distance = winning_chunks
412            .front()
413            .map(|winning_chunk| winning_chunk.solution_distance);
414
415        let s_bucket_offsets = sector_metadata.s_bucket_offsets();
416
417        let count = winning_chunks.len();
418
419        Ok(Self {
420            public_key_hash,
421            sector_id,
422            shard_commitments_roots_cache,
423            shard_membership_entropy,
424            num_shards,
425            s_bucket,
426            sector_metadata,
427            s_bucket_offsets,
428            erasure_coding,
429            sector_contents_map,
430            sector: ReadAt::from_sync(sector),
431            winning_chunks,
432            count,
433            best_solution_distance,
434            table_generator,
435        })
436    }
437}