1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
//! Representation and validation of Equi-X puzzle solutions
//!
//! Equi-X uses its own tweaked version of the Equihash algorithm. Solutions
//! are arrays of [`SolutionItem`]s, each representing one item in a space of
//! hash outputs. The `SolutionItem`s form a binary tree with constraints
//! on the sorting order of the items and on the sums of their corresponding
//! hashes.

use crate::Error;
use arrayvec::ArrayVec;
use hashx::HashX;
use std::{cmp, mem};

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

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

/// One item in the solution
///
/// The Equihash paper also calls these "indices", to reference the way they
/// index into a space of potential hash outputs. They form the leaf nodes in
/// a binary tree of hashes.
pub type SolutionItem = u16;

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

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

/// A bundle of solutions as returned by one invocation of the solver
///
/// The actual number of solutions found is random, depending on the number of
/// collisions that exist. This size is arbitrary, and in the rare case that
/// the solver finds more solutions they are discarded.
pub type SolutionArray = ArrayVec<Solution, 8>;

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

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

/// Potential solution to an EquiX puzzle
///
/// The `Solution` type itself verifies the well-formedness of an Equi-X
/// solution, but not its suitability for a particular challenge string.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Solution {
    /// Inner fixed-sized array of SolutionItem
    items: SolutionItemArray,
}

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

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

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

    /// Build a [`Solution`] from an array of items, checking that
    /// the solution is well-formed.
    pub fn try_from_array(items: &SolutionItemArray) -> Result<Self, Error> {
        if check_tree_order(items) {
            Ok(Self { items: *items })
        } else {
            Err(Error::Order)
        }
    }

    /// Build a [`Solution`] by sorting a [`SolutionItemArray`] as necessary,
    /// without the possibility of failure.
    ///    
    /// Used by the solver.
    pub(crate) fn sort_from_array(mut items: SolutionItemArray) -> Self {
        sort_into_tree_order(&mut items);
        Self { items }
    }

    /// Build a [`Solution`] from a fixed size byte array, checking
    /// that the solution is well-formed.
    pub fn try_from_bytes(bytes: &SolutionByteArray) -> Result<Self, Error> {
        let mut array: SolutionItemArray = Default::default();
        for i in 0..Self::NUM_ITEMS {
            array[i] = SolutionItem::from_le_bytes(
                bytes[i * Self::ITEM_SIZE..(i + 1) * Self::ITEM_SIZE]
                    .try_into()
                    .expect("slice length matches"),
            );
        }
        Self::try_from_array(&array)
    }

    /// Return the packed byte representation of this Solution.
    pub fn to_bytes(&self) -> SolutionByteArray {
        let mut result: SolutionByteArray = Default::default();
        for i in 0..Self::NUM_ITEMS {
            result[i * Self::ITEM_SIZE..(i + 1) * Self::ITEM_SIZE]
                .copy_from_slice(&self.items[i].to_le_bytes());
        }
        result
    }
}

impl AsRef<SolutionItemArray> for Solution {
    fn as_ref(&self) -> &SolutionItemArray {
        &self.items
    }
}

impl From<Solution> for SolutionItemArray {
    fn from(solution: Solution) -> SolutionItemArray {
        solution.items
    }
}

/// Ordering predicate for each node of the SolutionItem tree
#[inline(always)]
fn branches_are_sorted(left: &[SolutionItem], right: &[SolutionItem]) -> bool {
    matches!(
        left.iter().rev().cmp(right.iter().rev()),
        cmp::Ordering::Less | cmp::Ordering::Equal
    )
}

/// Check tree ordering recursively.
///
/// HashX uses a lexicographic ordering constraint applied at each tree level,
/// to resolve the ambiguity that would otherwise be present between each pair
/// of branches in the item tree.
///
/// When combined with the hash sum constraints, this fully constrains the order
/// of the items in a solution. On its own this constraint only partially defines
/// the order of the entire item array.
#[inline(always)]
fn check_tree_order(items: &[SolutionItem]) -> bool {
    let (left, right) = items.split_at(items.len() / 2);
    let sorted = branches_are_sorted(left, right);
    if items.len() == 2 {
        sorted
    } else {
        sorted && check_tree_order(left) && check_tree_order(right)
    }
}

/// Sort a solution in-place into tree order.
#[inline(always)]
fn sort_into_tree_order(items: &mut [SolutionItem]) {
    let len = items.len();
    let (left, right) = items.split_at_mut(items.len() / 2);
    if len > 2 {
        sort_into_tree_order(left);
        sort_into_tree_order(right);
    }
    if !branches_are_sorted(left, right) {
        left.swap_with_slice(right);
    }
}

/// Check hash sums recursively.
///
/// The main solution constraint in HashX is a partial sum at each tree level.
/// The overall match required is [`EQUIHASH_N`] bits, and each subsequent tree
/// level needs a match half this long.
///
/// Each recursive invocation returns the entire sum if its layer has the
/// indicated number of matching bits.
#[inline(always)]
fn check_tree_sums(func: &HashX, items: &[SolutionItem], n_bits: usize) -> Result<HashValue, ()> {
    let sum = if items.len() == 2 {
        item_hash(func, items[0]).wrapping_add(item_hash(func, items[1]))
    } else {
        let (left, right) = items.split_at(items.len() / 2);
        let left = check_tree_sums(func, left, n_bits / 2)?;
        let right = check_tree_sums(func, right, n_bits / 2)?;
        left.wrapping_add(right)
    };
    let mask = ((1 as HashValue) << n_bits) - 1;
    if (sum & mask) == 0 {
        Ok(sum)
    } else {
        Err(())
    }
}

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