Sfoglia il codice sorgente

Refactor API for easier dev UX

parazyd 1 anno fa
parent
commit
f23ab0c4dc
3 ha cambiato i file con 854 aggiunte e 193 eliminazioni
  1. 8 3
      Cargo.toml
  2. 202 13
      examples/multithreaded.rs
  3. 644 177
      src/lib.rs

+ 8 - 3
Cargo.toml

@@ -11,10 +11,15 @@ license = "BSD-3-Clause"
 edition = "2021"
 
 [target.'cfg(not(target = "x86_64-unknown-linux-musl"))'.build-dependencies]
-bindgen = "0.71.1"
+bindgen = "0.71"
  
 [target.'cfg(target = "x86_64-unknown-linux-musl")'.build-dependencies]
-bindgen = {version = "0.71.1", default-features = false, features = ["static"]}
+bindgen = {version = "0.71", default-features = false, features = ["static"]}
 
 [dependencies]
-bitflags = "2.8.0"
+bitflags = "2.8"
+libc = "0.2"
+
+[dev-dependencies]
+anyhow = "1.0"
+hex = "0.4"

+ 202 - 13
examples/multithreaded.rs

@@ -1,37 +1,226 @@
 //! randomx example that calculates many hashes using multiple threads
 
-use randomx::*;
-use std::sync::Arc;
+use std::collections::HashMap;
+use std::sync::{Arc, RwLock};
 use std::thread;
 use std::time::Instant;
-use std::vec::Vec;
+
+use anyhow::Result;
+use randomx::*;
+
+#[derive(Clone)]
+pub struct RandomXVMInstance {
+    instance: Arc<RwLock<RandomXVM>>,
+}
+
+unsafe impl Send for RandomXVMInstance {}
+unsafe impl Sync for RandomXVMInstance {}
+
+impl RandomXVMInstance {
+    fn create(
+        key: &[u8],
+        flags: RandomXFlags,
+        cache: Option<RandomXCache>,
+        dataset: Option<RandomXDataset>,
+    ) -> Result<Self> {
+        // Note: Memory requirement per VM in light mode is 256MB
+        // Note: RandomXFlags::FULLMEM and RandomXFlags::LARGEPAGES are incompatible
+        // with light mode. These are not set by RandomX automatically even in fast mode.
+        let (flags, cache) = match cache {
+            Some(c) => (flags, c),
+            None => match RandomXCache::new(flags, key) {
+                Ok(cache) => (flags, cache),
+                Err(_) => {
+                    // Fallback to default flags
+                    let flags = RandomXFlags::DEFAULT;
+                    let cache = RandomXCache::new(flags, key)?;
+                    (flags, cache)
+                }
+            },
+        };
+
+        let vm = RandomXVM::new(flags, Some(cache), dataset)?;
+
+        Ok(Self {
+            instance: Arc::new(RwLock::new(vm)),
+        })
+    }
+
+    /// Calculate the RandomX mining hash
+    pub fn calculate_hash(&self, input: &[u8]) -> Result<Vec<u8>> {
+        let lock = self.instance.write().unwrap();
+        Ok(lock.calculate_hash(input)?)
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct RandomXFactory {
+    inner: Arc<RwLock<RandomXFactoryInner>>,
+}
+
+impl Default for RandomXFactory {
+    fn default() -> Self {
+        Self::new(2)
+    }
+}
+
+impl RandomXFactory {
+    /// Create a new RandomX factory with the specified maximum number of VMs
+    pub fn new(max_vms: usize) -> Self {
+        Self {
+            inner: Arc::new(RwLock::new(RandomXFactoryInner::new(max_vms))),
+        }
+    }
+
+    pub fn new_with_flags(max_vms: usize, flags: RandomXFlags) -> Self {
+        Self {
+            inner: Arc::new(RwLock::new(RandomXFactoryInner::new_with_flags(
+                max_vms, flags,
+            ))),
+        }
+    }
+
+    /// Create a new RandomX VM instance with the specified key
+    pub fn create(
+        &self,
+        key: &[u8],
+        cache: Option<RandomXCache>,
+        dataset: Option<RandomXDataset>,
+    ) -> Result<RandomXVMInstance> {
+        let res;
+        {
+            let mut inner = self.inner.write().unwrap();
+            res = inner.create(key, cache, dataset)?;
+        }
+        Ok(res)
+    }
+
+    /// Get the number of VMs currently allocated
+    pub fn get_count(&self) -> Result<usize> {
+        let inner = self.inner.read().unwrap();
+        Ok(inner.get_count())
+    }
+
+    /// Get the flags used to create the VMs
+    pub fn get_flags(&self) -> Result<RandomXFlags> {
+        let inner = self.inner.read().unwrap();
+        Ok(inner.get_flags())
+    }
+}
+
+struct RandomXFactoryInner {
+    flags: RandomXFlags,
+    vms: HashMap<Vec<u8>, (Instant, RandomXVMInstance)>,
+    max_vms: usize,
+}
+
+impl std::fmt::Debug for RandomXFactoryInner {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("RandomXFactory")
+            .field("flags", &self.flags)
+            .field("max_vms", &self.max_vms)
+            .finish()
+    }
+}
+
+impl RandomXFactoryInner {
+    fn new(max_vms: usize) -> Self {
+        let flags = RandomXFlags::get_recommended_flags();
+        Self {
+            flags,
+            vms: Default::default(),
+            max_vms,
+        }
+    }
+
+    fn new_with_flags(max_vms: usize, flags: RandomXFlags) -> Self {
+        Self {
+            flags,
+            vms: Default::default(),
+            max_vms,
+        }
+    }
+
+    fn create(
+        &mut self,
+        key: &[u8],
+        cache: Option<RandomXCache>,
+        dataset: Option<RandomXDataset>,
+    ) -> Result<RandomXVMInstance> {
+        if let Some(entry) = self.vms.get_mut(key) {
+            let vm = entry.1.clone();
+            entry.0 = Instant::now();
+            return Ok(vm);
+        }
+
+        if self.vms.len() >= self.max_vms {
+            if let Some(oldest_key) = self
+                .vms
+                .iter()
+                .min_by_key(|(_, (i, _))| *i)
+                .map(|(k, _)| k.clone())
+            {
+                self.vms.remove(&oldest_key);
+            }
+        }
+
+        let vm = RandomXVMInstance::create(key, self.flags, cache, dataset)?;
+
+        self.vms
+            .insert(Vec::from(key), (Instant::now(), vm.clone()));
+
+        Ok(vm)
+    }
+
+    /// Get the number of VMs currently allocated
+    fn get_count(&self) -> usize {
+        self.vms.len()
+    }
+
+    /// Get the flags used to create the VMs
+    fn get_flags(&self) -> RandomXFlags {
+        self.flags
+    }
+}
 
 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();
+    const NUM_HASHES: u32 = 100;
 
     // Try adding `| RandomXFlags::LARGEPAGES`.
-    let flags = RandomXFlags::default() | RandomXFlags::FULLMEM;
-    let dataset = Arc::new(RandomXDataset::new(flags, b"key", NUM_THREADS as usize).unwrap());
+    let mut flags = RandomXFlags::get_recommended_flags() | RandomXFlags::FULLMEM;
+    if is_x86_feature_detected!("avx2") {
+        flags |= RandomXFlags::ARGON2_AVX2;
+    } else if is_x86_feature_detected!("ssse3") {
+        flags |= RandomXFlags::ARGON2_SSSE3;
+    }
 
-    println!("Dataset initialised in {}ms", start.elapsed().as_millis());
+    let factory = RandomXFactory::new_with_flags(1, flags);
 
-    let mut handles = Vec::new();
+    let key = b"key";
 
+    let start = Instant::now();
+    let cache = RandomXCache::new(flags, &key[..]).unwrap();
+    let dataset = RandomXDataset::new(flags, cache, 0).unwrap();
+    println!("Initialized RandomX dataset in {:?}", start.elapsed());
+
+    let mut handles = Vec::new();
     let start = Instant::now();
 
     for i in 0..NUM_THREADS {
+        let factory = factory.clone();
         let dataset = dataset.clone();
-
         handles.push(thread::spawn(move || {
+            let key = b"key";
+            let vm = factory.create(&key[..], None, Some(dataset)).unwrap();
+            println!("Created VM #{}", i);
+
             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());
+                let _ = vm.calculate_hash(&nonce.to_be_bytes()[..]);
+                //println!("VM #{} calculated hash with nonce {}", i, nonce);
 
                 // e.g. thread 0 will use nonces 0, 8, 16, ...
                 // and thread 1 will use nonces 1, 9, 17, ...

+ 644 - 177
src/lib.rs

@@ -2,42 +2,16 @@
 #![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.
+//! Rust bindings to randomx, a library for computing RandomX hashes.
 //!
-//! # Examples
+//! "RandomX is a proof-of-work (PoW) algorithm that is optimized for general-purpose CPUs. RandomX uses random code
+//! execution together with several memory-hard techniques to minimize the efficiency advantage of specialized
+//! hardware."
 //!
-//! ## Light mode hash
+//! Read more about how RandomX works in the [design document].
 //!
-//! 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>(())
-//! ```
+//! [RandomX github repo]: <https://github.com/tevador/RandomX>
+//! [design document]: <https://github.com/tevador/RandomX/blob/master/doc/design.md>
 //!
 //! # Errors
 //!
@@ -48,13 +22,13 @@
 
 include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
 
-use std::marker::PhantomData;
+use std::ptr;
 use std::sync::Arc;
-use std::thread;
 
 use bitflags::bitflags;
+use libc::{c_ulong, c_void, memcpy};
 
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Clone)]
 pub enum RandomXError {
     /// Occurs when allocating the RandomX cache fails.
     ///
@@ -64,27 +38,50 @@ pub enum RandomXError {
     /// * An invalid or unsupported ARGON2 value is set
     CacheAllocError,
 
+    /// Occurs when RandomX cache being reinitialized fails.
+    ///
+    /// Reasons include:
+    /// * VM is initialized with FULLMEM flag set.
+    CacheReinitError,
+
     /// Occurs when allocating a RandomX dataset fails.
     ///
     /// Reasons include:
     /// * Memory allocation fails
     DatasetAllocError,
 
+    /// Occurs when RandomX dataset being reinitialized fails.
+    ///
+    /// Reasons include:
+    /// * VM is initialized without FULLMEM flag set.
+    DatasetReinitError,
+
     /// Occurs when creating a VM fails.
     ///
     /// Reasons included:
     /// * Scratchpad memory allocation fails
     /// * Unsupported flags
     VmAllocError,
+
+    /// Various parameter errors; self-explanatory
+    ParameterError(String),
+
+    /// Other errors
+    Other(String),
 }
 
 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::CacheReinitError => write!(f, "Can't reinit RandomX cache w/ FULLMEM set"),
             Self::DatasetAllocError => write!(f, "Failed to allocate RandomX dataset"),
+            Self::DatasetReinitError => write!(f, "Can't reinit RandomX dataset w/o FULLMEM set"),
             Self::VmAllocError => write!(f, "Failed to allocate RandomX VM"),
+            Self::ParameterError(e) => write!(f, "{}", e),
+            Self::Other(e) => write!(f, "{}", e),
         }
     }
 }
@@ -92,7 +89,7 @@ impl std::fmt::Display for RandomXError {
 bitflags! {
     /// Represents options that can be used when allocating the
     /// RandomX dataset or VM.
-    #[derive(Copy, Clone)]
+    #[derive(Debug, Copy, Clone)]
     pub struct RandomXFlags: u32 {
         /// Use defaults.
         const DEFAULT = randomx_flags_RANDOMX_FLAG_DEFAULT;
@@ -124,232 +121,702 @@ bitflags! {
     }
 }
 
-impl Default for RandomXFlags {
-    /// Get the recommended flags to use on the current machine.
+impl RandomXFlags {
+    /// Returns the recommended flags to be used.
     ///
-    /// Does not include any of the following flags:
+    /// Does not include:
     /// * LARGEPAGES
-    /// * JIT
+    /// * FULLMEM
     /// * SECURE
-    fn default() -> Self {
-        // Explode if bits do not match up
+    ///
+    /// The above flags need to be set manually, if required.
+    pub fn get_recommended_flags() -> Self {
         unsafe { Self::from_bits(randomx_get_flags()).unwrap() }
     }
 }
 
-/// Dataset cache for light-mode hashing.
+impl Default for RandomXFlags {
+    /// Default value for RandomXFlags
+    fn default() -> RandomXFlags {
+        RandomXFlags::DEFAULT
+    }
+}
+
+#[derive(Debug)]
+struct RandomXCacheInner {
+    cache_ptr: *mut randomx_cache,
+}
+
+unsafe impl Send for RandomXCacheInner {}
+unsafe impl Sync for RandomXCacheInner {}
+
+impl Drop for RandomXCacheInner {
+    /// Deallocates memory for the `cache` object
+    fn drop(&mut self) {
+        unsafe {
+            randomx_release_cache(self.cache_ptr);
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+/// The Cache is used for light verification and Dataset construction.
 pub struct RandomXCache {
-    pub(crate) cache: *mut randomx_cache,
+    inner: Arc<RandomXCacheInner>,
 }
 
 impl RandomXCache {
-    pub fn new(flags: RandomXFlags, key: &[u8]) -> Result<Self, RandomXError> {
-        let cache = unsafe { randomx_alloc_cache(flags.bits()) };
+    /// Creates and allocates memory for a new cache object, and initializes
+    /// it with the key value.
+    ///
+    /// `flags` is any combination of the following two flags:
+    /// * LARGEPAGES
+    /// * JIT
+    ///
+    /// and (optionally) one of the following flags (depending on instruction set)
+    /// * ARGON2_SSSE3
+    /// * ARGON2_AVX2
+    ///
+    /// `key` is a sequence of u8 used to initialize SuperScalarHash.
+    pub fn new(flags: RandomXFlags, key: &[u8]) -> Result<RandomXCache, RandomXError> {
+        if key.is_empty() {
+            return Err(RandomXError::ParameterError(
+                "RandomX cache key is empty".to_string(),
+            ));
+        }
 
-        if cache.is_null() {
+        let cache_ptr = unsafe { randomx_alloc_cache(flags.bits()) };
+        if cache_ptr.is_null() {
             return Err(RandomXError::CacheAllocError);
         }
 
+        let inner = RandomXCacheInner { cache_ptr };
+        let result = RandomXCache {
+            inner: Arc::new(inner),
+        };
+        let key_ptr = key.as_ptr() as *mut c_void;
+        let key_size = key.len();
+
         unsafe {
-            randomx_init_cache(cache, key.as_ptr() as *const std::ffi::c_void, key.len());
+            randomx_init_cache(result.inner.cache_ptr, key_ptr, key_size);
         }
 
-        Ok(RandomXCache { cache })
+        Ok(result)
     }
 }
 
-impl Drop for RandomXCache {
+#[derive(Debug)]
+struct RandomXDatasetInner {
+    dataset_ptr: *mut randomx_dataset,
+    dataset_count: u32,
+    #[allow(dead_code)]
+    cache: RandomXCache,
+}
+
+unsafe impl Send for RandomXDatasetInner {}
+unsafe impl Sync for RandomXDatasetInner {}
+
+impl Drop for RandomXDatasetInner {
+    /// Deallocates memory for the `dataset` object.
     fn drop(&mut self) {
-        unsafe { randomx_release_cache(self.cache) }
+        unsafe {
+            randomx_release_dataset(self.dataset_ptr);
+        }
     }
 }
 
-unsafe impl Send for RandomXCache {}
-unsafe impl Sync for RandomXCache {}
-
+#[derive(Debug, Clone)]
+/// The Dataset is a read-only memory structure that is used during
+/// VM program execution.
 pub struct RandomXDataset {
-    pub(crate) dataset: *mut randomx_dataset,
+    inner: Arc<RandomXDatasetInner>,
 }
 
 impl RandomXDataset {
-    pub fn new(flags: RandomXFlags, key: &[u8], n_threads: usize) -> Result<Self, RandomXError> {
-        assert!(n_threads > 0);
+    /// Creates a new dataset object, allocates memory to the `dataset` object
+    /// and initializes it.
+    ///
+    /// `flags` is one of the following:
+    /// * DEFAULT
+    /// * LARGEPAGES
+    ///
+    /// `cache` is a cache object.
+    ///
+    /// `start` is the item number where initialization should start,
+    /// recommended to pass in 0.
+    ///
+    /// Conversions may be lossy on Windows or Linux.
+    #[allow(clippy::useless_conversion)]
+    pub fn new(
+        flags: RandomXFlags,
+        cache: RandomXCache,
+        start: u32,
+    ) -> Result<RandomXDataset, RandomXError> {
+        let item_count = RandomXDataset::count()?;
 
-        let cache = RandomXCache::new(flags, key)?;
-        let dataset = unsafe { randomx_alloc_dataset(flags.bits()) };
+        let test = unsafe { randomx_alloc_dataset(flags.bits()) };
+        if test.is_null() {
+            return Err(RandomXError::DatasetAllocError);
+        }
+
+        let inner = RandomXDatasetInner {
+            dataset_ptr: test,
+            dataset_count: item_count,
+            cache,
+        };
 
-        if dataset.is_null() {
+        let result = RandomXDataset {
+            inner: Arc::new(inner),
+        };
+
+        if start >= item_count {
             return Err(RandomXError::DatasetAllocError);
         }
 
-        let mut dataset = RandomXDataset { dataset };
+        unsafe {
+            randomx_init_dataset(
+                result.inner.dataset_ptr,
+                result.inner.cache.inner.cache_ptr,
+                c_ulong::from(start),
+                c_ulong::from(item_count),
+            );
+        }
 
-        let count = unsafe { randomx_dataset_item_count() };
+        Ok(result)
+    }
 
-        if n_threads == 1 {
-            unsafe {
-                randomx_init_dataset(dataset.dataset, cache.cache, 0, count);
+    /// Returns the number of items in the `dataset` or an error on failure.
+    pub fn count() -> Result<u32, RandomXError> {
+        match unsafe { randomx_dataset_item_count() } {
+            0 => Err(RandomXError::ParameterError(
+                "Dataset item count is zero".to_string(),
+            )),
+            x => {
+                // This weirdness brought to you by c_ulong being different on Windows and Linux
+                #[cfg(target_os = "windows")]
+                return Ok(x);
+                #[cfg(not(target_os = "windows"))]
+                return u32::try_from(x).map_err(|e| RandomXError::Other(e.to_string()));
             }
-        } 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);
-                }));
+    /// Returns the values of the internal memory buffer of the `dataset` or an error on failure.
+    pub fn get_data(&self) -> Result<Vec<u8>, RandomXError> {
+        let memory = unsafe { randomx_get_dataset_memory(self.inner.dataset_ptr) };
+        if memory.is_null() {
+            return Err(RandomXError::DatasetAllocError);
+        }
 
-                start += this_size;
-            }
+        let count = usize::try_from(self.inner.dataset_count)
+            .map_err(|e| RandomXError::Other(e.to_string()))?;
 
-            for handle in handles {
-                let _ = handle.join();
-            }
+        let mut result: Vec<u8> = vec![0u8; count];
 
-            dataset = match Arc::try_unwrap(dataset_arc) {
-                Ok(dataset) => dataset,
-                Err(_) => return Err(RandomXError::DatasetAllocError),
-            };
+        let n = usize::try_from(self.inner.dataset_count)
+            .map_err(|e| RandomXError::Other(e.to_string()))?;
+
+        unsafe {
+            memcpy(result.as_mut_ptr() as *mut c_void, memory, n);
         }
 
-        Ok(dataset)
+        Ok(result)
     }
 }
 
-impl Drop for RandomXDataset {
-    fn drop(&mut self) {
-        unsafe { randomx_release_dataset(self.dataset) }
-    }
+#[derive(Debug)]
+/// The RandomX Virtual Machine (VM) is a complex instruction set computer
+/// that executes generated programs.
+pub struct RandomXVM {
+    flags: RandomXFlags,
+    vm: *mut randomx_vm,
+    linked_cache: Option<RandomXCache>,
+    linked_dataset: Option<RandomXDataset>,
 }
 
-unsafe impl Send for RandomXDataset {}
-unsafe impl Sync for RandomXDataset {}
+unsafe impl Send for RandomXVM {}
+unsafe impl Sync for RandomXVM {}
 
-pub struct RandomXVM<'a, T: 'a> {
-    vm: *mut randomx_vm,
-    phantom: PhantomData<&'a T>,
+impl Drop for RandomXVM {
+    /// De-allocates memory for the `VM` object.
+    fn drop(&mut self) {
+        unsafe {
+            randomx_destroy_vm(self.vm);
+        }
+    }
 }
 
-impl RandomXVM<'_, RandomXCache> {
-    pub fn new(flags: RandomXFlags, cache: &'_ RandomXCache) -> Result<Self, RandomXError> {
-        if flags.contains(RandomXFlags::FULLMEM) {
-            return Err(RandomXError::VmAllocError);
+impl RandomXVM {
+    /// Creates a new `VM` and initializes it, error on failure.
+    ///
+    /// `flags` is any combination of the following 5 flags:
+    /// * LARGEPAGES
+    /// * HARDAES
+    /// * FULLMEM
+    /// * JIT
+    /// * SECURE
+    ///
+    /// Or
+    ///
+    /// * DEFAULT
+    ///
+    /// `cache` is a cache object, optional if FULLMEM is set.
+    ///
+    /// `dataset` is a dataset object, optional if FULLMEM is not set.
+    pub fn new(
+        flags: RandomXFlags,
+        cache: Option<RandomXCache>,
+        dataset: Option<RandomXDataset>,
+    ) -> Result<RandomXVM, RandomXError> {
+        let is_full_mem = flags.contains(RandomXFlags::FULLMEM);
+
+        match (cache, dataset) {
+            (None, None) => Err(RandomXError::VmAllocError),
+            (None, _) if !is_full_mem => Err(RandomXError::VmAllocError),
+            (_, None) if is_full_mem => Err(RandomXError::VmAllocError),
+            (cache, dataset) => {
+                let cache_ptr = cache
+                    .as_ref()
+                    .map(|stash| stash.inner.cache_ptr)
+                    .unwrap_or_else(ptr::null_mut);
+                let dataset_ptr = dataset
+                    .as_ref()
+                    .map(|data| data.inner.dataset_ptr)
+                    .unwrap_or_else(ptr::null_mut);
+
+                let vm = unsafe { randomx_create_vm(flags.bits(), cache_ptr, dataset_ptr) };
+
+                Ok(RandomXVM {
+                    vm,
+                    flags,
+                    linked_cache: cache,
+                    linked_dataset: dataset,
+                })
+            }
         }
+    }
 
-        let vm = unsafe { randomx_create_vm(flags.bits(), cache.cache, std::ptr::null_mut()) };
+    /// Reinitializes the `VM` with a new cache that was initialized without
+    /// `RandomXFlags::FULLMEM`.
+    pub fn reinit_cache(&mut self, cache: RandomXCache) -> Result<(), RandomXError> {
+        if self.flags.contains(RandomXFlags::FULLMEM) {
+            return Err(RandomXError::CacheReinitError);
+        }
 
-        if vm.is_null() {
-            return Err(RandomXError::VmAllocError);
+        unsafe {
+            randomx_vm_set_cache(self.vm, cache.inner.cache_ptr);
         }
 
-        Ok(Self {
-            vm,
-            phantom: PhantomData,
-        })
+        self.linked_cache = Some(cache);
+
+        Ok(())
     }
-}
 
-impl RandomXVM<'_, RandomXDataset> {
-    pub fn new_fast(
-        flags: RandomXFlags,
-        dataset: &'_ RandomXDataset,
-    ) -> Result<Self, RandomXError> {
-        if !flags.contains(RandomXFlags::FULLMEM) {
-            return Err(RandomXError::VmAllocError);
+    /// Reinitializes the `VM` with a new dataset that was initialized with
+    /// `RandomXFlags::FULLMEM`.
+    pub fn reinit_dataset(&mut self, dataset: RandomXDataset) -> Result<(), RandomXError> {
+        if !self.flags.contains(RandomXFlags::FULLMEM) {
+            return Err(RandomXError::DatasetReinitError);
         }
 
-        let vm = unsafe { randomx_create_vm(flags.bits(), std::ptr::null_mut(), dataset.dataset) };
-
-        if vm.is_null() {
-            return Err(RandomXError::VmAllocError);
+        unsafe {
+            randomx_vm_set_dataset(self.vm, dataset.inner.dataset_ptr);
         }
 
-        Ok(Self {
-            vm,
-            phantom: PhantomData,
-        })
+        self.linked_dataset = Some(dataset);
+
+        Ok(())
     }
-}
 
-impl<T> RandomXVM<'_, T> {
-    /// Calculate the RandomX hash of some data.
+    /// Calculates a RandomX hash value and returns it, error on failure.
     ///
-    /// ```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();
+    /// `input` is a sequence of u8 to be hashed.
+    pub fn calculate_hash(&self, input: &[u8]) -> Result<Vec<u8>, RandomXError> {
+        if input.is_empty() {
+            return Err(RandomXError::ParameterError(
+                "RandomX VM input empty".to_string(),
+            ));
+        }
+
+        let input_size = input.len();
+        let input_ptr = input.as_ptr() as *mut c_void;
+        let arr = [0; RANDOMX_HASH_SIZE as usize];
+        let output_ptr = arr.as_ptr() as *mut c_void;
 
         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,
-            );
+            randomx_calculate_hash(self.vm, input_ptr, input_size, output_ptr);
+        }
 
-            hash.assume_init()
+        // If this failed, arr should still be empty
+        if arr == [0; RANDOMX_HASH_SIZE as usize] {
+            return Err(RandomXError::Other(
+                "RandomX calculated hash was empty".to_string(),
+            ));
         }
+
+        Ok(arr.to_vec())
     }
-}
 
-impl<T> Drop for RandomXVM<'_, T> {
-    fn drop(&mut self) {
-        unsafe { randomx_destroy_vm(self.vm) }
+    /// Calculates hashes from a set of inputs.
+    ///
+    /// `input` is an array of a sequence of u8 to be hashed.
+    pub fn calculate_hash_set(&self, input: &[&[u8]]) -> Result<Vec<Vec<u8>>, RandomXError> {
+        if input.is_empty() {
+            // Empty set
+            return Err(RandomXError::ParameterError(
+                "RandomX VM input set empty".to_string(),
+            ));
+        }
+
+        let mut result = vec![];
+
+        // For single input
+        if input.len() == 1 {
+            let hash = self.calculate_hash(input[0])?;
+            result.push(hash);
+            return Ok(result);
+        }
+
+        // For multiple inputs
+        let mut output_ptr: *mut c_void = ptr::null_mut();
+        let arr = [0; RANDOMX_HASH_SIZE as usize];
+
+        // Not len() as last iteration assigns final hash
+        let iterations = input.len() + 1;
+
+        #[allow(clippy::needless_range_loop)]
+        for i in 0..iterations {
+            if i == iterations - 1 {
+                // For last iteration
+                unsafe {
+                    randomx_calculate_hash_last(self.vm, output_ptr);
+                }
+            } else {
+                if input[i].is_empty() {
+                    // Stop calculations
+                    if arr != [0; RANDOMX_HASH_SIZE as usize] {
+                        // Complete what was started
+                        unsafe {
+                            randomx_calculate_hash_last(self.vm, output_ptr);
+                        }
+                    }
+                    return Err(RandomXError::ParameterError(
+                        "RandomX VM input was empty".to_string(),
+                    ));
+                }
+
+                let input_size = input[i].len();
+                let input_ptr = input[i].as_ptr() as *mut c_void;
+                output_ptr = arr.as_ptr() as *mut c_void;
+
+                if i == 0 {
+                    // For first iteration
+                    unsafe {
+                        randomx_calculate_hash_first(self.vm, input_ptr, input_size);
+                    }
+                } else {
+                    // For every other iteration
+                    unsafe {
+                        randomx_calculate_hash_next(self.vm, input_ptr, input_size, output_ptr);
+                    }
+                }
+            }
+
+            if i != 0 {
+                // First hash is only available in 2nd iteration
+                if arr == [0; RANDOMX_HASH_SIZE as usize] {
+                    return Err(RandomXError::Other("RandomX hash was zero".to_string()));
+                }
+                let output: Vec<u8> = arr.to_vec();
+                result.push(output);
+            }
+        }
+
+        Ok(result)
     }
 }
 
-unsafe impl<T> Send for RandomXVM<'_, T> {}
-
 #[cfg(test)]
 mod tests {
     use super::*;
 
     #[test]
-    fn can_calc_hash() {
+    fn lib_alloc_cache() {
+        let flags = RandomXFlags::default();
+        let key = "Key";
+        let cache = RandomXCache::new(flags, key.as_bytes()).expect("Failed to allocate cache");
+        drop(cache);
+    }
+
+    #[test]
+    fn lib_alloc_dataset() {
+        let flags = RandomXFlags::default();
+        let key = "Key";
+        let cache = RandomXCache::new(flags, key.as_bytes()).unwrap();
+        let dataset =
+            RandomXDataset::new(flags, cache.clone(), 0).expect("Failed to allocate dataset");
+        drop(dataset);
+        drop(cache);
+    }
+
+    #[test]
+    fn lib_alloc_vm() {
+        let flags = RandomXFlags::default();
+        let key = "Key";
+        let cache = RandomXCache::new(flags, key.as_bytes()).unwrap();
+        let mut vm =
+            RandomXVM::new(flags, Some(cache.clone()), None).expect("Failed to allocate VM");
+        drop(vm);
+        let dataset = RandomXDataset::new(flags, cache.clone(), 0).unwrap();
+        vm = RandomXVM::new(flags, Some(cache.clone()), Some(dataset.clone()))
+            .expect("Failed to allocate VM");
+        drop(dataset);
+        drop(cache);
+        drop(vm);
+    }
+
+    #[test]
+    fn lib_dataset_memory() {
         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,
+        let key = "Key";
+        let cache = RandomXCache::new(flags, key.as_bytes()).unwrap();
+        let dataset = RandomXDataset::new(flags, cache.clone(), 0).unwrap();
+        let memory = dataset.get_data().unwrap_or_else(|_| Vec::new());
+        assert!(!memory.is_empty(), "Failed to get dataset memory");
+        let v = vec![0u8; memory.len()];
+        assert_ne!(memory, v);
+        drop(dataset);
+        drop(cache);
+    }
+
+    #[test]
+    fn lib_calculate_hash() {
+        let flags = RandomXFlags::get_recommended_flags();
+        let flags2 = flags | RandomXFlags::FULLMEM;
+        let key = "Key";
+        let input = "Input";
+
+        let cache1 = RandomXCache::new(flags, key.as_bytes()).unwrap();
+        let mut vm1 = RandomXVM::new(flags, Some(cache1.clone()), None).unwrap();
+        let hash1 = vm1.calculate_hash(input.as_bytes()).expect("no data");
+        let v = vec![0u8; hash1.len()];
+        assert_ne!(hash1, v);
+        assert!(vm1.reinit_cache(cache1.clone()).is_ok());
+        let hash2 = vm1.calculate_hash(input.as_bytes()).expect("no data");
+        assert_ne!(hash2, v);
+        assert_eq!(hash1, hash2);
+
+        let cache2 = RandomXCache::new(flags, key.as_bytes()).unwrap();
+        let vm2 = RandomXVM::new(flags, Some(cache2.clone()), None).unwrap();
+        let hash3 = vm2.calculate_hash(input.as_bytes()).expect("no data");
+        assert_eq!(hash2, hash3);
+
+        let cache3 = RandomXCache::new(flags, key.as_bytes()).unwrap();
+        let dataset3 = RandomXDataset::new(flags, cache3.clone(), 0).unwrap();
+        let mut vm3 = RandomXVM::new(flags2, None, Some(dataset3.clone())).unwrap();
+        let hash4 = vm3.calculate_hash(input.as_bytes()).expect("no data");
+        assert_ne!(hash3, v);
+        assert!(vm3.reinit_dataset(dataset3.clone()).is_ok());
+        let hash5 = vm3.calculate_hash(input.as_bytes()).expect("no data");
+        assert_ne!(hash4, v);
+        assert_eq!(hash4, hash5);
+
+        let cache4 = RandomXCache::new(flags, key.as_bytes()).unwrap();
+        let dataset4 = RandomXDataset::new(flags, cache4.clone(), 0).unwrap();
+        let vm4 = RandomXVM::new(flags2, Some(cache4), Some(dataset4.clone())).unwrap();
+        let hash6 = vm3.calculate_hash(input.as_bytes()).expect("no data");
+        assert_eq!(hash5, hash6);
+
+        drop(dataset3);
+        drop(dataset4);
+        drop(cache1);
+        drop(cache2);
+        drop(cache3);
+        drop(vm1);
+        drop(vm2);
+        drop(vm3);
+        drop(vm4);
+    }
+
+    #[test]
+    fn lib_calculate_hash_set() {
+        let flags = RandomXFlags::default();
+        let key = "Key";
+        let inputs = vec![
+            "Input".as_bytes(),
+            "Input 2".as_bytes(),
+            "Inputs 3".as_bytes(),
         ];
+        let cache = RandomXCache::new(flags, key.as_bytes()).unwrap();
+        let vm = RandomXVM::new(flags, Some(cache.clone()), None).unwrap();
+        let hashes = vm.calculate_hash_set(inputs.as_slice()).expect("no data");
+        assert_eq!(inputs.len(), hashes.len());
+        let mut prev_hash = Vec::new();
+        for (i, hash) in hashes.into_iter().enumerate() {
+            let v = vec![0u8; hash.len()];
+            assert_ne!(hash, v);
+            assert_ne!(hash, prev_hash);
+            let compare = vm.calculate_hash(inputs[i]).unwrap(); // sanity check
+            assert_eq!(hash, compare);
+            prev_hash = hash;
+        }
+        drop(cache);
+        drop(vm);
+    }
+
+    #[test]
+    fn lib_calculate_hash_is_consistent() {
+        let flags = RandomXFlags::get_recommended_flags();
+        let key = "Key";
+        let input = "Input";
+        let cache = RandomXCache::new(flags, key.as_bytes()).unwrap();
+        let dataset = RandomXDataset::new(flags, cache.clone(), 0).unwrap();
+        let vm = RandomXVM::new(flags, Some(cache.clone()), Some(dataset.clone())).unwrap();
+        let hash = vm.calculate_hash(input.as_bytes()).expect("no data");
+        assert_eq!(
+            hash,
+            [
+                114, 81, 192, 5, 165, 242, 107, 100, 184, 77, 37, 129, 52, 203, 217, 227, 65, 83,
+                215, 213, 59, 71, 32, 172, 253, 155, 204, 111, 183, 213, 157, 155
+            ]
+        );
+        drop(vm);
+        drop(dataset);
+        drop(cache);
+
+        let cache1 = RandomXCache::new(flags, key.as_bytes()).unwrap();
+        let dataset1 = RandomXDataset::new(flags, cache1.clone(), 0).unwrap();
+        let vm1 = RandomXVM::new(flags, Some(cache1.clone()), Some(dataset1.clone())).unwrap();
+        let hash1 = vm1.calculate_hash(input.as_bytes()).expect("no data");
+        assert_eq!(
+            hash1,
+            [
+                114, 81, 192, 5, 165, 242, 107, 100, 184, 77, 37, 129, 52, 203, 217, 227, 65, 83,
+                215, 213, 59, 71, 32, 172, 253, 155, 204, 111, 183, 213, 157, 155
+            ]
+        );
+        drop(vm1);
+        drop(dataset1);
+        drop(cache1);
+    }
 
-        assert_eq!(expected, hash);
+    #[test]
+    fn lib_check_cache_and_dataset_lifetimes() {
+        let flags = RandomXFlags::get_recommended_flags();
+        let key = "Key";
+        let input = "Input";
+        let cache = RandomXCache::new(flags, key.as_bytes()).unwrap();
+        let dataset = RandomXDataset::new(flags, cache.clone(), 0).unwrap();
+        let vm = RandomXVM::new(flags, Some(cache.clone()), Some(dataset.clone())).unwrap();
+        drop(dataset);
+        drop(cache);
+        let hash = vm.calculate_hash(input.as_bytes()).expect("no data");
+        assert_eq!(
+            hash,
+            [
+                114, 81, 192, 5, 165, 242, 107, 100, 184, 77, 37, 129, 52, 203, 217, 227, 65, 83,
+                215, 213, 59, 71, 32, 172, 253, 155, 204, 111, 183, 213, 157, 155
+            ]
+        );
+        drop(vm);
+
+        let cache1 = RandomXCache::new(flags, key.as_bytes()).unwrap();
+        let dataset1 = RandomXDataset::new(flags, cache1.clone(), 0).unwrap();
+        let vm1 = RandomXVM::new(flags, Some(cache1.clone()), Some(dataset1.clone())).unwrap();
+        drop(dataset1);
+        drop(cache1);
+        let hash1 = vm1.calculate_hash(input.as_bytes()).expect("no data");
+        assert_eq!(
+            hash1,
+            [
+                114, 81, 192, 5, 165, 242, 107, 100, 184, 77, 37, 129, 52, 203, 217, 227, 65, 83,
+                215, 213, 59, 71, 32, 172, 253, 155, 204, 111, 183, 213, 157, 155
+            ]
+        );
+        drop(vm1);
     }
 
     #[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,
+    fn randomx_hash_fast_vs_light() {
+        let input = b"input";
+        let key = b"key";
+
+        let flags = RandomXFlags::get_recommended_flags() | RandomXFlags::FULLMEM;
+        let cache = RandomXCache::new(flags, key).unwrap();
+        let dataset = RandomXDataset::new(flags, cache, 0).unwrap();
+        let fast_vm = RandomXVM::new(flags, None, Some(dataset)).unwrap();
+
+        let flags = RandomXFlags::get_recommended_flags();
+        let cache = RandomXCache::new(flags, key).unwrap();
+        let light_vm = RandomXVM::new(flags, Some(cache), None).unwrap();
+
+        let fast = fast_vm.calculate_hash(input).unwrap();
+        let light = light_vm.calculate_hash(input).unwrap();
+        assert_eq!(fast, light);
+    }
+
+    #[test]
+    fn test_vectors_fast_mode() {
+        // https://github.com/tevador/RandomX/blob/040f4500a6e79d54d84a668013a94507045e786f/src/tests/tests.cpp#L963-L979
+        let key = b"test key 000";
+        let vectors = [
+            (
+                b"This is a test".as_slice(),
+                "639183aae1bf4c9a35884cb46b09cad9175f04efd7684e7262a0ac1c2f0b4e3f",
+            ),
+            (
+                b"Lorem ipsum dolor sit amet".as_slice(),
+                "300a0adb47603dedb42228ccb2b211104f4da45af709cd7547cd049e9489c969",
+            ),
+            (
+                b"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua".as_slice(),
+                "c36d4ed4191e617309867ed66a443be4075014e2b061bcdaf9ce7b721d2b77a8",
+            ),
         ];
 
-        assert_eq!(expected, hash);
+        let flags = RandomXFlags::get_recommended_flags() | RandomXFlags::FULLMEM;
+        let cache = RandomXCache::new(flags, key).unwrap();
+        let dataset = RandomXDataset::new(flags, cache, 0).unwrap();
+        let vm = RandomXVM::new(flags, None, Some(dataset)).unwrap();
+
+        for (input, expected) in vectors {
+            let hash = vm.calculate_hash(input).unwrap();
+            assert_eq!(hex::decode(expected).unwrap(), hash);
+        }
+    }
+
+    #[test]
+    fn test_vectors_light_mode() {
+        // https://github.com/tevador/RandomX/blob/040f4500a6e79d54d84a668013a94507045e786f/src/tests/tests.cpp#L963-L985
+        let vectors = [
+            (
+                b"test key 000",
+                b"This is a test".as_slice(),
+                "639183aae1bf4c9a35884cb46b09cad9175f04efd7684e7262a0ac1c2f0b4e3f",
+            ),
+            (
+                b"test key 000",
+                b"Lorem ipsum dolor sit amet".as_slice(),
+                "300a0adb47603dedb42228ccb2b211104f4da45af709cd7547cd049e9489c969",
+            ),
+            (
+                b"test key 000",
+                b"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua".as_slice(),
+                "c36d4ed4191e617309867ed66a443be4075014e2b061bcdaf9ce7b721d2b77a8",
+            ),
+            (
+                b"test key 001",
+                b"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua".as_slice(),
+                "e9ff4503201c0c2cca26d285c93ae883f9b1d30c9eb240b820756f2d5a7905fc",
+            ),
+        ];
+
+        let flags = RandomXFlags::get_recommended_flags();
+        for (key, input, expected) in vectors {
+            let cache = RandomXCache::new(flags, key).unwrap();
+            let vm = RandomXVM::new(flags, Some(cache), None).unwrap();
+            let hash = vm.calculate_hash(input).unwrap();
+            assert_eq!(hex::decode(expected).unwrap(), hash);
+        }
     }
 }