subspace_farmer_components/
auditing.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
//! Auditing utilities
//!
//! There is a way to both audit a single sector (primarily helpful for testing purposes) and the
//! whole plot (which is heavily parallelized and much more efficient) created by functions in
//! [`plotting`](crate::plotting) module earlier.

use crate::proving::SolutionCandidates;
use crate::sector::{sector_size, SectorContentsMap, SectorMetadataChecksummed};
use crate::{ReadAtOffset, ReadAtSync};
use rayon::prelude::*;
use std::collections::HashSet;
use std::io;
use subspace_core_primitives::hashes::Blake3Hash;
use subspace_core_primitives::sectors::{SBucket, SectorId, SectorIndex, SectorSlotChallenge};
use subspace_core_primitives::solutions::SolutionRange;
use subspace_core_primitives::{PublicKey, ScalarBytes};
use subspace_verification::is_within_solution_range;
use thiserror::Error;

/// Errors that happen during proving
#[derive(Debug, Error)]
pub enum AuditingError {
    /// Failed read s-bucket
    #[error("Failed read s-bucket {s_bucket_audit_index} of sector {sector_index}: {error}")]
    SBucketReading {
        /// Sector index
        sector_index: SectorIndex,
        /// S-bucket audit index
        s_bucket_audit_index: SBucket,
        /// Low-level error
        error: io::Error,
    },
}

/// Result of sector audit
#[derive(Debug, Clone)]
pub struct AuditResult<'a, Sector> {
    /// Sector index
    pub sector_index: SectorIndex,
    /// Solution candidates
    pub solution_candidates: SolutionCandidates<'a, Sector>,
}

/// Chunk candidate, contains one or more potentially winning audit chunks (in case chunk itself was
/// encoded and eligible for claiming a reward)
#[derive(Debug, Clone)]
pub(crate) struct ChunkCandidate {
    /// Chunk offset within s-bucket
    pub(crate) chunk_offset: u32,
    /// Solution distance of this chunk, can be used to prioritize higher quality solutions
    pub(crate) solution_distance: SolutionRange,
}

/// Audit a single sector and generate a stream of solutions.
///
/// This is primarily helpful in test environment, prefer [`audit_plot_sync`] for auditing real plots.
pub fn audit_sector_sync<'a, Sector>(
    public_key: &'a PublicKey,
    global_challenge: &Blake3Hash,
    solution_range: SolutionRange,
    sector: Sector,
    sector_metadata: &'a SectorMetadataChecksummed,
) -> Result<Option<AuditResult<'a, Sector>>, AuditingError>
where
    Sector: ReadAtSync + 'a,
{
    let SectorAuditingDetails {
        sector_id,
        sector_slot_challenge,
        s_bucket_audit_index,
        s_bucket_audit_size,
        s_bucket_audit_offset_in_sector,
    } = collect_sector_auditing_details(public_key.hash(), global_challenge, sector_metadata);

    let mut s_bucket = vec![0; s_bucket_audit_size];
    sector
        .read_at(&mut s_bucket, s_bucket_audit_offset_in_sector)
        .map_err(|error| AuditingError::SBucketReading {
            sector_index: sector_metadata.sector_index,
            s_bucket_audit_index,
            error,
        })?;

    let Some(winning_chunks) = map_winning_chunks(
        &s_bucket,
        global_challenge,
        &sector_slot_challenge,
        solution_range,
    ) else {
        return Ok(None);
    };

    Ok(Some(AuditResult {
        sector_index: sector_metadata.sector_index,
        solution_candidates: SolutionCandidates::new(
            public_key,
            sector_id,
            s_bucket_audit_index,
            sector,
            sector_metadata,
            winning_chunks.into(),
        ),
    }))
}

/// Audit the whole plot and generate streams of results.
///
/// Each audit result contains a solution candidate that might be converted into solution (but will
/// not necessarily succeed, auditing only does quick checks and can't know for sure).
///
/// Plot is assumed to contain concatenated series of sectors as created by functions in
/// [`plotting`](crate::plotting) module earlier.
pub fn audit_plot_sync<'a, 'b, Plot>(
    public_key: &'a PublicKey,
    global_challenge: &Blake3Hash,
    solution_range: SolutionRange,
    plot: &'a Plot,
    sectors_metadata: &'a [SectorMetadataChecksummed],
    sectors_being_modified: &'b HashSet<SectorIndex>,
) -> Result<Vec<AuditResult<'a, ReadAtOffset<'a, Plot>>>, AuditingError>
where
    Plot: ReadAtSync + 'a,
{
    let public_key_hash = public_key.hash();

    // Create auditing info for all sectors in parallel
    sectors_metadata
        .par_iter()
        .map(|sector_metadata| {
            (
                collect_sector_auditing_details(public_key_hash, global_challenge, sector_metadata),
                sector_metadata,
            )
        })
        // Read s-buckets of all sectors, map to winning chunks and then to audit results, all in
        // parallel
        .filter_map(|(sector_auditing_info, sector_metadata)| {
            if sectors_being_modified.contains(&sector_metadata.sector_index) {
                // Skip sector that is being modified right now
                return None;
            }

            if sector_auditing_info.s_bucket_audit_size == 0 {
                // S-bucket is empty
                return None;
            }

            let sector = plot.offset(
                u64::from(sector_metadata.sector_index)
                    * sector_size(sector_metadata.pieces_in_sector) as u64,
            );

            let mut s_bucket = vec![0; sector_auditing_info.s_bucket_audit_size];

            if let Err(error) = sector.read_at(
                &mut s_bucket,
                sector_auditing_info.s_bucket_audit_offset_in_sector,
            ) {
                return Some(Err(AuditingError::SBucketReading {
                    sector_index: sector_metadata.sector_index,
                    s_bucket_audit_index: sector_auditing_info.s_bucket_audit_index,
                    error,
                }));
            }

            let winning_chunks = map_winning_chunks(
                &s_bucket,
                global_challenge,
                &sector_auditing_info.sector_slot_challenge,
                solution_range,
            )?;

            Some(Ok(AuditResult {
                sector_index: sector_metadata.sector_index,
                solution_candidates: SolutionCandidates::new(
                    public_key,
                    sector_auditing_info.sector_id,
                    sector_auditing_info.s_bucket_audit_index,
                    sector,
                    sector_metadata,
                    winning_chunks.into(),
                ),
            }))
        })
        .collect()
}

struct SectorAuditingDetails {
    sector_id: SectorId,
    sector_slot_challenge: SectorSlotChallenge,
    s_bucket_audit_index: SBucket,
    /// Size in bytes
    s_bucket_audit_size: usize,
    /// Offset in bytes
    s_bucket_audit_offset_in_sector: u64,
}

fn collect_sector_auditing_details(
    public_key_hash: Blake3Hash,
    global_challenge: &Blake3Hash,
    sector_metadata: &SectorMetadataChecksummed,
) -> SectorAuditingDetails {
    let sector_id = SectorId::new(
        public_key_hash,
        sector_metadata.sector_index,
        sector_metadata.history_size,
    );

    let sector_slot_challenge = sector_id.derive_sector_slot_challenge(global_challenge);
    let s_bucket_audit_index = sector_slot_challenge.s_bucket_audit_index();
    let s_bucket_audit_size = ScalarBytes::FULL_BYTES
        * usize::from(sector_metadata.s_bucket_sizes[usize::from(s_bucket_audit_index)]);
    let s_bucket_audit_offset = ScalarBytes::FULL_BYTES as u64
        * sector_metadata
            .s_bucket_sizes
            .iter()
            .take(s_bucket_audit_index.into())
            .copied()
            .map(u64::from)
            .sum::<u64>();

    let sector_contents_map_size =
        SectorContentsMap::encoded_size(sector_metadata.pieces_in_sector);

    let s_bucket_audit_offset_in_sector = sector_contents_map_size as u64 + s_bucket_audit_offset;

    SectorAuditingDetails {
        sector_id,
        sector_slot_challenge,
        s_bucket_audit_index,
        s_bucket_audit_size,
        s_bucket_audit_offset_in_sector,
    }
}

/// Map all winning chunks
fn map_winning_chunks(
    s_bucket: &[u8],
    global_challenge: &Blake3Hash,
    sector_slot_challenge: &SectorSlotChallenge,
    solution_range: SolutionRange,
) -> Option<Vec<ChunkCandidate>> {
    // Map all winning chunks
    let mut chunk_candidates = s_bucket
        .array_chunks::<{ ScalarBytes::FULL_BYTES }>()
        .enumerate()
        .filter_map(|(chunk_offset, chunk)| {
            is_within_solution_range(
                global_challenge,
                chunk,
                sector_slot_challenge,
                solution_range,
            )
            .map(|solution_distance| ChunkCandidate {
                chunk_offset: chunk_offset as u32,
                solution_distance,
            })
        })
        .collect::<Vec<_>>();

    // Check if there are any solutions possible
    if chunk_candidates.is_empty() {
        return None;
    }

    chunk_candidates.sort_by_key(|chunk_candidate| chunk_candidate.solution_distance);

    Some(chunk_candidates)
}