subspace_farmer_components/
auditing.rsuse 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;
#[derive(Debug, Error)]
pub enum AuditingError {
#[error("Failed read s-bucket {s_bucket_audit_index} of sector {sector_index}: {error}")]
SBucketReading {
sector_index: SectorIndex,
s_bucket_audit_index: SBucket,
error: io::Error,
},
}
#[derive(Debug, Clone)]
pub struct AuditResult<'a, Sector> {
pub sector_index: SectorIndex,
pub solution_candidates: SolutionCandidates<'a, Sector>,
}
#[derive(Debug, Clone)]
pub(crate) struct ChunkCandidate {
pub(crate) chunk_offset: u32,
pub(crate) solution_distance: SolutionRange,
}
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,
§or_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(),
),
}))
}
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();
sectors_metadata
.par_iter()
.map(|sector_metadata| {
(
collect_sector_auditing_details(public_key_hash, global_challenge, sector_metadata),
sector_metadata,
)
})
.filter_map(|(sector_auditing_info, sector_metadata)| {
if sectors_being_modified.contains(§or_metadata.sector_index) {
return None;
}
if sector_auditing_info.s_bucket_audit_size == 0 {
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,
§or_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,
s_bucket_audit_size: usize,
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,
}
}
fn map_winning_chunks(
s_bucket: &[u8],
global_challenge: &Blake3Hash,
sector_slot_challenge: &SectorSlotChallenge,
solution_range: SolutionRange,
) -> Option<Vec<ChunkCandidate>> {
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<_>>();
if chunk_candidates.is_empty() {
return None;
}
chunk_candidates.sort_by_key(|chunk_candidate| chunk_candidate.solution_distance);
Some(chunk_candidates)
}