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)]
45pub(crate) struct ChunkCandidate {
46 pub(crate) chunk_offset: u32,
48 pub(crate) solution_distance: SolutionDistance,
50}
51
52pub fn audit_sector_sync<'a, Sector>(
56 public_key_hash: &'a Blake3Hash,
57 global_challenge: &Blake3Hash,
58 solution_range: SolutionRange,
59 sector: Sector,
60 sector_metadata: &'a SectorMetadataChecksummed,
61) -> Result<Option<AuditResult<'a, Sector>>, AuditingError>
62where
63 Sector: ReadAtSync + 'a,
64{
65 let SectorAuditingDetails {
66 sector_id,
67 sector_slot_challenge,
68 s_bucket_audit_index,
69 s_bucket_audit_size,
70 s_bucket_audit_offset_in_sector,
71 } = collect_sector_auditing_details(public_key_hash, global_challenge, sector_metadata);
72
73 let mut s_bucket = vec![0; s_bucket_audit_size];
74 sector
75 .read_at(&mut s_bucket, s_bucket_audit_offset_in_sector)
76 .map_err(|error| AuditingError::SBucketReading {
77 sector_index: sector_metadata.sector_index,
78 s_bucket_audit_index,
79 error,
80 })?;
81
82 let Some(winning_chunks) = map_winning_chunks(
83 &s_bucket,
84 global_challenge,
85 §or_slot_challenge,
86 solution_range,
87 ) else {
88 return Ok(None);
89 };
90
91 Ok(Some(AuditResult {
92 sector_index: sector_metadata.sector_index,
93 solution_candidates: SolutionCandidates::new(
94 public_key_hash,
95 sector_id,
96 s_bucket_audit_index,
97 sector,
98 sector_metadata,
99 winning_chunks.into(),
100 ),
101 }))
102}
103
104pub fn audit_plot_sync<'a, 'b, Plot>(
112 public_key_hash: &'a Blake3Hash,
113 global_challenge: &Blake3Hash,
114 solution_range: SolutionRange,
115 plot: &'a Plot,
116 sectors_metadata: &'a [SectorMetadataChecksummed],
117 sectors_being_modified: &'b HashSet<SectorIndex>,
118) -> Result<Vec<AuditResult<'a, ReadAtOffset<'a, Plot>>>, AuditingError>
119where
120 Plot: ReadAtSync + 'a,
121{
122 sectors_metadata
124 .par_iter()
125 .map(|sector_metadata| {
126 (
127 collect_sector_auditing_details(public_key_hash, global_challenge, sector_metadata),
128 sector_metadata,
129 )
130 })
131 .filter_map(|(sector_auditing_info, sector_metadata)| {
134 if sectors_being_modified.contains(§or_metadata.sector_index) {
135 return None;
137 }
138
139 if sector_auditing_info.s_bucket_audit_size == 0 {
140 return None;
142 }
143
144 let sector = plot.offset(
145 u64::from(sector_metadata.sector_index)
146 * sector_size(sector_metadata.pieces_in_sector) as u64,
147 );
148
149 let mut s_bucket = vec![0; sector_auditing_info.s_bucket_audit_size];
150
151 if let Err(error) = sector.read_at(
152 &mut s_bucket,
153 sector_auditing_info.s_bucket_audit_offset_in_sector,
154 ) {
155 return Some(Err(AuditingError::SBucketReading {
156 sector_index: sector_metadata.sector_index,
157 s_bucket_audit_index: sector_auditing_info.s_bucket_audit_index,
158 error,
159 }));
160 }
161
162 let winning_chunks = map_winning_chunks(
163 &s_bucket,
164 global_challenge,
165 §or_auditing_info.sector_slot_challenge,
166 solution_range,
167 )?;
168
169 Some(Ok(AuditResult {
170 sector_index: sector_metadata.sector_index,
171 solution_candidates: SolutionCandidates::new(
172 public_key_hash,
173 sector_auditing_info.sector_id,
174 sector_auditing_info.s_bucket_audit_index,
175 sector,
176 sector_metadata,
177 winning_chunks.into(),
178 ),
179 }))
180 })
181 .collect()
182}
183
184struct SectorAuditingDetails {
185 sector_id: SectorId,
186 sector_slot_challenge: SectorSlotChallenge,
187 s_bucket_audit_index: SBucket,
188 s_bucket_audit_size: usize,
190 s_bucket_audit_offset_in_sector: u64,
192}
193
194fn collect_sector_auditing_details(
195 public_key_hash: &Blake3Hash,
196 global_challenge: &Blake3Hash,
197 sector_metadata: &SectorMetadataChecksummed,
198) -> SectorAuditingDetails {
199 let sector_id = SectorId::new(
200 public_key_hash,
201 sector_metadata.sector_index,
202 sector_metadata.history_size,
203 );
204
205 let sector_slot_challenge = sector_id.derive_sector_slot_challenge(global_challenge);
206 let s_bucket_audit_index = sector_slot_challenge.s_bucket_audit_index();
207 let s_bucket_audit_size = RecordChunk::SIZE
208 * usize::from(sector_metadata.s_bucket_sizes[usize::from(s_bucket_audit_index)]);
209 let s_bucket_audit_offset = RecordChunk::SIZE as u64
210 * sector_metadata
211 .s_bucket_sizes
212 .iter()
213 .take(s_bucket_audit_index.into())
214 .copied()
215 .map(u64::from)
216 .sum::<u64>();
217
218 let sector_contents_map_size =
219 SectorContentsMap::encoded_size(sector_metadata.pieces_in_sector);
220
221 let s_bucket_audit_offset_in_sector = sector_contents_map_size as u64 + s_bucket_audit_offset;
222
223 SectorAuditingDetails {
224 sector_id,
225 sector_slot_challenge,
226 s_bucket_audit_index,
227 s_bucket_audit_size,
228 s_bucket_audit_offset_in_sector,
229 }
230}
231
232fn map_winning_chunks(
234 s_bucket: &[u8],
235 global_challenge: &Blake3Hash,
236 sector_slot_challenge: &SectorSlotChallenge,
237 solution_range: SolutionRange,
238) -> Option<Vec<ChunkCandidate>> {
239 let mut chunk_candidates = s_bucket
241 .as_chunks::<{ RecordChunk::SIZE }>()
242 .0
243 .iter()
244 .enumerate()
245 .filter_map(|(chunk_offset, chunk)| {
246 let solution_distance =
247 SolutionDistance::calculate(global_challenge, chunk, sector_slot_challenge);
248 solution_distance
249 .is_within(solution_range)
250 .then_some(ChunkCandidate {
251 chunk_offset: chunk_offset as u32,
252 solution_distance,
253 })
254 })
255 .collect::<Vec<_>>();
256
257 if chunk_candidates.is_empty() {
259 return None;
260 }
261
262 chunk_candidates.sort_by_key(|chunk_candidate| chunk_candidate.solution_distance);
263
264 Some(chunk_candidates)
265}