Initial Vexta CPU Miner v0.1.0
This commit is contained in:
commit
703213cb9f
5 changed files with 1300 additions and 0 deletions
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
967
Program.cs
Normal 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
11
Vexta.CpuMiner.csproj
Normal 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
271
VextaRandomX.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue