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
314646016
pub(crate) fn item_hash(func: &HashX, item: SolutionItem) -> HashValue {
35
314646016
    func.hash_to_u64(item.into())
36
314646016
}
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
2545767
    pub fn try_from_array(items: &SolutionItemArray) -> Result<Self, Error> {
74
2545767
        if check_tree_order(items) {
75
24192
            Ok(Self { items: *items })
76
        } else {
77
2521575
            Err(Error::Order)
78
        }
79
2545767
    }
80

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

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

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

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

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

            
127
/// Ordering predicate for each node of the SolutionItem tree
128
#[inline(always)]
129
4590684
fn branches_are_sorted(left: &[SolutionItem], right: &[SolutionItem]) -> bool {
130
2560194
    matches!(
131
4590684
        left.iter().rev().cmp(right.iter().rev()),
132
        cmp::Ordering::Less | cmp::Ordering::Equal
133
    )
134
4590684
}
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
4512627
fn check_tree_order(items: &[SolutionItem]) -> bool {
147
4512627
    let (left, right) = items.split_at(items.len() / 2);
148
4512627
    let sorted = branches_are_sorted(left, right);
149
4512627
    if items.len() == 2 {
150
623385
        sorted
151
    } else {
152
3889242
        sorted && check_tree_order(left) && check_tree_order(right)
153
    }
154
4512627
}
155

            
156
/// Sort a solution in-place into tree order.
157
#[inline(always)]
158
78057
fn sort_into_tree_order(items: &mut [SolutionItem]) {
159
78057
    let len = items.len();
160
78057
    let (left, right) = items.split_at_mut(items.len() / 2);
161
78057
    if len > 2 {
162
33453
        sort_into_tree_order(left);
163
33453
        sort_into_tree_order(right);
164
44604
    }
165
78057
    if !branches_are_sorted(left, right) {
166
38619
        left.swap_with_slice(right);
167
39438
    }
168
78057
}
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
87255
fn check_tree_sums(func: &HashX, items: &[SolutionItem], n_bits: usize) -> Result<HashValue, ()> {
180
87255
    let sum = if items.len() == 2 {
181
36036
        item_hash(func, items[0]).wrapping_add(item_hash(func, items[1]))
182
    } else {
183
51219
        let (left, right) = items.split_at(items.len() / 2);
184
51219
        let left = check_tree_sums(func, left, n_bits / 2)?;
185
11970
        let right = check_tree_sums(func, right, n_bits / 2)?;
186
9387
        left.wrapping_add(right)
187
    };
188
45423
    let mask = ((1 as HashValue) << n_bits) - 1;
189
45423
    if (sum & mask) == 0 {
190
24318
        Ok(sum)
191
    } else {
192
21105
        Err(())
193
    }
194
87255
}
195

            
196
/// 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.
200
24066
pub(crate) fn check_all_tree_sums(func: &HashX, solution: &Solution) -> Result<(), Error> {
201
24066
    match check_tree_sums(func, solution.as_ref(), EQUIHASH_N) {
202
2961
        Ok(_unused_bits) => Ok(()),
203
21105
        Err(()) => Err(Error::HashSum),
204
    }
205
24066
}