ab_farmer_components/
auditing.rs

1//! Auditing utilities
2//!
3//! There is a way to both audit a single sector (primarily helpful for testing purposes) and the
4//! whole plot (which is heavily parallelized and much more efficient) created by functions in
5//! [`plotting`](crate::plotting) module earlier.
6
7use 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/// Errors that happen during proving
22#[derive(Debug, Error)]
23pub enum AuditingError {
24    /// Failed read s-bucket
25    #[error("Failed read s-bucket {s_bucket_audit_index} of sector {sector_index}: {error}")]
26    SBucketReading {
27        /// Sector index
28        sector_index: SectorIndex,
29        /// S-bucket audit index
30        s_bucket_audit_index: SBucket,
31        /// Low-level error
32        error: io::Error,
33    },
34}
35
36/// Result of sector audit
37#[derive(Debug, Clone)]
38pub struct AuditResult<'a, Sector> {
39    /// Sector index
40    pub sector_index: SectorIndex,
41    /// Solution candidates
42    pub solution_candidates: SolutionCandidates<'a, Sector>,
43}
44
45/// Chunk candidate, contains one or more winning audit chunks
46#[derive(Debug, Clone)]
47pub(crate) struct ChunkCandidate {
48    /// Chunk offset within s-bucket
49    pub(crate) chunk_offset: u32,
50    /// Solution distance of this chunk, which can be used to prioritize higher quality solutions
51    pub(crate) solution_distance: SolutionDistance,
52}
53
54/// Audit a single sector and generate a stream of solutions.
55///
56/// This is primarily helpful in test environment, prefer [`audit_plot_sync`] for auditing real
57/// plots.
58// TODO: Struct for arguments
59#[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        &sector_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/// Audit the whole plot and generate streams of results.
121///
122/// Each audit result contains a solution candidate that might be converted into solution (but will
123/// not necessarily succeed, auditing only does quick checks and can't know for sure).
124///
125/// Plot is assumed to contain concatenated series of sectors as created by functions in
126/// [`plotting`](crate::plotting) module earlier.
127// TODO: Struct for arguments
128#[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    // Create auditing info for all sectors in parallel
144    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        // Read s-buckets of all sectors, map to winning chunks and then to audit results, all in
158        // parallel
159        .filter_map(|(sector_auditing_info, sector_metadata)| {
160            if sectors_being_modified.contains(&sector_metadata.sector_index) {
161                // Skip sector that is being modified right now
162                return None;
163            }
164
165            if sector_auditing_info.s_bucket_audit_size == 0 {
166                // S-bucket is empty
167                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                &sector_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    /// Size in bytes
218    s_bucket_audit_size: usize,
219    /// Offset in bytes
220    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
263/// Map all winning chunks
264fn 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    // Map all winning chunks
271    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    // Check if there are any solutions possible
289    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}