subspace_farmer_components/
auditing.rs1use 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#[derive(Debug, Error)]
22pub enum AuditingError {
23 #[error("Failed read s-bucket {s_bucket_audit_index} of sector {sector_index}: {error}")]
25 SBucketReading {
26 sector_index: SectorIndex,
28 s_bucket_audit_index: SBucket,
30 error: io::Error,
32 },
33}
34
35#[derive(Debug, Clone)]
37pub struct AuditResult<'a, Sector> {
38 pub sector_index: SectorIndex,
40 pub solution_candidates: SolutionCandidates<'a, Sector>,
42}
43
44#[derive(Debug, Clone)]
47pub(crate) struct ChunkCandidate {
48 pub(crate) chunk_offset: u32,
50 pub(crate) solution_distance: SolutionRange,
52}
53
54pub 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 §or_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
106pub 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 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 .filter_map(|(sector_auditing_info, sector_metadata)| {
138 if sectors_being_modified.contains(§or_metadata.sector_index) {
139 return None;
141 }
142
143 if sector_auditing_info.s_bucket_audit_size == 0 {
144 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 §or_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 s_bucket_audit_size: usize,
194 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
236fn 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 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 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}