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.
8

            
9
use crate::Error;
10
use arrayvec::ArrayVec;
11
use hashx::HashX;
12
use std::{cmp, mem};
13

            
14
/// Equihash N parameter for Equi-X, number of bits used from the hash output
15
pub(crate) const EQUIHASH_N: usize = 60;
16

            
17
/// Equihash K parameter for Equi-X, the number of tree layers
18
pub(crate) const EQUIHASH_K: usize = 3;
19

            
20
/// 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.
25
pub type SolutionItem = u16;
26

            
27
/// One hash value, computed from a [`SolutionItem`]
28
///
29
/// Must hold [`EQUIHASH_N`] bits.
30
pub(crate) type HashValue = u64;
31

            
32
/// Compute a [`HashValue`] from a [`SolutionItem`]
33
#[inline(always)]
34
334311392
pub(crate) fn item_hash(func: &HashX, item: SolutionItem) -> HashValue {
35
334311392
    func.hash_to_u64(item.into())
36
334311392
}
37

            
38
/// 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.
43
pub type SolutionArray = ArrayVec<Solution, 8>;
44

            
45
/// A raw Item array which may or may not be a well-formed [`Solution`]
46
pub type SolutionItemArray = [SolutionItem; Solution::NUM_ITEMS];
47

            
48
/// A byte array of the right length to convert to/from a [`Solution`]
49
pub type SolutionByteArray = [u8; Solution::NUM_BYTES];
50

            
51
/// 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)]
56
pub struct Solution {
57
    /// Inner fixed-sized array of SolutionItem
58
    items: SolutionItemArray,
59
}
60

            
61
impl Solution {
62
    /// Number of items (selected hash inputs) in each solution
63
    pub const NUM_ITEMS: usize = 1 << EQUIHASH_K;
64

            
65
    /// Number of bytes in the packed representation of a solution
66
    pub const NUM_BYTES: usize = Self::NUM_ITEMS * mem::size_of::<SolutionItem>();
67

            
68
    /// Size of each [`SolutionItem`], in bytes
69
    const ITEM_SIZE: usize = mem::size_of::<SolutionItem>();
70

            
71
    /// Build a [`Solution`] from an array of items, checking that
72
    /// the solution is well-formed.
73
2707403
    pub fn try_from_array(items: &SolutionItemArray) -> Result<Self, Error> {
74
2707403
        if check_tree_order(items) {
75
25728
            Ok(Self { items: *items })
76
        } else {
77
2681675
            Err(Error::Order)
78
        }
79
2707403
    }
80

            
81
    /// Build a [`Solution`] by sorting a [`SolutionItemArray`] as necessary,
82
    /// without the possibility of failure.
83
    ///    
84
    /// Used by the solver.
85
11859
    pub(crate) fn sort_from_array(mut items: SolutionItemArray) -> Self {
86
11859
        sort_into_tree_order(&mut items);
87
11859
        Self { items }
88
11859
    }
89

            
90
    /// Build a [`Solution`] from a fixed size byte array, checking
91
    /// that the solution is well-formed.
92
1876
    pub fn try_from_bytes(bytes: &SolutionByteArray) -> Result<Self, Error> {
93
1876
        let mut array: SolutionItemArray = Default::default();
94
16884
        for i in 0..Self::NUM_ITEMS {
95
15008
            array[i] = SolutionItem::from_le_bytes(
96
15008
                bytes[i * Self::ITEM_SIZE..(i + 1) * Self::ITEM_SIZE]
97
15008
                    .try_into()
98
15008
                    .expect("slice length matches"),
99
15008
            );
100
15008
        }
101
1876
        Self::try_from_array(&array)
102
1876
    }
103

            
104
    /// Return the packed byte representation of this Solution.
105
8710
    pub fn to_bytes(&self) -> SolutionByteArray {
106
8710
        let mut result: SolutionByteArray = Default::default();
107
78390
        for i in 0..Self::NUM_ITEMS {
108
69680
            result[i * Self::ITEM_SIZE..(i + 1) * Self::ITEM_SIZE]
109
69680
                .copy_from_slice(&self.items[i].to_le_bytes());
110
69680
        }
111
8710
        result
112
8710
    }
113
}
114

            
115
impl AsRef<SolutionItemArray> for Solution {
116
28207
    fn as_ref(&self) -> &SolutionItemArray {
117
28207
        &self.items
118
28207
    }
119
}
120

            
121
impl From<Solution> for SolutionItemArray {
122
3886
    fn from(solution: Solution) -> SolutionItemArray {
123
3886
        solution.items
124
3886
    }
125
}
126

            
127
/// Ordering predicate for each node of the SolutionItem tree
128
#[inline(always)]
129
4882156
fn branches_are_sorted(left: &[SolutionItem], right: &[SolutionItem]) -> bool {
130
2722746
    matches!(
131
4882156
        left.iter().rev().cmp(right.iter().rev()),
132
        cmp::Ordering::Less | cmp::Ordering::Equal
133
    )
134
4882156
}
135

            
136
/// 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)]
146
4799143
fn check_tree_order(items: &[SolutionItem]) -> bool {
147
4799143
    let (left, right) = items.split_at(items.len() / 2);
148
4799143
    let sorted = branches_are_sorted(left, right);
149
4799143
    if items.len() == 2 {
150
662965
        sorted
151
    } else {
152
4136178
        sorted && check_tree_order(left) && check_tree_order(right)
153
    }
154
4799143
}
155

            
156
/// Sort a solution in-place into tree order.
157
#[inline(always)]
158
83013
fn sort_into_tree_order(items: &mut [SolutionItem]) {
159
83013
    let len = items.len();
160
83013
    let (left, right) = items.split_at_mut(items.len() / 2);
161
83013
    if len > 2 {
162
35577
        sort_into_tree_order(left);
163
35577
        sort_into_tree_order(right);
164
47436
    }
165
83013
    if !branches_are_sorted(left, right) {
166
41071
        left.swap_with_slice(right);
167
41942
    }
168
83013
}
169

            
170
/// 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)]
179
92795
fn check_tree_sums(func: &HashX, items: &[SolutionItem], n_bits: usize) -> Result<HashValue, ()> {
180
92795
    let sum = if items.len() == 2 {
181
38324
        item_hash(func, items[0]).wrapping_add(item_hash(func, items[1]))
182
    } else {
183
54471
        let (left, right) = items.split_at(items.len() / 2);
184
54471
        let left = check_tree_sums(func, left, n_bits / 2)?;
185
12730
        let right = check_tree_sums(func, right, n_bits / 2)?;
186
9983
        left.wrapping_add(right)
187
    };
188
48307
    let mask = ((1 as HashValue) << n_bits) - 1;
189
48307
    if (sum & mask) == 0 { Ok(sum) } else { Err(()) }
190
92795
}
191

            
192
/// Check all tree sums, using the full size defined by [`EQUIHASH_N`].
193
///
194
/// This will recurse at compile-time into
195
/// layered tests for 60-, 30-, and 15-bit masks.
196
25594
pub(crate) fn check_all_tree_sums(func: &HashX, solution: &Solution) -> Result<(), Error> {
197
25594
    match check_tree_sums(func, solution.as_ref(), EQUIHASH_N) {
198
3149
        Ok(_unused_bits) => Ok(()),
199
22445
        Err(()) => Err(Error::HashSum),
200
    }
201
25594
}