subspace_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::{sector_size, SectorContentsMap, SectorMetadataChecksummed};
9use crate::{ReadAtOffset, ReadAtSync};
10use rayon::prelude::*;
11use std::collections::HashSet;
12use std::io;
13use subspace_core_primitives::hashes::Blake3Hash;
14use subspace_core_primitives::sectors::{SBucket, SectorId, SectorIndex, SectorSlotChallenge};
15use subspace_core_primitives::solutions::SolutionRange;
16use subspace_core_primitives::{PublicKey, ScalarBytes};
17use subspace_verification::is_within_solution_range;
18use thiserror::Error;
19
20/// Errors that happen during proving
21#[derive(Debug, Error)]
22pub enum AuditingError {
23    /// Failed read s-bucket
24    #[error("Failed read s-bucket {s_bucket_audit_index} of sector {sector_index}: {error}")]
25    SBucketReading {
26        /// Sector index
27        sector_index: SectorIndex,
28        /// S-bucket audit index
29        s_bucket_audit_index: SBucket,
30        /// Low-level error
31        error: io::Error,
32    },
33}
34
35/// Result of sector audit
36#[derive(Debug, Clone)]
37pub struct AuditResult<'a, Sector> {
38    /// Sector index
39    pub sector_index: SectorIndex,
40    /// Solution candidates
41    pub solution_candidates: SolutionCandidates<'a, Sector>,
42}
43
44/// Chunk candidate, contains one or more potentially winning audit chunks (in case chunk itself was
45/// encoded and eligible for claiming a reward)
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, can be used to prioritize higher quality solutions
51    pub(crate) solution_distance: SolutionRange,
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 plots.
57pub fn audit_sector_sync<'a, Sector>(
58    public_key: &'a PublicKey,
59    global_challenge: &Blake3Hash,
60    solution_range: SolutionRange,
61    sector: Sector,
62    sector_metadata: &'a SectorMetadataChecksummed,
63) -> Result<Option<AuditResult<'a, Sector>>, AuditingError>
64where
65    Sector: ReadAtSync + 'a,
66{
67    let SectorAuditingDetails {
68        sector_id,
69        sector_slot_challenge,
70        s_bucket_audit_index,
71        s_bucket_audit_size,
72        s_bucket_audit_offset_in_sector,
73    } = collect_sector_auditing_details(public_key.hash(), global_challenge, sector_metadata);
74
75    let mut s_bucket = vec![0; s_bucket_audit_size];
76    sector
77        .read_at(&mut s_bucket, s_bucket_audit_offset_in_sector)
78        .map_err(|error| AuditingError::SBucketReading {
79            sector_index: sector_metadata.sector_index,
80            s_bucket_audit_index,
81            error,
82        })?;
83
84    let Some(winning_chunks) = map_winning_chunks(
85        &s_bucket,
86        global_challenge,
87        &sector_slot_challenge,
88        solution_range,
89    ) else {
90        return Ok(None);
91    };
92
93    Ok(Some(AuditResult {
94        sector_index: sector_metadata.sector_index,
95        solution_candidates: SolutionCandidates::new(
96            public_key,
97            sector_id,
98            s_bucket_audit_index,
99            sector,
100            sector_metadata,
101            winning_chunks.into(),
102        ),
103    }))
104}
105
106/// Audit the whole plot and generate streams of results.
107///
108/// Each audit result contains a solution candidate that might be converted into solution (but will
109/// not necessarily succeed, auditing only does quick checks and can't know for sure).
110///
111/// Plot is assumed to contain concatenated series of sectors as created by functions in
112/// [`plotting`](crate::plotting) module earlier.
113pub fn audit_plot_sync<'a, 'b, Plot>(
114    public_key: &'a PublicKey,
115    global_challenge: &Blake3Hash,
116    solution_range: SolutionRange,
117    plot: &'a Plot,
118    sectors_metadata: &'a [SectorMetadataChecksummed],
119    sectors_being_modified: &'b HashSet<SectorIndex>,
120) -> Result<Vec<AuditResult<'a, ReadAtOffset<'a, Plot>>>, AuditingError>
121where
122    Plot: ReadAtSync + 'a,
123{
124    let public_key_hash = public_key.hash();
125
126    // Create auditing info for all sectors in parallel
127    sectors_metadata
128        .par_iter()
129        .map(|sector_metadata| {
130            (
131                collect_sector_auditing_details(public_key_hash, global_challenge, sector_metadata),
132                sector_metadata,
133            )
134        })
135        // Read s-buckets of all sectors, map to winning chunks and then to audit results, all in
136        // parallel
137        .filter_map(|(sector_auditing_info, sector_metadata)| {
138            if sectors_being_modified.contains(&sector_metadata.sector_index) {
139                // Skip sector that is being modified right now
140                return None;
141            }
142
143            if sector_auditing_info.s_bucket_audit_size == 0 {
144                // S-bucket is empty
145                return None;
146            }
147
148            let sector = plot.offset(
149                u64::from(sector_metadata.sector_index)
150                    * sector_size(sector_metadata.pieces_in_sector) as u64,
151            );
152
153            let mut s_bucket = vec![0; sector_auditing_info.s_bucket_audit_size];
154
155            if let Err(error) = sector.read_at(
156                &mut s_bucket,
157                sector_auditing_info.s_bucket_audit_offset_in_sector,
158            ) {
159                return Some(Err(AuditingError::SBucketReading {
160                    sector_index: sector_metadata.sector_index,
161                    s_bucket_audit_index: sector_auditing_info.s_bucket_audit_index,
162                    error,
163                }));
164            }
165
166            let winning_chunks = map_winning_chunks(
167                &s_bucket,
168                global_challenge,
169                &sector_auditing_info.sector_slot_challenge,
170                solution_range,
171            )?;
172
173            Some(Ok(AuditResult {
174                sector_index: sector_metadata.sector_index,
175                solution_candidates: SolutionCandidates::new(
176                    public_key,
177                    sector_auditing_info.sector_id,
178                    sector_auditing_info.s_bucket_audit_index,
179                    sector,
180                    sector_metadata,
181                    winning_chunks.into(),
182                ),
183            }))
184        })
185        .collect()
186}
187
188struct SectorAuditingDetails {
189    sector_id: SectorId,
190    sector_slot_challenge: SectorSlotChallenge,
191    s_bucket_audit_index: SBucket,
192    /// Size in bytes
193    s_bucket_audit_size: usize,
194    /// Offset in bytes
195    s_bucket_audit_offset_in_sector: u64,
196}
197
198fn collect_sector_auditing_details(
199    public_key_hash: Blake3Hash,
200    global_challenge: &Blake3Hash,
201    sector_metadata: &SectorMetadataChecksummed,
202) -> SectorAuditingDetails {
203    let sector_id = SectorId::new(
204        public_key_hash,
205        sector_metadata.sector_index,
206        sector_metadata.history_size,
207    );
208
209    let sector_slot_challenge = sector_id.derive_sector_slot_challenge(global_challenge);
210    let s_bucket_audit_index = sector_slot_challenge.s_bucket_audit_index();
211    let s_bucket_audit_size = ScalarBytes::FULL_BYTES
212        * usize::from(sector_metadata.s_bucket_sizes[usize::from(s_bucket_audit_index)]);
213    let s_bucket_audit_offset = ScalarBytes::FULL_BYTES as u64
214        * sector_metadata
215            .s_bucket_sizes
216            .iter()
217            .take(s_bucket_audit_index.into())
218            .copied()
219            .map(u64::from)
220            .sum::<u64>();
221
222    let sector_contents_map_size =
223        SectorContentsMap::encoded_size(sector_metadata.pieces_in_sector);
224
225    let s_bucket_audit_offset_in_sector = sector_contents_map_size as u64 + s_bucket_audit_offset;
226
227    SectorAuditingDetails {
228        sector_id,
229        sector_slot_challenge,
230        s_bucket_audit_index,
231        s_bucket_audit_size,
232        s_bucket_audit_offset_in_sector,
233    }
234}
235
236/// Map all winning chunks
237fn map_winning_chunks(
238    s_bucket: &[u8],
239    global_challenge: &Blake3Hash,
240    sector_slot_challenge: &SectorSlotChallenge,
241    solution_range: SolutionRange,
242) -> Option<Vec<ChunkCandidate>> {
243    // Map all winning chunks
244    let mut chunk_candidates = s_bucket
245        .array_chunks::<{ ScalarBytes::FULL_BYTES }>()
246        .enumerate()
247        .filter_map(|(chunk_offset, chunk)| {
248            is_within_solution_range(
249                global_challenge,
250                chunk,
251                sector_slot_challenge,
252                solution_range,
253            )
254            .map(|solution_distance| ChunkCandidate {
255                chunk_offset: chunk_offset as u32,
256                solution_distance,
257            })
258        })
259        .collect::<Vec<_>>();
260
261    // Check if there are any solutions possible
262    if chunk_candidates.is_empty() {
263        return None;
264    }
265
266    chunk_candidates.sort_by_key(|chunk_candidate| chunk_candidate.solution_distance);
267
268    Some(chunk_candidates)
269}