Files
wikideck/docs/randomx-native.md
T
2026-10-19 16:47:27 -04:00

9.7 KiB

Native RandomX engine

This is the CPU proof engine used by the Wikideck testnet node and explicit opt-in native miner. Browser mining has been removed from the active application. The Docker image builds this pinned implementation and runs its known-vector test. There is no GPU path, simulated hash, or production fallback hash.

Source and build

RandomX v1.2.1 is fetched by CMake FetchContent from the official HTTPS Git repository, pinned to 102f8acf90a7649ada410de5499a7ec62e49e1da (also the upstream v1.2.1 tag). The first configuration requires network access and Git; later builds reuse the checkout. Keep upstream copyright/license notices when distributing binaries. Do not override FetchContent sources with untrusted local code.

Requirements: CMake 3.24+, Git, a C/C++17 toolchain, and a 64-bit host for fast mode. MSVC needs its C++ and MASM tools in a developer terminal. MinGW needs its compiler and make on PATH; select the MinGW Makefiles generator. Linux/macOS need their normal C/C++ build tools. No administrator privileges or huge pages are required. On MSVC, upstream's pinned build invokes its own PowerShell assembly-generation script; review upstream build tooling before running under restrictive policies.

From the repository root:

cmake -S native -B native/build -DCMAKE_BUILD_TYPE=Release -DWIKIDECK_RANDOMX_TESTS=OFF
cmake --build native/build --config Release --target wikideck-randomx --parallel 2

Convenience scripts (two compiler jobs by default):

sh scripts/build-randomx.sh
sh scripts/build-randomx.sh --test
./scripts/build-randomx.ps1
./scripts/build-randomx.ps1 -Generator 'MinGW Makefiles' -Test

The scripts propagate configure/build/test failures. PowerShell execution policy must permit the script; do not bypass a managed policy. POSIX customization: JOBS=2, RANDOMX_BUILD_DIR=/absolute/build/path; PowerShell customization: -Jobs 2, -BuildDirectory <path>. Select a toolchain/generator through the normal CMake environment or a manual configuration. Don't switch generators in an existing build directory.

All generators place the executable directly under native/build (with .exe on Windows), including Release builds with multi-config generators. Upstream tests/benchmark/codegen are excluded from the default build: v1.2.1 does not provide a tests-disable option. No benchmark or dataset-heavy fast-mode test runs automatically.

Modes and resource use

Run wikideck-randomx with no arguments for light mode (256 MiB cache plus approximately 2 MiB VM scratchpad and runtime overhead). --fast adds the real approximately 2080 MiB dataset; the 256 MiB cache is retained for key changes, so budget over 2.3 GiB per process. Dataset initialization is single-threaded and can be expensive. Neither mode allocates RandomX memory until its first valid request. Cache, dataset, and VM are reused for the same key. A changed key reinitializes the cache and, in fast mode, the entire dataset before creating a fresh VM.

Flags start with randomx_get_flags() (CPU-supported AES/JIT/Argon2 optimization). RANDOMX_FLAG_SECURE is explicitly added for VM JIT W^X, and upstream is compiled with RANDOMX_FORCE_SECURE for cache JIT policy too. Platforms without JIT use the reference interpreter; allocation/JIT failures are fatal, never a fake PoW result. No large pages are requested. RAII releases VM, dataset, then cache on normal EOF and caught errors. Public PoW key/cache data is not secret and is released, not claimed to be securely erased from physical memory.

Line protocol

One ASCII command per LF-terminated line; CRLF is also accepted. Exactly one ASCII response line, flushed immediately, per valid command:

hash <key64hex> <blobhex>
<hash64hex>

mine <key64hex> <blobhex> <target64hex> <nonceOffsetDecimal> <maxAttemptsDecimal>
found <hash64hex> <nonceDecimal>
exhausted

found and exhausted are alternatives, not two responses. Input hex may use either case; output hashes are lowercase in the RandomX byte order. Keys and targets are exactly 32 bytes (64 hex characters); blobs are 1..8192 bytes. Tokens use exactly one ASCII space, without leading/trailing whitespace. Commands are capped at 16600 bytes before LF. Invalid characters, malformed/overflowing unsigned decimal, extra tokens, invalid bounds, or a partial final line cause a concise stderr error and nonzero exit, with no fabricated response on stdout. Empty stdin EOF exits successfully. There are no stdout banners or diagnostic logs.

Mining requires maxAttempts in 1..10000 and nonceOffset + 8 <= blob.size. The initial nonce is read from the blob's existing eight little-endian bytes. The first attempt uses that nonce, then increments by one and rewrites LE8 for each attempt. Only those eight bytes change. At uint64 maximum, search ends without wrapping. The native response contains an exact unsigned decimal nonce.

Target convention: interpret the 32 output bytes and target bytes as unsigned BIG-ENDIAN integers; accept hash <= target. This intentionally differs from Monero's convention. In particular, equal hashes pass. Never reverse the hash or target when using this interface.

Node API

RandomX exports:

interface PowEngine {
  hash(key: string, blob: Uint8Array): Promise<string>;
  close(): void;
}
class RandomX implements PowEngine {
  constructor(options?: { binary?: string; fast?: boolean });
  hash(key: string, blob: Uint8Array): Promise<string>;
  mine(key: string, blob: Uint8Array, target: string,
       nonceOffset: number, maxAttempts: number):
    Promise<{ hash: string; nonce: number } | null>;
  close(): void;
}

Binary precedence: options.binary, RANDOMX_BINARY, then the executable under the repository's native build directory (resolved relative to this module, independent of working directory). Relative explicit paths are resolved against the caller's working directory. Configure only a trusted executable: this is a native-code trust boundary, not a sandbox. The adapter uses spawn with a fixed argument array, shell: false, and pipe stdio; user data only enters stdin after strict validation. Missing/non-executable files throw in the constructor. Other spawn failures are retained and reject current/future requests.

Each instance owns one long-lived child, one active command, and at most 64 total active/queued requests. Calls are FIFO, including mixed hash/mine calls and key changes. Inputs are snapshotted before queuing. The 120-second deadline starts when each command is sent (includes cold cache/dataset initialization); queued requests wait for earlier work. Keep mining batches small enough to finish on the host CPU. The process is killed on timeout, malformed/unsolicited/oversized output, stream error, or crash, rejecting all outstanding work and subsequent calls. Responses and retained stderr are bounded. No automatic retry or process restart.

mine returns null for exhaustion and validates returned hash syntax, target, and nonce range (it does not independently recompute a native result). Because the API returns number, its entire requested nonce range must fit Number.MAX_SAFE_INTEGER; higher uint64 nonces are supported only by the direct native protocol. Invalid inputs reject without sending native work.

Always call close() in finally. It is idempotent, rejects pending/queued work, and sends EOF for graceful RAII cleanup; a busy child is forcibly killed after one second. A failed/closed instance cannot be reopened.

Real upstream known-vector check

The opt-in --self-test executable argument validates upstream's exact Hash test 1a/2a:

  • Key: ASCII test key 000 (12 bytes, not NUL-terminated or zero-padded).
  • Input: ASCII This is a test (14 bytes, no terminator).
  • Expected RandomX hash: 639183aae1bf4c9a35884cb46b09cad9175f04efd7684e7262a0ac1c2f0b4e3f.

The self-test uses an internal API so the line protocol retains its strict 32-byte-key requirement. Padding this upstream key to 32 bytes would change the hash and is not a valid reproduction of the vector. Self-test success is reported on stderr only, with exit zero; mismatch exits nonzero. --fast --self-test checks the same real vector in dataset mode, but requires the full memory allocation.

cmake -S native -B native/build -DCMAKE_BUILD_TYPE=Release -DWIKIDECK_RANDOMX_TESTS=ON
cmake --build native/build --config Release --target wikideck-randomx --parallel 2
ctest --test-dir native/build -C Release --output-on-failure

This test is OFF by default and is separate from the application's tests. Without a built native binary, no native engine or native test is run by the application.

Validation record

Verified on Windows x64 with CMake 3.26.4, MinGW GCC 13.1.0 and Node 24.22.0:

  • Release native build with two compiler jobs; opt-in CTest upstream vector passed.
  • Production TypeScript/application build passed.
  • Ad-hoc actual-native light-mode checks passed: FIFO hashes, same-key reuse, key changes and restoration, mining success/exhaustion, target equality and big-endian comparison, LE8 nonce handling, 8192-byte blob, strict invalid-input rejection, native uint64 nonce output, missing binary, queue bounds, crash rejection, idempotent close, and stdin EOF.
  • Both build scripts passed syntax checks.

Fast-mode allocation/runtime and Linux/macOS/MSVC builds were not run. The memory and initialization cost of the full dataset remains an explicit opt-in.