Skip to main content

ab_core_primitives/
solutions.rs

1//! Solutions-related data structures and functions.
2
3use crate::block::BlockNumber;
4use crate::ed25519::Ed25519PublicKey;
5use crate::hashes::Blake3Hash;
6use crate::pieces::{PieceOffset, Record, RecordChunk, RecordProof, RecordRoot, SegmentProof};
7use crate::pos::{PosProof, PosSeed};
8use crate::pot::{PotOutput, SlotNumber};
9use crate::sectors::{SBucket, SectorId, SectorIndex, SectorSlotChallenge};
10use crate::segments::{
11    HistorySize, LocalSegmentIndex, SegmentIndex, SegmentPosition, SegmentRoot, SuperSegmentIndex,
12    SuperSegmentRoot,
13};
14use crate::shard::{NumShards, RealShardKind, ShardIndex, ShardKind};
15use ab_blake3::single_block_keyed_hash;
16use ab_io_type::trivial_type::TrivialType;
17use ab_merkle_tree::balanced::BalancedMerkleTree;
18use blake3::{Hash, OUT_LEN};
19use core::fmt;
20use core::simd::Simd;
21use derive_more::{
22    Add, AddAssign, AsMut, AsRef, Deref, DerefMut, Display, From, Into, Sub, SubAssign,
23};
24#[cfg(feature = "scale-codec")]
25use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
26#[cfg(feature = "serde")]
27use serde::{Deserialize, Serialize};
28#[cfg(feature = "serde")]
29use serde::{Deserializer, Serializer};
30#[cfg(feature = "serde")]
31use serde_big_array::BigArray;
32use transparent_wrapper::TransparentWrapper;
33
34/// Solution distance
35#[derive(
36    Debug, Display, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, From, Into,
37)]
38#[cfg_attr(feature = "scale-codec", derive(Encode, Decode, MaxEncodedLen))]
39#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
40#[repr(C)]
41pub struct SolutionDistance(u64);
42
43impl SolutionDistance {
44    /// Maximum value
45    pub const MAX: Self = Self(u64::MAX / 2);
46
47    // TODO: Remove once `From` is stable
48    /// Create a new instance
49    #[inline(always)]
50    pub const fn from_u64(n: u64) -> Self {
51        Self(n)
52    }
53
54    /// Calculate solution distance for given parameters.
55    ///
56    /// Typically used as a primitive to check whether solution distance is within solution range
57    /// (see [`Self::is_within()`]).
58    pub fn calculate(
59        global_challenge: &Blake3Hash,
60        chunk: &[u8; 32],
61        sector_slot_challenge: &SectorSlotChallenge,
62    ) -> Self {
63        // TODO: Is keyed hash really needed here?
64        let audit_chunk = single_block_keyed_hash(sector_slot_challenge, chunk)
65            .expect("Less than a single block worth of bytes; qed");
66        let audit_chunk_as_solution_range = SolutionRange::from_bytes([
67            audit_chunk[0],
68            audit_chunk[1],
69            audit_chunk[2],
70            audit_chunk[3],
71            audit_chunk[4],
72            audit_chunk[5],
73            audit_chunk[6],
74            audit_chunk[7],
75        ]);
76        let global_challenge_as_solution_range =
77            SolutionRange::from_bytes(global_challenge.as_chunks().0[0]);
78
79        global_challenge_as_solution_range.bidirectional_distance(audit_chunk_as_solution_range)
80    }
81
82    /// Check if solution distance is within the provided solution range
83    pub const fn is_within(self, solution_range: SolutionRange) -> bool {
84        self.0 <= u64::from(solution_range) / 2
85    }
86}
87
88/// Solution range
89#[derive(
90    Debug,
91    Display,
92    Default,
93    Copy,
94    Clone,
95    Ord,
96    PartialOrd,
97    Eq,
98    PartialEq,
99    Hash,
100    Add,
101    AddAssign,
102    Sub,
103    SubAssign,
104    TrivialType,
105)]
106#[cfg_attr(feature = "scale-codec", derive(Encode, Decode, MaxEncodedLen))]
107#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
108#[repr(C)]
109pub struct SolutionRange(u64);
110
111const impl From<u64> for SolutionRange {
112    #[inline(always)]
113    fn from(value: u64) -> Self {
114        Self(value)
115    }
116}
117
118const impl From<SolutionRange> for u64 {
119    #[inline(always)]
120    fn from(value: SolutionRange) -> Self {
121        value.0
122    }
123}
124
125impl SolutionRange {
126    /// Size in bytes
127    pub const SIZE: usize = size_of::<u64>();
128    /// Minimum value
129    pub const MIN: Self = Self(u64::MIN);
130    /// Maximum value
131    pub const MAX: Self = Self(u64::MAX);
132
133    /// Create a new instance from bytes
134    #[inline(always)]
135    pub fn to_bytes(self) -> [u8; 8] {
136        self.0.to_le_bytes()
137    }
138
139    /// Create a new instance from bytes
140    #[inline(always)]
141    pub fn from_bytes(bytes: [u8; 8]) -> Self {
142        Self(u64::from_le_bytes(bytes))
143    }
144
145    /// Computes the following:
146    /// ```text
147    /// MAX * slot_probability / chunks * s_buckets / pieces
148    /// ```
149    #[inline]
150    pub const fn from_pieces(pieces: u64, slot_probability: (u64, u64)) -> Self {
151        let solution_range = u64::MAX
152            // Account for slot probability
153            / slot_probability.1 * slot_probability.0
154            // Now take the probability of hitting occupied s-bucket in a piece into account
155            / Record::NUM_CHUNKS as u64
156            * Record::NUM_S_BUCKETS as u64;
157
158        // Take the number of pieces into account
159        Self(solution_range / pieces)
160    }
161
162    /// Computes the following:
163    /// ```text
164    /// MAX * slot_probability / chunks * s_buckets / solution_range
165    /// ```
166    #[inline]
167    pub const fn to_pieces(self, slot_probability: (u64, u64)) -> u64 {
168        let pieces = u64::MAX
169            // Account for slot probability
170            / slot_probability.1 * slot_probability.0
171            // Now take the probability of hitting occupied s-bucket in sector into account
172            / Record::NUM_CHUNKS as u64
173            * Record::NUM_S_BUCKETS as u64;
174
175        // Take solution range into account
176        pieces / self.0
177    }
178
179    /// Expands the global solution range to a solution range that corresponds to a leaf shard.
180    ///
181    /// Global solution range is updated based on the beacon chain information, while a farmer also
182    /// creates intermediate shard and leaf shard solutions with a wider solution range.
183    #[inline]
184    pub const fn to_leaf_shard(self, num_shards: NumShards) -> Self {
185        Self(
186            self.0
187                .saturating_mul(u64::from(num_shards.leaf_shards().get())),
188        )
189    }
190
191    /// Expands the global solution range to a solution range that corresponds to an intermediate
192    /// shard
193    #[inline]
194    pub const fn to_intermediate_shard(self, num_shards: NumShards) -> Self {
195        Self(
196            self.0
197                .saturating_mul(u64::from(num_shards.intermediate_shards().get())),
198        )
199    }
200
201    /// Bidirectional distance between two solution ranges
202    #[inline]
203    pub const fn bidirectional_distance(self, other: Self) -> SolutionDistance {
204        let a = self.0;
205        let b = other.0;
206        let diff = a.wrapping_sub(b);
207        let diff2 = b.wrapping_sub(a);
208        // Find smaller diff between 2 directions
209        SolutionDistance::from_u64(if diff < diff2 { diff } else { diff2 })
210    }
211
212    /// Derives next solution range
213    #[inline]
214    pub fn derive_next(
215        self,
216        slots_in_last_interval: SlotNumber,
217        slot_probability: (u64, u64),
218        retarget_interval: BlockNumber,
219    ) -> Self {
220        // The idea here is to keep block production at the same pace while space pledged on the
221        // network changes. For this, we adjust the previous solution range according to actual and
222        // expected number of blocks per retarget interval.
223        //
224        // Below is code analogous to the following, but without using floats:
225        // ```rust
226        // let actual_slots_per_block = slots_in_last_interval as f64 / retarget_interval as f64;
227        // let expected_slots_per_block =
228        //     slot_probability.1 as f64 / slot_probability.0 as f64;
229        // let adjustment_factor =
230        //     (actual_slots_per_block / expected_slots_per_block).clamp(0.25, 4.0);
231        //
232        // next_solution_range =
233        //     (solution_ranges.current as f64 * adjustment_factor).round() as u64;
234        // ```
235        let current_solution_range = self.0;
236        let next_solution_range = u64::try_from(
237            u128::from(current_solution_range)
238                .saturating_mul(u128::from(slots_in_last_interval))
239                .saturating_mul(u128::from(slot_probability.0))
240                / u128::from(u64::from(retarget_interval))
241                / u128::from(slot_probability.1),
242        );
243
244        Self(next_solution_range.unwrap_or(u64::MAX).clamp(
245            current_solution_range / 4,
246            current_solution_range.saturating_mul(4),
247        ))
248    }
249}
250
251// Quick test to ensure the functions above are the inverse of each other
252const {
253    assert!(SolutionRange::from_pieces(1, (1, 6)).to_pieces((1, 6)) == 1);
254    assert!(SolutionRange::from_pieces(3, (1, 6)).to_pieces((1, 6)) == 3);
255    assert!(SolutionRange::from_pieces(5, (1, 6)).to_pieces((1, 6)) == 5);
256}
257
258/// Proof for chunk contained within a record.
259#[derive(Copy, Clone, Eq, PartialEq, Hash, Deref, DerefMut, From, Into, TrivialType)]
260#[cfg_attr(feature = "scale-codec", derive(Encode, Decode, MaxEncodedLen))]
261#[repr(C)]
262pub struct ChunkProof([[u8; OUT_LEN]; ChunkProof::NUM_HASHES]);
263
264impl fmt::Debug for ChunkProof {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        write!(f, "[")?;
267        for hash in self.0 {
268            for byte in hash {
269                write!(f, "{byte:02x}")?;
270            }
271            write!(f, ", ")?;
272        }
273        write!(f, "]")?;
274        Ok(())
275    }
276}
277
278#[cfg(feature = "serde")]
279#[derive(Serialize, Deserialize)]
280#[serde(transparent)]
281struct ChunkProofBinary(#[serde(with = "BigArray")] [[u8; OUT_LEN]; ChunkProof::NUM_HASHES]);
282
283#[cfg(feature = "serde")]
284#[derive(Serialize, Deserialize, TransparentWrapper)]
285#[serde(transparent)]
286#[repr(transparent)]
287struct ChunkProofHexHash(#[serde(with = "hex")] [u8; OUT_LEN]);
288
289#[cfg(feature = "serde")]
290#[derive(Serialize, Deserialize)]
291#[serde(transparent)]
292struct ChunkProofHex([ChunkProofHexHash; ChunkProof::NUM_HASHES]);
293
294#[cfg(feature = "serde")]
295impl Serialize for ChunkProof {
296    #[inline]
297    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
298    where
299        S: Serializer,
300    {
301        if serializer.is_human_readable() {
302            ChunkProofHex(<[ChunkProofHexHash; Self::NUM_HASHES]>::wrap(self.0))
303                .serialize(serializer)
304        } else {
305            ChunkProofBinary(self.0).serialize(serializer)
306        }
307    }
308}
309
310#[cfg(feature = "serde")]
311impl<'de> Deserialize<'de> for ChunkProof {
312    #[inline]
313    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
314    where
315        D: Deserializer<'de>,
316    {
317        Ok(Self(if deserializer.is_human_readable() {
318            ChunkProofHex::deserialize(deserializer)?.0.peel()
319        } else {
320            ChunkProofBinary::deserialize(deserializer)?.0
321        }))
322    }
323}
324
325impl Default for ChunkProof {
326    #[inline]
327    fn default() -> Self {
328        Self([[0; OUT_LEN]; _])
329    }
330}
331
332impl AsRef<[u8]> for ChunkProof {
333    #[inline]
334    fn as_ref(&self) -> &[u8] {
335        self.0.as_flattened()
336    }
337}
338
339impl AsMut<[u8]> for ChunkProof {
340    #[inline]
341    fn as_mut(&mut self) -> &mut [u8] {
342        self.0.as_flattened_mut()
343    }
344}
345
346impl ChunkProof {
347    /// Size of chunk proof in bytes.
348    pub const SIZE: usize = OUT_LEN * Self::NUM_HASHES;
349    const NUM_HASHES: usize = Record::NUM_S_BUCKETS.ilog2() as usize;
350}
351
352/// Solution verification errors
353#[derive(Debug, Eq, PartialEq, thiserror::Error)]
354pub enum SolutionVerifyError {
355    /// Invalid piece offset
356    #[error("Piece verification failed")]
357    InvalidPieceOffset {
358        /// Index of the piece that failed verification
359        piece_offset: u16,
360        /// How many pieces one sector is supposed to contain (max)
361        max_pieces_in_sector: u16,
362    },
363    /// History size is in the future
364    #[error("History size {solution} is in the future, current is {current}")]
365    FutureHistorySize {
366        /// Current history size
367        current: HistorySize,
368        /// History size solution was created for
369        solution: HistorySize,
370    },
371    /// Sector expired
372    #[error("Sector expired")]
373    SectorExpired {
374        /// Expiration history size
375        expiration_history_size: HistorySize,
376        /// Current history size
377        current_history_size: HistorySize,
378    },
379    /// Record does not belong to the segment
380    #[error("Record does not belong to the segment")]
381    RecordNotInSegment,
382    /// Segment doesn't belong to the super segment
383    #[error("Segment doesn't belong to the super segment")]
384    SegmentNotInSuperSegment,
385    /// Solution is outside the solution range
386    #[error("Solution distance {solution_distance} is outside of solution range {solution_range}")]
387    OutsideSolutionRange {
388        /// Solution range
389        solution_range: SolutionRange,
390        /// Solution distance
391        solution_distance: SolutionDistance,
392    },
393    /// Invalid proof of space
394    #[error("Invalid proof of space")]
395    InvalidProofOfSpace,
396    /// Invalid shard commitment
397    #[error("Invalid shard commitment")]
398    InvalidShardCommitment,
399    /// Invalid input shard
400    #[error("Invalid input shard {shard_index} ({shard_kind:?})")]
401    InvalidInputShard {
402        /// Input shard index
403        shard_index: ShardIndex,
404        /// Input shard kind
405        shard_kind: Option<ShardKind>,
406    },
407    /// Invalid solution shard
408    #[error(
409        "Invalid solution shard {solution_shard_index} (parent {solution_parent_shard_index:?}), \
410        expected shard {expected_shard_index} ({expected_shard_kind:?})"
411    )]
412    InvalidSolutionShard {
413        /// Solution shard index
414        solution_shard_index: ShardIndex,
415        /// Solution shard index
416        solution_parent_shard_index: Option<ShardIndex>,
417        /// Expected shard index
418        expected_shard_index: ShardIndex,
419        /// Expected shard kind
420        expected_shard_kind: RealShardKind,
421    },
422    /// Invalid chunk proof
423    #[error("Invalid chunk proof")]
424    InvalidChunkProof,
425    /// Invalid history size
426    #[error("Invalid history size")]
427    InvalidHistorySize,
428}
429
430/// Parameters for stateless solution verification.
431///
432/// These only include the information already contained in the block itself, meaning verification
433/// of different blocks can be done concurrently.
434#[derive(Debug, Clone)]
435#[cfg_attr(feature = "scale-codec", derive(Encode, Decode, MaxEncodedLen))]
436pub struct SolutionVerifyStatelessParams {
437    /// Shard for which the solution is built
438    pub shard_index: ShardIndex,
439    /// Proof of time for which solution is built
440    pub proof_of_time: PotOutput,
441    /// Solution range
442    pub solution_range: SolutionRange,
443    /// Shard membership entropy
444    pub shard_membership_entropy: ShardMembershipEntropy,
445    /// The number of shards in the network
446    pub num_shards: NumShards,
447}
448
449/// Parameters for checking piece validity used in a solution
450#[derive(Debug, Clone)]
451#[cfg_attr(feature = "scale-codec", derive(Encode, Decode, MaxEncodedLen))]
452pub struct SolutionVerifyPieceParams {
453    /// How many pieces one sector is supposed to contain (max)
454    pub max_pieces_in_sector: u16,
455    /// Super segment root of the segment to which piece belongs
456    pub super_segment_root: SuperSegmentRoot,
457    /// Number of segments in the super segment
458    pub num_segments: u32,
459    /// Number of latest archived segments that are considered "recent history"
460    pub recent_segments: HistorySize,
461    /// Fraction of pieces from the "recent history" (`recent_segments`) in each sector
462    pub recent_history_fraction: (HistorySize, HistorySize),
463    /// Minimum lifetime of a plotted sector, measured in archived segments
464    pub min_sector_lifetime: HistorySize,
465    /// Current size of the history
466    pub current_history_size: HistorySize,
467    /// Super segment root that contains a segment at `min_sector_lifetime` from sector creation
468    /// (if exists)
469    pub sector_expiration_check_super_segment_root: Option<SuperSegmentRoot>,
470}
471
472/// Parameters for full solution verification
473#[derive(Debug, Clone)]
474#[cfg_attr(feature = "scale-codec", derive(Encode, Decode, MaxEncodedLen))]
475pub struct SolutionVerifyFullParams {
476    /// Parameters for stateless solution verification
477    pub stateless: SolutionVerifyStatelessParams,
478    /// Parameters for checking piece validity used in a solution
479    pub piece: SolutionVerifyPieceParams,
480}
481
482/// Proof-of-time verifier to be used in [`Solution::verify_full()`]
483pub trait SolutionPotVerifier {
484    /// Check whether proof created earlier is valid
485    fn is_proof_valid(seed: &PosSeed, s_bucket: SBucket, proof: &PosProof) -> bool;
486}
487
488/// Entropy used for shard membership assignment
489#[derive(
490    Default,
491    Copy,
492    Clone,
493    Eq,
494    PartialEq,
495    Ord,
496    PartialOrd,
497    Hash,
498    From,
499    Into,
500    AsRef,
501    AsMut,
502    Deref,
503    DerefMut,
504    TrivialType,
505    TransparentWrapper,
506)]
507#[cfg_attr(feature = "scale-codec", derive(Encode, Decode, MaxEncodedLen))]
508#[repr(C)]
509pub struct ShardMembershipEntropy([u8; ShardMembershipEntropy::SIZE]);
510
511impl fmt::Display for ShardMembershipEntropy {
512    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
513        for byte in self.0 {
514            write!(f, "{byte:02x}")?;
515        }
516        Ok(())
517    }
518}
519
520#[cfg(feature = "serde")]
521#[derive(Serialize, Deserialize)]
522#[serde(transparent)]
523struct ShardMembershipEntropyBinary([u8; ShardMembershipEntropy::SIZE]);
524
525#[cfg(feature = "serde")]
526#[derive(Serialize, Deserialize)]
527#[serde(transparent)]
528struct ShardMembershipEntropyHex(#[serde(with = "hex")] [u8; ShardMembershipEntropy::SIZE]);
529
530#[cfg(feature = "serde")]
531impl Serialize for ShardMembershipEntropy {
532    #[inline]
533    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
534    where
535        S: Serializer,
536    {
537        if serializer.is_human_readable() {
538            ShardMembershipEntropyHex(self.0).serialize(serializer)
539        } else {
540            ShardMembershipEntropyBinary(self.0).serialize(serializer)
541        }
542    }
543}
544
545#[cfg(feature = "serde")]
546impl<'de> Deserialize<'de> for ShardMembershipEntropy {
547    #[inline]
548    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
549    where
550        D: Deserializer<'de>,
551    {
552        Ok(Self(if deserializer.is_human_readable() {
553            ShardMembershipEntropyHex::deserialize(deserializer)?.0
554        } else {
555            ShardMembershipEntropyBinary::deserialize(deserializer)?.0
556        }))
557    }
558}
559
560impl fmt::Debug for ShardMembershipEntropy {
561    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562        for byte in self.0 {
563            write!(f, "{byte:02x}")?;
564        }
565        Ok(())
566    }
567}
568
569impl AsRef<[u8]> for ShardMembershipEntropy {
570    #[inline(always)]
571    fn as_ref(&self) -> &[u8] {
572        &self.0
573    }
574}
575
576impl AsMut<[u8]> for ShardMembershipEntropy {
577    #[inline(always)]
578    fn as_mut(&mut self) -> &mut [u8] {
579        &mut self.0
580    }
581}
582
583impl ShardMembershipEntropy {
584    /// Size in bytes
585    pub const SIZE: usize = PotOutput::SIZE;
586
587    /// Create a new instance
588    #[inline(always)]
589    pub const fn new(bytes: [u8; Self::SIZE]) -> Self {
590        Self(bytes)
591    }
592
593    /// Get internal representation
594    #[inline(always)]
595    pub const fn as_bytes(&self) -> &[u8; Self::SIZE] {
596        &self.0
597    }
598
599    /// Convenient conversion from slice of underlying representation for efficiency purposes
600    #[inline(always)]
601    pub const fn slice_from_repr(value: &[[u8; Self::SIZE]]) -> &[Self] {
602        Self::wrap_slice(value)
603    }
604
605    /// Convenient conversion to slice of underlying representation for efficiency purposes
606    #[inline(always)]
607    pub const fn repr_from_slice(value: &[Self]) -> &[[u8; Self::SIZE]] {
608        Self::peel_slice(value)
609    }
610}
611
612/// Reduced hash used for shard assignment
613#[derive(
614    Default,
615    Copy,
616    Clone,
617    Eq,
618    PartialEq,
619    Ord,
620    PartialOrd,
621    Hash,
622    From,
623    Into,
624    AsRef,
625    AsMut,
626    Deref,
627    DerefMut,
628    TrivialType,
629    TransparentWrapper,
630)]
631#[cfg_attr(feature = "scale-codec", derive(Encode, Decode, MaxEncodedLen))]
632#[repr(C)]
633pub struct ShardCommitmentHash([u8; ShardCommitmentHash::SIZE]);
634
635impl fmt::Display for ShardCommitmentHash {
636    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
637        for byte in self.0 {
638            write!(f, "{byte:02x}")?;
639        }
640        Ok(())
641    }
642}
643
644#[cfg(feature = "serde")]
645#[derive(Serialize, Deserialize)]
646#[serde(transparent)]
647struct ShardCommitmentHashBinary([u8; ShardCommitmentHash::SIZE]);
648
649#[cfg(feature = "serde")]
650#[derive(Serialize, Deserialize)]
651#[serde(transparent)]
652struct ShardCommitmentHashHex(#[serde(with = "hex")] [u8; ShardCommitmentHash::SIZE]);
653
654#[cfg(feature = "serde")]
655impl Serialize for ShardCommitmentHash {
656    #[inline]
657    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
658    where
659        S: Serializer,
660    {
661        if serializer.is_human_readable() {
662            ShardCommitmentHashHex(self.0).serialize(serializer)
663        } else {
664            ShardCommitmentHashBinary(self.0).serialize(serializer)
665        }
666    }
667}
668
669#[cfg(feature = "serde")]
670impl<'de> Deserialize<'de> for ShardCommitmentHash {
671    #[inline]
672    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
673    where
674        D: Deserializer<'de>,
675    {
676        Ok(Self(if deserializer.is_human_readable() {
677            ShardCommitmentHashHex::deserialize(deserializer)?.0
678        } else {
679            ShardCommitmentHashBinary::deserialize(deserializer)?.0
680        }))
681    }
682}
683
684impl fmt::Debug for ShardCommitmentHash {
685    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
686        for byte in self.0 {
687            write!(f, "{byte:02x}")?;
688        }
689        Ok(())
690    }
691}
692
693impl AsRef<[u8]> for ShardCommitmentHash {
694    #[inline(always)]
695    fn as_ref(&self) -> &[u8] {
696        &self.0
697    }
698}
699
700impl AsMut<[u8]> for ShardCommitmentHash {
701    #[inline(always)]
702    fn as_mut(&mut self) -> &mut [u8] {
703        &mut self.0
704    }
705}
706
707impl From<Hash> for ShardCommitmentHash {
708    #[inline(always)]
709    fn from(value: Hash) -> Self {
710        let bytes = value.as_bytes();
711        Self(*bytes)
712        // Self([
713        //     bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
714        // ])
715    }
716}
717
718impl ShardCommitmentHash {
719    // TODO: Reduce to 8 bytes once Merkle Tree implementation exists that produces such hashes
720    /// Size in bytes
721    pub const SIZE: usize = 32;
722
723    /// Create a new instance
724    #[inline(always)]
725    pub const fn new(hash: [u8; Self::SIZE]) -> Self {
726        Self(hash)
727    }
728
729    /// Get internal representation
730    #[inline(always)]
731    pub const fn as_bytes(&self) -> &[u8; Self::SIZE] {
732        &self.0
733    }
734
735    /// Convenient conversion from slice of underlying representation for efficiency purposes
736    #[inline(always)]
737    pub const fn slice_from_repr(value: &[[u8; Self::SIZE]]) -> &[Self] {
738        Self::wrap_slice(value)
739    }
740
741    /// Convenient conversion from array of underlying representation for efficiency purposes
742    #[inline(always)]
743    pub const fn array_from_repr<const N: usize>(value: [[u8; Self::SIZE]; N]) -> [Self; N] {
744        <[Self; N]>::wrap(value)
745    }
746
747    /// Convenient conversion to a slice of underlying representation for efficiency purposes
748    #[inline(always)]
749    pub const fn repr_from_slice(value: &[Self]) -> &[[u8; Self::SIZE]] {
750        Self::peel_slice(value)
751    }
752
753    /// Convenient conversion to an array of underlying representation for efficiency purposes
754    #[inline(always)]
755    pub const fn repr_from_array<const N: usize>(value: [Self; N]) -> [[u8; Self::SIZE]; N] {
756        value.peel()
757    }
758}
759
760/// Information about shard commitments in the solution
761#[derive(Clone, Copy, Debug, Eq, PartialEq, TrivialType)]
762#[cfg_attr(feature = "scale-codec", derive(Encode, Decode, MaxEncodedLen))]
763#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
764#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
765#[repr(C)]
766pub struct SolutionShardCommitment {
767    /// Root of the Merkle Tree of shard commitments
768    pub root: ShardCommitmentHash,
769    /// Proof for the shard commitment used the solution
770    pub proof: [ShardCommitmentHash; SolutionShardCommitment::NUM_LEAVES.ilog2() as usize],
771    /// Shard commitment leaf used for the solution
772    pub leaf: ShardCommitmentHash,
773}
774
775impl SolutionShardCommitment {
776    /// Number of leaves in a Merkle Tree of shard commitments
777    pub const NUM_LEAVES: usize = 2u32.pow(20) as usize;
778}
779
780/// Farmer solution for slot challenge.
781#[derive(Clone, Copy, Debug, Eq, PartialEq, TrivialType)]
782#[cfg_attr(feature = "scale-codec", derive(Encode, Decode, MaxEncodedLen))]
783#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
784#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
785#[repr(C)]
786pub struct Solution {
787    /// Public key of the farmer that created the solution
788    pub public_key_hash: Blake3Hash,
789    /// Farmer's shard commitment
790    pub shard_commitment: SolutionShardCommitment,
791    /// Local segment index of the piece
792    pub piece_local_segment_index: LocalSegmentIndex,
793    /// Super segment index
794    pub piece_super_segment_index: SuperSegmentIndex,
795    /// Segment root
796    pub segment_root: SegmentRoot,
797    /// Segment proof
798    pub segment_proof: SegmentProof,
799    /// Record root that can use used to verify that the piece was included in blockchain history
800    pub record_root: RecordRoot,
801    /// Proof that the record (root) belongs to a segment
802    pub record_proof: RecordProof,
803    /// Chunk at the below piece offset
804    pub chunk: RecordChunk,
805    /// Proof for the above chunk
806    pub chunk_proof: ChunkProof,
807    /// Proof of space for piece offset
808    pub proof_of_space: PosProof,
809    /// Size of the blockchain history at the time of sector creation
810    pub history_size: HistorySize,
811    /// Index of the sector where the solution was found
812    pub sector_index: SectorIndex,
813    /// Pieces offset within sector
814    pub piece_offset: PieceOffset,
815    /// Position of the segment in the super segment
816    pub segment_position: SegmentPosition,
817    /// Shard index on which the piece was archived
818    pub piece_shard_index: ShardIndex,
819    /// Padding for data structure alignment
820    pub padding: [u8; 4],
821}
822
823impl Solution {
824    /// Fake solution for the genesis block
825    pub fn genesis_solution() -> Self {
826        Self {
827            public_key_hash: Ed25519PublicKey::default().hash(),
828            shard_commitment: SolutionShardCommitment {
829                root: ShardCommitmentHash::default(),
830                proof: [ShardCommitmentHash::default(); _],
831                leaf: ShardCommitmentHash::default(),
832            },
833            piece_local_segment_index: LocalSegmentIndex::ZERO,
834            piece_super_segment_index: SuperSegmentIndex::ZERO,
835            segment_root: SegmentRoot::default(),
836            segment_proof: SegmentProof::default(),
837            record_root: RecordRoot::default(),
838            record_proof: RecordProof::default(),
839            chunk: RecordChunk::default(),
840            chunk_proof: ChunkProof::default(),
841            proof_of_space: PosProof::default(),
842            history_size: HistorySize::from(SegmentIndex::ZERO),
843            sector_index: SectorIndex::ZERO,
844            piece_offset: PieceOffset::default(),
845            segment_position: SegmentPosition::default(),
846            piece_shard_index: ShardIndex::BEACON_CHAIN,
847            padding: [0; _],
848        }
849    }
850
851    /// Check solution validity
852    pub fn verify_full<PotVerifier>(
853        &self,
854        slot: SlotNumber,
855        params: &SolutionVerifyFullParams,
856    ) -> Result<(), SolutionVerifyError>
857    where
858        PotVerifier: SolutionPotVerifier,
859    {
860        let sector_id = SectorId::new(
861            &self.public_key_hash,
862            &self.shard_commitment.root,
863            self.sector_index,
864            self.history_size,
865        );
866
867        self.verify_stateless_inner::<PotVerifier>(&sector_id, slot, &params.stateless)?;
868
869        self.verify_piece_inner(&sector_id, &params.piece)
870    }
871
872    /// Stateless solution verification.
873    ///
874    /// Checks most things, except checking that the piece belongs to the global history.
875    ///
876    /// For piece verification use [`Self::verify_piece()`] or call [`Self::verify_full()`] for more
877    /// efficient verification of both at once.
878    pub fn verify_stateless<PotVerifier>(
879        &self,
880        slot: SlotNumber,
881        params: &SolutionVerifyStatelessParams,
882    ) -> Result<(), SolutionVerifyError>
883    where
884        PotVerifier: SolutionPotVerifier,
885    {
886        let sector_id = SectorId::new(
887            &self.public_key_hash,
888            &self.shard_commitment.root,
889            self.sector_index,
890            self.history_size,
891        );
892
893        self.verify_stateless_inner::<PotVerifier>(&sector_id, slot, params)
894    }
895
896    fn verify_stateless_inner<PotVerifier>(
897        &self,
898        sector_id: &SectorId,
899        slot: SlotNumber,
900        params: &SolutionVerifyStatelessParams,
901    ) -> Result<(), SolutionVerifyError>
902    where
903        PotVerifier: SolutionPotVerifier,
904    {
905        let SolutionVerifyStatelessParams {
906            shard_index,
907            proof_of_time,
908            solution_range,
909            shard_membership_entropy,
910            num_shards,
911        } = params;
912
913        let shard_kind = shard_index
914            .shard_kind()
915            .and_then(ShardKind::to_real)
916            .ok_or(SolutionVerifyError::InvalidInputShard {
917                shard_index: *shard_index,
918                shard_kind: shard_index.shard_kind(),
919            })?;
920
921        let (solution_shard_index, shard_commitment_index) = num_shards
922            .derive_shard_index_and_shard_commitment_index(
923                &self.public_key_hash,
924                &self.shard_commitment.root,
925                shard_membership_entropy,
926                self.history_size,
927            );
928
929        // Adjust solution range according to shard kind
930        let solution_range = match shard_kind {
931            RealShardKind::BeaconChain => *solution_range,
932            RealShardKind::IntermediateShard => {
933                if solution_shard_index.parent_shard() != Some(*shard_index) {
934                    return Err(SolutionVerifyError::InvalidSolutionShard {
935                        solution_shard_index,
936                        solution_parent_shard_index: solution_shard_index.parent_shard(),
937                        expected_shard_index: *shard_index,
938                        expected_shard_kind: RealShardKind::IntermediateShard,
939                    });
940                }
941
942                solution_range.to_intermediate_shard(*num_shards)
943            }
944            RealShardKind::LeafShard => {
945                if solution_shard_index != *shard_index {
946                    return Err(SolutionVerifyError::InvalidSolutionShard {
947                        solution_shard_index,
948                        solution_parent_shard_index: solution_shard_index.parent_shard(),
949                        expected_shard_index: *shard_index,
950                        expected_shard_kind: RealShardKind::LeafShard,
951                    });
952                }
953
954                solution_range.to_leaf_shard(*num_shards)
955            }
956        };
957
958        if !BalancedMerkleTree::<{ SolutionShardCommitment::NUM_LEAVES }>::verify(
959            &self.shard_commitment.root,
960            &ShardCommitmentHash::repr_from_array(self.shard_commitment.proof),
961            shard_commitment_index as usize,
962            *self.shard_commitment.leaf,
963        ) {
964            return Err(SolutionVerifyError::InvalidShardCommitment);
965        }
966
967        let global_challenge = proof_of_time.derive_global_challenge(slot);
968        let sector_slot_challenge = sector_id.derive_sector_slot_challenge(&global_challenge);
969        let s_bucket_audit_index = sector_slot_challenge.s_bucket_audit_index();
970
971        // Check that proof of space is valid
972        if !PotVerifier::is_proof_valid(
973            &sector_id.derive_evaluation_seed(self.piece_offset),
974            s_bucket_audit_index,
975            &self.proof_of_space,
976        ) {
977            return Err(SolutionVerifyError::InvalidProofOfSpace);
978        }
979
980        let masked_chunk =
981            (Simd::from(*self.chunk) ^ Simd::from(*self.proof_of_space.hash())).to_array();
982
983        let solution_distance =
984            SolutionDistance::calculate(&global_challenge, &masked_chunk, &sector_slot_challenge);
985
986        if !solution_distance.is_within(solution_range) {
987            return Err(SolutionVerifyError::OutsideSolutionRange {
988                solution_range,
989                solution_distance,
990            });
991        }
992
993        // Check that chunk belongs to the record
994        if !BalancedMerkleTree::<{ Record::NUM_S_BUCKETS }>::verify(
995            &self.record_root,
996            &self.chunk_proof,
997            usize::from(s_bucket_audit_index),
998            *self.chunk,
999        ) {
1000            return Err(SolutionVerifyError::InvalidChunkProof);
1001        }
1002
1003        Ok(())
1004    }
1005
1006    /// Verify the piece details of the solution
1007    pub fn verify_piece(
1008        &self,
1009        piece_check_params: &SolutionVerifyPieceParams,
1010    ) -> Result<(), SolutionVerifyError> {
1011        let sector_id = SectorId::new(
1012            &self.public_key_hash,
1013            &self.shard_commitment.root,
1014            self.sector_index,
1015            self.history_size,
1016        );
1017
1018        self.verify_piece_inner(&sector_id, piece_check_params)
1019    }
1020
1021    fn verify_piece_inner(
1022        &self,
1023        sector_id: &SectorId,
1024        piece_check_params: &SolutionVerifyPieceParams,
1025    ) -> Result<(), SolutionVerifyError> {
1026        let SolutionVerifyPieceParams {
1027            max_pieces_in_sector,
1028            super_segment_root,
1029            num_segments,
1030            recent_segments,
1031            recent_history_fraction,
1032            min_sector_lifetime,
1033            current_history_size,
1034            sector_expiration_check_super_segment_root,
1035        } = piece_check_params;
1036
1037        if &self.history_size > current_history_size {
1038            return Err(SolutionVerifyError::FutureHistorySize {
1039                current: *current_history_size,
1040                solution: self.history_size,
1041            });
1042        }
1043
1044        if u16::from(self.piece_offset) >= *max_pieces_in_sector {
1045            return Err(SolutionVerifyError::InvalidPieceOffset {
1046                piece_offset: u16::from(self.piece_offset),
1047                max_pieces_in_sector: *max_pieces_in_sector,
1048            });
1049        }
1050
1051        if let Some(sector_expiration_check_super_segment_root) =
1052            sector_expiration_check_super_segment_root
1053        {
1054            let Some(expiration_history_size) = sector_id.derive_expiration_history_size(
1055                self.history_size,
1056                sector_expiration_check_super_segment_root,
1057                *min_sector_lifetime,
1058            ) else {
1059                return Err(SolutionVerifyError::InvalidHistorySize);
1060            };
1061
1062            if expiration_history_size <= *current_history_size {
1063                return Err(SolutionVerifyError::SectorExpired {
1064                    expiration_history_size,
1065                    current_history_size: *current_history_size,
1066                });
1067            }
1068        }
1069
1070        let position = sector_id
1071            .derive_piece_index(
1072                self.piece_offset,
1073                self.history_size,
1074                *max_pieces_in_sector,
1075                *recent_segments,
1076                *recent_history_fraction,
1077            )
1078            .position();
1079
1080        // Check that record belongs to the segment
1081        if !self
1082            .record_root
1083            .is_valid(&self.segment_root, &self.record_proof, position)
1084        {
1085            return Err(SolutionVerifyError::RecordNotInSegment);
1086        }
1087
1088        // Check that segment belongs to the super segment (global history)
1089        if !self.segment_root.is_valid(
1090            self.piece_shard_index,
1091            self.piece_local_segment_index,
1092            self.segment_position,
1093            &self.segment_proof,
1094            *num_segments,
1095            super_segment_root,
1096        ) {
1097            return Err(SolutionVerifyError::SegmentNotInSuperSegment);
1098        }
1099
1100        Ok(())
1101    }
1102}