From 3ea128a727e1d33b61ff10f9e8b9e03a1fbc770c Mon Sep 17 00:00:00 2001 From: parazyd Date: Thu, 10 Aug 2023 20:00:56 +0200 Subject: [PATCH 1/4] Add Rust bindings using bindgen. Signed-off-by: parazyd --- .gitignore | 2 + Cargo.toml | 17 +++ build.rs | 79 ++++++++++++ src/lib.rs | 354 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 452 insertions(+) create mode 100644 Cargo.toml create mode 100644 build.rs create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore index ec94c2c6..6c2d4eb8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ x64/ Release/ Debug/ build/ +Cargo.lock +target/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..a20191d8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "randomx" +version = "1.1.11" +homepage = "https://github.com/tevador/RandomX/" +authors = [ + "Dyne.org Foundation ", + "tevador ", + "The Monero Project", +] +license = "BSD-3-Clause" +edition = "2021" + +[build-dependencies] +bindgen = "0.66.1" + +[dependencies] +bitflags = "1" diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..b5930cb3 --- /dev/null +++ b/build.rs @@ -0,0 +1,79 @@ +use std::env; +use std::io::Write; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let target = env::var("TARGET").unwrap(); + let n_threads = std::thread::available_parallelism() + .unwrap() + .get() + .to_string(); + + let cargo_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_dir = &cargo_dir.join("build"); + std::fs::create_dir_all(build_dir).unwrap(); + env::set_current_dir(build_dir).unwrap(); + + // Generate CMake cache files + let b = Command::new("cmake") + .arg("-DARCH=native") + .arg("..") + .output() + .expect("Failed to generate Makefile with CMake"); + std::io::stdout().write_all(&b.stdout).unwrap(); + std::io::stderr().write_all(&b.stderr).unwrap(); + assert!(b.status.success()); + + // Build the library + let b = Command::new("cmake") + .arg("--build") + .arg(".") + .arg("--config") + .arg("Release") + .arg("-j") + .arg(n_threads) + .output() + .expect("Failed to build RandomX library with CMake"); + std::io::stdout().write_all(&b.stdout).unwrap(); + std::io::stderr().write_all(&b.stderr).unwrap(); + assert!(b.status.success()); + + env::set_current_dir(cargo_dir).unwrap(); + + // Tell cargo how to find the static library + println!( + "cargo:rustc-link-search=native={}", + build_dir.to_string_lossy() + ); + println!("cargo:rustc-link-lib=static=randomx"); + + if target.contains("apple") { + println!("cargo:rustc-link-lib=dylib=c++"); + } else if target.contains("linux") { + println!("cargo:rustc-link-lib=dylib=stdc++"); + } else { + unimplemented!() + } + + // The bindgen::Builder is the main entry point + // to bindgen, and lets you build up options for + // the resulting bindings. + let bindings = bindgen::Builder::default() + // The input header we would like to generate + // bindings for. + .header("src/randomx.h") + // Tell cargo to invalidate the built crate whenever any of the + // included header files changed. + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + // Finish the builder and generate the bindings. + .generate() + // Unwrap the Result and panic on failure. + .expect("Unable to generate bindings"); + + // Write the bindings to the $OUT_DIR/bindings.rs file. + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..14750eb3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,354 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +//! (Most of the code is taken from https://github.com/moneromint/randomx4r) +//! Rust bindings to librandomx, a library for computing RandomX hashes. +//! +//! # Examples +//! +//! ## Light mode hash +//! +//! Requires 256M of shared memory. +//! +//! ```no_run +//! use randomx::{RandomXCache, RandomXError, RandomXFlags, RandomXVM}; +//! +//! // Get flags supported by this system. +//! let flags = RandomXFlags::default(); +//! let cache = RandomXCache::new(flags, b"key")?; +//! let vm = RandomXVM::new(flags, &cache)?; +//! let hash = vm.hash(b"input"); // is a [u8; 32] +//! # Ok::<(), RandomXError>(()) +//! ``` +//! +//! ## Fast mode hash +//! +//! Requires 2080M of shared memory. +//! +//! ```no_run +//! use randomx::{RandomXDataset, RandomXError, RandomXFlags, RandomXVM}; +//! +//! // OR the default flags with FULLMEM (aka. fast mode) +//! let flags = RandomXFlags::default() | RandomXFlags::FULLMEM; +//! // Speed up dataset initialisation +//! let threads = std::thread::available_parallelism().unwrap().get(); +//! let dataset = RandomXDataset::new(flags, b"key", threads)?; +//! let vm = RandomXVM::new_fast(flags, &dataset)?; +//! let hash = vm.hash(b"input"); +//! # Ok::<(), RandomXError>(()) +//! ``` +//! +//! # Errors +//! +//! Some operations (e.g. allocating a VM or dataset) can fail if the +//! system doesn't have enough free memory, or if you tried to force a +//! feature like large pages or AVX2 on a system that does not support +//! it. + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +use std::marker::PhantomData; +use std::sync::Arc; +use std::thread; + +use bitflags::bitflags; + +#[derive(Debug, Copy, Clone)] +pub enum RandomXError { + /// Occurs when allocating the RandomX cache fails. + /// + /// Reasons include: + /// * Memory allocation fails + /// * The JIT flag is set but the current platform does not support it + /// * An invalid or unsupported ARGON2 value is set + CacheAllocError, + + /// Occurs when allocating a RandomX dataset fails. + /// + /// Reasons include: + /// * Memory allocation fails + DatasetAllocError, + + /// Occurs when creating a VM fails. + /// + /// Reasons included: + /// * Scratchpad memory allocation fails + /// * Unsupported flags + VmAllocError, +} + +impl std::error::Error for RandomXError {} +impl std::fmt::Display for RandomXError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::CacheAllocError => write!(f, "Failed to allocate RandomX cache"), + Self::DatasetAllocError => write!(f, "Failed to allocate RandomX dataset"), + Self::VmAllocError => write!(f, "Failed to allocate RandomX VM"), + } + } +} + +bitflags! { + /// Represents options that can be used when allocating the + /// RandomX dataset or VM. + pub struct RandomXFlags: u32 { + /// Use defaults. + const DEFAULT = randomx_flags_RANDOMX_FLAG_DEFAULT; + + /// Allocate memory in large pages. + const LARGEPAGES = randomx_flags_RANDOMX_FLAG_LARGE_PAGES; + + /// The RandomX VM will use hardware accelerated AES. + const HARDAES = randomx_flags_RANDOMX_FLAG_HARD_AES; + + /// The RandomX VM will use the full dataset. + const FULLMEM = randomx_flags_RANDOMX_FLAG_FULL_MEM; + + /// The RandomX VM will use a JIT compiler. + const JIT = randomx_flags_RANDOMX_FLAG_JIT; + + /// Make sure that JIT pages are never writable and executable + /// at the same time. + const SECURE = randomx_flags_RANDOMX_FLAG_SECURE; + + /// Use the SSSE3 extension to speed up Argon2 operations. + const ARGON2_SSSE3 = randomx_flags_RANDOMX_FLAG_ARGON2_SSSE3; + + /// Use the AVX2 extension to speed up Argon2 operations. + const ARGON2_AVX2 = randomx_flags_RANDOMX_FLAG_ARGON2_AVX2; + + /// Do not use SSSE3 or AVX2 extensions. + const ARGON2 = randomx_flags_RANDOMX_FLAG_ARGON2; + } +} + +impl Default for RandomXFlags { + /// Get the recommended flags to use on the current machine. + /// + /// Does not include any of the following flags: + /// * LARGEPAGES + /// * JIT + /// * SECURE + fn default() -> Self { + // Explode if bits do not match up + unsafe { Self::from_bits(randomx_get_flags()).unwrap() } + } +} + +/// Dataset cache for light-mode hashing. +pub struct RandomXCache { + pub(crate) cache: *mut randomx_cache, +} + +impl RandomXCache { + pub fn new(flags: RandomXFlags, key: &[u8]) -> Result { + let cache = unsafe { randomx_alloc_cache(flags.bits()) }; + + if cache.is_null() { + return Err(RandomXError::CacheAllocError); + } + + unsafe { + randomx_init_cache(cache, key.as_ptr() as *const std::ffi::c_void, key.len()); + } + + Ok(RandomXCache { cache }) + } +} + +impl Drop for RandomXCache { + fn drop(&mut self) { + unsafe { randomx_release_cache(self.cache) } + } +} + +unsafe impl Send for RandomXCache {} +unsafe impl Sync for RandomXCache {} + +pub struct RandomXDataset { + pub(crate) dataset: *mut randomx_dataset, +} + +impl RandomXDataset { + pub fn new(flags: RandomXFlags, key: &[u8], n_threads: usize) -> Result { + assert!(n_threads > 0); + + let cache = RandomXCache::new(flags, key)?; + let dataset = unsafe { randomx_alloc_dataset(flags.bits()) }; + + if dataset.is_null() { + return Err(RandomXError::DatasetAllocError); + } + + let mut dataset = RandomXDataset { dataset }; + + let count = unsafe { randomx_dataset_item_count() }; + + if n_threads == 1 { + unsafe { + randomx_init_dataset(dataset.dataset, cache.cache, 0, count); + } + } else { + let mut handles = Vec::new(); + let cache_arc = Arc::new(cache); + let dataset_arc = Arc::new(dataset); + + let size = count / n_threads as u64; + let last = count % n_threads as u64; + let mut start = 0; + + for i in 0..n_threads { + let cache = cache_arc.clone(); + let dataset = dataset_arc.clone(); + let mut this_size = size; + if i == n_threads - 1 { + this_size += last; + } + let this_start = start; + + handles.push(thread::spawn(move || unsafe { + randomx_init_dataset(dataset.dataset, cache.cache, this_start, this_size); + })); + + start += this_size; + } + + for handle in handles { + let _ = handle.join(); + } + + dataset = match Arc::try_unwrap(dataset_arc) { + Ok(dataset) => dataset, + Err(_) => return Err(RandomXError::DatasetAllocError), + }; + } + + Ok(dataset) + } +} + +impl Drop for RandomXDataset { + fn drop(&mut self) { + unsafe { randomx_release_dataset(self.dataset) } + } +} + +unsafe impl Send for RandomXDataset {} +unsafe impl Sync for RandomXDataset {} + +pub struct RandomXVM<'a, T: 'a> { + vm: *mut randomx_vm, + phantom: PhantomData<&'a T>, +} + +impl RandomXVM<'_, RandomXCache> { + pub fn new(flags: RandomXFlags, cache: &'_ RandomXCache) -> Result { + if flags.contains(RandomXFlags::FULLMEM) { + return Err(RandomXError::VmAllocError); + } + + let vm = unsafe { randomx_create_vm(flags.bits(), cache.cache, std::ptr::null_mut()) }; + + if vm.is_null() { + return Err(RandomXError::VmAllocError); + } + + Ok(Self { + vm, + phantom: PhantomData, + }) + } +} + +impl RandomXVM<'_, RandomXDataset> { + pub fn new_fast( + flags: RandomXFlags, + dataset: &'_ RandomXDataset, + ) -> Result { + if !flags.contains(RandomXFlags::FULLMEM) { + return Err(RandomXError::VmAllocError); + } + + let vm = unsafe { randomx_create_vm(flags.bits(), std::ptr::null_mut(), dataset.dataset) }; + + if vm.is_null() { + return Err(RandomXError::VmAllocError); + } + + Ok(Self { + vm, + phantom: PhantomData, + }) + } +} + +impl RandomXVM<'_, T> { + /// Calculate the RandomX hash of some data. + /// + /// ```no_run + /// # // ^ no_run, this is already tested in the actual tests + /// use randomx::*; + /// let flags = RandomXFlags::default(); + /// let cache = RandomXCache::new(flags, "key".as_bytes())?; + /// let vm = RandomXVM::new(flags, &cache)?; + /// let hash = vm.hash("input".as_bytes()); + /// # Ok::<(), RandomXError>(()) + /// ``` + pub fn hash(&self, input: &[u8]) -> [u8; RANDOMX_HASH_SIZE as usize] { + let mut hash = std::mem::MaybeUninit::<[u8; RANDOMX_HASH_SIZE as usize]>::uninit(); + + unsafe { + randomx_calculate_hash( + self.vm, + input.as_ptr() as *const std::ffi::c_void, + input.len(), + hash.as_mut_ptr() as *mut std::ffi::c_void, + ); + + hash.assume_init() + } + } +} + +impl Drop for RandomXVM<'_, T> { + fn drop(&mut self) { + unsafe { randomx_destroy_vm(self.vm) } + } +} + +unsafe impl Send for RandomXVM<'_, T> {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_calc_hash() { + let flags = RandomXFlags::default(); + let cache = RandomXCache::new(flags, "RandomX example key\0".as_bytes()).unwrap(); + let vm = RandomXVM::new(flags, &cache).unwrap(); + let hash = vm.hash("RandomX example input\0".as_bytes()); + let expected = [ + 138, 72, 229, 249, 219, 69, 171, 121, 217, 8, 5, 116, 196, 216, 25, 84, 254, 106, 198, + 56, 66, 33, 74, 255, 115, 194, 68, 178, 99, 48, 183, 201, + ]; + + assert_eq!(expected, hash); + } + + #[test] + fn can_calc_hash_fast() { + let flags = RandomXFlags::default() | RandomXFlags::FULLMEM; + let n = thread::available_parallelism().unwrap().get(); + let dataset = RandomXDataset::new(flags, "RandomX example key\0".as_bytes(), n).unwrap(); + let vm = RandomXVM::new_fast(flags, &dataset).unwrap(); + let hash = vm.hash("RandomX example input\0".as_bytes()); + let expected = [ + 138, 72, 229, 249, 219, 69, 171, 121, 217, 8, 5, 116, 196, 216, 25, 84, 254, 106, 198, + 56, 66, 33, 74, 255, 115, 194, 68, 178, 99, 48, 183, 201, + ]; + + assert_eq!(expected, hash); + } +} From 65c0b322edf5ab66fad53bd0d163b7a2e9c63125 Mon Sep 17 00:00:00 2001 From: parazyd Date: Thu, 10 Aug 2023 21:10:24 +0200 Subject: [PATCH 2/4] Add Rust usage example. --- examples/multithreaded.rs | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 examples/multithreaded.rs diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs new file mode 100644 index 00000000..eeb15943 --- /dev/null +++ b/examples/multithreaded.rs @@ -0,0 +1,52 @@ +//! randomx example that calculates many hashes using multiple threads + +use randomx::*; +use std::sync::Arc; +use std::thread; +use std::time::Instant; +use std::vec::Vec; + +fn main() { + const NUM_THREADS: u32 = 8; + // number of hashes to perform in each thread, not the total. + const NUM_HASHES: u32 = 5000; + + let start = Instant::now(); + + // Try adding `| RandomXFlags::LARGEPAGES`. + let flags = RandomXFlags::default() | RandomXFlags::FULLMEM; + let dataset = Arc::new(RandomXDataset::new(flags, b"key", NUM_THREADS as usize).unwrap()); + + println!("Dataset initialised in {}ms", start.elapsed().as_millis()); + + let mut handles = Vec::new(); + + let start = Instant::now(); + + for i in 0..NUM_THREADS { + let dataset = dataset.clone(); + + handles.push(thread::spawn(move || { + let mut nonce: u32 = i; + let vm = RandomXVM::new_fast(flags, &dataset).unwrap(); + + for _ in 0..NUM_HASHES { + let _ = vm.hash(&nonce.to_be_bytes()); + + // e.g. thread 0 will use nonces 0, 8, 16, ... + // and thread 1 will use nonces 1, 9, 17, ... + nonce += NUM_THREADS; + } + })); + } + + for handle in handles { + let _ = handle.join(); + } + + println!( + "Completed {} hashes in {}ms", + NUM_THREADS * NUM_HASHES, + start.elapsed().as_millis() + ); +} From 42fe8c60742c946d09c214c5609f461e8ae04aad Mon Sep 17 00:00:00 2001 From: parazyd Date: Fri, 28 Jun 2024 15:35:22 +0200 Subject: [PATCH 3/4] Update Cargo.toml with static musl target --- Cargo.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a20191d8..ffdd66d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,11 @@ authors = [ license = "BSD-3-Clause" edition = "2021" -[build-dependencies] -bindgen = "0.66.1" +[target.'cfg(not(target = "x86_64-unknown-linux-musl"))'.build-dependencies] +bindgen = "0.69.4" + +[target.'cfg(target = "x86_64-unknown-linux-musl")'.build-dependencies] +bindgen = {version = "0.69.4", default-features = false, features = ["static"]} [dependencies] bitflags = "1" From 257ba10fe2f2f4fdfe815b39da1e8321ba518e2f Mon Sep 17 00:00:00 2001 From: Mark Evenson Date: Thu, 13 Feb 2025 17:11:09 +0100 Subject: [PATCH 4/4] build: add working c++ linkage under freebsd --- build.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.rs b/build.rs index b5930cb3..0c588659 100644 --- a/build.rs +++ b/build.rs @@ -52,6 +52,8 @@ fn main() { println!("cargo:rustc-link-lib=dylib=c++"); } else if target.contains("linux") { println!("cargo:rustc-link-lib=dylib=stdc++"); + } else if target.contains("freebsd") { + println!("cargo:rustc-link-lib=dylib=c++"); } else { unimplemented!() }