Initial Vexta CPU Miner v0.1.0

This commit is contained in:
VextaProject 2026-09-20 21:24:31 +00:00
commit 703213cb9f
5 changed files with 1300 additions and 0 deletions

30
.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# .NET build output
bin/
obj/
# Publish / release working directories
publish-*/
release-*/
# Release archives - uploaded separately to GitHub Releases
*.zip
*.tar.gz
*.sha256
# Local backups and development snapshots
*.before-*
*.working-*
*.failed-*
*.v0.1.0-working
# Local third-party build/source tree
third_party/
# IDE / OS files
.vs/
.vscode/
.idea/
*.user
*.suo
.DS_Store
Thumbs.db

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Vexta Project
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.

967
Program.cs Normal file
View file

@ -0,0 +1,967 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net.Sockets;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.DataEncoders;
using Vexta.CpuMiner;
internal static class Program
{
private static readonly object ConsoleLock = new();
private static readonly BigInteger Diff1 =
BigInteger.Parse(
"00ffff0000000000000000000000000000000000000000000000000000",
NumberStyles.HexNumber);
private const string Realm = "vexta-cpuminer";
private static readonly SemaphoreSlim WriterLock = new(1, 1);
private static readonly SemaphoreSlim JobLock = new(1, 1);
private static readonly ConcurrentDictionary<long, string> PendingSubmits = new();
private static StreamWriter? writer;
private static string worker = "";
private static string password = "x";
private static string extraNonce1 = "";
private static int extraNonce2Size;
private static int threadCount;
private static double currentDifficulty;
private static CancellationTokenSource? currentJobCts;
private static Task? currentMiningTask;
private static string? currentSeed;
private static long nextSubmitId = 100;
private static long totalHashes;
private static long acceptedShares;
private static long rejectedShares;
private static async Task Main(string[] args)
{
try
{
await RunAsync(args);
}
catch(OperationCanceledException)
{
}
catch(Exception ex)
{
Console.Error.WriteLine();
Console.Error.WriteLine($"FATAL: {ex.Message}");
Environment.ExitCode = 1;
}
}
private static async Task RunAsync(string[] args)
{
if(args.Length == 0 || Array.Exists(args, x => x == "-h" || x == "--help"))
{
PrintHelp();
return;
}
var poolUrl = GetArg(args, "-o")
?? throw new ArgumentException("Missing required option: -o");
worker = GetArg(args, "-u")
?? throw new ArgumentException("Missing required option: -u");
password = GetArg(args, "-p") ?? "x";
var threadText = GetArg(args, "-t");
threadCount = threadText != null
? int.Parse(threadText, CultureInfo.InvariantCulture)
: Environment.ProcessorCount;
if(threadCount < 1)
throw new ArgumentException("Thread count must be at least 1");
if(!poolUrl.StartsWith("stratum+tcp://", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("Pool URL must start with stratum+tcp://");
var endpoint = poolUrl["stratum+tcp://".Length..];
var colon = endpoint.LastIndexOf(':');
if(colon <= 0 ||
!int.TryParse(endpoint[(colon + 1)..], out var port) ||
port < 1 ||
port > 65535)
{
throw new ArgumentException(
"Invalid Stratum URL. Expected stratum+tcp://HOST:PORT");
}
var host = endpoint[..colon];
using var shutdown = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
if(!shutdown.IsCancellationRequested)
{
Console.WriteLine();
Console.WriteLine("Stopping miner...");
shutdown.Cancel();
}
};
PrintBanner();
Console.WriteLine($"Pool : {host}:{port}");
Console.WriteLine($"Worker : {worker}");
Console.WriteLine($"Threads : {threadCount}");
Console.WriteLine();
var statsTask = StatsLoopAsync(shutdown.Token);
while(!shutdown.IsCancellationRequested)
{
try
{
await RunSessionAsync(host, port, shutdown.Token);
}
catch(OperationCanceledException) when(shutdown.IsCancellationRequested)
{
break;
}
catch(Exception ex)
{
if(!shutdown.IsCancellationRequested)
{
Console.WriteLine();
Console.WriteLine($"Disconnected: {ex.Message}");
}
await StopCurrentJobAsync();
if(shutdown.IsCancellationRequested)
break;
Console.WriteLine("Reconnecting in 5 seconds...");
try
{
await Task.Delay(TimeSpan.FromSeconds(5), shutdown.Token);
}
catch(OperationCanceledException)
{
break;
}
}
}
await StopCurrentJobAsync();
try
{
await statsTask;
}
catch(OperationCanceledException)
{
}
Console.WriteLine();
Console.WriteLine(
$"Stopped. Accepted: {Interlocked.Read(ref acceptedShares)}, " +
$"Rejected: {Interlocked.Read(ref rejectedShares)}");
}
private static async Task RunSessionAsync(
string host,
int port,
CancellationToken shutdown)
{
Console.WriteLine($"Connecting to {host}:{port} ...");
using var tcp = new TcpClient();
await tcp.ConnectAsync(host, port, shutdown);
// ReadLineAsync() in .NET 6 cannot take a CancellationToken.
// Closing the socket on shutdown unblocks any pending Stratum read.
using var shutdownRegistration = shutdown.Register(() =>
{
try
{
tcp.Dispose();
}
catch
{
}
});
using var stream = tcp.GetStream();
using var reader = new StreamReader(stream, Encoding.ASCII);
writer = new StreamWriter(stream, Encoding.ASCII)
{
AutoFlush = true,
NewLine = "\n"
};
extraNonce1 = "";
extraNonce2Size = 0;
currentDifficulty = 0;
await SendLineAsync(
"{\"id\":1,\"method\":\"mining.subscribe\",\"params\":[\"VextaCpuMiner/0.1\"]}",
shutdown);
JsonElement? firstNotify = null;
var subscribed = false;
var authorized = false;
var authorizeSent = false;
while(!shutdown.IsCancellationRequested &&
(!subscribed || !authorized || firstNotify == null || currentDifficulty <= 0))
{
var line = await reader.ReadLineAsync()
?? throw new IOException("Stratum connection closed");
using var doc = JsonDocument.Parse(line);
var root = doc.RootElement;
if(root.TryGetProperty("id", out var id) &&
id.ValueKind == JsonValueKind.Number)
{
var value = id.GetInt64();
if(value == 1)
{
var result = root.GetProperty("result");
extraNonce1 = result[1].GetString()
?? throw new Exception("Pool returned invalid ExtraNonce1");
extraNonce2Size = result[2].GetInt32();
subscribed = true;
Console.WriteLine(
$"Subscribed. ExtraNonce1={extraNonce1}, " +
$"ExtraNonce2Size={extraNonce2Size}");
if(!authorizeSent)
{
await SendAuthorizeAsync(shutdown);
authorizeSent = true;
}
continue;
}
if(value == 2)
{
if(root.TryGetProperty("result", out var result) &&
result.ValueKind == JsonValueKind.True)
{
authorized = true;
WriteColor("Authorized.", ConsoleColor.Green);
}
else
{
throw new Exception("Pool authorization failed");
}
continue;
}
}
if(root.TryGetProperty("method", out var method) &&
method.ValueKind == JsonValueKind.String)
{
var methodName = method.GetString();
if(methodName == "mining.block_found")
{
var parameters = root.GetProperty("params");
var height = parameters[0].GetInt64();
var hash = parameters[1].GetString() ?? "";
WriteColor(
$"*** BLOCK FOUND! Height {height} ***",
ConsoleColor.Magenta);
WriteColor(
$"Hash: {hash}",
ConsoleColor.Magenta);
continue;
}
if(methodName == "mining.set_difficulty")
{
currentDifficulty = root
.GetProperty("params")[0]
.GetDouble();
Console.WriteLine(
$"Difficulty set to {currentDifficulty.ToString("G", CultureInfo.InvariantCulture)}");
continue;
}
if(methodName == "mining.notify")
{
firstNotify = root.GetProperty("params").Clone();
continue;
}
}
}
if(firstNotify == null)
throw new Exception("Pool did not provide an initial mining job");
await SwitchJobAsync(firstNotify.Value, shutdown);
Console.WriteLine("Mining started.");
Console.WriteLine();
while(!shutdown.IsCancellationRequested)
{
var line = await reader.ReadLineAsync()
?? throw new IOException("Stratum connection closed");
using var doc = JsonDocument.Parse(line);
var root = doc.RootElement;
if(root.TryGetProperty("method", out var method) &&
method.ValueKind == JsonValueKind.String)
{
var methodName = method.GetString();
if(methodName == "mining.block_found")
{
var parameters = root.GetProperty("params");
var height = parameters[0].GetInt64();
var hash = parameters[1].GetString() ?? "";
WriteColor(
$"*** BLOCK FOUND! Height {height} ***",
ConsoleColor.Magenta);
WriteColor(
$"Hash: {hash}",
ConsoleColor.Magenta);
continue;
}
if(methodName == "mining.set_difficulty")
{
var diff = root
.GetProperty("params")[0]
.GetDouble();
if(diff > 0)
{
currentDifficulty = diff;
Console.WriteLine(
$"Difficulty changed to {diff.ToString("G", CultureInfo.InvariantCulture)}");
}
continue;
}
if(methodName == "mining.notify")
{
var job = root.GetProperty("params").Clone();
await SwitchJobAsync(job, shutdown);
continue;
}
continue;
}
if(root.TryGetProperty("id", out var id) &&
id.ValueKind == JsonValueKind.Number)
{
var submitId = id.GetInt64();
if(PendingSubmits.TryRemove(submitId, out var submittedJob))
{
var accepted =
root.TryGetProperty("result", out var result) &&
result.ValueKind == JsonValueKind.True;
if(accepted)
{
var count = Interlocked.Increment(ref acceptedShares);
WriteColor(
$"ACCEPTED share #{count} job={submittedJob}",
ConsoleColor.Green);
}
else
{
var count = Interlocked.Increment(ref rejectedShares);
string errorText = "";
if(root.TryGetProperty("error", out var error) &&
error.ValueKind != JsonValueKind.Null)
{
errorText = $" error={error}";
}
WriteColor(
$"REJECTED share #{count} job={submittedJob}{errorText}",
ConsoleColor.Red);
}
}
}
}
}
private static async Task SendAuthorizeAsync(CancellationToken ct)
{
var json = JsonSerializer.Serialize(new
{
id = 2,
method = "mining.authorize",
@params = new object[] { worker, password }
});
await SendLineAsync(json, ct);
}
private static async Task SendLineAsync(
string line,
CancellationToken ct)
{
var target = writer
?? throw new IOException("Stratum writer is not connected");
await WriterLock.WaitAsync(ct);
try
{
await target.WriteLineAsync(line);
await target.FlushAsync();
}
finally
{
WriterLock.Release();
}
}
private static async Task SwitchJobAsync(
JsonElement job,
CancellationToken shutdown)
{
await JobLock.WaitAsync(shutdown);
try
{
if(job.ValueKind != JsonValueKind.Array || job.GetArrayLength() < 10)
{
throw new Exception(
"RandomX Stratum job does not contain the 10th randomx_seed parameter");
}
var parsed = ParseJob(job);
await StopCurrentJobAsync();
if(currentSeed != null &&
!currentSeed.Equals(parsed.Seed, StringComparison.OrdinalIgnoreCase))
{
VextaRandomX.DeleteSeed(Realm, currentSeed);
currentSeed = null;
}
if(currentSeed == null)
{
WriteColor(
"Preparing RandomX dataset...",
ConsoleColor.Cyan);
Console.WriteLine($"Seed: {parsed.Seed}");
VextaRandomX.CreateSeed(
Realm,
parsed.Seed,
vmCount: threadCount);
currentSeed = parsed.Seed;
WriteColor(
"RandomX dataset ready.",
ConsoleColor.Green);
}
currentJobCts =
CancellationTokenSource.CreateLinkedTokenSource(shutdown);
var token = currentJobCts.Token;
WriteColor(
$"NEW JOB {parsed.JobId} nTime={parsed.NTimeHex} seed={parsed.Seed}",
ConsoleColor.Yellow);
currentMiningTask =
MineJobAsync(parsed, token);
}
finally
{
JobLock.Release();
}
}
private static async Task StopCurrentJobAsync()
{
var cts = currentJobCts;
var task = currentMiningTask;
currentJobCts = null;
currentMiningTask = null;
if(cts != null)
{
try
{
cts.Cancel();
}
catch
{
}
}
if(task != null)
{
try
{
await task;
}
catch(OperationCanceledException)
{
}
}
cts?.Dispose();
}
private static MiningJob ParseJob(JsonElement job)
{
var jobId = job[0].GetString()
?? throw new Exception("Invalid job id");
var prevHashLE = job[1].GetString()
?? throw new Exception("Invalid previous block hash");
var coinbase1 = job[2].GetString()
?? throw new Exception("Invalid coinbase1");
var coinbase2 = job[3].GetString()
?? throw new Exception("Invalid coinbase2");
var branches = job[4].Clone();
var versionHex = job[5].GetString()
?? throw new Exception("Invalid version");
var bitsHex = job[6].GetString()
?? throw new Exception("Invalid bits");
var nTimeHex = job[7].GetString()
?? throw new Exception("Invalid nTime");
var seed = job[9].GetString()
?? throw new Exception("Invalid RandomX seed");
var extraNonce2 = new string('0', extraNonce2Size * 2);
var coinbaseHex =
coinbase1 +
extraNonce1 +
extraNonce2 +
coinbase2;
var coinbase = Convert.FromHexString(coinbaseHex);
var coinbaseHash = Sha256D(coinbase);
var merkleRoot = BuildMerkleRoot(coinbaseHash, branches);
var prevBytes = Convert.FromHexString(prevHashLE);
// Miningcore sends previousBlockHash using ReverseByteOrder():
// reverse the order of eight 4-byte words, not all 32 bytes.
var restoredPrevBytes = new byte[prevBytes.Length];
for(var i = 0; i < prevBytes.Length; i += 4)
{
Array.Copy(
prevBytes,
prevBytes.Length - 4 - i,
restoredPrevBytes,
i,
4);
}
var prevDisplay =
Convert.ToHexString(restoredPrevBytes).ToLowerInvariant();
var version = uint.Parse(
versionHex,
NumberStyles.HexNumber,
CultureInfo.InvariantCulture);
var nTime = uint.Parse(
nTimeHex,
NumberStyles.HexNumber,
CultureInfo.InvariantCulture);
return new MiningJob(
jobId,
prevDisplay,
merkleRoot,
version,
bitsHex,
nTime,
nTimeHex,
seed,
extraNonce2);
}
private static async Task MineJobAsync(
MiningJob job,
CancellationToken ct)
{
var tasks = new Task[threadCount];
for(var workerIndex = 0; workerIndex < threadCount; workerIndex++)
{
var localIndex = workerIndex;
tasks[workerIndex] = Task.Run(
async () =>
{
for(ulong n = (ulong)localIndex;
n <= uint.MaxValue && !ct.IsCancellationRequested;
n += (ulong)threadCount)
{
var nonce = (uint)n;
#pragma warning disable 618
var header = new BlockHeader
#pragma warning restore 618
{
Version = unchecked((int)job.Version),
HashPrevBlock = uint256.Parse(job.PrevDisplay),
HashMerkleRoot = new uint256(job.MerkleRoot),
BlockTime = DateTimeOffset.FromUnixTimeSeconds(job.NTime),
Bits = new Target(
Encoders.Hex.DecodeData(job.BitsHex)),
Nonce = nonce
};
var headerBytes = header.ToBytes();
var rxHash = new byte[32];
VextaRandomX.CalculateHash(
Realm,
job.Seed,
headerBytes,
rxHash);
Interlocked.Increment(ref totalHashes);
var hashValue = new BigInteger(
rxHash,
isUnsigned: true,
isBigEndian: false);
if(hashValue.IsZero)
continue;
var shareDifficulty =
(double)Diff1 /
(double)hashValue;
var required =
Volatile.Read(ref currentDifficulty) * 0.99d;
if(required <= 0 ||
shareDifficulty < required)
{
continue;
}
var nonceHex =
nonce.ToString("x8", CultureInfo.InvariantCulture);
Console.WriteLine(
$"FOUND share job={job.JobId} nonce={nonceHex} diff={shareDifficulty:E6}");
try
{
await SubmitShareAsync(
job,
nonceHex,
ct);
}
catch(OperationCanceledException)
{
break;
}
catch(Exception ex)
{
Console.WriteLine(
$"Submit failed: {ex.Message}");
}
}
},
ct);
}
await Task.WhenAll(tasks);
}
private static async Task SubmitShareAsync(
MiningJob job,
string nonceHex,
CancellationToken ct)
{
var submitId =
Interlocked.Increment(ref nextSubmitId);
PendingSubmits[submitId] = job.JobId;
var submit = JsonSerializer.Serialize(new
{
id = submitId,
method = "mining.submit",
@params = new object[]
{
worker,
job.JobId,
job.ExtraNonce2,
job.NTimeHex,
nonceHex
}
});
try
{
await SendLineAsync(submit, ct);
}
catch
{
PendingSubmits.TryRemove(submitId, out _);
throw;
}
}
private static async Task StatsLoopAsync(CancellationToken ct)
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
var sw = Stopwatch.StartNew();
var lastHashes = Interlocked.Read(ref totalHashes);
var lastTime = sw.Elapsed;
var measuring = false;
try
{
while(await timer.WaitForNextTickAsync(ct))
{
var now = sw.Elapsed;
var hashes = Interlocked.Read(ref totalHashes);
if(hashes == 0)
{
lastHashes = hashes;
lastTime = now;
continue;
}
if(measuring && hashes == lastHashes)
{
lastTime = now;
measuring = false;
continue;
}
if(!measuring)
{
lastHashes = hashes;
lastTime = now;
measuring = true;
continue;
}
var deltaHashes = hashes - lastHashes;
var deltaTime = (now - lastTime).TotalSeconds;
var rate =
deltaTime > 0
? deltaHashes / deltaTime
: 0;
WriteColor(
$"HASHRATE {FormatHashrate(rate)} " +
$"A:{Interlocked.Read(ref acceptedShares)} " +
$"R:{Interlocked.Read(ref rejectedShares)}",
ConsoleColor.Cyan);
lastHashes = hashes;
lastTime = now;
}
}
finally
{
timer.Dispose();
}
}
private static string FormatHashrate(double value)
{
if(value >= 1_000_000)
return $"{value / 1_000_000:F2} MH/s";
if(value >= 1_000)
return $"{value / 1_000:F2} kH/s";
return $"{value:F2} H/s";
}
private static byte[] Sha256D(byte[] data)
{
using var sha = SHA256.Create();
var first = sha.ComputeHash(data);
return sha.ComputeHash(first);
}
private static byte[] BuildMerkleRoot(
byte[] coinbaseHash,
JsonElement branches)
{
var current = coinbaseHash;
foreach(var branch in branches.EnumerateArray())
{
var branchBytes =
Convert.FromHexString(branch.GetString()!);
var combined =
new byte[current.Length + branchBytes.Length];
Buffer.BlockCopy(
current, 0,
combined, 0,
current.Length);
Buffer.BlockCopy(
branchBytes, 0,
combined, current.Length,
branchBytes.Length);
current = Sha256D(combined);
}
return current;
}
private static string? GetArg(
string[] args,
string name)
{
for(var i = 0; i < args.Length - 1; i++)
{
if(args[i].Equals(
name,
StringComparison.OrdinalIgnoreCase))
{
return args[i + 1];
}
}
return null;
}
private static void PrintBanner()
{
var oldColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(@"* * ***** * * ***** ***
* * * * * * * *
* * **** * * *****
* * * * * * * *
* ***** * * * * *");
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine();
Console.WriteLine("Vexta CPU Miner 0.1.0");
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine("Copyright (c) 2026 Vexta Project");
Console.WriteLine("Licensed under the MIT License");
Console.ForegroundColor = oldColor;
Console.WriteLine();
}
private static void WriteColor(string text, ConsoleColor color)
{
lock(ConsoleLock)
{
var oldColor = Console.ForegroundColor;
try
{
Console.ForegroundColor = color;
Console.WriteLine(text);
}
finally
{
Console.ForegroundColor = oldColor;
}
}
}
private static void PrintHelp()
{
PrintBanner();
Console.WriteLine("Usage:");
Console.WriteLine(
" Vexta.CpuMiner -o stratum+tcp://HOST:PORT -u ADDRESS.worker [-p x] [-t THREADS]");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine(" -o Stratum pool URL");
Console.WriteLine(" -u Vexta address / worker name");
Console.WriteLine(" -p Password (default: x)");
Console.WriteLine(
" -t CPU threads (default: logical CPU count)");
Console.WriteLine();
Console.WriteLine("Ctrl+C stops the miner cleanly.");
}
private sealed record MiningJob(
string JobId,
string PrevDisplay,
byte[] MerkleRoot,
uint Version,
string BitsHex,
uint NTime,
string NTimeHex,
string Seed,
string ExtraNonce2);
}

11
Vexta.CpuMiner.csproj Normal file
View file

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="7.0.42" />
</ItemGroup>
</Project>

271
VextaRandomX.cs Normal file
View file

@ -0,0 +1,271 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace Vexta.CpuMiner;
public static unsafe class VextaRandomX
{
[Flags]
public enum RandomXFlags
{
Default = 0,
LargePages = 1,
HardAes = 2,
FullMem = 4,
Jit = 8,
Secure = 16,
Argon2Ssse3 = 32,
Argon2Avx2 = 64,
Argon2 = 96
}
[DllImport("librandomx", EntryPoint = "randomx_get_flags", CallingConvention = CallingConvention.Cdecl)]
private static extern RandomXFlags GetFlags();
[DllImport("librandomx", EntryPoint = "randomx_alloc_cache", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr AllocCache(RandomXFlags flags);
[DllImport("librandomx", EntryPoint = "randomx_init_cache", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr InitCache(IntPtr cache, IntPtr key, int keySize);
[DllImport("librandomx", EntryPoint = "randomx_release_cache", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr ReleaseCache(IntPtr cache);
[DllImport("librandomx", EntryPoint = "randomx_alloc_dataset", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr AllocDataset(RandomXFlags flags);
[DllImport("librandomx", EntryPoint = "randomx_dataset_item_count", CallingConvention = CallingConvention.Cdecl)]
private static extern ulong DatasetItemCount();
[DllImport("librandomx", EntryPoint = "randomx_init_dataset", CallingConvention = CallingConvention.Cdecl)]
private static extern void InitDataset(
IntPtr dataset,
IntPtr cache,
ulong startItem,
ulong itemCount);
[DllImport("librandomx", EntryPoint = "randomx_release_dataset", CallingConvention = CallingConvention.Cdecl)]
private static extern void ReleaseDataset(IntPtr dataset);
[DllImport("librandomx", EntryPoint = "randomx_create_vm", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr CreateVm(
RandomXFlags flags,
IntPtr cache,
IntPtr dataset);
[DllImport("librandomx", EntryPoint = "randomx_destroy_vm", CallingConvention = CallingConvention.Cdecl)]
private static extern void DestroyVm(IntPtr machine);
[DllImport("librandomx", EntryPoint = "randomx_calculate_hash", CallingConvention = CallingConvention.Cdecl)]
private static extern void CalculateHashNative(
IntPtr machine,
byte* input,
int inputSize,
byte* output);
private sealed class GenContext : IDisposable
{
public int VmCount { get; init; }
public IntPtr Cache { get; private set; }
public IntPtr Dataset { get; private set; }
public void Init(ReadOnlySpan<byte> key, RandomXFlags flags)
{
Cache = AllocCache(flags);
if(Cache == IntPtr.Zero)
throw new InvalidOperationException("Unable to allocate RandomX cache.");
fixed(byte* keyPtr = key)
{
InitCache(Cache, (IntPtr)keyPtr, key.Length);
}
if((flags & RandomXFlags.FullMem) != 0)
{
Dataset = AllocDataset(flags);
if(Dataset == IntPtr.Zero)
throw new InvalidOperationException("Unable to allocate RandomX dataset.");
var itemCount = DatasetItemCount();
InitDataset(Dataset, Cache, 0, itemCount);
ReleaseCache(Cache);
Cache = IntPtr.Zero;
}
}
public void Dispose()
{
if(Dataset != IntPtr.Zero)
{
ReleaseDataset(Dataset);
Dataset = IntPtr.Zero;
}
if(Cache != IntPtr.Zero)
{
ReleaseCache(Cache);
Cache = IntPtr.Zero;
}
}
}
private sealed class RxVm : IDisposable
{
private IntPtr vm;
public void Init(
RandomXFlags flags,
IntPtr cache,
IntPtr dataset)
{
vm = CreateVm(flags, cache, dataset);
if(vm == IntPtr.Zero)
throw new InvalidOperationException("Unable to create RandomX VM.");
}
public void CalculateHash(
ReadOnlySpan<byte> data,
Span<byte> result)
{
fixed(byte* input = data)
fixed(byte* output = result)
{
CalculateHashNative(vm, input, data.Length, output);
}
}
public void Dispose()
{
if(vm != IntPtr.Zero)
{
DestroyVm(vm);
vm = IntPtr.Zero;
}
}
}
private sealed class SeedContext
{
public GenContext Context { get; init; } = null!;
public BlockingCollection<RxVm> Vms { get; init; } = null!;
}
private static readonly Dictionary<string, Dictionary<string, SeedContext>> Realms = new();
public static void CreateSeed(
string realm,
string seedHex,
int vmCount = 1)
{
lock(Realms)
{
if(!Realms.TryGetValue(realm, out var seeds))
{
seeds = new Dictionary<string, SeedContext>();
Realms[realm] = seeds;
}
if(seeds.ContainsKey(seedHex))
return;
if(vmCount == -1)
vmCount = Environment.ProcessorCount;
var flags = GetFlags() | RandomXFlags.FullMem;
var context = new GenContext
{
VmCount = vmCount
};
context.Init(Convert.FromHexString(seedHex), flags);
var vms = new BlockingCollection<RxVm>();
Parallel.For(0, vmCount, _ =>
{
var vm = new RxVm();
vm.Init(flags, context.Cache, context.Dataset);
vms.Add(vm);
});
seeds[seedHex] = new SeedContext
{
Context = context,
Vms = vms
};
}
}
public static void DeleteSeed(string realm, string seedHex)
{
SeedContext? seed;
lock(Realms)
{
if(!Realms.TryGetValue(realm, out var seeds))
return;
if(!seeds.Remove(seedHex, out seed))
return;
}
for(var i = 0; i < seed.Context.VmCount; i++)
{
var vm = seed.Vms.Take();
vm.Dispose();
}
seed.Context.Dispose();
seed.Vms.Dispose();
}
public static void CalculateHash(
string realm,
string seedHex,
ReadOnlySpan<byte> data,
Span<byte> result)
{
if(result.Length < 32)
throw new ArgumentException(
"RandomX result buffer must be at least 32 bytes.",
nameof(result));
SeedContext? seed = null;
lock(Realms)
{
if(Realms.TryGetValue(realm, out var seeds))
seeds.TryGetValue(seedHex, out seed);
}
if(seed is null)
{
result[..32].Clear();
return;
}
RxVm? vm = null;
try
{
vm = seed.Vms.Take();
vm.CalculateHash(data, result);
}
finally
{
if(vm is not null)
seed.Vms.Add(vm);
}
}
}