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::{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/// Errors that happen during proving
20#[derive(Debug, Error)]
21pub enum AuditingError {
22    /// Failed read s-bucket
23    #[error("Failed read s-bucket {s_bucket_audit_index} of sector {sector_index}: {error}")]
24    SBucketReading {
25        /// Sector index
26        sector_index: SectorIndex,
27        /// S-bucket audit index
28        s_bucket_audit_index: SBucket,
29        /// Low-level error
30        error: io::Error,
31    },
32}
33
34/// Result of sector audit
35#[derive(Debug, Clone)]
36pub struct AuditResult<'a, Sector> {
37    /// Sector index
38    pub sector_index: SectorIndex,
39    /// Solution candidates
40    pub solution_candidates: SolutionCandidates<'a, Sector>,
41}
42
43/// Chunk candidate, contains one or more winning audit chunks
44#[derive(Debug, Clone)]
45pub(crate) struct ChunkCandidate {
46    /// Chunk offset within s-bucket
47    pub(crate) chunk_offset: u32,
48    /// Solution distance of this chunk, which can be used to prioritize higher quality solutions
49    pub(crate) solution_distance: SolutionDistance,
50}
51
52/// Audit a single sector and generate a stream of solutions.
53///
54/// This is primarily helpful in test environment, prefer [`audit_plot_sync`] for auditing real plots.
55pub 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        &sector_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
104/// Audit the whole plot and generate streams of results.
105///
106/// Each audit result contains a solution candidate that might be converted into solution (but will
107/// not necessarily succeed, auditing only does quick checks and can't know for sure).
108///
109/// Plot is assumed to contain concatenated series of sectors as created by functions in
110/// [`plotting`](crate::plotting) module earlier.
111pub 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    // Create auditing info for all sectors in parallel
123    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        // Read s-buckets of all sectors, map to winning chunks and then to audit results, all in
132        // parallel
133        .filter_map(|(sector_auditing_info, sector_metadata)| {
134            if sectors_being_modified.contains(&sector_metadata.sector_index) {
135                // Skip sector that is being modified right now
136                return None;
137            }
138
139            if sector_auditing_info.s_bucket_audit_size == 0 {
140                // S-bucket is empty
141                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                &sector_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    /// Size in bytes
189    s_bucket_audit_size: usize,
190    /// Offset in bytes
191    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
232/// Map all winning chunks
233fn 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    // Map all winning chunks
240    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    // Check if there are any solutions possible
258    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}