diff --git a/README.md b/README.md index e914ea5..68039a8 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ To include `xdagj-native-randomx` in your project, add the following dependency io.xdag xdagj-native-randomx - 0.2.0 + 0.2.2 ``` @@ -138,27 +138,27 @@ public class Example { byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); // Get supported RandomX flags for the current CPU - Set flags = RandomXUtils.getFlagsSet(); + Set flags = RandomXUtils.getRecommendedFlags(); System.out.println("Supported flags: " + flags); - int combinedFlags = RandomXFlag.toValue(flags); - System.out.println("Combined flags value: " + combinedFlags); - // Initialize RandomX cache with the supported flags + // Initialize RandomX cache (key will be set via template) RandomXCache cache = new RandomXCache(flags); - cache.init(keyBytes); // Create and configure RandomXTemplate using builder pattern byte[] hash; try (RandomXTemplate template = RandomXTemplate.builder() .cache(cache) - .miningMode(false) // Set to false for normal hashing mode + .miningMode(false) .flags(flags) .build()) { - // Initialize the template with the configured settings + // Set the key for RandomX operations. This will initialize the cache. + template.changeKey(keyBytes); + + // Initialize the template's VM with the configured settings template.init(); - // Calculate hash of the input key + // Calculate hash of the input hash = template.calculateHash(keyBytes); } @@ -184,10 +184,10 @@ public class Example { ### Linux Performance Results | Benchmark | Mode | Cnt | Score | Error | Units | |:------------------------------:|:-----:|:---:|:-------:|:------:|:-----:| -| RandomXBenchmark.lightBatch | thrpt | | 328.736 | | ops/s | -| RandomXBenchmark.lightNoBatch | thrpt | | 325.383 | | ops/s | -| RandomXBenchmark.miningBatch | thrpt | | 2777.939 | | ops/s | -| RandomXBenchmark.miningNoBatch | thrpt | | 2817.811 | | ops/s | +| RandomXBenchmark.lightBatch | thrpt | | 416.114 | | ops/s | +| RandomXBenchmark.lightNoBatch | thrpt | | 424.865 | | ops/s | +| RandomXBenchmark.miningBatch | thrpt | | 1818.991 | | ops/s | +| RandomXBenchmark.miningNoBatch | thrpt | | 2191.774 | | ops/s | --- @@ -196,17 +196,15 @@ public class Example { - **CPU**: Apple M3 Pro - **RAM**: 36 GB - **thread**: 8 -- **RandomX Flags**: [DEFAULT, HARD_AES, SECURE] - -JIT flag will cause jvm to crash in MacOS +- **RandomX Flags**: [DEFAULT, JIT, SECURE] ### MacOS Performance Results -| Benchmark | Mode | Cnt | Score | Error | Units | -|:------------------------------:|:-----:|:---:|:-------:|:------:|:-----:| -| RandomXBenchmark.lightBatch | thrpt | | 32.864 | | ops/s | -| RandomXBenchmark.lightNoBatch | thrpt | | 33.683 | | ops/s | -| RandomXBenchmark.miningBatch | thrpt | | 554.966 | | ops/s | -| RandomXBenchmark.miningNoBatch | thrpt | | 570.060 | | ops/s | +| Benchmark | Mode | Cnt | Score | Error | Units | +|:------------------------------:|:-----:|:---:|:--------:|:------:|:-----:| +| RandomXBenchmark.lightBatch | thrpt | | 416.114 | | ops/s | +| RandomXBenchmark.lightNoBatch | thrpt | | 424.865 | | ops/s | +| RandomXBenchmark.miningBatch | thrpt | | 1818.991 | | ops/s | +| RandomXBenchmark.miningNoBatch | thrpt | | 2191.774 | | ops/s | --- diff --git a/src/main/java/io/xdag/crypto/randomx/Example.java b/src/main/java/io/xdag/crypto/randomx/Example.java index 32f756a..7dfda1b 100644 --- a/src/main/java/io/xdag/crypto/randomx/Example.java +++ b/src/main/java/io/xdag/crypto/randomx/Example.java @@ -40,34 +40,33 @@ public class Example { * @param args Command line arguments (not used) */ public static void main(String[] args) { - // Key to be hashed + // Key (or message) to be hashed String key = "hello xdagj-native-randomx"; byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); + System.out.println("Input message: " + key); - // Get supported RandomX flags for the current CPU - Set flags = RandomXUtils.getFlagsSet(); - System.out.println("Supported flags: " + flags); - int combinedFlags = RandomXFlag.toValue(flags); - System.out.println("Combined flags value: " + combinedFlags); + // Get recommended RandomX flags for the current CPU + Set flags = RandomXUtils.getRecommendedFlags(); - // Initialize RandomX cache with the supported flags + // Allocate RandomX cache with the recommended flags + // The key will be set and cache initialized via RandomXTemplate RandomXCache cache = new RandomXCache(flags); - cache.init(keyBytes); - // Create and configure RandomXTemplate using builder pattern + // Create and configure RandomXTemplate using a builder pattern byte[] hash; try (RandomXTemplate template = RandomXTemplate.builder() - .cache(cache) - .miningMode(false) // Set to false for normal hashing mode - .flags(flags) + .cache(cache) // Provide the cache instance (not yet initialized with key) + .miningMode(false) // Set to false for light hashing mode (no dataset) + .flags(flags) // Provide the base flags .build()) { + + // Set the key for RandomX operations. This will initialize the cache. + template.changeKey(keyBytes); - // Initialize the template with the configured settings + // Initialize the template. This creates the VM. template.init(); - - // Calculate hash of the input key hash = template.calculateHash(keyBytes); - } + } // try-with-resources automatically calls template.close() // Format and display the results HexFormat hex = HexFormat.of(); diff --git a/src/main/java/io/xdag/crypto/randomx/RandomXCache.java b/src/main/java/io/xdag/crypto/randomx/RandomXCache.java index 211249b..3575f43 100644 --- a/src/main/java/io/xdag/crypto/randomx/RandomXCache.java +++ b/src/main/java/io/xdag/crypto/randomx/RandomXCache.java @@ -26,119 +26,108 @@ import com.sun.jna.Memory; import com.sun.jna.Pointer; import lombok.Getter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; +import java.io.Closeable; import java.util.Set; -import java.util.concurrent.locks.ReentrantLock; /** - * A class that encapsulates the RandomX cache functionality. - * This class manages the allocation, initialization and release of RandomX cache memory. - * It implements AutoCloseable to ensure proper resource cleanup. + * Represents a RandomX Cache object. + * This class manages the allocation, initialization, and release of the native RandomX cache structure. */ -@Getter -public class RandomXCache implements AutoCloseable { - private static final Logger logger = LoggerFactory.getLogger(RandomXCache.class); - - /** - * Pointer to the allocated RandomX cache memory. - * -- GETTER -- - * Returns the pointer to the allocated cache memory. - */ - @Getter +@Slf4j +public class RandomXCache implements Closeable { private final Pointer cachePointer; - - /** - * Pointer to the key used for cache initialization - */ - private volatile Pointer keyPointer; - private int keyLength; - private final ReentrantLock lock = new ReentrantLock(); + @Getter + private final Set flags; /** - * Constructs a new RandomXCache with the specified flags. - * Allocates memory for the cache using the native RandomX library. + * Allocates a new RandomX cache. * - * @param flags Set of RandomXFlag values that configure the cache behavior - * @throws IllegalArgumentException if flags is null or empty - * @throws IllegalStateException if cache allocation fails + * @param flags Flags used to initialize the cache. + * @throws RuntimeException if cache allocation fails. */ public RandomXCache(Set flags) { - if (flags == null || flags.isEmpty()) { - throw new IllegalArgumentException("Flags cannot be null or empty"); - } - + this.flags = flags; int combinedFlags = RandomXFlag.toValue(flags); - logger.debug("Allocating RandomX cache with flags: {}", flags); - - this.cachePointer = RandomXJNALoader.getInstance().randomx_alloc_cache(combinedFlags); + log.debug("Allocating RandomX cache with flags: {} ({})", flags, combinedFlags); + // Use RandomXNative for allocation + this.cachePointer = RandomXNative.randomx_alloc_cache(combinedFlags); if (this.cachePointer == null) { - throw new IllegalStateException("Failed to allocate RandomX cache"); + String errorMsg = String.format("Failed to allocate RandomX cache with flags: %s (%d)", flags, combinedFlags); + log.error(errorMsg); + throw new RuntimeException(errorMsg); } - - logger.debug("RandomX cache allocated successfully"); + log.info("RandomX cache allocated successfully at pointer: {}", Pointer.nativeValue(this.cachePointer)); } /** - * Initializes the cache with the provided key. - * This method is thread-safe and can be called multiple times with different keys. + * Initializes the RandomX cache with the specified key. * - * @param key byte array containing the key data - * @throws IllegalArgumentException if key is null or empty + * @param key Key (seed) used to initialize the cache. + * @throws RuntimeException if cache initialization fails. + * @throws IllegalStateException if the cache is not allocated. */ public void init(byte[] key) { + if (cachePointer == null) { + throw new IllegalStateException("Cache is not allocated."); + } if (key == null || key.length == 0) { - throw new IllegalArgumentException("Key cannot be null or empty"); + throw new IllegalArgumentException("Key cannot be null or empty for cache initialization."); } - lock.lock(); + // Use JNA Memory to manage native memory + Memory keyPointer = new Memory(key.length); try { - long startTime = System.nanoTime(); - logger.debug("Initializing cache with key length: {}", key.length); - - // Free old key pointer if exists - if (keyPointer != null) { - keyPointer.clear(keyLength); - } - - // Allocate and initialize new key - keyLength = key.length; - keyPointer = new Memory(key.length); keyPointer.write(0, key, 0, key.length); - - RandomXJNALoader.getInstance().randomx_init_cache( - this.cachePointer, - keyPointer, - key.length + log.debug("Initializing RandomX cache with key of length: {}", key.length); + // Use RandomXNative for initialization + RandomXNative.randomx_init_cache( + this.cachePointer, + keyPointer, + key.length ); - - long endTime = System.nanoTime(); - logger.debug("Cache initialization completed in {} ms", (endTime - startTime) / 1_000_000); + log.info("RandomX cache initialized successfully."); + } catch (Exception e) { + log.error("Failed to initialize RandomX cache", e); + // Even if initialization fails, attempt to release memory + close(); // Release cachePointer + throw new RuntimeException("Failed to initialize RandomX cache", e); } finally { - lock.unlock(); + // Memory objects do not need to be manually released; JNA's GC will handle it, + // but nullifying the reference immediately might help GC reclaim it faster. + keyPointer = null; // Help GC } } /** - * Releases the allocated cache memory and key memory. - * This method is thread-safe and idempotent. + * Gets the pointer to the underlying native RandomX cache structure. + * + * @return Pointer to the native cache. + * @throws IllegalStateException if the cache is not allocated. + */ + public Pointer getCachePointer() { + if (cachePointer == null) { + throw new IllegalStateException("Cache is not allocated."); + } + return cachePointer; + } + + /** + * Releases the resources occupied by the native RandomX cache. + * This method should be called after finishing with the cache to prevent memory leaks. */ @Override public void close() { - lock.lock(); - try { - if (keyPointer != null) { - keyPointer.clear(keyLength); - keyPointer = null; + if (cachePointer != null) { + log.debug("Releasing RandomX cache at pointer: {}", Pointer.nativeValue(cachePointer)); + try { + // Use RandomXNative for release + RandomXNative.randomx_release_cache(cachePointer); + log.info("RandomX cache released successfully"); + } catch (Throwable t) { + log.error("Error occurred while releasing RandomX cache. Pointer: {}", Pointer.nativeValue(cachePointer), t); } - - if (cachePointer != null) { - RandomXJNALoader.getInstance().randomx_release_cache(cachePointer); - logger.debug("RandomX cache released successfully"); - } - } finally { - lock.unlock(); } } } \ No newline at end of file diff --git a/src/main/java/io/xdag/crypto/randomx/RandomXDataset.java b/src/main/java/io/xdag/crypto/randomx/RandomXDataset.java index 03179d4..97f375e 100644 --- a/src/main/java/io/xdag/crypto/randomx/RandomXDataset.java +++ b/src/main/java/io/xdag/crypto/randomx/RandomXDataset.java @@ -36,71 +36,83 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; /** - * A class that encapsulates the RandomX dataset functionality with multi-threaded initialization support. - * This class manages the allocation, initialization and release of RandomX dataset memory. - * It implements AutoCloseable to ensure proper resource cleanup. + * Encapsulates the RandomX dataset functionality with multi-threaded initialization support. + * Manages the allocation, initialization, and release of RandomX datasets, with support for multi-threaded initialization. + * This class implements AutoCloseable for resource management. + * Implement the AutoCloseable interface for resource management. */ +@Slf4j public class RandomXDataset implements AutoCloseable { - private static final Logger logger = LoggerFactory.getLogger(RandomXDataset.class); - /** - * Pointer to the allocated RandomX dataset memory + * Pointer to the allocated RandomX dataset memory. */ - private final Pointer dataset; + private final Pointer datasetPointer; + + @Getter + private final Set flags; // Store flags used for allocation /** - * Constructs a new RandomXDataset with the specified flags. - * Allocates memory for the dataset using the native RandomX library. + * Constructs a new RandomXDataset and allocates memory for it. * - * @param flags Set of RandomXFlag values that configure the dataset behavior - * @throws IllegalStateException if dataset allocation fails + * @param flags Set of RandomXFlag values used to configure the dataset behavior. + * @throws RuntimeException if dataset allocation fails. */ public RandomXDataset(Set flags) { if (flags == null || flags.isEmpty()) { - throw new IllegalArgumentException("Flags cannot be null or empty"); + throw new IllegalArgumentException("Flags cannot be null or empty for dataset allocation."); } - + this.flags = flags; int combinedFlags = RandomXFlag.toValue(flags); - this.dataset = RandomXJNALoader.getInstance().randomx_alloc_dataset(combinedFlags); - - if (dataset == null) { - throw new IllegalStateException("Failed to allocate RandomX dataset"); + log.debug("Allocating RandomX dataset with flags: {} ({})", flags, combinedFlags); + + // Use RandomXNative for allocation + this.datasetPointer = RandomXNative.randomx_alloc_dataset(combinedFlags); + + if (datasetPointer == null) { + String errorMsg = String.format("Failed to allocate RandomX dataset with flags: %s (%d)", flags, combinedFlags); + log.error(errorMsg); + throw new RuntimeException(errorMsg); // Use RuntimeException } - - logger.debug("RandomX dataset allocated successfully with flags: {}", flags); + + log.info("RandomX dataset allocated successfully at pointer: {} with flags: {}", Pointer.nativeValue(datasetPointer), flags); } /** * Initializes the dataset using multiple threads. - * The initialization work is divided equally among threads based on available CPU cores. - * Each thread initializes its assigned portion of the dataset items. + * The initialization work is divided among threads based on available CPU cores. * - * @param cache The RandomXCache instance used to initialize the dataset - * @throws RuntimeException if the initialization is interrupted + * @param cache The RandomXCache instance required for dataset initialization. + * @throws RuntimeException if initialization is interrupted or fails. + * @throws IllegalStateException if the dataset is not allocated. */ public void init(RandomXCache cache) { - if (cache == null) { - throw new IllegalArgumentException("Cache cannot be null"); + if (datasetPointer == null) { + throw new IllegalStateException("Dataset is not allocated."); + } + if (cache == null || cache.getCachePointer() == null) { + throw new IllegalArgumentException("Valid cache instance with allocated cache pointer is required for dataset initialization."); } - + long startTime = System.nanoTime(); - - // Get total items count - long totalItems = RandomXJNALoader.getInstance().randomx_dataset_item_count().longValue(); - - // Calculate optimal thread count + + // Get total items count using RandomXNative + long totalItems = RandomXNative.randomx_dataset_item_count().longValue(); + if (totalItems <= 0) { + log.warn("RandomX dataset item count is zero or negative ({}). Skipping initialization.", totalItems); + return; // No items to initialize + } + + // Calculate optimal thread count (using half of available processors by default) int availableProcessors = Runtime.getRuntime().availableProcessors(); - // Default to half of available processors, but can be configured int initThreadCount = Math.max(1, availableProcessors / 2); - - logger.info("Initializing dataset with {} threads for {} items", initThreadCount, totalItems); - - // Create thread pool + log.info("Initializing dataset ({} items) using {} threads.", totalItems, initThreadCount); + + // Create thread pool with custom thread factory for naming ExecutorService executor = Executors.newFixedThreadPool(initThreadCount, new ThreadFactory() { private final AtomicInteger threadNumber = new AtomicInteger(1); @Override @@ -113,66 +125,74 @@ public Thread newThread(Runnable r) { }); try { - // Calculate items per thread - ensure even distribution - long perThread = totalItems / initThreadCount; + // Calculate items per thread and handle remainder + long itemsPerThread = totalItems / initThreadCount; long remainder = totalItems % initThreadCount; List> futures = new ArrayList<>(initThreadCount); - - // Submit initialization tasks - long startItem = 0; + long currentItemStart = 0; + + // Submit initialization tasks for each thread for (int i = 0; i < initThreadCount; i++) { - // Add remainder items to the last thread - long itemCount = perThread + (i == initThreadCount - 1 ? remainder : 0); - final long start = startItem; + long itemCount = itemsPerThread + (i < remainder ? 1 : 0); // Distribute remainder evenly + if (itemCount == 0) continue; // Skip threads with no work + + final long start = currentItemStart; final long count = itemCount; - + final Pointer cachePtr = cache.getCachePointer(); // Pass cache pointer explicitly + futures.add(executor.submit(() -> { + String threadName = Thread.currentThread().getName(); try { - logger.debug("Thread {} initializing items [{}, {})", - Thread.currentThread().getName(), start, start + count); - - RandomXJNALoader.getInstance().randomx_init_dataset( - dataset, - cache.getCachePointer(), + log.debug("{} starting initialization for items [{}, {})", threadName, start, start + count); + // Use RandomXNative for dataset initialization + RandomXNative.randomx_init_dataset( + datasetPointer, + cachePtr, new NativeLong(start), new NativeLong(count) ); - - logger.debug("Thread {} completed initialization of {} items", - Thread.currentThread().getName(), count); + log.debug("{} finished initialization for items [{}, {})", threadName, start, start + count); } catch (Exception e) { - logger.error("Dataset initialization failed for range [{}, {})", start, start + count, e); - throw new RuntimeException("Dataset initialization failed", e); + log.error("{} failed during initialization for items [{}, {}). Error: {}", + threadName, start, start + count, e.getMessage(), e); + // Propagate exception to be caught by future.get() + throw new RuntimeException("Dataset initialization failed in thread " + threadName, e); } })); - - startItem += itemCount; + currentItemStart += itemCount; } - - // Wait for all threads to complete + + // Wait for all threads to complete and check for exceptions for (Future future : futures) { try { - future.get(); + future.get(); // Throws ExecutionException if the task threw an exception } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + Thread.currentThread().interrupt(); // Preserve interrupt status + log.error("Dataset initialization interrupted.", e); throw new RuntimeException("Dataset initialization interrupted", e); } catch (ExecutionException e) { + log.error("Dataset initialization failed.", e.getCause()); + // Unwrap the original exception thrown by the task throw new RuntimeException("Dataset initialization failed", e.getCause()); } } - - long duration = (System.nanoTime() - startTime) / 1_000_000; - logger.info("Dataset initialization completed in {} ms", duration); - + + long durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime); + log.info("Dataset initialization completed successfully in {} ms.", durationMillis); + } finally { + // Shutdown executor service gracefully executor.shutdown(); try { - if (!executor.awaitTermination(1, TimeUnit.MINUTES)) { + // Wait a reasonable time for tasks to finish + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + log.warn("Executor did not terminate in 60 seconds. Forcing shutdown."); executor.shutdownNow(); } } catch (InterruptedException e) { + log.error("Interrupted while waiting for executor termination.", e); executor.shutdownNow(); - Thread.currentThread().interrupt(); + Thread.currentThread().interrupt(); // Preserve interrupt status } } } @@ -180,21 +200,31 @@ public Thread newThread(Runnable r) { /** * Gets the pointer to the allocated dataset memory. * - * @return Pointer to the dataset memory + * @return Pointer to the dataset memory. + * @throws IllegalStateException if the dataset is not allocated. */ - public Pointer getPointer() { - return dataset; + public Pointer getDatasetPointer() { + if (datasetPointer == null) { + throw new IllegalStateException("Dataset is not allocated."); + } + return datasetPointer; } - /** + /** * Releases the allocated dataset memory. * This method is called automatically when using try-with-resources. */ @Override public void close() { - if (dataset != null) { - RandomXJNALoader.getInstance().randomx_release_dataset(dataset); - logger.debug("RandomX dataset released successfully"); + if (datasetPointer != null) { + log.debug("Releasing RandomX dataset at pointer: {}", Pointer.nativeValue(datasetPointer)); + try { + // Use RandomXNative for release + RandomXNative.randomx_release_dataset(datasetPointer); + log.info("RandomX dataset released successfully."); + } catch (Throwable t) { + log.error("Error occurred while releasing RandomX dataset. Pointer: {}", Pointer.nativeValue(datasetPointer), t); + } } } } \ No newline at end of file diff --git a/src/main/java/io/xdag/crypto/randomx/RandomXJNA.java b/src/main/java/io/xdag/crypto/randomx/RandomXJNA.java deleted file mode 100644 index 38bdf6b..0000000 --- a/src/main/java/io/xdag/crypto/randomx/RandomXJNA.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2022-2030 The XdagJ Developers - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package io.xdag.crypto.randomx; - -import com.sun.jna.Library; -import com.sun.jna.NativeLong; -import com.sun.jna.Pointer; - -/** - * JNA interface for the RandomX native library functions. - * This interface provides direct access to the native RandomX functions through JNA. - */ -public interface RandomXJNA extends Library { - - /** - * Gets the recommended flags for the current CPU. - * These flags are determined based on CPU features and capabilities. - * - * @return Integer value representing the recommended flags - */ - int randomx_get_flags(); - - /** - * Allocates memory for a RandomX cache. - * The allocated memory must be released using randomx_release_cache. - * - * @param flags Configuration flags for the cache - * @return Pointer to the allocated cache memory, or null if allocation fails - */ - Pointer randomx_alloc_cache(int flags); - - /** - * Initializes a RandomX cache with the provided key. - * This operation is required before the cache can be used. - * - * @param cache Pointer to the allocated cache memory - * @param key Pointer to the key data - * @param keySize Size of the key in bytes - */ - void randomx_init_cache(Pointer cache, Pointer key, long keySize); - - /** - * Releases the memory allocated for a RandomX cache. - * This function must be called to prevent memory leaks. - * - * @param cache Pointer to the cache memory to be released - */ - void randomx_release_cache(Pointer cache); - - /** - * Creates a new RandomX virtual machine instance. - * The instance must be destroyed using randomx_destroy_vm when no longer needed. - * - * @param flags Configuration flags for the VM - * @param cache Pointer to the initialized cache - * @param dataset Pointer to the initialized dataset, or null if not using a dataset - * @return Pointer to the created VM instance, or null if creation fails - */ - Pointer randomx_create_vm(int flags, Pointer cache, Pointer dataset); - - /** - * Destroys a RandomX virtual machine instance. - * This function must be called to prevent memory leaks. - * - * @param machine Pointer to the VM instance to be destroyed - */ - void randomx_destroy_vm(Pointer machine); - - /** - * Allocates memory for a RandomX dataset. - * The allocated memory must be released using randomx_release_dataset. - * - * @param flags Configuration flags for the dataset - * @return Pointer to the allocated dataset memory, or null if allocation fails - */ - Pointer randomx_alloc_dataset(int flags); - - /** - * Initializes items in a RandomX dataset. - * This operation can be performed in parallel by multiple threads. - * - * @param dataset Pointer to the dataset memory - * @param cache Pointer to the initialized cache - * @param startItem Index of the first item to initialize - * @param itemCount Number of items to initialize - */ - void randomx_init_dataset(Pointer dataset, Pointer cache, NativeLong startItem, NativeLong itemCount); - - /** - * Gets the number of items in a RandomX dataset. - * - * @return Number of items in the dataset - */ - NativeLong randomx_dataset_item_count(); - - /** - * Releases the memory allocated for a RandomX dataset. - * This function must be called to prevent memory leaks. - * - * @param dataset Pointer to the dataset memory to be released - */ - void randomx_release_dataset(Pointer dataset); - - /** - * Updates the cache used by a RandomX VM instance. - * - * @param machine Pointer to the VM instance - * @param cache Pointer to the new cache - */ - void randomx_vm_set_cache(Pointer machine, Pointer cache); - - /** - * Updates the dataset used by a RandomX VM instance. - * - * @param machine Pointer to the VM instance - * @param dataset Pointer to the new dataset - */ - void randomx_vm_set_dataset(Pointer machine, Pointer dataset); - - /** - * Calculates a RandomX hash value. - * - * @param machine Pointer to the VM instance - * @param input Pointer to the input data - * @param inputSize Size of the input data in bytes - * @param output Pointer to the output buffer (32 bytes) - */ - void randomx_calculate_hash(Pointer machine, Pointer input, long inputSize, Pointer output); - - /** - * Begins a multi-part RandomX hashing operation. - * - * @param machine Pointer to the VM instance - * @param input Pointer to the input data - * @param inputSize Size of the input data in bytes - */ - void randomx_calculate_hash_first(Pointer machine, Pointer input, long inputSize); - - /** - * Continues a multi-part RandomX hashing operation. - * - * @param machine Pointer to the VM instance - * @param nextInput Pointer to the next input data - * @param nextInputSize Size of the next input data in bytes - * @param output Pointer to the output buffer (32 bytes) - */ - void randomx_calculate_hash_next(Pointer machine, Pointer nextInput, long nextInputSize, Pointer output); - - /** - * Finalizes a multi-part RandomX hashing operation. - * - * @param machine Pointer to the VM instance - * @param output Pointer to the output buffer (32 bytes) - */ - void randomx_calculate_hash_last(Pointer machine, Pointer output); - - /** - * Calculates a RandomX commitment hash. - * - * @param input Pointer to the input data - * @param inputSize Size of the input data in bytes - * @param hash_in Pointer to the input hash - * @param com_out Pointer to the output commitment buffer - */ - void randomx_calculate_commitment(Pointer input, long inputSize, Pointer hash_in, Pointer com_out); -} \ No newline at end of file diff --git a/src/main/java/io/xdag/crypto/randomx/RandomXJNALoader.java b/src/main/java/io/xdag/crypto/randomx/RandomXJNALoader.java deleted file mode 100644 index f0d527f..0000000 --- a/src/main/java/io/xdag/crypto/randomx/RandomXJNALoader.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2022-2030 The XdagJ Developers - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package io.xdag.crypto.randomx; - -import com.sun.jna.Native; -import org.apache.commons.lang3.StringUtils; - -import java.io.File; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; - -/** - * A singleton class responsible for loading and managing the RandomX native library using JNA. - * This class handles the platform-specific library loading and provides access to the RandomX native functions. - */ -public final class RandomXJNALoader { - - /** - * The singleton instance of the RandomX JNA interface - */ - private static volatile RandomXJNA instance; - - /** - * Lock object for thread synchronization - */ - private static final Object LOCK = new Object(); - - /** - * Private constructor to prevent instantiation - */ - private RandomXJNALoader() { - // Prevent instantiation - } - - static { - init(); - } - - /** - * Initializes the RandomX native library - */ - public static void init() { - loadLibrary("librandomx"); - } - - /** - * Gets or creates the singleton instance of the RandomX JNA interface. - * Uses double-checked locking for thread safety and better performance. - * - * @return The singleton instance of RandomXJNA - */ - public static RandomXJNA getInstance() { - RandomXJNA result = instance; - if (result == null) { - synchronized (LOCK) { - result = instance; - if (result == null) { - String osName = System.getProperty("os.name").toLowerCase(); - if (osName.contains("win")) { - String libFilePath = "native/librandomx_windows_x86_64.dll"; - instance = result = Native.load(libFilePath, RandomXJNA.class); - } else { - instance = result = Native.load("randomx", RandomXJNA.class); - } - } - } - } - return result; - } - - /** - * Loads the native library for the current platform and architecture. - * Supports Windows, macOS, and Linux (x86_64) platforms. - * The library is extracted from resources to a temporary file before loading. - * - * @param libraryName The base name of the library to load - * @throws UnsupportedOperationException if the current platform is not supported - * @throws RuntimeException if the library loading fails - */ - public static void loadLibrary(String libraryName) { - String os = System.getProperty("os.name").toLowerCase(); - String arch = System.getProperty("os.arch").toLowerCase(); - String libFileName = getLibraryFileName(libraryName, os, arch); - - // Load from resources - try (InputStream libStream = getLibraryStream(libFileName)) { - File tempFile = createTempLibraryFile(libraryName, libFileName); - Files.copy(libStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - System.load(tempFile.getAbsolutePath()); - } catch (Exception e) { - throw new RuntimeException("Failed to load native library: " + libraryName, e); - } - } - - /** - * Gets the platform-specific library file name. - * - * @param libraryName Base name of the library - * @param os Operating system name - * @param arch System architecture - * @return The complete library file name - * @throws UnsupportedOperationException if the platform is not supported - */ - private static String getLibraryFileName(String libraryName, String os, String arch) { - if (os.contains("win")) { - if (StringUtils.containsAny(arch, "amd64", "x86_64")) { - return String.format("native/%s_windows_x86_64.dll", libraryName); - } - } else if (os.contains("mac")) { - return String.format("native/%s_macos_%s.dylib", libraryName, arch); - } else if (StringUtils.contains(os, "linux")) { - if (StringUtils.containsAny(arch, "amd64", "x86_64")) { - return String.format("native/%s_linux_x86_64.so", libraryName); - } - } - throw new UnsupportedOperationException( - String.format("Unsupported platform: OS=%s, Architecture=%s", os, arch)); - } - - /** - * Gets the input stream for the library resource. - * - * @param libFileName Library file name - * @return InputStream for the library resource - * @throws IllegalStateException if the library resource is not found - */ - private static InputStream getLibraryStream(String libFileName) { - InputStream libStream = RandomXJNALoader.class.getClassLoader().getResourceAsStream(libFileName); - if (libStream == null) { - throw new IllegalStateException("Native library not found: " + libFileName); - } - return libStream; - } - - /** - * Creates a temporary file for the native library. - * - * @param libraryName Base name of the library - * @param libFileName Complete library file name - * @return Temporary File object - * @throws RuntimeException if file creation fails - */ - private static File createTempLibraryFile(String libraryName, String libFileName) { - try { - File tempFile = File.createTempFile( - libraryName, - libFileName.substring(libFileName.lastIndexOf('.'))); - tempFile.deleteOnExit(); - return tempFile; - } catch (Exception e) { - throw new RuntimeException("Failed to create temporary library file", e); - } - } -} \ No newline at end of file diff --git a/src/main/java/io/xdag/crypto/randomx/RandomXLibraryLoader.java b/src/main/java/io/xdag/crypto/randomx/RandomXLibraryLoader.java new file mode 100644 index 0000000..fa93b89 --- /dev/null +++ b/src/main/java/io/xdag/crypto/randomx/RandomXLibraryLoader.java @@ -0,0 +1,203 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2022-2030 The XdagJ Developers + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.xdag.crypto.randomx; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import lombok.extern.slf4j.Slf4j; + +/** + * Handles the loading of the RandomX native library. + * This class is responsible for extracting the library from resources if necessary, + * loading it into the JVM, and configuring JNA library paths. + */ +@Slf4j +final class RandomXLibraryLoader { + + private static boolean isLoaded = false; + private static String loadedLibraryPath = null; // Store the path of the loaded library for logging + + // Private constructor to prevent instantiation + private RandomXLibraryLoader() {} + + /** + * Ensures that the RandomX native library is loaded. + * This method is synchronized and will only attempt to load the library once. + * It extracts the library from resources, loads it using System.load(), + * and sets the jna.library.path. + * + * @throws UnsatisfiedLinkError if the library cannot be loaded for any reason. + * @throws Exception for other unexpected errors during loading. + */ + public static synchronized void load() throws Exception { + if (isLoaded) { + log.info("Native library already loaded from: {}", loadedLibraryPath); + return; + } + + try { + File tempFile = extractAndLoadNativeLibrary(); + loadedLibraryPath = tempFile.getAbsolutePath(); + + String tempLibDir = tempFile.getParent(); + if (tempLibDir != null) { + String currentJnaPath = System.getProperty("jna.library.path"); + if (currentJnaPath == null || currentJnaPath.isEmpty()) { + System.setProperty("jna.library.path", tempLibDir); + } else if (!currentJnaPath.contains(tempLibDir)) { + System.setProperty("jna.library.path", currentJnaPath + File.pathSeparator + tempLibDir); + } + log.info("Set jna.library.path to include: {}", tempLibDir); + } else { + log.warn("Could not get parent directory for temporary library file: {}", loadedLibraryPath); + } + + isLoaded = true; + log.info("RandomX native library loaded successfully via RandomXLibraryLoader from: {}", loadedLibraryPath); + + } catch (UnsatisfiedLinkError ule) { // Catch specifically from System.load() + log.error("Failed to load native library (UnsatisfiedLinkError from System.load()): {}: {}", + (loadedLibraryPath != null ? loadedLibraryPath : ""), ule.getMessage(), ule); + logLibraryPaths(); // Log paths for diagnostics + throw ule; // Re-throw to be handled by RandomXNative's static block + } catch (Exception e) { + log.error("An unexpected error occurred during native library loading: {}", e.getMessage(), e); + logLibraryPaths(); // Log paths for diagnostics + throw e; // Re-throw to be handled by RandomXNative's static block + } + } + + /** + * Logs current JNA and Java library paths for diagnostic purposes. + */ + public static void logLibraryPaths() { + String libPath = System.getProperty("jna.library.path"); + log.error("Current jna.library.path: {}", (libPath != null ? libPath : "")); + String javaLibPath = System.getProperty("java.library.path"); + log.error("Current java.library.path: {}", (javaLibPath != null ? javaLibPath : "")); + } + + /** + * Extracts the native library from resources to a temporary file and loads it using System.load(). + * @return The File object of the loaded temporary library. + * @throws IOException if file operations fail. + * @throws IllegalStateException if the resource is not found. + * @throws UnsatisfiedLinkError if System.load() fails. + */ + private static File extractAndLoadNativeLibrary() throws IOException, IllegalStateException, UnsatisfiedLinkError { + String libraryLogicalName = "randomx"; + String resourceBaseName = "librandomx"; + + String os = System.getProperty("os.name", "").toLowerCase(); + String arch = System.getProperty("os.arch", "").toLowerCase(); + + String libFileNameInResources = getPlatformSpecificResourceName(resourceBaseName, os, arch); + + Path tempFilePath = null; + try (InputStream libStream = RandomXLibraryLoader.class.getClassLoader().getResourceAsStream(libFileNameInResources)) { + if (libStream == null) { + log.error("Native library resource not found: {}", libFileNameInResources); + try { + java.net.URL resourceUrl = RandomXLibraryLoader.class.getResource(""); + if (resourceUrl != null) { + log.error("Checked relative to class location: {}", resourceUrl); + } + java.net.URL rootResourceUrl = RandomXLibraryLoader.class.getClassLoader().getResource(""); + if (rootResourceUrl != null) { + log.error("Checked relative to classloader root: {}", rootResourceUrl); + } + } catch(Exception e) { /* ignore secondary errors */ } + + throw new IllegalStateException("Native library resource not found: " + libFileNameInResources + + ". Check classpath and resource packaging."); + } + + String mappedLibName = System.mapLibraryName(libraryLogicalName); + String tempFilePrefix = mappedLibName.substring(0, mappedLibName.lastIndexOf('.')); + String tempFileSuffix = mappedLibName.substring(mappedLibName.lastIndexOf('.')); + + tempFilePath = Files.createTempFile(tempFilePrefix + "-", tempFileSuffix); + File tempFile = tempFilePath.toFile(); + tempFile.deleteOnExit(); + + Files.copy(libStream, tempFilePath, StandardCopyOption.REPLACE_EXISTING); + + System.load(tempFile.getAbsolutePath()); // This can throw UnsatisfiedLinkError + log.info("Native library extracted to and loaded from: {} (resource: {})", tempFile.getAbsolutePath(), libFileNameInResources); + return tempFile; + + } catch (IOException e) { + log.error("IOException during library extraction from resource '{}': {}", libFileNameInResources, e.getMessage(), e); + if (tempFilePath != null) { + log.error("Temporary file path was: {}", tempFilePath.toString()); + } + throw e; + } catch (UnsatisfiedLinkError ule) { // Catch from System.load() + log.error("UnsatisfiedLinkError during System.load() for '{}': {}", + (tempFilePath != null ? tempFilePath.toString() : libFileNameInResources), ule.getMessage(), ule); + throw ule; // Re-throw + } catch (Exception e) { // Catch other unexpected errors + log.error("Unexpected exception during library extraction/loading from resource '{}': {}", + libFileNameInResources, e.getMessage(), e); + // Wrap in a more specific runtime exception if desired, or re-throw as is. + throw new RuntimeException("Failed to load native library from resource: " + libFileNameInResources, e); + } + } + + /** + * Constructs the platform-specific library file name as it exists in the resources. + */ + private static String getPlatformSpecificResourceName(String resourceBaseName, String os, String arch) { + String prefix = "native/"; + String suffix; + String platformArch = arch; + + if (os.contains("win")) { + suffix = "_windows_x86_64.dll"; + } else if (os.contains("mac")) { + if ("arm64".equals(arch)) { + platformArch = "aarch64"; + } else if (!"aarch64".equals(arch) && !"x86_64".equals(arch)){ + log.warn("Uncommon macOS arch detected: {}, attempting to use it directly in resource path.", arch); + } + suffix = "_macos_" + platformArch + ".dylib"; + } else if (os.contains("linux")) { + if ("amd64".equals(arch)) { + platformArch = "x86_64"; + } + suffix = "_linux_" + platformArch + ".so"; + } else { + throw new UnsupportedOperationException( + String.format("Unsupported platform: OS=%s, Architecture=%s", os, arch)); + } + String resourcePath = prefix + resourceBaseName + suffix; + log.info("Determined resource path: {} (OS: {}, Arch: {} -> {})", + resourcePath, os, arch, platformArch); + return resourcePath; + } +} \ No newline at end of file diff --git a/src/main/java/io/xdag/crypto/randomx/RandomXNative.java b/src/main/java/io/xdag/crypto/randomx/RandomXNative.java new file mode 100644 index 0000000..0596f19 --- /dev/null +++ b/src/main/java/io/xdag/crypto/randomx/RandomXNative.java @@ -0,0 +1,243 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2022-2030 The XdagJ Developers + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.xdag.crypto.randomx; + +import com.sun.jna.Native; +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; + +/** + * JNA direct mapping class for RandomX native library functions. + * Native library loading is handled by {@link RandomXLibraryLoader}. + * This class registers the native methods after the library is successfully loaded. + */ +public class RandomXNative { + static { + try { + // Step 1: Initialize the library using the loader. + // This will handle extraction, loading (System.load), and setting jna.library.path. + RandomXLibraryLoader.load(); + + // Step 2: Register the native methods with JNA. + // "randomx" is the logical name. JNA should find the loaded library + // via the jna.library.path set by the loader, or because it's already loaded. + Native.register("randomx"); + + // Simple log to confirm registration attempt after loader success. + // For more detailed logging, rely on RandomXLibraryLoader or a dedicated logging framework. + System.out.println("[INFO] RandomXNative: Native methods registration attempted for 'randomx'."); + + } catch (UnsatisfiedLinkError ule) { + // Logged by RandomXLibraryLoader, but good to catch and re-throw here + // to ensure the class loading fails clearly if registration itself has an issue + // or if the library loaded but methods can't be bound. + System.err.println("[ERROR] RandomXNative: Failed to link native library or register methods. " + + "Ensure 'randomx' library was loaded correctly by RandomXLibraryLoader. " + + "Error: " + ule.getMessage()); + ule.printStackTrace(System.err); // Print stack trace for more details + throw ule; // Re-throw to indicate critical failure + } catch (Exception e) { + System.err.println("[ERROR] RandomXNative: An unexpected error occurred during static initialization: " + e.getMessage()); + e.printStackTrace(System.err); + throw new RuntimeException("Failed to initialize RandomXNative due to an unexpected error", e); + } + } + + // --- Native Method Declarations --- + // Add all the necessary methods you intend to call directly + + /** + * Retrieves the RandomX flags supported by the current CPU. + * @return An integer value representing the combination of supported flags. + * @see RandomXFlag + */ + public static native int randomx_get_flags(); + + /** + * Allocates a RandomX cache object. + * This cache is used to store precomputed data to speed up hash calculations, especially in "fast" mode. + * It needs to be initialized using {@link #randomx_init_cache(Pointer, Pointer, long)}. + * After use, it should be released via {@link #randomx_release_cache(Pointer)}. + * + * @param flags Flags used to initialize the cache. See {@link RandomXFlag}. + * @return A pointer to the allocated RandomX cache. Behavior is undefined if allocation fails. + */ + public static native Pointer randomx_alloc_cache(int flags); + + /** + * Initializes a RandomX cache previously allocated by {@link #randomx_alloc_cache(int)}. + * + * @param cache Pointer to the cache object obtained from {@code randomx_alloc_cache}. + * @param key The key (seed) used to initialize the cache. + * @param keySize The length of the key in bytes. + */ + public static native void randomx_init_cache(Pointer cache, Pointer key, long keySize); + + /** + * Releases a RandomX cache previously allocated by {@link #randomx_alloc_cache(int)}. + * + * @param cache Pointer to the cache object to be released. + */ + public static native void randomx_release_cache(Pointer cache); + + // --- Dataset related methods --- + + /** + * Allocates a RandomX dataset object. + * The dataset is used for "slow" hashing mode and requires a large amount of memory. + * It needs to be initialized using {@link #randomx_init_dataset(Pointer, Pointer, NativeLong, NativeLong)}. + * After use, it should be released via {@link #randomx_release_dataset(Pointer)}. + * + * @param flags Flags used to initialize the dataset. See {@link RandomXFlag}. + * @return A pointer to the allocated RandomX dataset. Behavior is undefined if allocation fails. + */ + public static native Pointer randomx_alloc_dataset(int flags); + + /** + * Gets the number of items required to build a full dataset. + * @return The total number of items in the dataset, as an unsigned long represented by {@link NativeLong}. + */ + public static native NativeLong randomx_dataset_item_count(); // Returns unsigned long, use NativeLong + + /** + * Initializes a portion of a RandomX dataset previously allocated by {@link #randomx_alloc_dataset(int)}. + * This process is typically done in chunks, as the full dataset can be very large. + * + * @param dataset Pointer to the dataset object obtained from {@code randomx_alloc_dataset}. + * @param cache Pointer to an initialized RandomX cache. + * @param startItem The starting item index (0-based) of the dataset to initialize. + * @param itemCount The number of items to initialize, starting from {@code startItem}. + */ + public static native void randomx_init_dataset(Pointer dataset, Pointer cache, NativeLong startItem, NativeLong itemCount); + + /** + * Releases a RandomX dataset previously allocated by {@link #randomx_alloc_dataset(int)}. + * + * @param dataset Pointer to the dataset object to be released. + */ + public static native void randomx_release_dataset(Pointer dataset); + + // --- VM related methods --- + + /** + * Creates a RandomX virtual machine (VM). + * The VM is used to perform hash calculations. + * It can be configured for "fast" mode (using a cache) or "slow" mode (using a dataset). + * After use, it should be destroyed via {@link #randomx_destroy_vm(Pointer)}. + * + * @param flags Flags used to create the VM. See {@link RandomXFlag}. + * @param cache Pointer to an initialized RandomX cache. Can be {@code null} if using "slow" mode. + * @param dataset Pointer to an initialized RandomX dataset. Can be {@code null} if using "fast" mode. + * @return A pointer to the created RandomX VM. Behavior is undefined if creation fails. + */ + public static native Pointer randomx_create_vm(int flags, Pointer cache, Pointer dataset); // dataset can be NULL for light VM + + /** + * Changes the cache used by an existing RandomX VM. + * + * @param vm Pointer to the VM to be modified. + * @param cache Pointer to the new initialized RandomX cache. + */ + public static native void randomx_vm_set_cache(Pointer vm, Pointer cache); + + /** + * Changes the dataset used by an existing RandomX VM. + * + * @param vm Pointer to the VM to be modified. + * @param dataset Pointer to the new initialized RandomX dataset. Can be {@code null} to switch to "fast" mode (if supported by the VM). + */ + public static native void randomx_vm_set_dataset(Pointer vm, Pointer dataset); // dataset can be NULL + + /** + * Destroys a RandomX VM previously created by {@link #randomx_create_vm(int, Pointer, Pointer)}. + * + * @param vm Pointer to the VM to be destroyed. + */ + public static native void randomx_destroy_vm(Pointer vm); + + /** + * Calculates the RandomX hash of the input data using the specified VM. + * + * @param vm Pointer to an initialized VM. + * @param input Pointer to the input data to be hashed. + * @param inputSize Size of the input data in bytes. + * @param output Pointer to a buffer where the calculated hash will be stored. The buffer size should be {@link RandomXUtils#RANDOMX_HASH_SIZE}. + */ + public static native void randomx_calculate_hash(Pointer vm, Pointer input, long inputSize, Pointer output); + + // --- Multi-part hashing methods --- + + /** + * Starts a multi-part RandomX hash calculation. + * This is the first step in a multi-part hash, typically used for streaming data or very large inputs. + * + * @param vm Pointer to an initialized VM. + * @param input Pointer to the first part of the input data. + * @param inputSize Size of the first part of the input data in bytes. + */ + public static native void randomx_calculate_hash_first(Pointer vm, Pointer input, long inputSize); + + /** + * Continues a multi-part RandomX hash calculation, processing the next chunk of data. + * Must be called after {@link #randomx_calculate_hash_first(Pointer, Pointer, long)}. + * Can be called multiple times for sequential data chunks. + * + * @param vm Pointer to the VM undergoing multi-part hashing. + * @param input Pointer to the next block of input data. + * @param inputSize Size of the next block of input data in bytes. + * @param output Pointer to a buffer for storing an intermediate (or final, if no subsequent 'last' call) hash. + * Note: The RandomX C API's randomx_calculate_hash_next doesn't always output the full hash directly; + * its behavior may depend on the specific implementation. + * Typically, the final hash is obtained via {@link #randomx_calculate_hash_last(Pointer, Pointer)}. + */ + public static native void randomx_calculate_hash_next(Pointer vm, Pointer input, long inputSize, Pointer output); + + /** + * Finalizes a multi-part RandomX hash calculation and retrieves the final hash value. + * Must be called after {@link #randomx_calculate_hash_first(Pointer, Pointer, long)} and any number of + * {@link #randomx_calculate_hash_next(Pointer, Pointer, long, Pointer)} calls. + * + * @param vm Pointer to the VM undergoing multi-part hashing. + * @param output Pointer to a buffer where the final calculated hash will be stored. The buffer size should be {@link RandomXUtils#RANDOMX_HASH_SIZE}. + */ + public static native void randomx_calculate_hash_last(Pointer vm, Pointer output); + + // --- Commitment hash method (assuming this C API exists as used) --- + + /** + * Calculates a commitment value for a two-phase commit scheme. + * This is an optimization specific to certain PoW variants, allowing pre-computation of part of the work + * without knowing the full input. + * Note: This method operates directly on input data and a pre-calculated hash, not through a VM. + * + * @param input Pointer to the original input data (e.g., the first part of a block header). + * @param inputSize Size of the original input data in bytes. + * @param hash_in Pointer to a pre-calculated hash of some transformation of the original input + * (e.g., a hash of the first part of the block header). + * This hash will be part of the commitment calculation. + * @param com_out Pointer to a buffer where the calculated commitment will be stored. The buffer size should be {@link RandomXUtils#RANDOMX_HASH_SIZE}. + */ + public static native void randomx_calculate_commitment(Pointer input, long inputSize, Pointer hash_in, Pointer com_out); + +} \ No newline at end of file diff --git a/src/main/java/io/xdag/crypto/randomx/RandomXTemplate.java b/src/main/java/io/xdag/crypto/randomx/RandomXTemplate.java index f73d583..509b8ab 100644 --- a/src/main/java/io/xdag/crypto/randomx/RandomXTemplate.java +++ b/src/main/java/io/xdag/crypto/randomx/RandomXTemplate.java @@ -26,6 +26,7 @@ import lombok.Builder; import lombok.Getter; import lombok.ToString; +import lombok.extern.slf4j.Slf4j; import java.util.*; @@ -35,9 +36,10 @@ */ @Builder @ToString +@Slf4j public class RandomXTemplate implements AutoCloseable { - /** Flag indicating if the template is in mining mode */ + @Getter private final boolean miningMode; /** Set of RandomX flags for configuring the algorithm behavior */ @@ -45,124 +47,216 @@ public class RandomXTemplate implements AutoCloseable { private final Set flags; /** Cache for RandomX operations */ + @Getter private final RandomXCache cache; /** Dataset for RandomX mining operations */ + @Getter private RandomXDataset dataset; /** Virtual machine instance for RandomX operations */ + @Getter private RandomXVM vm; + /** Stores the current key used for cache initialization to avoid redundant re-initializations. */ + @Getter + private byte[] currentKey; + /** - * Initializes the RandomX cache or dataset based on the mining mode. - * If in mining mode, enables FULL_MEM flag and initializes dataset. - * Otherwise, removes FULL_MEM flag and sets dataset to null. + * Initializes the RandomX virtual machine (VM) with the configured settings. + * This method must be called before any hash calculation. + * If in mining mode, the dataset should be initialized before calling this method, + * and if in light mode, the cache should be initialized. */ public void init() { Set vmFlags = EnumSet.copyOf(flags); if (miningMode) { vmFlags.add(RandomXFlag.FULL_MEM); - dataset = new RandomXDataset(vmFlags); - dataset.init(cache); + // Ensure cache is initialized with currentKey before creating dataset + if (this.currentKey == null) { + log.warn("Initializing RandomXTemplate without a key set for the cache. Dataset initialization might rely on an uninitialized cache if not subsequently set."); + } else if (cache.getCachePointer() == null) { // Cache might be allocated but not initialized + log.warn("Cache pointer is null during init despite currentKey being set. This should not happen if cache is managed correctly."); + } + + log.debug("Mining mode enabled. Creating and initializing dataset with flags: {}", vmFlags); + dataset = new RandomXDataset(vmFlags); // Dataset uses its own flags, usually including FULL_MEM + dataset.init(cache); // Dataset initialization depends on an initialized cache } else { vmFlags.remove(RandomXFlag.FULL_MEM); + if (dataset != null) { + dataset.close(); // Ensure previous dataset is closed if switching modes + } dataset = null; + log.debug("Light mode enabled. Dataset will not be used."); } + + log.debug("Creating RandomXVM with flags: {} (Cache: {}, Dataset: {})", + vmFlags, + cache != null ? "Present" : "Null", + dataset != null ? "Present" : "Null"); vm = new RandomXVM(vmFlags, cache, dataset); + log.info("RandomXTemplate initialized. VM created."); } /** - * Changes the current RandomX key by reinitializing the dataset or cache. - * If the key is unchanged, returns without performing reinitialization. - * - * @param key The key to initialize RandomX with (generally a hash) + * Changes the current RandomX key by reinitializing the cache and, if in mining mode, the dataset. + * If the provided key is the same as the current key, this method returns without reinitialization. + * + * @param key The new key (typically a seed hash) to initialize RandomX components with. + * @throws IllegalArgumentException if the key is null or empty. */ public void changeKey(byte[] key) { if (key == null || key.length == 0) { - throw new IllegalArgumentException("Key cannot be null or empty"); + throw new IllegalArgumentException("Key cannot be null or empty for changeKey operation."); } - RandomXCache currentCache = vm.getCache(); - // Check if key is unchanged - if (currentCache != null && currentCache.getCachePointer() != null && - currentCache.getKeyPointer() != null && - Arrays.equals(key, currentCache.getKeyPointer().getByteArray(0, key.length))) { + // Check if the new key is the same as the current key. + if (Arrays.equals(this.currentKey, key)) { + log.debug("Key is unchanged. Skipping reinitialization."); return; } + log.info("Changing RandomX key. Old key hash (if any): {}, New key hash: {}", + (this.currentKey != null ? Arrays.hashCode(this.currentKey) : "N/A"), Arrays.hashCode(key)); + + // Initialize the cache with the new key. + // The cache instance itself is final, but its internal state is changed by init(). cache.init(key); - vm.setCache(cache); - + this.currentKey = Arrays.copyOf(key, key.length); // Store a copy of the new key. + + // If a VM instance exists, update its cache. + // If init() hasn't been called yet, vm will be null. The new cache will be used when vm is created in init(). + if (vm != null) { + log.debug("Updating VM with the new cache."); + vm.setCache(cache); + } else { + log.warn("VM is null during changeKey. The new cache will be used upon VM creation in init()."); + } + + // If in mining mode, the dataset also needs to be reinitialized with the new cache. if (miningMode) { + log.debug("Mining mode: Reinitializing dataset due to key change."); if (dataset != null) { - dataset.close(); + dataset.close(); // Close the old dataset + } + // Create and initialize a new dataset with the (now re-initialized) cache. + // The flags for the dataset should include FULL_MEM. + Set datasetFlags = EnumSet.copyOf(this.flags); // Start with base flags + datasetFlags.add(RandomXFlag.FULL_MEM); + + dataset = new RandomXDataset(datasetFlags); + dataset.init(cache); // Initialize with the cache that has the new key. + + if (vm != null) { + log.debug("Updating VM with the new dataset."); + vm.setDataset(dataset); + } else { + // This case should ideally not happen if init() is called after key setting or if builder manages initial key. + log.warn("VM is null during dataset reinitialization in changeKey. Dataset will be set when VM is created."); + } + } else { + // In light mode, ensure dataset is null if it was somehow set + if (vm != null && vm.getDataset() != null) { + log.debug("Light mode: Ensuring VM dataset is null after key change."); + vm.setDataset(null); } - dataset = new RandomXDataset(flags); - dataset.init(cache); - vm.setDataset(dataset); } + log.info("RandomX key changed and components reinitialized successfully."); } /** * Performs a single hash calculation using the RandomX VM. * - * @param input Input data for the hash calculation - * @return A 32-byte array containing the calculated hash + * @param input Input data for the hash calculation. + * @return A 32-byte array containing the calculated hash. + * @throws IllegalStateException if the VM is not initialized. */ public byte[] calculateHash(byte[] input) { + if (vm == null) { + throw new IllegalStateException("RandomX VM is not initialized. Call init() first or ensure key is set."); + } return vm.calculateHash(input); } /** * Begins a multi-part hash calculation by processing the first input. * - * @param input Initial input data for the hash calculation + * @param input Initial input data for the hash calculation. + * @throws IllegalStateException if the VM is not initialized. */ public void calculateHashFirst(byte[] input) { + if (vm == null) { + throw new IllegalStateException("RandomX VM is not initialized. Call init() first or ensure key is set."); + } vm.calculateHashFirst(input); } /** * Continues a multi-part hash calculation by processing the next input. * - * @param nextInput Next chunk of input data for the hash calculation - * @return A 32-byte array containing the intermediate hash result + * @param nextInput Next chunk of input data for the hash calculation. + * @return A 32-byte array containing the intermediate hash result. + * @throws IllegalStateException if the VM is not initialized. */ public byte[] calculateHashNext(byte[] nextInput) { + if (vm == null) { + throw new IllegalStateException("RandomX VM is not initialized. Call init() first or ensure key is set."); + } return vm.calculateHashNext(nextInput); } /** * Finalizes a multi-part hash calculation. * - * @return A 32-byte array containing the final hash result + * @return A 32-byte array containing the final hash result. + * @throws IllegalStateException if the VM is not initialized. */ public byte[] calculateHashLast() { + if (vm == null) { + throw new IllegalStateException("RandomX VM is not initialized. Call init() first or ensure key is set."); + } return vm.calculateHashLast(); } /** - * Calculates a commitment hash for the given input string. + * Calculates a commitment hash for the given input data. * - * @param input The input byte array to calculate commitment for - * @return A byte array containing the calculated commitment hash + * @param input The input byte array to calculate commitment for. + * @return A byte array containing the calculated commitment hash. + * @throws IllegalStateException if the VM is not initialized. */ - public byte[] calcStringCommitment(byte[] input) { - return vm.calcStringCommitment(input); + public byte[] calculateCommitment(byte[] input) { + if (vm == null) { + throw new IllegalStateException("RandomX VM is not initialized. Call init() first or ensure key is set."); + } + byte[] hashOfInput = vm.calculateHash(input); + + // Then, use the original input and this calculated hash to get the commitment. + return vm.calculateCommitment(input, hashOfInput); } /** - * Releases all allocated resources. + * Releases all allocated resources (VM and Dataset). + * The Cache is managed externally if passed to the builder, or internally if created by this template. + * The Current implementation assumes cache is provided via builder and its lifecycle is managed outside this close(). + * If RandomXTemplate were to create its own RandomXCache, it should also close it here. */ @Override public void close() { + log.debug("Closing RandomXTemplate resources..."); if (vm != null) { + log.debug("Closing RandomX VM..."); vm.close(); vm = null; } if (dataset != null) { + log.debug("Closing RandomX Dataset..."); dataset.close(); dataset = null; } + // currentKey does not need explicit closing. + // Cache is not closed here as it's assumed to be managed externally (passed in via builder). + log.info("RandomXTemplate resources closed."); } } diff --git a/src/main/java/io/xdag/crypto/randomx/RandomXUtils.java b/src/main/java/io/xdag/crypto/randomx/RandomXUtils.java index 766987a..8298ff7 100644 --- a/src/main/java/io/xdag/crypto/randomx/RandomXUtils.java +++ b/src/main/java/io/xdag/crypto/randomx/RandomXUtils.java @@ -23,42 +23,79 @@ */ package io.xdag.crypto.randomx; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.SystemUtils; - import java.util.Set; +// No SystemUtils or StringUtils needed if we remove platform-specific logic +import java.util.stream.Collectors; /** * Utility class for RandomX constants and helper methods. - * This class provides static methods to get RandomX flags and flag sets. + * This class provides static methods to get RandomX flags. */ public final class RandomXUtils { /** - * Gets the recommended RandomX flags for the current CPU. - * This method calls the native RandomX library to determine optimal flags. - * - * @return An integer representing the combined RandomX flags + * The size of a RandomX hash in bytes (usually 32 bytes). */ - public static int getFlags() { - return RandomXJNALoader.getInstance().randomx_get_flags(); + public static final int RANDOMX_HASH_SIZE = 32; + + // Use System.out for basic logging to avoid SLF4J dependency for this utility class + private static void logInfo(String message) { + System.out.println("[INFO] RandomXUtils: " + message); } /** - * Gets a set of RandomX flags appropriate for the current system. - * This method converts the raw flags to a Set of RandomXFlag enums and - * applies platform-specific adjustments (e.g., removing JIT flag on macOS). + * Gets the recommended RandomX flags from the native library. * - * @return A Set of RandomXFlag enums representing the enabled flags + * @return An integer representing the combined RandomX flags from the native library. */ - public static Set getFlagsSet() { - int flags = getFlags(); - Set flagsSet = RandomXFlag.fromValue(flags); - if (SystemUtils.IS_OS_MAC_OSX && StringUtils.containsIgnoreCase(SystemUtils.OS_ARCH, "aarch64")) { - flagsSet.remove(RandomXFlag.JIT); + public static int getNativeFlags() { + // Ensure RandomXNative is loaded by accessing it before calling native method + try { + Class.forName(RandomXNative.class.getName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException("RandomXNative class not found", e); + } catch (ExceptionInInitializerError e) { + System.err.println("ERROR in RandomXUtils: Failed to initialize RandomXNative: " + e.getCause().getMessage()); + e.getCause().printStackTrace(); + throw e; // Re-throw to indicate failure } + return RandomXNative.randomx_get_flags(); + } + + /** + * Gets a set of RandomX flags recommended by the native library. + * This method ensures that the DEFAULT flag is included if the native library + * returns an empty set or a set that doesn't explicitly include optimizations + * that would imply DEFAULT. + * + * @return A Set of RandomXFlag enums representing the enabled flags. + */ + public static Set getRecommendedFlags() { + int nativeFlagsValue = getNativeFlags(); + logInfo("Native recommended flags value: " + nativeFlagsValue); + + Set flagsSet = RandomXFlag.fromValue(nativeFlagsValue); + logInfo("Parsed native flags set: " + flagsSet.stream().map(Enum::name).collect(Collectors.joining(", "))); + // Ensure a DEFAULT flag is present if the set is empty or only contains non-functional flags. + // The native library should ideally always return DEFAULT (0) or a combination including it + // if no other specific flags like JIT are set. + // If JIT or other major flags are set, DEFAULT (0) might be implicitly part of the mode. + // Let's ensure the set isn't empty and contains DEFAULT if no major operational flags are present. + if (flagsSet.isEmpty()) { + logInfo("Native flags resulted in an empty set. Adding DEFAULT."); + flagsSet.add(RandomXFlag.DEFAULT); + } else if (!flagsSet.contains(RandomXFlag.DEFAULT) && + flagsSet.stream().noneMatch(flag -> + flag == RandomXFlag.JIT || + flag == RandomXFlag.FULL_MEM || + flag == RandomXFlag.LARGE_PAGES)) { + // If no major operational flags are set, and DEFAULT is also missing, add DEFAULT. + logInfo("No major operational flags (JIT, FULL_MEM, LARGE_PAGES) or DEFAULT found. Adding DEFAULT."); + flagsSet.add(RandomXFlag.DEFAULT); + } + + logInfo("Final recommended flags set: " + flagsSet.stream().map(Enum::name).collect(Collectors.joining(", "))); return flagsSet; } - } \ No newline at end of file diff --git a/src/main/java/io/xdag/crypto/randomx/RandomXVM.java b/src/main/java/io/xdag/crypto/randomx/RandomXVM.java index 32f70a1..c7b74d7 100644 --- a/src/main/java/io/xdag/crypto/randomx/RandomXVM.java +++ b/src/main/java/io/xdag/crypto/randomx/RandomXVM.java @@ -26,8 +26,7 @@ import com.sun.jna.Memory; import com.sun.jna.Pointer; import lombok.Getter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import java.util.Set; @@ -35,45 +34,51 @@ * Wrapper class for RandomX virtual machine operations. * Manages the lifecycle and state of a RandomX VM instance. */ -@Getter +@Slf4j public class RandomXVM implements AutoCloseable { - private static final Logger logger = LoggerFactory.getLogger(RandomXVM.class); - /** - * The RandomX flags used to configure this VM + * The RandomX flags used to configure this VM. */ + @Getter private final Set flags; /** - * Pointer to the native VM instance + * Pointer to the native VM instance. */ @Getter - private final Pointer point; + private final Pointer vmPointer; /** - * The cache used by this VM + * The cache used by this VM. */ + @Getter private RandomXCache cache; /** - * The dataset used by this VM (may be null in light mode) + * The dataset used by this VM (may be null in light mode). */ + @Getter private RandomXDataset dataset; /** * Creates a new RandomX VM instance with the specified configuration. * - * @param flags Configuration flags for the VM - * @param cache The cache to use for VM operations - * @param dataset The dataset to use for VM operations (may be null) - * @throws IllegalStateException if VM creation fails + * @param flags Configuration flags for the VM. + * @param cache The cache to use for VM operations. + * @param dataset The dataset to use for VM operations (may be null for light mode). + * @throws RuntimeException if VM creation fails. + * @throws IllegalArgumentException if parameters are invalid. */ public RandomXVM(Set flags, RandomXCache cache, RandomXDataset dataset) { if (flags == null || flags.isEmpty()) { - throw new IllegalArgumentException("Flags cannot be null or empty"); + throw new IllegalArgumentException("Flags cannot be null or empty."); } - if (cache == null) { - throw new IllegalArgumentException("Cache cannot be null"); + if (cache == null || cache.getCachePointer() == null) { + throw new IllegalArgumentException("Cache instance or its pointer cannot be null."); + } + // Dataset can be null (light mode) + if (dataset != null && dataset.getDatasetPointer() == null) { + throw new IllegalArgumentException("If a dataset is provided, its pointer cannot be null."); } this.flags = flags; @@ -81,154 +86,216 @@ public RandomXVM(Set flags, RandomXCache cache, RandomXDataset data this.dataset = dataset; int flagsValue = RandomXFlag.toValue(flags); - Pointer datasetPointer = dataset != null ? dataset.getPointer() : null; + Pointer cachePtr = cache.getCachePointer(); + Pointer datasetPtr = (dataset != null) ? dataset.getDatasetPointer() : null; + + log.debug("Preparing to create RandomX VM. Flags: {} ({}), Cache Ptr: {}, Dataset Ptr: {}", + flags, flagsValue, Pointer.nativeValue(cachePtr), (datasetPtr != null ? Pointer.nativeValue(datasetPtr) : "null")); - this.point = RandomXJNALoader.getInstance().randomx_create_vm( - flagsValue, - cache.getCachePointer(), - datasetPointer - ); + this.vmPointer = RandomXNative.randomx_create_vm(flagsValue, cachePtr, datasetPtr); - if (point == null) { - throw new IllegalStateException("Failed to create RandomX VM"); + if (vmPointer == null) { + String errorMsg = String.format("Failed to create RandomX VM with flags: %s (%d)", flags, flagsValue); + log.error(errorMsg); + throw new RuntimeException(errorMsg); } - logger.debug("RandomX VM created with flags: {}", flags); + log.info("RandomX VM created successfully. Pointer: {}, Flags: {}", Pointer.nativeValue(vmPointer), flags); } /** * Updates the cache used by this VM. * - * @param cache The new cache to use - * @throws IllegalArgumentException if cache is null + * @param newCache The new cache to use. + * @throws IllegalArgumentException if newCache is null or its pointer is null. + * @throws IllegalStateException if the VM pointer is null. */ - public void setCache(RandomXCache cache) { - if (cache == null) { - throw new IllegalArgumentException("Cache cannot be null"); + public void setCache(RandomXCache newCache) { + if (vmPointer == null) { + throw new IllegalStateException("VM pointer is null, cannot set cache."); } - RandomXJNALoader.getInstance().randomx_vm_set_cache(point, cache.getCachePointer()); - this.cache = cache; - logger.debug("VM cache updated"); + if (newCache == null || newCache.getCachePointer() == null) { + throw new IllegalArgumentException("New cache instance or its pointer cannot be null."); + } + RandomXNative.randomx_vm_set_cache(vmPointer, newCache.getCachePointer()); + this.cache = newCache; + log.debug("VM cache updated. New Cache Ptr: {}", Pointer.nativeValue(newCache.getCachePointer())); } /** * Updates the dataset used by this VM. * - * @param dataset The new dataset to use (may be null in light mode) + * @param newDataset The new dataset to use (can be null for light mode). + * @throws IllegalArgumentException if newDataset is not null but its pointer is null. + * @throws IllegalStateException if the VM pointer is null. */ - public void setDataset(RandomXDataset dataset) { - Pointer datasetPointer = dataset != null ? dataset.getPointer() : null; - RandomXJNALoader.getInstance().randomx_vm_set_dataset(point, datasetPointer); - this.dataset = dataset; + public void setDataset(RandomXDataset newDataset) { + if (vmPointer == null) { + throw new IllegalStateException("VM pointer is null, cannot set dataset."); + } + // If newDataset is not null, its pointer also cannot be null + if (newDataset != null && newDataset.getDatasetPointer() == null) { + throw new IllegalArgumentException("If a new dataset is provided, its pointer cannot be null."); + } + Pointer datasetPtr = (newDataset != null) ? newDataset.getDatasetPointer() : null; + RandomXNative.randomx_vm_set_dataset(vmPointer, datasetPtr); + this.dataset = newDataset; + log.debug("VM dataset updated. New Dataset Ptr: {}", (datasetPtr != null ? Pointer.nativeValue(datasetPtr) : "null")); } /** * Calculates a RandomX hash using the current VM configuration. * - * @param input The input data to be hashed - * @return A 32-byte array containing the calculated hash - * @throws IllegalArgumentException if input is null + * @param input The input data to be hashed. + * @return A 32-byte array containing the calculated hash. + * @throws IllegalArgumentException if input is null. + * @throws IllegalStateException if the VM pointer is null. */ public byte[] calculateHash(byte[] input) { - if (input == null) { - throw new IllegalArgumentException("Input cannot be null"); + if (vmPointer == null) { + throw new IllegalStateException("VM pointer is null, cannot calculate hash."); } - byte[] output = new byte[32]; - - try (Memory inputMem = new Memory(input.length); - Memory outputMem = new Memory(32)) { - - inputMem.write(0, input, 0, input.length); - RandomXJNALoader.getInstance().randomx_calculate_hash(point, inputMem, input.length, outputMem); - outputMem.read(0, output, 0, output.length); + if (input == null) { + throw new IllegalArgumentException("Input cannot be null."); } + byte[] output = new byte[32]; // RandomX hash is always 32 bytes - return output; + // JNA Memory objects automatically manage native memory allocation and deallocation + // (at the end of their scope or during GC). + Memory inputMem = new Memory(input.length > 0 ? input.length : 1); // JNA Memory does not accept size 0 + Memory outputMem = new Memory(output.length); + if (input.length > 0) { + inputMem.write(0, input, 0, input.length); + } + RandomXNative.randomx_calculate_hash(vmPointer, inputMem, input.length, outputMem); + outputMem.read(0, output, 0, output.length); + return output; } /** * Begins a multi-part hash calculation. * - * @param input The input data - * @throws IllegalArgumentException if input is null + * @param input The input data. + * @throws IllegalArgumentException if input is null. + * @throws IllegalStateException if the VM pointer is null. */ public void calculateHashFirst(byte[] input) { - if (input == null) { - throw new IllegalArgumentException("Input cannot be null"); + if (vmPointer == null) { + throw new IllegalStateException("VM pointer is null, cannot start multi-part hash."); } - - try (Memory inputMem = new Memory(input.length)) { - inputMem.write(0, input, 0, input.length); - RandomXJNALoader.getInstance().randomx_calculate_hash_first(point, inputMem, input.length); + if (input == null) { + throw new IllegalArgumentException("Input cannot be null."); } + Memory inputMem = new Memory(input.length > 0 ? input.length : 1); + if (input.length > 0) { + inputMem.write(0, input, 0, input.length); + } + RandomXNative.randomx_calculate_hash_first(vmPointer, inputMem, input.length); } /** * Continues a multi-part hash calculation. * - * @param input The input data - * @return A 32-byte array containing the intermediate hash result - * @throws IllegalArgumentException if input is null + * @param input The input data. + * @return A 32-byte array containing the intermediate hash result. + * @throws IllegalArgumentException if input is null. + * @throws IllegalStateException if the VM pointer is null. */ public byte[] calculateHashNext(byte[] input) { + if (vmPointer == null) { + throw new IllegalStateException("VM pointer is null, cannot continue multi-part hash."); + } if (input == null) { - throw new IllegalArgumentException("Input cannot be null"); + throw new IllegalArgumentException("Input cannot be null."); } byte[] output = new byte[32]; - - try (Memory inputMem = new Memory(input.length); - Memory outputMem = new Memory(32)) { - - inputMem.write(0, input, 0, input.length); - RandomXJNALoader.getInstance().randomx_calculate_hash_next(point, inputMem, input.length, outputMem); - outputMem.read(0, output, 0, output.length); - } - return output; + Memory inputMem = new Memory(input.length > 0 ? input.length : 1); + Memory outputMem = new Memory(output.length); + if (input.length > 0) { + inputMem.write(0, input, 0, input.length); + } + RandomXNative.randomx_calculate_hash_next(vmPointer, inputMem, input.length, outputMem); + outputMem.read(0, output, 0, output.length); + return output; } /** * Finalizes a multi-part hash calculation. * - * @return A 32-byte array containing the final hash result + * @return A 32-byte array containing the final hash result. + * @throws IllegalStateException if the VM pointer is null. */ public byte[] calculateHashLast() { - byte[] output = new byte[32]; - - try (Memory outputMem = new Memory(32)) { - RandomXJNALoader.getInstance().randomx_calculate_hash_last(point, outputMem); - outputMem.read(0, output, 0, output.length); + if (vmPointer == null) { + throw new IllegalStateException("VM pointer is null, cannot finalize multi-part hash."); } - return output; + byte[] output = new byte[32]; + Memory outputMem = new Memory(output.length); + RandomXNative.randomx_calculate_hash_last(vmPointer, outputMem); + outputMem.read(0, output, 0, output.length); + return output; } /** * Calculates a commitment hash for the given input data. + * Note: The implementation of this method is based on observation of the original code. + * It first calculates a regular hash, then uses that hash as a seed to calculate the commitment. + * The exact behavior and signature of the {@code randomx_calculate_commitment} C API should be confirmed. * - * @param input The input data to calculate commitment for - * @return A 32-byte array containing the calculated commitment hash + * @param originalInput The original input data that was hashed. + * @param preCalculatedHash The hash previously calculated from originalInput. + * @return A 32-byte array containing the calculated commitment. + * @throws IllegalArgumentException if originalInput or preCalculatedHash is null, or if preCalculatedHash is not 32 bytes. + * @throws IllegalStateException if the VM pointer is null. */ - public byte[] calcStringCommitment(byte[] input) { - Pointer inputPointer = new Memory(input.length); - inputPointer.write(0, input, 0, input.length); + public byte[] calculateCommitment(byte[] originalInput, byte[] preCalculatedHash) { + if (vmPointer == null) { + throw new IllegalStateException("VM pointer is null, cannot calculate commitment."); + } + if (originalInput == null) { + throw new IllegalArgumentException("Original input cannot be null."); + } + if (preCalculatedHash == null || preCalculatedHash.length != RandomXUtils.RANDOMX_HASH_SIZE) { + throw new IllegalArgumentException("Pre-calculated hash cannot be null and must be " + RandomXUtils.RANDOMX_HASH_SIZE + " bytes long."); + } - byte[] output = new byte[32]; - Pointer outputPointer = new Memory(output.length); - RandomXJNALoader.getInstance().randomx_calculate_hash(point, inputPointer, input.length, outputPointer); - outputPointer.read(0, output, 0, output.length); - RandomXJNALoader.getInstance().randomx_calculate_commitment(inputPointer, input.length, outputPointer, outputPointer); - outputPointer.read(0, output, 0, output.length); - return output; + byte[] commitmentOutput = new byte[RandomXUtils.RANDOMX_HASH_SIZE]; + Memory originalInputMem = new Memory(originalInput.length > 0 ? originalInput.length : 1); // JNA requires non-zero size for empty inputs + Memory preCalculatedHashMem = new Memory(preCalculatedHash.length); + Memory commitmentOutputMem = new Memory(commitmentOutput.length); + + if (originalInput.length > 0) { + originalInputMem.write(0, originalInput, 0, originalInput.length); + } + // preCalculatedHash is guaranteed to be non-null and 32 bytes here + preCalculatedHashMem.write(0, preCalculatedHash, 0, preCalculatedHash.length); + + // Call the native method with parameters matching the C API + // (Pointer input, long inputSize, Pointer hash_in, Pointer com_out) + RandomXNative.randomx_calculate_commitment(originalInputMem, originalInput.length, preCalculatedHashMem, commitmentOutputMem); + commitmentOutputMem.read(0, commitmentOutput, 0, commitmentOutput.length); + return commitmentOutput; } /** - * Releases the native VM resources. + * Releases native VM resources. * This method is idempotent and can be called multiple times safely. */ @Override public void close() { - if (point != null) { - RandomXJNALoader.getInstance().randomx_destroy_vm(point); - logger.debug("RandomX VM destroyed"); + if (vmPointer != null) { + // Check if vmPointer is still valid to prevent operations on an already destroyed VM. + // While JNA's destroy is generally safe, this is an extra layer of protection. + try { + RandomXNative.randomx_destroy_vm(vmPointer); + log.info("RandomX VM destroyed. Pointer: {}", Pointer.nativeValue(vmPointer)); + } catch (Throwable t) { + // Generally, destroy should not throw an error, but just in case. + log.error("Error occurred while destroying RandomX VM. Pointer: {}", Pointer.nativeValue(vmPointer), t); + } + // Do not set vmPointer to null as it is final. The object itself will no longer be usable. + } else { + log.debug("Attempting to destroy RandomX VM, but pointer is already null (possibly already destroyed or never created)."); } } } \ No newline at end of file diff --git a/src/test/java/io/xdag/crypto/randomx/RandomXBenchmark.java b/src/test/java/io/xdag/crypto/randomx/RandomXBenchmark.java index b889fe0..95844c8 100644 --- a/src/test/java/io/xdag/crypto/randomx/RandomXBenchmark.java +++ b/src/test/java/io/xdag/crypto/randomx/RandomXBenchmark.java @@ -50,6 +50,9 @@ public class RandomXBenchmark { private static final Logger logger = LoggerFactory.getLogger(RandomXBenchmark.class); + @Param({"NO_JIT", "JIT"}) // Parameter for controlling JIT flag + public String compilationMode; + // Sample block template for hash calculation private static final byte[] BLOCK_TEMPLATE = { (byte)0x07, (byte)0x07, (byte)0xf7, (byte)0xa4, (byte)0xf0, (byte)0xd6, (byte)0x05, (byte)0xb3, @@ -109,15 +112,56 @@ public void tearDown() { */ @Setup(Level.Trial) public void setup() { - logger.info("Setting up shared resources"); - flags = RandomXUtils.getFlagsSet(); + logger.info("Setting up shared resources for RandomXBenchmark"); + + Set baseFlags = RandomXUtils.getRecommendedFlags(); // Get base recommended flags + + logger.info("Base recommended flags: {}", baseFlags); + + String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT); + boolean isMac = osName.contains("mac"); + + if ("JIT".equals(compilationMode)) { + logger.info("Compilation Mode: JIT selected. Enabling JIT flag."); + baseFlags.add(RandomXFlag.JIT); + + if (isMac) { + logger.info("On macOS, ensuring SECURE flag is also enabled when JIT is selected, as per user observation."); + baseFlags.add(RandomXFlag.SECURE); // Ensure SECURE is present for JIT on macOS + } else { + // For non-macOS systems, if SECURE is present and potentially conflicts with JIT, + // it might be safer to remove SECURE when JIT is prioritized. + if (baseFlags.contains(RandomXFlag.SECURE)) { + logger.warn("JIT and SECURE flags may be incompatible on non-macOS. Removing SECURE flag as JIT is prioritized for this mode."); + baseFlags.remove(RandomXFlag.SECURE); + } + } + } else { // NO_JIT or any other value + logger.info("Compilation Mode: NO_JIT selected. Ensuring JIT flag is disabled."); + baseFlags.remove(RandomXFlag.JIT); + // If not using JIT, SECURE flag (if recommended by getRecommendedFlags()) should typically be kept. + // No specific action needed here for SECURE if getRecommendedFlags() already handles it well for NO_JIT. + } + + this.flags = EnumSet.copyOf(baseFlags); // Use a defensive copy + logger.info("Benchmark (compilationMode={}) will use final flags for VMs and Cache: {}", compilationMode, this.flags); - cache = new RandomXCache(flags); - cache.init(BLOCK_TEMPLATE); + cache = new RandomXCache(this.flags); + cache.init(BLOCK_TEMPLATE); + logger.info("Shared RandomXCache initialized with BLOCK_TEMPLATE using flags: {}", this.flags); - dataset = new RandomXDataset(flags); + Set datasetAllocFlags = EnumSet.noneOf(RandomXFlag.class); + datasetAllocFlags.add(RandomXFlag.FULL_MEM); + + if (this.flags.contains(RandomXFlag.LARGE_PAGES)) { + datasetAllocFlags.add(RandomXFlag.LARGE_PAGES); + logger.info("LARGE_PAGES flag detected in VM/Cache flags, adding it to dataset allocation flags."); + } + + dataset = new RandomXDataset(datasetAllocFlags); + logger.info("Shared RandomXDataset allocated with specific dataset allocation flags: {}. It will be initialized by ThreadState if mining mode is used.", datasetAllocFlags); - logger.info("Shared resources setup completed"); + logger.info("Shared resources setup completed for RandomXBenchmark."); } /** diff --git a/src/test/java/io/xdag/crypto/randomx/RandomXCacheTest.java b/src/test/java/io/xdag/crypto/randomx/RandomXCacheTest.java index d810875..eb66285 100644 --- a/src/test/java/io/xdag/crypto/randomx/RandomXCacheTest.java +++ b/src/test/java/io/xdag/crypto/randomx/RandomXCacheTest.java @@ -26,7 +26,6 @@ import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; -import java.util.Set; import static org.junit.jupiter.api.Assertions.*; @@ -36,7 +35,6 @@ */ public class RandomXCacheTest { - private final Set flagsSet = RandomXUtils.getFlagsSet(); private final byte[] keyBytes = "test_key".getBytes(StandardCharsets.UTF_8); /** @@ -45,7 +43,7 @@ public class RandomXCacheTest { */ @Test public void testAllocAndRelease() { - try (RandomXCache cache = new RandomXCache(flagsSet)) { + try (RandomXCache cache = new RandomXCache(RandomXUtils.getRecommendedFlags())) { assertNotNull(cache.getCachePointer(), "Cache pointer should not be null."); } // Cache is automatically released here. } @@ -56,7 +54,7 @@ public void testAllocAndRelease() { */ @Test public void testInit() { - try (RandomXCache cache = new RandomXCache(flagsSet)) { + try (RandomXCache cache = new RandomXCache(RandomXUtils.getRecommendedFlags())) { assertNotNull(cache.getCachePointer(), "Cache pointer should not be null."); cache.init(keyBytes); } // Cache is automatically released here. diff --git a/src/test/java/io/xdag/crypto/randomx/RandomXDatasetTest.java b/src/test/java/io/xdag/crypto/randomx/RandomXDatasetTest.java index 69eb8fc..62836b7 100644 --- a/src/test/java/io/xdag/crypto/randomx/RandomXDatasetTest.java +++ b/src/test/java/io/xdag/crypto/randomx/RandomXDatasetTest.java @@ -23,6 +23,8 @@ */ package io.xdag.crypto.randomx; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; @@ -31,45 +33,104 @@ import static org.junit.jupiter.api.Assertions.*; /** - * Unit tests for RandomXDataset class. - * Tests the allocation, initialization and release of RandomX dataset resources. - * Includes tests for multi-threaded initialization based on CPU cores. + * Unit tests for the RandomXDataset class. */ public class RandomXDatasetTest { - private final Set flagsSet = RandomXUtils.getFlagsSet(); - private final byte[] keyBytes = "test_key".getBytes(StandardCharsets.UTF_8); + // Member variables initialized in setUp() + private Set flags; + private RandomXCache cache; + private RandomXDataset dataset; /** - * Tests the allocation and automatic release of RandomX dataset resources. - * Verifies that the dataset pointer is properly initialized. + * Sets up the necessary components before each test. + * Allocates and initializes a cache, and allocates a dataset. */ - @Test - public void testAllocAndRelease() { - try (RandomXDataset dataset = new RandomXDataset(flagsSet)) { - assertNotNull(dataset.getPointer(), "Dataset pointer should not be null."); + @BeforeEach + void setUp() { + // Get recommended flags + flags = RandomXUtils.getRecommendedFlags(); // Use refactored method + assertNotNull(flags, "Flags should not be null"); + + // Initialize cache, as dataset initialization depends on it + cache = new RandomXCache(flags); + // Use a specific key for dataset tests + cache.init("test_key_for_dataset".getBytes(StandardCharsets.UTF_8)); + assertNotNull(cache.getCachePointer(), "Cache pointer should not be null in setUp"); + + // Allocate dataset + dataset = new RandomXDataset(flags); + assertNotNull(dataset.getDatasetPointer(), "Dataset pointer should not be null after allocation in setUp"); // Use getDatasetPointer() + } + + /** + * Cleans up resources after each test. + * Releases the dataset and cache. + */ + @AfterEach + void tearDown() { + if (dataset != null) { + dataset.close(); + dataset = null; + } + if (cache != null) { + cache.close(); + cache = null; } } /** - * Tests the initialization of RandomX dataset with a cache. - * Measures and logs the initialization time. - * Verifies that initialization completes successfully and dataset pointer is valid. + * Tests if the dataset is allocated correctly in setUp and can be released. + * Allocation happens in setUp, release happens in tearDown. */ @Test - void testInitialization() { - try (RandomXCache cache = new RandomXCache(flagsSet); - RandomXDataset dataset = new RandomXDataset(flagsSet)) { - - cache.init(keyBytes); - long startTime = System.currentTimeMillis(); - - assertDoesNotThrow(() -> dataset.init(cache)); - - long elapsedTime = System.currentTimeMillis() - startTime; - System.out.println("Dataset initialized in " + elapsedTime + " ms."); - assertNotNull(dataset.getPointer(), "Dataset pointer should not be null."); - } + void testDatasetAllocationAndRelease() { + // Allocation is done in setUp, test existence and pointer validity here + assertNotNull(dataset, "Dataset should have been allocated in setUp"); + assertNotNull(dataset.getDatasetPointer(), "Dataset pointer should not be null after allocation"); // Use getDatasetPointer() + } + + /** + * Tests the initialization of the dataset. + * Assumes the dataset is allocated and cache is initialized in setUp. + */ + @Test + void testDatasetInit() { + assertNotNull(cache, "Cache should be initialized in setUp"); + assertNotNull(dataset, "Dataset should be allocated in setUp"); + assertNotNull(dataset.getDatasetPointer(), "Dataset pointer should not be null before init"); // Use getDatasetPointer() + + // Attempt to initialize the dataset using the initialized cache + assertDoesNotThrow(() -> dataset.init(cache), "Dataset initialization should not throw an exception"); } + /** + * Tests that initializing the dataset with a null cache throws an exception. + */ + @Test + void testDatasetInitWithNullCache() { + assertNotNull(dataset, "Dataset should be allocated in setUp"); + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + dataset.init(null); // Pass null cache + }); + assertTrue(exception.getMessage().contains("Valid cache instance"), "Exception message should indicate invalid cache"); + } + + /** + * Tests allocating a dataset with null flags throws an exception. + */ + @Test + void testAllocateWithNullFlags() { + assertThrows(IllegalArgumentException.class, () -> new RandomXDataset(null)); + } + + /** + * Tests allocating a dataset with empty flags throws an exception. + */ + @Test + void testAllocateWithEmptyFlags() { + assertThrows(IllegalArgumentException.class, () -> { + new RandomXDataset(java.util.EnumSet.noneOf(RandomXFlag.class)); + }); + } } diff --git a/src/test/java/io/xdag/crypto/randomx/RandomXJNALoaderTest.java b/src/test/java/io/xdag/crypto/randomx/RandomXJNALoaderTest.java deleted file mode 100644 index 509a52d..0000000 --- a/src/test/java/io/xdag/crypto/randomx/RandomXJNALoaderTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2022-2030 The XdagJ Developers - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package io.xdag.crypto.randomx; - -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -/** - * Unit tests for RandomXJNALoader class. - * Tests the loading and initialization of the RandomX native library through JNA. - */ -public class RandomXJNALoaderTest { - - /** - * Tests that the RandomX native library is properly loaded and initialized. - * Verifies that the singleton instance is not null after loading. - */ - @Test - public void testLibraryLoading() { - assertNotNull(RandomXJNALoader.getInstance(), "RandomXJNA instance should not be null."); - } - -} \ No newline at end of file diff --git a/src/test/java/io/xdag/crypto/randomx/RandomXTemplateTest.java b/src/test/java/io/xdag/crypto/randomx/RandomXTemplateTest.java index 01d825c..520267f 100644 --- a/src/test/java/io/xdag/crypto/randomx/RandomXTemplateTest.java +++ b/src/test/java/io/xdag/crypto/randomx/RandomXTemplateTest.java @@ -32,10 +32,7 @@ import static org.junit.jupiter.api.Assertions.*; /** - * Unit tests for RandomXTemplate class. - * Tests the hash calculation and key change functionality. - * Verifies that the RandomX hash calculations produce expected results - * and that key changes are handled correctly. + * Unit tests for the RandomXTemplate class. */ public class RandomXTemplateTest { @@ -51,17 +48,17 @@ public void testCalculateHash() { byte[] key3Bytes = key3.getBytes(StandardCharsets.UTF_8); byte[] key4Bytes = key4.getBytes(StandardCharsets.UTF_8); - Set flagSet = RandomXUtils.getFlagsSet(); + Set flagSet = RandomXUtils.getRecommendedFlags(); RandomXCache cache = new RandomXCache(flagSet); cache.init(key1Bytes); HexFormat hex = HexFormat.of(); RandomXTemplate template = RandomXTemplate.builder() - .cache(cache) - .miningMode(false) - .flags(flagSet) - .build(); + .cache(cache) + .miningMode(false) + .flags(flagSet) + .build(); template.init(); byte[] hash = template.calculateHash(key2Bytes); assertEquals("781315d3e78dc16a5060cb87677ca548d8b9aabdef5221a2851b2cc72aa2875b", hex.formatHex(hash)); @@ -69,10 +66,10 @@ public void testCalculateHash() { cache = new RandomXCache(flagSet); cache.init(key3Bytes); template = RandomXTemplate.builder() - .cache(cache) - .miningMode(false) - .flags(flagSet) - .build(); + .cache(cache) + .miningMode(false) + .flags(flagSet) + .build(); template.init(); hash = template.calculateHash(key3Bytes); assertEquals("33e17472f3f691252d1f28a2e945b990c5878f514034006df5a06a23dc1cada0", hex.formatHex(hash)); @@ -80,10 +77,10 @@ public void testCalculateHash() { cache = new RandomXCache(flagSet); cache.init(key4Bytes); template = RandomXTemplate.builder() - .cache(cache) - .miningMode(false) - .flags(flagSet) - .build(); + .cache(cache) + .miningMode(false) + .flags(flagSet) + .build(); template.init(); hash = template.calculateHash(key4Bytes); assertEquals("5d4155322b69284bf45fa8ac182384490a87c55a6af47b7e72558cafa8832bd9", hex.formatHex(hash)); @@ -97,17 +94,17 @@ public void testChangeKey() { String key2 = "world xdagj-native-randomx"; byte[] key2Bytes = key2.getBytes(StandardCharsets.UTF_8); - Set flagSet = RandomXUtils.getFlagsSet(); + Set flagSet = RandomXUtils.getRecommendedFlags(); RandomXCache cache = new RandomXCache(flagSet); cache.init(key1Bytes); HexFormat hex = HexFormat.of(); RandomXTemplate template = RandomXTemplate.builder() - .cache(cache) - .miningMode(false) - .flags(flagSet) - .build(); + .cache(cache) + .miningMode(false) + .flags(flagSet) + .build(); template.init(); byte[] hash = template.calculateHash(key1Bytes); assertEquals("5d4155322b69284bf45fa8ac182384490a87c55a6af47b7e72558cafa8832bd9", hex.formatHex(hash)); diff --git a/src/test/java/io/xdag/crypto/randomx/RandomXTests.java b/src/test/java/io/xdag/crypto/randomx/RandomXTests.java index 921d8a8..aeb7163 100644 --- a/src/test/java/io/xdag/crypto/randomx/RandomXTests.java +++ b/src/test/java/io/xdag/crypto/randomx/RandomXTests.java @@ -23,6 +23,7 @@ */ package io.xdag.crypto.randomx; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -31,8 +32,10 @@ import java.nio.charset.StandardCharsets; import java.util.HexFormat; import java.util.Set; +import java.util.EnumSet; +import java.util.Arrays; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for RandomX hash calculation functionality. @@ -41,26 +44,52 @@ public class RandomXTests { private RandomXTemplate template; private HexFormat hex; + private RandomXCache cache; + private RandomXDataset dataset; /** * Sets up the test environment before each test. - * Initializes RandomX template with cache and dataset. + * Initializes RandomX template for light mode operations. */ @BeforeEach public void setUp() { - Set flagsSet = RandomXUtils.getFlagsSet(); - RandomXCache cache = new RandomXCache(flagsSet); - RandomXDataset dataset = new RandomXDataset(flagsSet); + Set flags = RandomXUtils.getRecommendedFlags(); + assertNotNull(flags, "Flags should not be null in setUp"); + + // For these tests, which are light-mode (non-mining), a dataset is not strictly necessary + // for the template's operation. We will initialize the cache only. + cache = new RandomXCache(flags); + // this.dataset = new RandomXDataset(flags); // Not creating dataset for light mode tests + this.dataset = null; // Explicitly null + template = RandomXTemplate.builder() .cache(cache) - .dataset(dataset) + .dataset(null) // Explicitly pass null for dataset in light mode .miningMode(false) - .flags(flagsSet) + .flags(flags) .build(); template.init(); hex = HexFormat.of(); } + @AfterEach + public void tearDown() { + if (template != null) { + template.close(); + template = null; + } + if (cache != null) { + cache.close(); + cache = null; + } + // Since this.dataset is now explicitly set to null or not created in setUp for these tests, + // this check will correctly skip closing a null dataset. + if (dataset != null) { + dataset.close(); + dataset = null; + } + } + /** * Tests string hash calculation with different key-input pairs. * Verifies that the hash output matches expected values. @@ -93,7 +122,7 @@ void testCalcStringCommitment(String key, String input, String output) { byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8); template.changeKey(keyBytes); - assertEquals(output, hex.formatHex(template.calcStringCommitment(inputBytes))); + assertEquals(output, hex.formatHex(template.calculateCommitment(inputBytes))); } /** diff --git a/src/test/java/io/xdag/crypto/randomx/RandomXVMTest.java b/src/test/java/io/xdag/crypto/randomx/RandomXVMTest.java index f15ed6e..fbfff9d 100644 --- a/src/test/java/io/xdag/crypto/randomx/RandomXVMTest.java +++ b/src/test/java/io/xdag/crypto/randomx/RandomXVMTest.java @@ -24,41 +24,217 @@ package io.xdag.crypto.randomx; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; import java.util.Set; import static org.junit.jupiter.api.Assertions.*; /** - * Unit tests for RandomXVM class. - * Tests the hash calculation functionality using RandomX virtual machine. - * Verifies the basic operations and output format of the VM implementation. + * Unit tests for the RandomXVM class. + * Focuses on VM creation, state changes (cache/dataset), and hash calculations. */ public class RandomXVMTest { /** - * Tests hash calculation using RandomXVM. - * This test verifies: - * 1. VM initialization with given flags and cache - * 2. Hash calculation with test input - * 3. Output validation: - * - Not null check - * - Correct length (32 bytes) check - * 4. Proper resource cleanup using try-with-resources + * Tests the creation of a RandomX VM in light mode (no dataset). + * Verifies that the VM pointer is not null after creation. */ @Test - public void testVMHashCalculation() { - Set flags = RandomXUtils.getFlagsSet(); - byte[] keyBytes = "test_key".getBytes(); - byte[] input = "test_input".getBytes(); - - try (RandomXCache cache = new RandomXCache(flags); - RandomXVM vm = new RandomXVM(flags, cache, null)) { - cache.init(keyBytes); - byte[] output = vm.calculateHash(input); - assertNotNull(output, "Output should not be null."); - assertEquals(32, output.length, "Output size should be 32 bytes."); + void testVMCreationLightMode() { + Set testFlags = RandomXUtils.getRecommendedFlags(); + // Ensure FULL_MEM is not present for a true light mode test, + // though getRecommendedFlags might include it. + // RandomXVM itself doesn't add/remove FULL_MEM; RandomXTemplate does. + // For this test, we explicitly remove it if present to test light VM creation. + testFlags.remove(RandomXFlag.FULL_MEM); + + try (RandomXCache localCache = new RandomXCache(testFlags)) { + localCache.init("test_key_vm_light".getBytes(StandardCharsets.UTF_8)); + assertNotNull(localCache.getCachePointer(), "Cache pointer should not be null for VM creation."); + + try (RandomXVM vm = new RandomXVM(testFlags, localCache, null /* No dataset for light mode */)) { + assertNotNull(vm.getVmPointer(), "VM pointer should not be null in light mode."); + assertEquals(testFlags, vm.getFlags(), "VM flags should match those used for creation."); + assertSame(localCache, vm.getCache(), "VM should hold the provided cache instance."); + assertNull(vm.getDataset(), "Dataset should be null in light mode."); + } + } + } + + /** + * Tests the creation of a RandomX VM in full mode (with a dataset). + * Verifies that the VM pointer is not null and dataset is correctly associated. + */ + @Test + void testVMCreationFullMode() { + Set recommendedFlags = RandomXUtils.getRecommendedFlags(); + + // For full mode, a VM operates with a dataset. + // The FULL_MEM flag is primarily for dataset allocation/initialization. + // We ensure flags used for creating cache, dataset, and VM are consistent for this mode. + Set fullModeFlags = EnumSet.copyOf(recommendedFlags); + fullModeFlags.add(RandomXFlag.FULL_MEM); // Ensure FULL_MEM is present for full mode context + + try (RandomXCache localCache = new RandomXCache(fullModeFlags); + RandomXDataset localDataset = new RandomXDataset(fullModeFlags)) { + + localCache.init("test_key_vm_full".getBytes(StandardCharsets.UTF_8)); + assertNotNull(localCache.getCachePointer(), "Cache pointer should not be null."); + + localDataset.init(localCache); // Dataset needs initialized cache + assertNotNull(localDataset.getDatasetPointer(), "Dataset pointer should not be null."); + + try (RandomXVM vm = new RandomXVM(fullModeFlags, localCache, localDataset)) { + assertNotNull(vm.getVmPointer(), "VM pointer should not be null in full mode."); + assertEquals(fullModeFlags, vm.getFlags(), "VM flags should match."); + assertSame(localCache, vm.getCache(), "VM should use the provided cache."); + assertSame(localDataset, vm.getDataset(), "VM should use the provided dataset."); + } + } + } + + /** + * Tests hash calculation using the RandomX VM. + * Ensures that a hash can be calculated and is of the correct length. + */ + @Test + void testVMHashCalculation() { + Set testFlags = RandomXUtils.getRecommendedFlags(); + testFlags.remove(RandomXFlag.FULL_MEM); // Typically light mode for simple hash tests + + byte[] keyBytes = "test_key_hash_calc".getBytes(StandardCharsets.UTF_8); + byte[] input = "test_input_for_hash".getBytes(StandardCharsets.UTF_8); + + try (RandomXCache localCache = new RandomXCache(testFlags)) { + localCache.init(keyBytes); + try (RandomXVM vm = new RandomXVM(testFlags, localCache, null)) { + byte[] hash = vm.calculateHash(input); + assertNotNull(hash, "Calculated hash should not be null."); + assertEquals(32, hash.length, "Hash should be 32 bytes long."); + } } } + /** + * Tests changing the cache in an existing VM. + */ + @Test + void testVMSetCache() { + Set initialFlags = RandomXUtils.getRecommendedFlags(); + initialFlags.remove(RandomXFlag.FULL_MEM); + + try (RandomXCache cache1 = new RandomXCache(initialFlags); + RandomXCache cache2 = new RandomXCache(initialFlags)) { + + cache1.init("key_for_cache1".getBytes(StandardCharsets.UTF_8)); + cache2.init("key_for_cache2".getBytes(StandardCharsets.UTF_8)); + + try (RandomXVM vm = new RandomXVM(initialFlags, cache1, null)) { + assertSame(cache1, vm.getCache(), "Initial cache should be cache1."); + byte[] hash1 = vm.calculateHash("input".getBytes(StandardCharsets.UTF_8)); + + vm.setCache(cache2); // Change the cache + assertSame(cache2, vm.getCache(), "Updated cache should be cache2."); + byte[] hash2 = vm.calculateHash("input".getBytes(StandardCharsets.UTF_8)); + + assertNotNull(hash1, "Hash1 should not be null."); + assertNotNull(hash2, "Hash2 should not be null."); + assertFalse(java.util.Arrays.equals(hash1, hash2), + "Hashes from VMs with different caches (different keys) should differ."); + } + } + } + + /** + * Tests changing the dataset in an existing VM. + * This test assumes the VM is created with flags compatible with dataset usage. + */ + @Test + // @Disabled("Dataset changes and interactions can be complex; needs careful C API review for direct VM manipulation if not using RandomXTemplate") // Re-enabling test + void testVMSetDataset() { + Set fullModeFlags = RandomXUtils.getRecommendedFlags(); + fullModeFlags.add(RandomXFlag.FULL_MEM); // Ensure FULL_MEM for dataset context + + byte[] inputBytes = "test_input_for_dataset_change".getBytes(StandardCharsets.UTF_8); + + try (RandomXCache cacheForDs1 = new RandomXCache(fullModeFlags); + RandomXDataset dataset1 = new RandomXDataset(fullModeFlags); + RandomXCache cacheForDs2 = new RandomXCache(fullModeFlags); // Separate cache for dataset2 + RandomXDataset dataset2 = new RandomXDataset(fullModeFlags)) { + + cacheForDs1.init("key_for_dataset1".getBytes(StandardCharsets.UTF_8)); + dataset1.init(cacheForDs1); + assertNotNull(dataset1.getDatasetPointer(), "Dataset1 pointer should not be null."); + + cacheForDs2.init("key_for_dataset2".getBytes(StandardCharsets.UTF_8)); // Different key for cache2 + dataset2.init(cacheForDs2); + assertNotNull(dataset2.getDatasetPointer(), "Dataset2 pointer should not be null."); + + // VM initially uses dataset1 (via its cache, cacheForDs1) + try (RandomXVM vm = new RandomXVM(fullModeFlags, cacheForDs1, dataset1)) { + assertSame(dataset1, vm.getDataset(), "Initial dataset should be dataset1."); + assertSame(cacheForDs1, vm.getCache(), "Initial cache for VM should be cacheForDs1."); + + byte[] hash1 = vm.calculateHash(inputBytes); + assertNotNull(hash1, "Hash1 should not be null."); + + vm.setCache(cacheForDs2); // Change cache first + vm.setDataset(dataset2); // Then change dataset + + assertSame(dataset2, vm.getDataset(), "Updated dataset should be dataset2."); + assertSame(cacheForDs2, vm.getCache(), "Updated cache for VM should be cacheForDs2."); + + byte[] hash2 = vm.calculateHash(inputBytes); + assertNotNull(hash2, "Hash2 should not be null."); + + assertFalse(java.util.Arrays.equals(hash1, hash2), + "Hashes from VMs with different datasets (and their associated caches) should differ."); + } + } + } + + /** + * Tests creation of VM with null flags (should throw IllegalArgumentException). + */ + @Test + void testVMCreationNullFlags() { + try (RandomXCache localCache = new RandomXCache(RandomXUtils.getRecommendedFlags())) { + localCache.init("key".getBytes(StandardCharsets.UTF_8)); + assertThrows(IllegalArgumentException.class, () -> { + new RandomXVM(null, localCache, null); + }); + } + } + + /** + * Tests creation of VM with null cache (should throw IllegalArgumentException). + */ + @Test + void testVMCreationNullCache() { + Set testFlags = RandomXUtils.getRecommendedFlags(); + assertThrows(IllegalArgumentException.class, () -> { + new RandomXVM(testFlags, null, null); + }); + } + + /** + * Tests setting a null cache on an existing VM (should throw IllegalArgumentException). + */ + @Test + void testVMSetCacheWithNull() { + Set testFlags = RandomXUtils.getRecommendedFlags(); + testFlags.remove(RandomXFlag.FULL_MEM); // Light mode is sufficient + + try (RandomXCache initialCache = new RandomXCache(testFlags)) { + initialCache.init("initial_key".getBytes(StandardCharsets.UTF_8)); + try (RandomXVM vm = new RandomXVM(testFlags, initialCache, null)) { + assertThrows(IllegalArgumentException.class, () -> { + vm.setCache(null); // Attempt to set null cache + }, "Setting a null cache should throw IllegalArgumentException."); + } + } + } } \ No newline at end of file diff --git a/src/main/resources/simplelogger.properties b/src/test/resources/simplelogger.properties similarity index 100% rename from src/main/resources/simplelogger.properties rename to src/test/resources/simplelogger.properties