1use crate::proving::SolutionCandidates;
8use crate::sector::{SectorContentsMap, SectorMetadataChecksummed, sector_size};
9use crate::shard_commitment::ShardCommitmentsRootsCache;
10use crate::{ReadAtOffset, ReadAtSync};
11use ab_core_primitives::hashes::Blake3Hash;
12use ab_core_primitives::pieces::RecordChunk;
13use ab_core_primitives::sectors::{SBucket, SectorId, SectorIndex, SectorSlotChallenge};
14use ab_core_primitives::shard::NumShards;
15use ab_core_primitives::solutions::{ShardMembershipEntropy, SolutionDistance, SolutionRange};
16use rayon::prelude::*;
17use std::collections::HashSet;
18use std::io;
19use thiserror::Error;
20
21#[derive(Debug, Error)]
23pub enum AuditingError {
24 #[error("Failed read s-bucket {s_bucket_audit_index} of sector {sector_index}: {error}")]
26 SBucketReading {
27 sector_index: SectorIndex,
29 s_bucket_audit_index: SBucket,
31 error: io::Error,
33 },
34}
35
36#[derive(Debug, Clone)]
38pub struct AuditResult<'a, Sector> {
39 pub sector_index: SectorIndex,
41 pub solution_candidates: SolutionCandidates<'a, Sector>,
43}
44
45#[derive(Debug, Clone)]
47pub(crate) struct ChunkCandidate {
48 pub(crate) chunk_offset: u32,
50 pub(crate) solution_distance: SolutionDistance,
52}
53
54#[expect(clippy::too_many_arguments)]
60pub fn audit_sector_sync<'a, Sector>(
61 public_key_hash: &'a Blake3Hash,
62 shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
63 shard_membership_entropy: ShardMembershipEntropy,
64 num_shards: NumShards,
65 global_challenge: &Blake3Hash,
66 solution_range: SolutionRange,
67 sector: Sector,
68 sector_metadata: &'a SectorMetadataChecksummed,
69) -> Result<Option<AuditResult<'a, Sector>>, AuditingError>
70where
71 Sector: ReadAtSync + 'a,
72{
73 let SectorAuditingDetails {
74 sector_id,
75 sector_slot_challenge,
76 s_bucket_audit_index,
77 s_bucket_audit_size,
78 s_bucket_audit_offset_in_sector,
79 } = collect_sector_auditing_details(
80 public_key_hash,
81 shard_commitments_roots_cache,
82 global_challenge,
83 sector_metadata,
84 );
85
86 let mut s_bucket = vec![0; s_bucket_audit_size];
87 sector
88 .read_at(&mut s_bucket, s_bucket_audit_offset_in_sector)
89 .map_err(|error| AuditingError::SBucketReading {
90 sector_index: sector_metadata.sector_index,
91 s_bucket_audit_index,
92 error,
93 })?;
94
95 let Some(winning_chunks) = map_winning_chunks(
96 &s_bucket,
97 global_challenge,
98 §or_slot_challenge,
99 solution_range,
100 ) else {
101 return Ok(None);
102 };
103
104 Ok(Some(AuditResult {
105 sector_index: sector_metadata.sector_index,
106 solution_candidates: SolutionCandidates::new(
107 public_key_hash,
108 sector_id,
109 shard_commitments_roots_cache,
110 shard_membership_entropy,
111 num_shards,
112 s_bucket_audit_index,
113 sector,
114 sector_metadata,
115 winning_chunks.into(),
116 ),
117 }))
118}
119
120#[expect(clippy::too_many_arguments)]
129pub fn audit_plot_sync<'a, 'b, Plot>(
130 public_key_hash: &'a Blake3Hash,
131 shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
132 shard_membership_entropy: ShardMembershipEntropy,
133 num_shards: NumShards,
134 global_challenge: &Blake3Hash,
135 solution_range: SolutionRange,
136 plot: &'a Plot,
137 sectors_metadata: &'a [SectorMetadataChecksummed],
138 sectors_being_modified: &'b HashSet<SectorIndex>,
139) -> Result<Vec<AuditResult<'a, ReadAtOffset<'a, Plot>>>, AuditingError>
140where
141 Plot: ReadAtSync + 'a,
142{
143 sectors_metadata
145 .par_iter()
146 .map(|sector_metadata| {
147 (
148 collect_sector_auditing_details(
149 public_key_hash,
150 shard_commitments_roots_cache,
151 global_challenge,
152 sector_metadata,
153 ),
154 sector_metadata,
155 )
156 })
157 .filter_map(|(sector_auditing_info, sector_metadata)| {
160 if sectors_being_modified.contains(§or_metadata.sector_index) {
161 return None;
163 }
164
165 if sector_auditing_info.s_bucket_audit_size == 0 {
166 return None;
168 }
169
170 let sector = plot.offset(
171 u64::from(sector_metadata.sector_index)
172 * sector_size(sector_metadata.pieces_in_sector) as u64,
173 );
174
175 let mut s_bucket = vec![0; sector_auditing_info.s_bucket_audit_size];
176
177 if let Err(error) = sector.read_at(
178 &mut s_bucket,
179 sector_auditing_info.s_bucket_audit_offset_in_sector,
180 ) {
181 return Some(Err(AuditingError::SBucketReading {
182 sector_index: sector_metadata.sector_index,
183 s_bucket_audit_index: sector_auditing_info.s_bucket_audit_index,
184 error,
185 }));
186 }
187
188 let winning_chunks = map_winning_chunks(
189 &s_bucket,
190 global_challenge,
191 §or_auditing_info.sector_slot_challenge,
192 solution_range,
193 )?;
194
195 Some(Ok(AuditResult {
196 sector_index: sector_metadata.sector_index,
197 solution_candidates: SolutionCandidates::new(
198 public_key_hash,
199 sector_auditing_info.sector_id,
200 shard_commitments_roots_cache,
201 shard_membership_entropy,
202 num_shards,
203 sector_auditing_info.s_bucket_audit_index,
204 sector,
205 sector_metadata,
206 winning_chunks.into(),
207 ),
208 }))
209 })
210 .collect()
211}
212
213struct SectorAuditingDetails {
214 sector_id: SectorId,
215 sector_slot_challenge: SectorSlotChallenge,
216 s_bucket_audit_index: SBucket,
217 s_bucket_audit_size: usize,
219 s_bucket_audit_offset_in_sector: u64,
221}
222
223fn collect_sector_auditing_details(
224 public_key_hash: &Blake3Hash,
225 shard_commitments_roots_cache: &ShardCommitmentsRootsCache,
226 global_challenge: &Blake3Hash,
227 sector_metadata: &SectorMetadataChecksummed,
228) -> SectorAuditingDetails {
229 let sector_id = SectorId::new(
230 public_key_hash,
231 &shard_commitments_roots_cache.get(sector_metadata.history_size),
232 sector_metadata.sector_index,
233 sector_metadata.history_size,
234 );
235
236 let sector_slot_challenge = sector_id.derive_sector_slot_challenge(global_challenge);
237 let s_bucket_audit_index = sector_slot_challenge.s_bucket_audit_index();
238 let s_bucket_audit_size = RecordChunk::SIZE
239 * usize::from(sector_metadata.s_bucket_sizes[usize::from(s_bucket_audit_index)]);
240 let s_bucket_audit_offset = RecordChunk::SIZE as u64
241 * sector_metadata
242 .s_bucket_sizes
243 .iter()
244 .take(s_bucket_audit_index.into())
245 .copied()
246 .map(u64::from)
247 .sum::<u64>();
248
249 let sector_contents_map_size =
250 SectorContentsMap::encoded_size(sector_metadata.pieces_in_sector);
251
252 let s_bucket_audit_offset_in_sector = sector_contents_map_size as u64 + s_bucket_audit_offset;
253
254 SectorAuditingDetails {
255 sector_id,
256 sector_slot_challenge,
257 s_bucket_audit_index,
258 s_bucket_audit_size,
259 s_bucket_audit_offset_in_sector,
260 }
261}
262
263fn map_winning_chunks(
265 s_bucket: &[u8],
266 global_challenge: &Blake3Hash,
267 sector_slot_challenge: &SectorSlotChallenge,
268 solution_range: SolutionRange,
269) -> Option<Vec<ChunkCandidate>> {
270 let mut chunk_candidates = s_bucket
272 .as_chunks::<{ RecordChunk::SIZE }>()
273 .0
274 .iter()
275 .enumerate()
276 .filter_map(|(chunk_offset, chunk)| {
277 let solution_distance =
278 SolutionDistance::calculate(global_challenge, chunk, sector_slot_challenge);
279 solution_distance
280 .is_within(solution_range)
281 .then_some(ChunkCandidate {
282 chunk_offset: chunk_offset as u32,
283 solution_distance,
284 })
285 })
286 .collect::<Vec<_>>();
287
288 if chunk_candidates.is_empty() {
290 return None;
291 }
292
293 chunk_candidates.sort_by_key(|chunk_candidate| chunk_candidate.solution_distance);
294
295 Some(chunk_candidates)
296}