1use 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::shard_commitment::{ShardCommitmentsRootsCache, derive_solution_shard_commitment};
14use crate::{ReadAt, ReadAtSync};
15use ab_core_primitives::hashes::Blake3Hash;
16use ab_core_primitives::pieces::{PieceOffset, Record, RecordChunk};
17use ab_core_primitives::pos::PosSeed;
18use ab_core_primitives::sectors::{SBucket, SectorId};
19use ab_core_primitives::shard::NumShards;
20use ab_core_primitives::solutions::{
21 ChunkProof, ShardMembershipEntropy, Solution, SolutionDistance,
22};
23use ab_erasure_coding::ErasureCoding;
24use ab_merkle_tree::balanced::BalancedMerkleTree;
25use ab_proof_of_space::PosProofs;
26use futures::FutureExt;
27use std::collections::VecDeque;
28use std::io;
29use thiserror::Error;
30
31pub trait ProvableSolutions: ExactSizeIterator {
35 fn best_solution_distance(&self) -> Option<SolutionDistance>;
37}
38
39#[derive(Debug, Error)]
41pub enum ProvingError {
42 #[error("Failed to create polynomial for record at offset {piece_offset}: {error}")]
44 FailedToCreatePolynomialForRecord {
45 piece_offset: PieceOffset,
47 error: String,
49 },
50 #[error("Failed to decode sector contents map: {0}")]
52 FailedToDecodeSectorContentsMap(#[from] SectorContentsMapFromBytesError),
53 #[error("Proving I/O error: {0}")]
55 Io(#[from] io::Error),
56 #[error("Record reading error: {0}")]
58 RecordReadingError(#[from] ReadingError),
59}
60
61impl ProvingError {
62 pub fn is_fatal(&self) -> bool {
64 match self {
65 ProvingError::FailedToCreatePolynomialForRecord { .. } => false,
66 ProvingError::FailedToDecodeSectorContentsMap(_) => false,
67 ProvingError::Io(_) => true,
68 ProvingError::RecordReadingError(error) => error.is_fatal(),
69 }
70 }
71}
72
73#[derive(Debug, Clone)]
74struct WinningChunk {
75 piece_offset: PieceOffset,
77 solution_distance: SolutionDistance,
79}
80
81#[derive(Debug)]
86pub struct SolutionCandidates<'a, Sector>
87where
88 Sector: 'a,
89{
90 public_key_hash: &'a Blake3Hash,
91 sector_id: SectorId,
92 shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
93 shard_membership_entropy: ShardMembershipEntropy,
94 num_shards: NumShards,
95 s_bucket: SBucket,
96 sector: Sector,
97 sector_metadata: &'a SectorMetadataChecksummed,
98 chunk_candidates: VecDeque<ChunkCandidate>,
99}
100
101impl<'a, Sector> Clone for SolutionCandidates<'a, Sector>
102where
103 Sector: Clone + 'a,
104{
105 fn clone(&self) -> Self {
106 Self {
107 public_key_hash: self.public_key_hash,
108 sector_id: self.sector_id,
109 shard_commitments_roots_cache: self.shard_commitments_roots_cache,
110 shard_membership_entropy: self.shard_membership_entropy,
111 num_shards: self.num_shards,
112 s_bucket: self.s_bucket,
113 sector: self.sector.clone(),
114 sector_metadata: self.sector_metadata,
115 chunk_candidates: self.chunk_candidates.clone(),
116 }
117 }
118}
119
120impl<'a, Sector> SolutionCandidates<'a, Sector>
121where
122 Sector: ReadAtSync + 'a,
123{
124 #[expect(clippy::too_many_arguments, reason = "Private API")]
125 pub(crate) fn new(
126 public_key_hash: &'a Blake3Hash,
127 sector_id: SectorId,
128 shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
129 shard_membership_entropy: ShardMembershipEntropy,
130 num_shards: NumShards,
131 s_bucket: SBucket,
132 sector: Sector,
133 sector_metadata: &'a SectorMetadataChecksummed,
134 chunk_candidates: VecDeque<ChunkCandidate>,
135 ) -> Self {
136 Self {
137 public_key_hash,
138 sector_id,
139 shard_commitments_roots_cache,
140 shard_membership_entropy,
141 num_shards,
142 s_bucket,
143 sector,
144 sector_metadata,
145 chunk_candidates,
146 }
147 }
148
149 pub fn len(&self) -> usize {
151 self.chunk_candidates.len()
152 }
153
154 pub fn is_empty(&self) -> bool {
156 self.chunk_candidates.is_empty()
157 }
158
159 pub fn into_solutions<PosProofGenerator>(
161 self,
162 erasure_coding: &'a ErasureCoding,
163 table_generator: PosProofGenerator,
164 ) -> Result<impl ProvableSolutions<Item = MaybeSolution> + 'a, ProvingError>
165 where
166 PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
167 {
168 SolutionsIterator::<'a, _, _>::new(
169 self.public_key_hash,
170 self.sector_id,
171 self.shard_commitments_roots_cache,
172 self.shard_membership_entropy,
173 self.num_shards,
174 self.s_bucket,
175 self.sector,
176 self.sector_metadata,
177 erasure_coding,
178 self.chunk_candidates,
179 table_generator,
180 )
181 }
182}
183
184type MaybeSolution = Result<Solution, ProvingError>;
185
186struct SolutionsIterator<'a, PosProofGenerator, Sector>
187where
188 Sector: ReadAtSync + 'a,
189 PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
190{
191 public_key_hash: &'a Blake3Hash,
192 sector_id: SectorId,
193 shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
194 shard_membership_entropy: ShardMembershipEntropy,
195 num_shards: NumShards,
196 s_bucket: SBucket,
197 sector_metadata: &'a SectorMetadataChecksummed,
198 s_bucket_offsets: Box<[u32; Record::NUM_S_BUCKETS]>,
199 erasure_coding: &'a ErasureCoding,
200 sector_contents_map: SectorContentsMap,
201 sector: ReadAt<Sector, !>,
202 winning_chunks: VecDeque<WinningChunk>,
203 count: usize,
204 best_solution_distance: Option<SolutionDistance>,
205 table_generator: PosProofGenerator,
206}
207
208impl<'a, PosProofGenerator, Sector> ExactSizeIterator
209 for SolutionsIterator<'a, PosProofGenerator, Sector>
210where
211 Sector: ReadAtSync + 'a,
212 PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
213{
214}
215
216impl<'a, PosProofGenerator, Sector> Iterator for SolutionsIterator<'a, PosProofGenerator, Sector>
217where
218 Sector: ReadAtSync + 'a,
219 PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
220{
221 type Item = MaybeSolution;
222
223 fn next(&mut self) -> Option<Self::Item> {
224 let WinningChunk {
225 piece_offset,
226 solution_distance: _,
227 } = self.winning_chunks.pop_front()?;
228
229 self.count -= 1;
230
231 let pos_proofs =
233 (self.table_generator)(&self.sector_id.derive_evaluation_seed(piece_offset));
234
235 let maybe_solution = try {
236 let sector_record_chunks_fut = read_sector_record_chunks(
237 piece_offset,
238 self.sector_metadata.pieces_in_sector,
239 &self.s_bucket_offsets,
240 &self.sector_contents_map,
241 &pos_proofs,
242 &self.sector,
243 );
244 let sector_record_chunks = sector_record_chunks_fut
245 .now_or_never()
246 .expect("Sync reader; qed")
247 .map_err(ProvingError::RecordReadingError)?;
248
249 let chunk = sector_record_chunks
250 .get(usize::from(self.s_bucket))
251 .expect("Within s-bucket range; qed")
252 .expect("Winning chunk was plotted; qed");
253
254 let chunks = recover_extended_record_chunks(
255 §or_record_chunks,
256 piece_offset,
257 self.erasure_coding,
258 )
259 .map_err(ProvingError::RecordReadingError)?;
260 drop(sector_record_chunks);
261
262 const _: () = {
266 assert!(Record::NUM_S_BUCKETS == 65536);
267 };
268 let record_merkle_tree = BalancedMerkleTree::<65536>::new_boxed(
269 RecordChunk::slice_to_repr(chunks.as_slice())
270 .try_into()
271 .expect("Statically guaranteed to have correct length; qed"),
272 );
273
274 let record_metadata_fut = read_record_metadata(
277 piece_offset,
278 self.sector_metadata.pieces_in_sector,
279 &self.sector,
280 );
281 let record_metadata = record_metadata_fut
282 .now_or_never()
283 .expect("Sync reader; qed")
284 .map_err(ProvingError::RecordReadingError)?;
285
286 let proof_of_space = pos_proofs.for_s_bucket(self.s_bucket).expect(
287 "Proof exists for this s-bucket, otherwise it wouldn't be a winning chunk; qed",
288 );
289
290 let chunk_proof = record_merkle_tree
291 .all_proofs()
292 .nth(usize::from(self.s_bucket))
293 .expect("Chunk offset is valid, hence corresponding proof exists; qed");
294
295 let history_size = self.sector_metadata.history_size;
296 let shard_commitment = derive_solution_shard_commitment(
297 self.public_key_hash,
298 &self.shard_commitments_roots_cache.shard_commitments_seed(),
299 &self.shard_commitments_roots_cache.get(history_size),
300 history_size,
301 &self.shard_membership_entropy,
302 self.num_shards,
303 );
304
305 Solution {
306 public_key_hash: *self.public_key_hash,
307 shard_commitment,
308 record_root: record_metadata.root,
309 record_proof: record_metadata.proof,
310 chunk,
311 chunk_proof: ChunkProof::from(chunk_proof),
312 proof_of_space,
313 history_size,
314 sector_index: self.sector_metadata.sector_index,
315 piece_offset,
316 padding: [0; _],
317 }
318 };
319
320 match maybe_solution {
321 Ok(solution) => Some(Ok(solution)),
322 Err(error) => Some(Err(error)),
323 }
324 }
325
326 fn size_hint(&self) -> (usize, Option<usize>) {
327 (self.count, Some(self.count))
328 }
329}
330
331impl<'a, PosProofGenerator, Sector> ProvableSolutions
332 for SolutionsIterator<'a, PosProofGenerator, Sector>
333where
334 Sector: ReadAtSync + 'a,
335 PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
336{
337 fn best_solution_distance(&self) -> Option<SolutionDistance> {
338 self.best_solution_distance
339 }
340}
341
342impl<'a, PosProofGenerator, Sector> SolutionsIterator<'a, PosProofGenerator, Sector>
343where
344 Sector: ReadAtSync + 'a,
345 PosProofGenerator: (FnMut(&PosSeed) -> Box<PosProofs>) + 'a,
346{
347 #[expect(clippy::too_many_arguments)]
348 fn new(
349 public_key_hash: &'a Blake3Hash,
350 sector_id: SectorId,
351 shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
352 shard_membership_entropy: ShardMembershipEntropy,
353 num_shards: NumShards,
354 s_bucket: SBucket,
355 sector: Sector,
356 sector_metadata: &'a SectorMetadataChecksummed,
357 erasure_coding: &'a ErasureCoding,
358 chunk_candidates: VecDeque<ChunkCandidate>,
359 table_generator: PosProofGenerator,
360 ) -> Result<Self, ProvingError> {
361 let sector_contents_map = {
362 let mut sector_contents_map_bytes =
363 vec![0; SectorContentsMap::encoded_size(sector_metadata.pieces_in_sector)];
364
365 sector.read_at(&mut sector_contents_map_bytes, 0)?;
366
367 SectorContentsMap::from_bytes(
368 §or_contents_map_bytes,
369 sector_metadata.pieces_in_sector,
370 )?
371 };
372
373 let s_bucket_piece_offsets = sector_contents_map
374 .iter_s_bucket_piece_offsets(s_bucket)
375 .expect("S-bucket audit index is guaranteed to be in range; qed")
376 .collect::<Vec<_>>();
377 let winning_chunks = chunk_candidates
378 .into_iter()
379 .map(move |chunk_candidate| {
380 let piece_offset = s_bucket_piece_offsets
381 .get(chunk_candidate.chunk_offset as usize)
382 .expect("Wouldn't be a candidate if wasn't within s-bucket; qed");
383
384 WinningChunk {
385 piece_offset: *piece_offset,
386 solution_distance: chunk_candidate.solution_distance,
387 }
388 })
389 .collect::<VecDeque<_>>();
390
391 let best_solution_distance = winning_chunks
392 .front()
393 .map(|winning_chunk| winning_chunk.solution_distance);
394
395 let s_bucket_offsets = sector_metadata.s_bucket_offsets();
396
397 let count = winning_chunks.len();
398
399 Ok(Self {
400 public_key_hash,
401 sector_id,
402 shard_commitments_roots_cache,
403 shard_membership_entropy,
404 num_shards,
405 s_bucket,
406 sector_metadata,
407 s_bucket_offsets,
408 erasure_coding,
409 sector_contents_map,
410 sector: ReadAt::from_sync(sector),
411 winning_chunks,
412 count,
413 best_solution_distance,
414 table_generator,
415 })
416 }
417}