1use crate::proving::SolutionCandidates;
8use crate::sector::{SectorContentsMap, SectorMetadataChecksummed, sector_size};
9use crate::{ReadAtOffset, ReadAtSync};
10use ab_core_primitives::hashes::Blake3Hash;
11use ab_core_primitives::pieces::RecordChunk;
12use ab_core_primitives::sectors::{SBucket, SectorId, SectorIndex, SectorSlotChallenge};
13use ab_core_primitives::solutions::{SolutionDistance, SolutionRange};
14use rayon::prelude::*;
15use std::collections::HashSet;
16use std::io;
17use thiserror::Error;
18
19#[derive(Debug, Error)]
21pub enum AuditingError {
22 #[error("Failed read s-bucket {s_bucket_audit_index} of sector {sector_index}: {error}")]
24 SBucketReading {
25 sector_index: SectorIndex,
27 s_bucket_audit_index: SBucket,
29 error: io::Error,
31 },
32}
33
34#[derive(Debug, Clone)]
36pub struct AuditResult<'a, Sector> {
37 pub sector_index: SectorIndex,
39 pub solution_candidates: SolutionCandidates<'a, Sector>,
41}
42
43#[derive(Debug, Clone)]
46pub(crate) struct ChunkCandidate {
47 pub(crate) chunk_offset: u32,
49 pub(crate) solution_distance: SolutionDistance,
51}
52
53pub fn audit_sector_sync<'a, Sector>(
57 public_key_hash: &'a Blake3Hash,
58 global_challenge: &Blake3Hash,
59 solution_range: SolutionRange,
60 sector: Sector,
61 sector_metadata: &'a SectorMetadataChecksummed,
62) -> Result<Option<AuditResult<'a, Sector>>, AuditingError>
63where
64 Sector: ReadAtSync + 'a,
65{
66 let SectorAuditingDetails {
67 sector_id,
68 sector_slot_challenge,
69 s_bucket_audit_index,
70 s_bucket_audit_size,
71 s_bucket_audit_offset_in_sector,
72 } = collect_sector_auditing_details(public_key_hash, global_challenge, sector_metadata);
73
74 let mut s_bucket = vec![0; s_bucket_audit_size];
75 sector
76 .read_at(&mut s_bucket, s_bucket_audit_offset_in_sector)
77 .map_err(|error| AuditingError::SBucketReading {
78 sector_index: sector_metadata.sector_index,
79 s_bucket_audit_index,
80 error,
81 })?;
82
83 let Some(winning_chunks) = map_winning_chunks(
84 &s_bucket,
85 global_challenge,
86 §or_slot_challenge,
87 solution_range,
88 ) else {
89 return Ok(None);
90 };
91
92 Ok(Some(AuditResult {
93 sector_index: sector_metadata.sector_index,
94 solution_candidates: SolutionCandidates::new(
95 public_key_hash,
96 sector_id,
97 s_bucket_audit_index,
98 sector,
99 sector_metadata,
100 winning_chunks.into(),
101 ),
102 }))
103}
104
105pub fn audit_plot_sync<'a, 'b, Plot>(
113 public_key_hash: &'a Blake3Hash,
114 global_challenge: &Blake3Hash,
115 solution_range: SolutionRange,
116 plot: &'a Plot,
117 sectors_metadata: &'a [SectorMetadataChecksummed],
118 sectors_being_modified: &'b HashSet<SectorIndex>,
119) -> Result<Vec<AuditResult<'a, ReadAtOffset<'a, Plot>>>, AuditingError>
120where
121 Plot: ReadAtSync + 'a,
122{
123 sectors_metadata
125 .par_iter()
126 .map(|sector_metadata| {
127 (
128 collect_sector_auditing_details(public_key_hash, global_challenge, sector_metadata),
129 sector_metadata,
130 )
131 })
132 .filter_map(|(sector_auditing_info, sector_metadata)| {
135 if sectors_being_modified.contains(§or_metadata.sector_index) {
136 return None;
138 }
139
140 if sector_auditing_info.s_bucket_audit_size == 0 {
141 return None;
143 }
144
145 let sector = plot.offset(
146 u64::from(sector_metadata.sector_index)
147 * sector_size(sector_metadata.pieces_in_sector) as u64,
148 );
149
150 let mut s_bucket = vec![0; sector_auditing_info.s_bucket_audit_size];
151
152 if let Err(error) = sector.read_at(
153 &mut s_bucket,
154 sector_auditing_info.s_bucket_audit_offset_in_sector,
155 ) {
156 return Some(Err(AuditingError::SBucketReading {
157 sector_index: sector_metadata.sector_index,
158 s_bucket_audit_index: sector_auditing_info.s_bucket_audit_index,
159 error,
160 }));
161 }
162
163 let winning_chunks = map_winning_chunks(
164 &s_bucket,
165 global_challenge,
166 §or_auditing_info.sector_slot_challenge,
167 solution_range,
168 )?;
169
170 Some(Ok(AuditResult {
171 sector_index: sector_metadata.sector_index,
172 solution_candidates: SolutionCandidates::new(
173 public_key_hash,
174 sector_auditing_info.sector_id,
175 sector_auditing_info.s_bucket_audit_index,
176 sector,
177 sector_metadata,
178 winning_chunks.into(),
179 ),
180 }))
181 })
182 .collect()
183}
184
185struct SectorAuditingDetails {
186 sector_id: SectorId,
187 sector_slot_challenge: SectorSlotChallenge,
188 s_bucket_audit_index: SBucket,
189 s_bucket_audit_size: usize,
191 s_bucket_audit_offset_in_sector: u64,
193}
194
195fn collect_sector_auditing_details(
196 public_key_hash: &Blake3Hash,
197 global_challenge: &Blake3Hash,
198 sector_metadata: &SectorMetadataChecksummed,
199) -> SectorAuditingDetails {
200 let sector_id = SectorId::new(
201 public_key_hash,
202 sector_metadata.sector_index,
203 sector_metadata.history_size,
204 );
205
206 let sector_slot_challenge = sector_id.derive_sector_slot_challenge(global_challenge);
207 let s_bucket_audit_index = sector_slot_challenge.s_bucket_audit_index();
208 let s_bucket_audit_size = RecordChunk::SIZE
209 * usize::from(sector_metadata.s_bucket_sizes[usize::from(s_bucket_audit_index)]);
210 let s_bucket_audit_offset = RecordChunk::SIZE as u64
211 * sector_metadata
212 .s_bucket_sizes
213 .iter()
214 .take(s_bucket_audit_index.into())
215 .copied()
216 .map(u64::from)
217 .sum::<u64>();
218
219 let sector_contents_map_size =
220 SectorContentsMap::encoded_size(sector_metadata.pieces_in_sector);
221
222 let s_bucket_audit_offset_in_sector = sector_contents_map_size as u64 + s_bucket_audit_offset;
223
224 SectorAuditingDetails {
225 sector_id,
226 sector_slot_challenge,
227 s_bucket_audit_index,
228 s_bucket_audit_size,
229 s_bucket_audit_offset_in_sector,
230 }
231}
232
233fn map_winning_chunks(
235 s_bucket: &[u8],
236 global_challenge: &Blake3Hash,
237 sector_slot_challenge: &SectorSlotChallenge,
238 solution_range: SolutionRange,
239) -> Option<Vec<ChunkCandidate>> {
240 let mut chunk_candidates = s_bucket
242 .array_chunks::<{ RecordChunk::SIZE }>()
243 .enumerate()
244 .filter_map(|(chunk_offset, chunk)| {
245 let solution_distance =
246 SolutionDistance::calculate(global_challenge, chunk, sector_slot_challenge);
247 solution_distance
248 .is_within(solution_range)
249 .then_some(ChunkCandidate {
250 chunk_offset: chunk_offset as u32,
251 solution_distance,
252 })
253 })
254 .collect::<Vec<_>>();
255
256 if chunk_candidates.is_empty() {
258 return None;
259 }
260
261 chunk_candidates.sort_by_key(|chunk_candidate| chunk_candidate.solution_distance);
262
263 Some(chunk_candidates)
264}