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 potentially winning audit chunks (in case chunk itself was
44/// encoded and eligible for claiming a reward)
45#[derive(Debug, Clone)]
46pub(crate) struct ChunkCandidate {
47    /// Chunk offset within s-bucket
48    pub(crate) chunk_offset: u32,
49    /// Solution distance of this chunk, which can be used to prioritize higher quality solutions
50    pub(crate) solution_distance: SolutionDistance,
51}
52
53/// Audit a single sector and generate a stream of solutions.
54///
55/// This is primarily helpful in test environment, prefer [`audit_plot_sync`] for auditing real plots.
56pub 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        &sector_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
105/// Audit the whole plot and generate streams of results.
106///
107/// Each audit result contains a solution candidate that might be converted into solution (but will
108/// not necessarily succeed, auditing only does quick checks and can't know for sure).
109///
110/// Plot is assumed to contain concatenated series of sectors as created by functions in
111/// [`plotting`](crate::plotting) module earlier.
112pub 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    // Create auditing info for all sectors in parallel
124    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        // Read s-buckets of all sectors, map to winning chunks and then to audit results, all in
133        // parallel
134        .filter_map(|(sector_auditing_info, sector_metadata)| {
135            if sectors_being_modified.contains(&sector_metadata.sector_index) {
136                // Skip sector that is being modified right now
137                return None;
138            }
139
140            if sector_auditing_info.s_bucket_audit_size == 0 {
141                // S-bucket is empty
142                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                &sector_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    /// Size in bytes
190    s_bucket_audit_size: usize,
191    /// Offset in bytes
192    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
233/// Map all winning chunks
234fn 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    // Map all winning chunks
241    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    // Check if there are any solutions possible
257    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}