Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

equihash: Add Rust API for Tromp solver #1083

Merged
merged 21 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
45652a2
equihash: Import Tromp solver
str4d Jan 4, 2024
7ab6c47
equihash: Import `blake2b_simd` C bindings from `zcashd`
str4d Jan 4, 2024
3aaeb8b
equihash: Modify Tromp solver to compile as C
str4d Jan 4, 2024
45e7238
equihash: Pass `blake2b_simd` bindings to Tromp solver as callbacks
str4d Jan 4, 2024
d07505d
equihash: Add Rust API for Tromp solver
str4d Jan 4, 2024
1b20c15
equihash: Add Rust APIs for compressed solutions
teor2345 Jan 4, 2024
fe3b269
equihash: Verify compressed solutions in tests
teor2345 Jan 4, 2024
463e7d9
equihash: Move allocation out of the loop
teor2345 Jan 5, 2024
3c78bf6
equihash: Set C pointers to NULL after freeing them to avoid double-f…
teor2345 Jan 7, 2024
5f77bd7
equihash: Place solver behind a feature flag
teor2345 Jan 7, 2024
d7ccd07
equihash: Ensure returned compressed solutions are unique
teor2345 Jan 10, 2024
989f40e
equihash: Add a portable endian.h for `htole32()` on macOS and Windows
teor2345 Jan 11, 2024
b737d0f
equihash: Remove unused thread support to enable Windows compilation
teor2345 Jan 11, 2024
2bd7bc8
equihash: Don't import a header that's missing in Windows CI
teor2345 Jan 11, 2024
9391e65
equihash: Clear slots when setting the hash state
teor2345 Jan 12, 2024
76131db
Add commented-out prints of solution candidates for debugging
teor2345 Jan 5, 2024
634285d
Note in Cargo.toml that this crate is experimental
daira Oct 31, 2024
aed3ecb
Merge branch 'main' into equihash-solver-tromp
str4d Feb 13, 2025
6c7cce2
equihash: Only expose a solver for compressed solutions
str4d Feb 13, 2025
e8e3e87
equihash: Update changelog
str4d Feb 13, 2025
f3d5a5c
equihash: Remove BLAKE2b bindings from public API
str4d Feb 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions components/equihash/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,21 @@ license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.56.1"

[features]
default = []

## Builds the C++ tromp solver and Rust FFI layer.
solver = ["dep:cc"]

[dependencies]
blake2b_simd = "1"
byteorder = "1"

[build-dependencies]
cc = { version = "1", optional = true }

[dev-dependencies]
hex = "0.4"

[lib]
bench = false
17 changes: 17 additions & 0 deletions components/equihash/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//! Build script for the equihash tromp solver in C.

fn main() {
#[cfg(feature = "solver")]
build_tromp_solver();
}

#[cfg(feature = "solver")]
fn build_tromp_solver() {
cc::Build::new()
.include("tromp/")
.file("tromp/equi_miner.c")
.compile("equitromp");

// Tell Cargo to only rerun this build script if the tromp C files or headers change.
println!("cargo:rerun-if-changed=tromp");
}
59 changes: 59 additions & 0 deletions components/equihash/src/blake2b.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) 2020-2022 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .

// This module uses unsafe code for FFI into blake2b.
#![allow(unsafe_code)]

use blake2b_simd::{State, PERSONALBYTES};

use std::ptr;
use std::slice;

#[no_mangle]
pub extern "C" fn blake2b_init(
output_len: usize,
personalization: *const [u8; PERSONALBYTES],
) -> *mut State {
let personalization = unsafe { personalization.as_ref().unwrap() };

Box::into_raw(Box::new(
blake2b_simd::Params::new()
.hash_length(output_len)
.personal(personalization)
.to_state(),
))
}

#[no_mangle]
pub extern "C" fn blake2b_clone(state: *const State) -> *mut State {
unsafe { state.as_ref() }
.map(|state| Box::into_raw(Box::new(state.clone())))
.unwrap_or(ptr::null_mut())
}

#[no_mangle]
pub extern "C" fn blake2b_free(state: *mut State) {
if !state.is_null() {
drop(unsafe { Box::from_raw(state) });
}
}

#[no_mangle]
pub extern "C" fn blake2b_update(state: *mut State, input: *const u8, input_len: usize) {
let state = unsafe { state.as_mut().unwrap() };
let input = unsafe { slice::from_raw_parts(input, input_len) };

state.update(input);
}

#[no_mangle]
pub extern "C" fn blake2b_finalize(state: *mut State, output: *mut u8, output_len: usize) {
let state = unsafe { state.as_mut().unwrap() };
let output = unsafe { slice::from_raw_parts_mut(output, output_len) };

// Allow consuming only part of the output.
let hash = state.finalize();
assert!(output_len <= hash.as_bytes().len());
output.copy_from_slice(&hash.as_bytes()[..output_len]);
}
5 changes: 5 additions & 0 deletions components/equihash/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ mod verify;
mod test_vectors;

pub use verify::{is_valid_solution, Error};

#[cfg(feature = "solver")]
mod blake2b;
#[cfg(feature = "solver")]
pub mod tromp;
82 changes: 76 additions & 6 deletions components/equihash/src/minimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,49 @@ use byteorder::{BigEndian, ReadBytesExt};

use crate::params::Params;

// Rough translation of CompressArray() from:
// https://github.com/zcash/zcash/blob/6fdd9f1b81d3b228326c9826fa10696fc516444b/src/crypto/equihash.cpp#L39-L76
#[cfg(any(feature = "solver", test))]
fn compress_array(array: &[u8], bit_len: usize, byte_pad: usize) -> Vec<u8> {
let index_bytes = (u32::BITS / 8) as usize;
assert!(bit_len >= 8);
assert!(8 * index_bytes >= 7 + bit_len);

let in_width: usize = (bit_len + 7) / 8 + byte_pad;
let out_len = bit_len * array.len() / (8 * in_width);

let mut out = Vec::with_capacity(out_len);
let bit_len_mask: u32 = (1 << (bit_len as u32)) - 1;

// The acc_bits least-significant bits of acc_value represent a bit sequence
// in big-endian order.
let mut acc_bits: usize = 0;
let mut acc_value: u32 = 0;

let mut j: usize = 0;
for _i in 0..out_len {
// When we have fewer than 8 bits left in the accumulator, read the next
// input element.
if acc_bits < 8 {
acc_value <<= bit_len;
for x in byte_pad..in_width {
acc_value |= (
// Apply bit_len_mask across byte boundaries
(array[j + x] & ((bit_len_mask >> (8 * (in_width - x - 1))) as u8)) as u32
)
.wrapping_shl(8 * (in_width - x - 1) as u32); // Big-endian
Comment on lines +36 to +37
Copy link
Contributor

@arya2 arya2 Jul 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like an unnecessary change from the c++ logic, it should panic in minimal_from_indices() before it gets here when using parameters that would be affected by the change.

Suggested change
)
.wrapping_shl(8 * (in_width - x - 1) as u32); // Big-endian
) << (8 * (in_width - x - 1) as u32); // Big-endian

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible this is a problem if it's different than what the original code was doing?

}
j += in_width;
acc_bits += bit_len;
}

acc_bits -= 8;
out.push((acc_value >> acc_bits) as u8);
}

out
}

pub(crate) fn expand_array(vin: &[u8], bit_len: usize, byte_pad: usize) -> Vec<u8> {
assert!(bit_len >= 8);
assert!(u32::BITS as usize >= 7 + bit_len);
Expand Down Expand Up @@ -50,6 +93,31 @@ pub(crate) fn expand_array(vin: &[u8], bit_len: usize, byte_pad: usize) -> Vec<u
vout
}

// Rough translation of GetMinimalFromIndices() from:
// https://github.com/zcash/zcash/blob/6fdd9f1b81d3b228326c9826fa10696fc516444b/src/crypto/equihash.cpp#L130-L145
#[cfg(any(feature = "solver", test))]
pub(crate) fn minimal_from_indices(p: Params, indices: &[u32]) -> Vec<u8> {
let c_bit_len = p.collision_bit_length();
let index_bytes = (u32::BITS / 8) as usize;
let digit_bytes = ((c_bit_len + 1) + 7) / 8;
assert!(digit_bytes <= index_bytes);

let len_indices = indices.len() * index_bytes;
let byte_pad = index_bytes - digit_bytes;

// Rough translation of EhIndexToArray(index, array_pointer) from:
// https://github.com/zcash/zcash/blob/6fdd9f1b81d3b228326c9826fa10696fc516444b/src/crypto/equihash.cpp#L123-L128
//
// Big-endian so that lexicographic array comparison is equivalent to integer comparison.
let array: Vec<u8> = indices
.iter()
.flat_map(|index| index.to_be_bytes())
.collect();
assert_eq!(array.len(), len_indices);

compress_array(&array, c_bit_len + 1, byte_pad)
}

/// Returns `None` if the parameters are invalid for this minimal encoding.
pub(crate) fn indices_from_minimal(p: Params, minimal: &[u8]) -> Option<Vec<u32>> {
let c_bit_len = p.collision_bit_length();
Expand All @@ -76,11 +144,14 @@ pub(crate) fn indices_from_minimal(p: Params, minimal: &[u8]) -> Option<Vec<u32>

#[cfg(test)]
mod tests {
use super::{expand_array, indices_from_minimal, Params};
use crate::minimal::minimal_from_indices;

use super::{compress_array, expand_array, indices_from_minimal, Params};

#[test]
fn array_expansion() {
fn array_compression_and_expansion() {
let check_array = |(bit_len, byte_pad), compact, expanded| {
assert_eq!(compress_array(expanded, bit_len, byte_pad), compact);
assert_eq!(expand_array(compact, bit_len, byte_pad), expanded);
};

Expand Down Expand Up @@ -149,10 +220,9 @@ mod tests {
#[test]
fn minimal_solution_repr() {
let check_repr = |minimal, indices| {
assert_eq!(
indices_from_minimal(Params { n: 80, k: 3 }, minimal).unwrap(),
indices,
);
let p = Params { n: 80, k: 3 };
assert_eq!(minimal_from_indices(p, indices), minimal);
assert_eq!(indices_from_minimal(p, minimal).unwrap(), indices);
};

// The solutions here are not intended to be valid.
Expand Down
Loading
Loading