1use 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
29pub trait ProvableSolutions: ExactSizeIterator {
33 fn best_solution_distance(&self) -> Option<SolutionDistance>;
35}
36
37#[derive(Debug, Error)]
39pub enum ProvingError {
40 #[error("Failed to create polynomial for record at offset {piece_offset}: {error}")]
42 FailedToCreatePolynomialForRecord {
43 piece_offset: PieceOffset,
45 error: String,
47 },
48 #[error("Failed to decode sector contents map: {0}")]
50 FailedToDecodeSectorContentsMap(#[from] SectorContentsMapFromBytesError),
51 #[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: SBucket,
59 expected: u16,
61 actual: usize,
63 },
64 #[error("Proving I/O error: {0}")]
66 Io(#[from] io::Error),
67 #[error("Record reading error: {0}")]
69 RecordReadingError(#[from] ReadingError),
70}
71
72impl ProvingError {
73 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: PieceOffset,
93 solution_distance: SolutionDistance,
95}
96
97#[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 pub fn len(&self) -> usize {
167 self.chunk_candidates.len()
168 }
169
170 pub fn is_empty(&self) -> bool {
172 self.chunk_candidates.is_empty()
173 }
174
175 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 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, §or_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 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 #[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 §or_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 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}