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