1#![forbid(unsafe_code)]
3#![warn(rust_2018_idioms, missing_debug_implementations, missing_docs)]
4#![feature(portable_simd)]
5#![allow(incomplete_features)]
7#![feature(generic_const_exprs)]
10#![cfg_attr(not(feature = "std"), no_std)]
11
12#[cfg(not(feature = "std"))]
13extern crate alloc;
14
15#[cfg(not(feature = "std"))]
16use alloc::string::String;
17#[cfg(all(feature = "kzg", not(feature = "std")))]
18use alloc::vec::Vec;
19use core::mem;
20#[cfg(feature = "kzg")]
21use core::num::NonZeroU64;
22#[cfg(feature = "kzg")]
23use core::simd::Simd;
24use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
25use schnorrkel::SignatureError;
26use schnorrkel::context::SigningContext;
27#[cfg(feature = "kzg")]
28use subspace_core_primitives::hashes::blake3_254_hash_to_scalar;
29use subspace_core_primitives::hashes::{Blake3Hash, blake3_hash_list, blake3_hash_with_key};
30#[cfg(feature = "kzg")]
31use subspace_core_primitives::pieces::{PieceArray, Record, RecordWitness};
32use subspace_core_primitives::pot::PotOutput;
33#[cfg(feature = "kzg")]
34use subspace_core_primitives::sectors::SectorId;
35use subspace_core_primitives::sectors::SectorSlotChallenge;
36#[cfg(feature = "kzg")]
37use subspace_core_primitives::segments::ArchivedHistorySegment;
38use subspace_core_primitives::segments::{HistorySize, SegmentCommitment};
39#[cfg(feature = "kzg")]
40use subspace_core_primitives::solutions::Solution;
41use subspace_core_primitives::solutions::{RewardSignature, SolutionRange};
42use subspace_core_primitives::{BlockForkWeight, BlockNumber, PublicKey, ScalarBytes, SlotNumber};
43#[cfg(feature = "kzg")]
44use subspace_kzg::{Commitment, Kzg, Scalar, Witness};
45#[cfg(feature = "kzg")]
46use subspace_proof_of_space::Table;
47
48#[derive(Debug, Eq, PartialEq, thiserror::Error)]
50pub enum Error {
51 #[error("Piece verification failed")]
53 InvalidPieceOffset {
54 piece_offset: u16,
56 max_pieces_in_sector: u16,
58 },
59 #[error("History size {solution} is in the future, current is {current}")]
61 FutureHistorySize {
62 current: HistorySize,
64 solution: HistorySize,
66 },
67 #[error("Sector expired")]
69 SectorExpired {
70 expiration_history_size: HistorySize,
72 current_history_size: HistorySize,
74 },
75 #[error("Piece verification failed")]
77 InvalidPiece,
78 #[error(
80 "Solution distance {solution_distance} is outside of solution range \
81 {half_solution_range} (half of actual solution range)"
82 )]
83 OutsideSolutionRange {
84 half_solution_range: SolutionRange,
86 solution_distance: SolutionRange,
88 },
89 #[error("Invalid proof of space")]
91 InvalidProofOfSpace,
92 #[error("Invalid audit chunk offset")]
94 InvalidAuditChunkOffset,
95 #[error("Invalid chunk: {0}")]
97 InvalidChunk(String),
98 #[error("Invalid chunk witness")]
100 InvalidChunkWitness,
101 #[error("Invalid history size")]
103 InvalidHistorySize,
104}
105
106pub fn check_reward_signature(
108 hash: &[u8],
109 signature: &RewardSignature,
110 public_key: &PublicKey,
111 reward_signing_context: &SigningContext,
112) -> Result<(), SignatureError> {
113 let public_key = schnorrkel::PublicKey::from_bytes(public_key.as_ref())?;
114 let signature = schnorrkel::Signature::from_bytes(signature.as_ref())?;
115 public_key.verify(reward_signing_context.bytes(hash), &signature)
116}
117
118fn calculate_solution_distance(
121 global_challenge: &Blake3Hash,
122 chunk: &[u8; 32],
123 sector_slot_challenge: &SectorSlotChallenge,
124) -> SolutionRange {
125 let audit_chunk = blake3_hash_with_key(sector_slot_challenge, chunk);
126 let audit_chunk_as_solution_range: SolutionRange = SolutionRange::from_le_bytes(
127 *audit_chunk
128 .as_chunks::<{ mem::size_of::<SolutionRange>() }>()
129 .0
130 .iter()
131 .next()
132 .expect("Solution range is smaller in size than global challenge; qed"),
133 );
134 let global_challenge_as_solution_range: SolutionRange = SolutionRange::from_le_bytes(
135 *global_challenge
136 .as_chunks::<{ mem::size_of::<SolutionRange>() }>()
137 .0
138 .iter()
139 .next()
140 .expect("Solution range is smaller in size than global challenge; qed"),
141 );
142 subspace_core_primitives::solutions::bidirectional_distance(
143 &global_challenge_as_solution_range,
144 &audit_chunk_as_solution_range,
145 )
146}
147
148pub fn is_within_solution_range(
151 global_challenge: &Blake3Hash,
152 chunk: &[u8; 32],
153 sector_slot_challenge: &SectorSlotChallenge,
154 solution_range: SolutionRange,
155) -> Option<SolutionRange> {
156 let solution_distance =
157 calculate_solution_distance(global_challenge, chunk, sector_slot_challenge);
158 (solution_distance <= solution_range / 2).then_some(solution_distance)
159}
160
161#[derive(Debug, Clone, Encode, Decode, MaxEncodedLen)]
163pub struct PieceCheckParams {
164 pub max_pieces_in_sector: u16,
166 pub segment_commitment: SegmentCommitment,
168 pub recent_segments: HistorySize,
170 pub recent_history_fraction: (HistorySize, HistorySize),
172 pub min_sector_lifetime: HistorySize,
174 pub current_history_size: HistorySize,
176 pub sector_expiration_check_segment_commitment: Option<SegmentCommitment>,
178}
179
180#[derive(Debug, Clone, Encode, Decode, MaxEncodedLen)]
182pub struct VerifySolutionParams {
183 pub proof_of_time: PotOutput,
185 pub solution_range: SolutionRange,
187 pub piece_check_params: Option<PieceCheckParams>,
191}
192
193pub fn calculate_block_fork_weight(solution_range: SolutionRange) -> BlockForkWeight {
196 #[cfg(feature = "testing")]
199 let solution_range = if solution_range == SolutionRange::MAX {
200 SolutionRange::MAX - 1
201 } else {
202 solution_range
203 };
204
205 BlockForkWeight::from(SolutionRange::MAX - solution_range)
206}
207
208#[cfg(feature = "kzg")]
211pub fn verify_solution<'a, PosTable, RewardAddress>(
212 solution: &'a Solution<RewardAddress>,
213 slot: SlotNumber,
214 params: &'a VerifySolutionParams,
215 kzg: &'a Kzg,
216) -> Result<SolutionRange, Error>
217where
218 PosTable: Table,
219{
220 use subspace_core_primitives::solutions::SolutionPotVerifier;
221
222 let VerifySolutionParams {
223 proof_of_time,
224 solution_range,
225 piece_check_params,
226 } = params;
227
228 let sector_id = SectorId::new(
229 solution.public_key.hash(),
230 solution.sector_index,
231 solution.history_size,
232 );
233
234 let global_randomness = proof_of_time.derive_global_randomness();
235 let global_challenge = global_randomness.derive_global_challenge(slot);
236 let sector_slot_challenge = sector_id.derive_sector_slot_challenge(&global_challenge);
237 let s_bucket_audit_index = sector_slot_challenge.s_bucket_audit_index();
238
239 if !<PosTable as SolutionPotVerifier>::is_proof_valid(
241 §or_id.derive_evaluation_seed(solution.piece_offset),
242 s_bucket_audit_index.into(),
243 &solution.proof_of_space,
244 ) {
245 return Err(Error::InvalidProofOfSpace);
246 };
247
248 let masked_chunk =
249 (Simd::from(*solution.chunk) ^ Simd::from(*solution.proof_of_space.hash())).to_array();
250
251 let solution_distance =
252 calculate_solution_distance(&global_challenge, &masked_chunk, §or_slot_challenge);
253
254 if solution_distance > solution_range / 2 {
256 return Err(Error::OutsideSolutionRange {
257 half_solution_range: solution_range / 2,
258 solution_distance,
259 });
260 }
261
262 if !kzg.verify(
264 &Commitment::try_from(solution.record_commitment)
265 .map_err(|_error| Error::InvalidChunkWitness)?,
266 Record::NUM_S_BUCKETS,
267 s_bucket_audit_index.into(),
268 &Scalar::try_from(solution.chunk).map_err(Error::InvalidChunk)?,
269 &Witness::try_from(solution.chunk_witness).map_err(|_error| Error::InvalidChunkWitness)?,
270 ) {
271 return Err(Error::InvalidChunkWitness);
272 }
273
274 if let Some(PieceCheckParams {
275 max_pieces_in_sector,
276 segment_commitment,
277 recent_segments,
278 recent_history_fraction,
279 min_sector_lifetime,
280 current_history_size,
281 sector_expiration_check_segment_commitment,
282 }) = piece_check_params
283 {
284 if NonZeroU64::from(solution.history_size).get()
288 > NonZeroU64::from(*current_history_size).get() + 1
289 {
290 return Err(Error::FutureHistorySize {
291 current: *current_history_size,
292 solution: solution.history_size,
293 });
294 }
295
296 if u16::from(solution.piece_offset) >= *max_pieces_in_sector {
297 return Err(Error::InvalidPieceOffset {
298 piece_offset: u16::from(solution.piece_offset),
299 max_pieces_in_sector: *max_pieces_in_sector,
300 });
301 }
302
303 if let Some(sector_expiration_check_segment_commitment) =
304 sector_expiration_check_segment_commitment
305 {
306 let expiration_history_size = match sector_id.derive_expiration_history_size(
307 solution.history_size,
308 sector_expiration_check_segment_commitment,
309 *min_sector_lifetime,
310 ) {
311 Some(expiration_history_size) => expiration_history_size,
312 None => {
313 return Err(Error::InvalidHistorySize);
314 }
315 };
316
317 if expiration_history_size <= *current_history_size {
318 return Err(Error::SectorExpired {
319 expiration_history_size,
320 current_history_size: *current_history_size,
321 });
322 }
323 }
324
325 let position = sector_id
326 .derive_piece_index(
327 solution.piece_offset,
328 solution.history_size,
329 *max_pieces_in_sector,
330 *recent_segments,
331 *recent_history_fraction,
332 )
333 .position();
334
335 if !is_record_commitment_hash_valid(
337 kzg,
338 &Scalar::try_from(blake3_254_hash_to_scalar(
339 solution.record_commitment.as_ref(),
340 ))
341 .expect("Create correctly by dedicated hash function; qed"),
342 segment_commitment,
343 &solution.record_witness,
344 position,
345 ) {
346 return Err(Error::InvalidPiece);
347 }
348 }
349
350 Ok(solution_distance)
351}
352
353#[cfg(feature = "kzg")]
355pub fn is_piece_valid(
356 kzg: &Kzg,
357 piece: &PieceArray,
358 segment_commitment: &SegmentCommitment,
359 position: u32,
360) -> bool {
361 let (record, commitment, witness) = piece.split();
362 let witness = match Witness::try_from_bytes(witness) {
363 Ok(witness) => witness,
364 _ => {
365 return false;
366 }
367 };
368
369 let mut scalars = Vec::with_capacity(record.len().next_power_of_two());
370
371 for record_chunk in record.iter() {
372 match Scalar::try_from(record_chunk) {
373 Ok(scalar) => {
374 scalars.push(scalar);
375 }
376 _ => {
377 return false;
378 }
379 }
380 }
381
382 scalars.resize(scalars.capacity(), Scalar::default());
384
385 let polynomial = match kzg.poly(&scalars) {
386 Ok(polynomial) => polynomial,
387 _ => {
388 return false;
389 }
390 };
391
392 if kzg
393 .commit(&polynomial)
394 .map(|commitment| commitment.to_bytes())
395 .as_ref()
396 != Ok(commitment)
397 {
398 return false;
399 }
400
401 let Ok(segment_commitment) = Commitment::try_from(segment_commitment) else {
402 return false;
403 };
404
405 let commitment_hash = Scalar::try_from(blake3_254_hash_to_scalar(commitment.as_ref()))
406 .expect("Create correctly by dedicated hash function; qed");
407
408 kzg.verify(
409 &segment_commitment,
410 ArchivedHistorySegment::NUM_PIECES,
411 position,
412 &commitment_hash,
413 &witness,
414 )
415}
416
417#[cfg(feature = "kzg")]
419pub fn is_record_commitment_hash_valid(
420 kzg: &Kzg,
421 record_commitment_hash: &Scalar,
422 commitment: &SegmentCommitment,
423 witness: &RecordWitness,
424 position: u32,
425) -> bool {
426 let Ok(commitment) = Commitment::try_from(commitment) else {
427 return false;
428 };
429 let Ok(witness) = Witness::try_from(witness) else {
430 return false;
431 };
432
433 kzg.verify(
434 &commitment,
435 ArchivedHistorySegment::NUM_PIECES,
436 position,
437 record_commitment_hash,
438 &witness,
439 )
440}
441
442#[inline]
444pub fn derive_pot_entropy(chunk: &ScalarBytes, proof_of_time: PotOutput) -> Blake3Hash {
445 blake3_hash_list(&[chunk.as_ref(), proof_of_time.as_ref()])
446}
447
448pub fn derive_next_solution_range(
450 start_slot: SlotNumber,
451 current_slot: SlotNumber,
452 slot_probability: (u64, u64),
453 current_solution_range: SolutionRange,
454 era_duration: BlockNumber,
455) -> u64 {
456 let era_slot_count = current_slot - start_slot;
458
459 u64::try_from(
475 u128::from(current_solution_range)
476 .saturating_mul(u128::from(era_slot_count))
477 .saturating_mul(u128::from(slot_probability.0))
478 / u128::from(era_duration)
479 / u128::from(slot_probability.1),
480 )
481 .unwrap_or(u64::MAX)
482 .clamp(
483 current_solution_range / 4,
484 current_solution_range.saturating_mul(4),
485 )
486}