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::{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
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("Proving I/O error: {0}")]
53 Io(#[from] io::Error),
54 #[error("Record reading error: {0}")]
56 RecordReadingError(#[from] ReadingError),
57}
58
59impl ProvingError {
60 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: PieceOffset,
79 solution_distance: SolutionDistance,
81}
82
83#[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 pub fn len(&self) -> usize {
153 self.chunk_candidates.len()
154 }
155
156 pub fn is_empty(&self) -> bool {
158 self.chunk_candidates.is_empty()
159 }
160
161 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 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 §or_record_chunks.present,
258 )
259 .map_err(|error| ReadingError::FailedToErasureDecodeRecord {
260 piece_offset,
261 error,
262 })
263 .map_err(ProvingError::RecordReadingError)?;
264
265 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 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 #[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 §or_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}