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 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
//! Find Equi-X solutions.
//!
//! This is a particular instance of Equihash, a tree-structured search for
//! partial hash collisions using temporary memory. Equi-X modifies it to use
//! sums instead of XOR, and chooses specific parameters.
use crate::bucket_array::{
hash::Insert, hash::KeyValueBucketArray, mem::BucketArrayMemory, mem::Uninit,
};
use crate::collision::{self, PackedCollision};
use crate::solution::{self, HashValue, Solution, SolutionArray, SolutionItem, EQUIHASH_N};
use arrayvec::ArrayVec;
use hashx::HashX;
// The hash table bucket counts here are mostly constrained by the shape of
// the Equihash tree, but the bucket capacities are somewhat arbitrary. Larger
// buckets use more memory, smaller buckets may discard solutions if there are
// sufficient collisions during the earlier layers. This uses the same bucket
// counts as the original Equi-X implementation, in order to exactly match
// its solution discarding behavior and thus its output.
/// First layer hash table, needs room for the full set of [`HashValue`]s and
/// [`SolutionItem`]s. Key remainder is `(EQUIHASH_N - 8 == 52)` bits here.
type Layer0<'k, 'v> =
KeyValueBucketArray<'k, 'v, 256, 336, u16, HashValue, HashValue, SolutionItem>;
/// Key backing storage for [`Layer0`]
type Layer0KeyMem = BucketArrayMemory<256, 336, HashValue>;
/// Value backing storage for [`Layer0`]
type Layer0ValueMem = BucketArrayMemory<256, 336, SolutionItem>;
/// Packed collision type for [`Layer0`]
type Layer0Collision = PackedCollision<u32, 8, 9>;
/// Next layer maps residual hash (3/4 full width == 45 bits) to packed
/// [`Layer0Collision`]. Key remainder is 37 bits after this.
type Layer1<'k, 'v> = KeyValueBucketArray<'k, 'v, 256, 336, u16, u64, u64, u32>;
/// Key backing storage for [`Layer1`]
type Layer1KeyMem = BucketArrayMemory<256, 336, u64>;
/// Value backing storage for [`Layer1`]
type Layer1ValueMem = BucketArrayMemory<256, 336, u32>;
/// Packed collision type for [`Layer1`]
type Layer1Collision = PackedCollision<u32, 8, 9>;
/// Final layer maps the (N/2 == 30 bits) residual hash sum to packed
/// [`Layer1Collision`]. Key remainder is 22 bits.
type Layer2<'k, 'v> = KeyValueBucketArray<'k, 'v, 256, 336, u16, u32, u32, u32>;
/// Key backing storage for [`Layer2`]
type Layer2KeyMem = BucketArrayMemory<256, 336, u32>;
/// Value backing storage for [`Layer2`]
type Layer2ValueMem = BucketArrayMemory<256, 336, u32>;
/// Temporary value hash for resolving collisions between buckets.
/// This removes 7 bits of key between each of the other layers.
/// Value type may hold a [`SolutionItem`] or a temporary item-in-bucket index.
type TempMem = BucketArrayMemory<128, 12, u16>;
/// Internal solver memory, inside the heap allocation
///
/// Contains exclusively [`std::mem::MaybeUninit`] members. Other than the
/// temporary memory used between each pair of tree layers, the memory regions
/// are organized by which layer of the solution tree they form starting
/// from leaf `(hash, index)` items and moving toward the root of the tree.
#[derive(Copy, Clone)]
struct SolverMemoryInner {
/// Temporary memory for each [`collision::search()`]
temp: TempMem,
/// Memory overlay union, access controlled with mutable references
overlay: Overlay,
/// Temporary value memory for [`Layer0`]
layer0_values: Layer0ValueMem,
/// Temporary value memory for [`Layer1`]
layer1_values: Layer1ValueMem,
/// Temporary key storage memory for [`Layer1`]
layer1_keys: Layer1KeyMem,
}
/// SAFETY: We are proimising that [`SolverMemoryInner`] is
/// made only from [`Uninit`] types like [`BucketArrayMemory`].
unsafe impl Uninit for SolverMemoryInner {}
/// Union of overlay layouts used in solver memory
///
/// As a memory optimization, some of the allocations in [`SolverMemoryInner`]
/// are overlapped. We only use one of these at a time, checked statically
/// using a mutable borrow.
#[derive(Copy, Clone)]
union Overlay {
/// Initial memory overlay layout
first: OverlayFirst,
/// Layout which replaces the first once we can drop `layer0_keys`
second: OverlaySecond,
}
impl Overlay {
/// Access the first overlay layout, via a mutable reference
fn first(&mut self) -> &mut OverlayFirst {
// SAFETY: This union and its members implement [`Uninit`], promising
// that we can soundly create an instance out of uninitialized
// or reused memory. Initialized state is controlled via a
// mutable reference, and by reborrowing a union field we ensure
// exclusive access using the borrow checker.
unsafe { &mut self.first }
}
/// Access the second overlay layout, via a mutable reference
fn second(&mut self) -> &mut OverlaySecond {
// SAFETY: As above, we implement [`Uninit`] and use &mut to control access
unsafe { &mut self.second }
}
}
/// First memory overlay, contains the key portion of [`Layer0`]
#[derive(Copy, Clone)]
struct OverlayFirst {
/// Key remainder table for [`Layer0`], which we can drop
/// after running [`collision::search()`] on that layer.
layer0_keys: Layer0KeyMem,
}
/// Second overlay, with both parts of Layer2
#[derive(Copy, Clone)]
struct OverlaySecond {
/// Temporary key storage memory for [`Layer2`]
layer2_keys: Layer2KeyMem,
/// Temporary value memory for [`Layer2`]
layer2_values: Layer2ValueMem,
}
/// Search for solutions, iterating the entire [`SolutionItem`] space and using
/// temporary memory to locate partial sum collisions at each tree layer.
pub(crate) fn find_solutions(func: &HashX, mem: &mut SolverMemory, results: &mut SolutionArray) {
// Use the first memory overlay layout.
let overlay = mem.heap.overlay.first();
// Enumerate all hash values into the first layer
let mut layer0 = Layer0::new(&mut overlay.layer0_keys, &mut mem.heap.layer0_values);
for item in SolutionItem::MIN..=SolutionItem::MAX {
let hash = solution::item_hash(func, item);
let _ = layer0.insert(hash, item);
}
// Now form the first layer of the Equihash tree,
// with collisions in the low N/4 (15) bits
let layer1_n = EQUIHASH_N / 4;
let mut layer1 = Layer1::new(&mut mem.heap.layer1_keys, &mut mem.heap.layer1_values);
collision::search(&layer0, &mut mem.heap.temp, layer1_n, |sum, loc| {
let _ = layer1.insert(sum, Layer0Collision::pack(&loc).into_inner());
});
// Once we finish searching a layer for collisions,
// we can drop the key data and make the rest immutable.
// This drops the last mutable reference into the first overlay.
let layer0 = layer0.drop_key_storage();
// Now switch to the second memory overlay layout.
let overlay = mem.heap.overlay.second();
// Next Equihash layer, collisions in the low N/2 (30) bits
let layer2_n = EQUIHASH_N / 2;
let mut layer2 = Layer2::new(&mut overlay.layer2_keys, &mut overlay.layer2_values);
collision::search(
&layer1,
&mut mem.heap.temp,
layer2_n - layer1_n,
|sum, loc| {
let _ = layer2.insert(sum as u32, Layer1Collision::pack(&loc).into_inner());
},
);
// Final layer, match the entire N bits and assemble complete solutions
let layer3_n = EQUIHASH_N;
collision::search(
&layer2,
&mut mem.heap.temp,
layer3_n - layer2_n,
|_sum, loc| {
let mut items = ArrayVec::<SolutionItem, { Solution::NUM_ITEMS }>::new();
// Walk the binary tree of collision locations, in order.
// The leaf layer will have our SolutionItems.
loc.pair(&layer2).map(|loc| {
Layer1Collision::new(loc).unpack().pair(&layer1).map(|loc| {
Layer0Collision::new(loc)
.unpack()
.pair(&layer0)
.map(|item| items.push(item));
});
});
// Apply the ordering constraints and check for duplicates
let solution = Solution::sort_from_array(
items
.into_inner()
.expect("always collected a full SolutionItem tree"),
);
if results.last() != Some(&solution) {
let _ = results.try_push(solution);
}
},
);
}
/// Temporary memory used by the Equi-X solver
///
/// This space is needed temporarily during a solver run. It will be
/// allocated on the heap by [`SolverMemory::new()`], and the solver
/// provides a [`crate::EquiX::solve_with_memory()`] interface for reusing
/// this memory between runs.
pub struct SolverMemory {
/// Inner heap allocation which holds the actual solver memory
heap: Box<SolverMemoryInner>,
}
impl SolverMemory {
/// Size of the solver memory region, in bytes
pub const SIZE: usize = std::mem::size_of::<SolverMemoryInner>();
/// New uninitialized memory, usable as solver temporary space.
pub fn new() -> Self {
Self {
heap: SolverMemoryInner::alloc(),
}
}
}
impl Default for SolverMemory {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod test {
#[test]
fn solver_memory_size() {
// Regression test for memory usage. Our actual memory size is very
// similar to the original C implementation, the only difference that
// our bucket counters are stored outside this structure.
let size = super::SolverMemory::SIZE;
assert_eq!(size, 1_895_424);
}
}