1//! Representation and validation of Equi-X puzzle solutions
2//!
3//! Equi-X uses its own tweaked version of the Equihash algorithm. Solutions
4//! are arrays of [`SolutionItem`]s, each representing one item in a space of
5//! hash outputs. The `SolutionItem`s form a binary tree with constraints
6//! on the sorting order of the items and on the sums of their corresponding
7//! hashes.
89use crate::Error;
10use arrayvec::ArrayVec;
11use hashx::HashX;
12use std::{cmp, mem};
1314/// Equihash N parameter for Equi-X, number of bits used from the hash output
15pub(crate) const EQUIHASH_N: usize = 60;
1617/// Equihash K parameter for Equi-X, the number of tree layers
18pub(crate) const EQUIHASH_K: usize = 3;
1920/// One item in the solution
21///
22/// The Equihash paper also calls these "indices", to reference the way they
23/// index into a space of potential hash outputs. They form the leaf nodes in
24/// a binary tree of hashes.
25pub type SolutionItem = u16;
2627/// One hash value, computed from a [`SolutionItem`]
28///
29/// Must hold [`EQUIHASH_N`] bits.
30pub(crate) type HashValue = u64;
3132/// Compute a [`HashValue`] from a [`SolutionItem`]
33#[inline(always)]
34pub(crate) fn item_hash(func: &HashX, item: SolutionItem) -> HashValue {
35 func.hash_to_u64(item.into())
36}
3738/// A bundle of solutions as returned by one invocation of the solver
39///
40/// The actual number of solutions found is random, depending on the number of
41/// collisions that exist. This size is arbitrary, and in the rare case that
42/// the solver finds more solutions they are discarded.
43pub type SolutionArray = ArrayVec<Solution, 8>;
4445/// A raw Item array which may or may not be a well-formed [`Solution`]
46pub type SolutionItemArray = [SolutionItem; Solution::NUM_ITEMS];
4748/// A byte array of the right length to convert to/from a [`Solution`]
49pub type SolutionByteArray = [u8; Solution::NUM_BYTES];
5051/// Potential solution to an EquiX puzzle
52///
53/// The `Solution` type itself verifies the well-formedness of an Equi-X
54/// solution, but not its suitability for a particular challenge string.
55#[derive(Debug, Clone, Eq, PartialEq)]
56pub struct Solution {
57/// Inner fixed-sized array of SolutionItem
58items: SolutionItemArray,
59}
6061impl Solution {
62/// Number of items (selected hash inputs) in each solution
63pub const NUM_ITEMS: usize = 1 << EQUIHASH_K;
6465/// Number of bytes in the packed representation of a solution
66pub const NUM_BYTES: usize = Self::NUM_ITEMS * mem::size_of::<SolutionItem>();
6768/// Size of each [`SolutionItem`], in bytes
69const ITEM_SIZE: usize = mem::size_of::<SolutionItem>();
7071/// Build a [`Solution`] from an array of items, checking that
72 /// the solution is well-formed.
73pub fn try_from_array(items: &SolutionItemArray) -> Result<Self, Error> {
74if check_tree_order(items) {
75Ok(Self { items: *items })
76 } else {
77Err(Error::Order)
78 }
79 }
8081/// Build a [`Solution`] by sorting a [`SolutionItemArray`] as necessary,
82 /// without the possibility of failure.
83 ///
84 /// Used by the solver.
85pub(crate) fn sort_from_array(mut items: SolutionItemArray) -> Self {
86 sort_into_tree_order(&mut items);
87Self { items }
88 }
8990/// Build a [`Solution`] from a fixed size byte array, checking
91 /// that the solution is well-formed.
92pub fn try_from_bytes(bytes: &SolutionByteArray) -> Result<Self, Error> {
93let mut array: SolutionItemArray = Default::default();
94for i in 0..Self::NUM_ITEMS {
95 array[i] = SolutionItem::from_le_bytes(
96 bytes[i * Self::ITEM_SIZE..(i + 1) * Self::ITEM_SIZE]
97 .try_into()
98 .expect("slice length matches"),
99 );
100 }
101Self::try_from_array(&array)
102 }
103104/// Return the packed byte representation of this Solution.
105pub fn to_bytes(&self) -> SolutionByteArray {
106let mut result: SolutionByteArray = Default::default();
107for i in 0..Self::NUM_ITEMS {
108 result[i * Self::ITEM_SIZE..(i + 1) * Self::ITEM_SIZE]
109 .copy_from_slice(&self.items[i].to_le_bytes());
110 }
111 result
112 }
113}
114115impl AsRef<SolutionItemArray> for Solution {
116fn as_ref(&self) -> &SolutionItemArray {
117&self.items
118 }
119}
120121impl From<Solution> for SolutionItemArray {
122fn from(solution: Solution) -> SolutionItemArray {
123 solution.items
124 }
125}
126127/// Ordering predicate for each node of the SolutionItem tree
128#[inline(always)]
129fn branches_are_sorted(left: &[SolutionItem], right: &[SolutionItem]) -> bool {
130matches!(
131 left.iter().rev().cmp(right.iter().rev()),
132 cmp::Ordering::Less | cmp::Ordering::Equal
133 )
134}
135136/// Check tree ordering recursively.
137///
138/// HashX uses a lexicographic ordering constraint applied at each tree level,
139/// to resolve the ambiguity that would otherwise be present between each pair
140/// of branches in the item tree.
141///
142/// When combined with the hash sum constraints, this fully constrains the order
143/// of the items in a solution. On its own this constraint only partially defines
144/// the order of the entire item array.
145#[inline(always)]
146fn check_tree_order(items: &[SolutionItem]) -> bool {
147let (left, right) = items.split_at(items.len() / 2);
148let sorted = branches_are_sorted(left, right);
149if items.len() == 2 {
150 sorted
151 } else {
152 sorted && check_tree_order(left) && check_tree_order(right)
153 }
154}
155156/// Sort a solution in-place into tree order.
157#[inline(always)]
158fn sort_into_tree_order(items: &mut [SolutionItem]) {
159let len = items.len();
160let (left, right) = items.split_at_mut(items.len() / 2);
161if len > 2 {
162 sort_into_tree_order(left);
163 sort_into_tree_order(right);
164 }
165if !branches_are_sorted(left, right) {
166 left.swap_with_slice(right);
167 }
168}
169170/// Check hash sums recursively.
171///
172/// The main solution constraint in HashX is a partial sum at each tree level.
173/// The overall match required is [`EQUIHASH_N`] bits, and each subsequent tree
174/// level needs a match half this long.
175///
176/// Each recursive invocation returns the entire sum if its layer has the
177/// indicated number of matching bits.
178#[inline(always)]
179fn check_tree_sums(func: &HashX, items: &[SolutionItem], n_bits: usize) -> Result<HashValue, ()> {
180let sum = if items.len() == 2 {
181 item_hash(func, items[0]).wrapping_add(item_hash(func, items[1]))
182 } else {
183let (left, right) = items.split_at(items.len() / 2);
184let left = check_tree_sums(func, left, n_bits / 2)?;
185let right = check_tree_sums(func, right, n_bits / 2)?;
186 left.wrapping_add(right)
187 };
188let mask = ((1 as HashValue) << n_bits) - 1;
189if (sum & mask) == 0 {
190Ok(sum)
191 } else {
192Err(())
193 }
194}
195196/// Check all tree sums, using the full size defined by [`EQUIHASH_N`].
197///
198/// This will recurse at compile-time into
199/// layered tests for 60-, 30-, and 15-bit masks.
200pub(crate) fn check_all_tree_sums(func: &HashX, solution: &Solution) -> Result<(), Error> {
201match check_tree_sums(func, solution.as_ref(), EQUIHASH_N) {
202Ok(_unused_bits) => Ok(()),
203Err(()) => Err(Error::HashSum),
204 }
205}