Vexta-CPU-Miner/Program.cs

1349 lines
39 KiB
C#
Raw Permalink Normal View History

2026-09-20 21:24:31 +00:00
using System;
2026-09-24 13:48:16 +00:00
using System.Buffers.Binary;
2026-09-20 21:24:31 +00:00
using System.Collections.Concurrent;
2026-09-27 17:41:36 +00:00
using System.Collections.Generic;
2026-09-20 21:24:31 +00:00
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net.Sockets;
using System.Numerics;
using System.Security.Cryptography;
2026-09-27 17:41:36 +00:00
using System.Runtime.InteropServices;
2026-09-20 21:24:31 +00:00
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;
2026-09-24 13:48:16 +00:00
private static long nextExtraNonce2;
2026-09-20 21:24:31 +00:00
private static long totalHashes;
private static long acceptedShares;
private static long rejectedShares;
2026-09-24 13:48:16 +00:00
private static long foundBlocks;
2026-09-20 21:24:31 +00:00
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");
2026-09-27 17:41:36 +00:00
var threadsAuto = threadText == null;
2026-09-20 21:24:31 +00:00
2026-09-27 17:41:36 +00:00
threadCount = threadsAuto
? GetPhysicalCoreCount()
: int.Parse(threadText!, CultureInfo.InvariantCulture);
2026-09-20 21:24:31 +00:00
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();
2026-09-27 17:41:36 +00:00
PrintStartupDiagnostics();
Console.WriteLine($"Pool : {host}:{port}");
Console.WriteLine($"Worker : {worker}");
Console.WriteLine(
$"Mining Threads : {threadCount}" +
(threadsAuto ? " (auto)" : " (manual)"));
2026-09-20 21:24:31 +00:00
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)}, " +
2026-09-24 13:48:16 +00:00
$"Rejected: {Interlocked.Read(ref rejectedShares)}, " +
$"Blocks: {Interlocked.Read(ref foundBlocks)}");
2026-09-20 21:24:31 +00:00
}
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() ?? "";
2026-09-24 13:48:16 +00:00
var blockCount =
Interlocked.Increment(ref foundBlocks);
2026-09-20 21:24:31 +00:00
WriteColor(
2026-09-24 13:48:16 +00:00
$"*** BLOCK FOUND! #{blockCount} Height {height} ***",
2026-09-20 21:24:31 +00:00
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() ?? "";
2026-09-24 13:48:16 +00:00
var blockCount =
Interlocked.Increment(ref foundBlocks);
2026-09-20 21:24:31 +00:00
WriteColor(
2026-09-24 13:48:16 +00:00
$"*** BLOCK FOUND! #{blockCount} Height {height} ***",
2026-09-20 21:24:31 +00:00
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();
}
2026-09-24 13:48:16 +00:00
private static string NextExtraNonce2()
{
if(extraNonce2Size <= 0)
return string.Empty;
var value = unchecked(
(ulong)(Interlocked.Increment(ref nextExtraNonce2) - 1));
var width = extraNonce2Size * 2;
var hex = value.ToString(
"x",
CultureInfo.InvariantCulture);
if(hex.Length > width)
hex = hex[^width..];
return hex.PadLeft(width, '0');
}
2026-09-20 21:24:31 +00:00
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");
2026-09-24 13:48:16 +00:00
var extraNonce2 = NextExtraNonce2();
2026-09-20 21:24:31 +00:00
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 () =>
{
2026-09-24 13:48:16 +00:00
#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 = 0
};
// Bitcoin/Vexta block header is always 80 bytes.
// Only the final 4 bytes (nonce) change inside the hot loop.
var headerBytes = header.ToBytes();
var rxHash = new byte[32];
using var vmLease =
VextaRandomX.AcquireVm(
Realm,
job.Seed,
ct)
?? throw new InvalidOperationException(
"Unable to acquire RandomX VM.");
if(headerBytes.Length != 80)
throw new InvalidOperationException(
$"Unexpected block-header size: {headerBytes.Length}");
2026-09-20 21:24:31 +00:00
for(ulong n = (ulong)localIndex;
n <= uint.MaxValue && !ct.IsCancellationRequested;
n += (ulong)threadCount)
{
var nonce = (uint)n;
2026-09-24 13:48:16 +00:00
BinaryPrimitives.WriteUInt32LittleEndian(
headerBytes.AsSpan(76, 4),
nonce);
vmLease.CalculateHash(
2026-09-20 21:24:31 +00:00
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;
}
}
2026-09-27 17:41:36 +00:00
private static void PrintStartupDiagnostics()
{
var logicalCpus = Environment.ProcessorCount;
var physicalCores = GetPhysicalCoreCount();
var cpuModel = GetCpuModel();
var aesSupported = GetAesSupport();
var hugePages = GetHugePagesStatus();
string randomXStatus;
string randomXPath;
if(OperatingSystem.IsLinux())
{
// Accessing these properties initializes VextaRandomX and
// explicitly loads librandomx.so from the miner directory.
randomXStatus = VextaRandomX.NativeLibraryLoaded
? "Loaded OK"
: "Not loaded";
randomXPath = VextaRandomX.NativeLibraryPath ?? "Unknown";
}
else
{
randomXStatus = "System loader";
randomXPath = "Platform default";
}
Console.WriteLine($"CPU : {cpuModel}");
Console.WriteLine($"Physical Cores : {physicalCores}");
Console.WriteLine($"Logical CPUs : {logicalCpus}");
Console.WriteLine($"AES : {aesSupported}");
Console.WriteLine($"Huge Pages : {hugePages}");
Console.WriteLine($"RandomX Library: {randomXStatus}");
Console.WriteLine($"Library Path : {randomXPath}");
Console.WriteLine();
}
private static string GetCpuModel()
{
try
{
if(OperatingSystem.IsLinux() && File.Exists("/proc/cpuinfo"))
{
foreach(var line in File.ReadLines("/proc/cpuinfo"))
{
if(line.StartsWith("model name", StringComparison.OrdinalIgnoreCase))
{
var colon = line.IndexOf(':');
if(colon >= 0)
return line[(colon + 1)..].Trim();
}
}
}
var identifier = Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER");
if(!string.IsNullOrWhiteSpace(identifier))
return identifier.Trim();
}
catch
{
}
return "Unknown";
}
private static int GetPhysicalCoreCount()
{
try
{
if(OperatingSystem.IsLinux() && File.Exists("/proc/cpuinfo"))
{
var cores = new HashSet<string>();
string? physicalId = null;
string? coreId = null;
foreach(var rawLine in File.ReadLines("/proc/cpuinfo"))
{
var line = rawLine.Trim();
if(line.Length == 0)
{
if(physicalId != null && coreId != null)
cores.Add($"{physicalId}:{coreId}");
physicalId = null;
coreId = null;
continue;
}
if(line.StartsWith("physical id", StringComparison.OrdinalIgnoreCase))
{
var colon = line.IndexOf(':');
if(colon >= 0)
physicalId = line[(colon + 1)..].Trim();
}
else if(line.StartsWith("core id", StringComparison.OrdinalIgnoreCase))
{
var colon = line.IndexOf(':');
if(colon >= 0)
coreId = line[(colon + 1)..].Trim();
}
}
if(physicalId != null && coreId != null)
cores.Add($"{physicalId}:{coreId}");
if(cores.Count > 0)
return cores.Count;
}
if(OperatingSystem.IsWindows())
{
var windowsCores = GetWindowsPhysicalCoreCount();
if(windowsCores > 0)
return windowsCores;
}
}
catch
{
}
return Environment.ProcessorCount;
}
private static int GetWindowsPhysicalCoreCount()
{
uint length = 0;
GetLogicalProcessorInformation(IntPtr.Zero, ref length);
if(length == 0)
return 0;
var buffer = Marshal.AllocHGlobal((int)length);
try
{
if(!GetLogicalProcessorInformation(buffer, ref length))
return 0;
// SYSTEM_LOGICAL_PROCESSOR_INFORMATION is 32 bytes on x64
// and 24 bytes on x86.
var entrySize = IntPtr.Size == 8 ? 32 : 24;
var relationshipOffset = IntPtr.Size;
var count = 0;
for(var offset = 0;
offset + entrySize <= length;
offset += entrySize)
{
var relationship = Marshal.ReadInt32(
buffer,
offset + relationshipOffset);
// RelationProcessorCore = 0
if(relationship == 0)
count++;
}
return count;
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetLogicalProcessorInformation(
IntPtr buffer,
ref uint returnedLength);
private static string GetAesSupport()
{
try
{
if(System.Runtime.Intrinsics.X86.Aes.IsSupported)
return "Supported";
}
catch
{
}
return "Not detected";
}
private static string GetHugePagesStatus()
{
try
{
if(OperatingSystem.IsLinux() && File.Exists("/proc/meminfo"))
{
foreach(var line in File.ReadLines("/proc/meminfo"))
{
if(!line.StartsWith("HugePages_Total:", StringComparison.Ordinal))
continue;
var value = line["HugePages_Total:".Length..].Trim();
var parts = value.Split(
' ',
StringSplitOptions.RemoveEmptyEntries);
if(parts.Length > 0 &&
long.TryParse(
parts[0],
NumberStyles.Integer,
CultureInfo.InvariantCulture,
out var total))
{
return total > 0
? $"Configured ({total})"
: "Not configured";
}
}
}
}
catch
{
}
return "Unknown";
}
2026-09-20 21:24:31 +00:00
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;
2026-09-27 17:41:36 +00:00
var averageHistory = new List<(TimeSpan Time, long Hashes)>();
TimeSpan? averageStartTime = null;
var nextAverageReport = TimeSpan.Zero;
2026-09-20 21:24:31 +00:00
try
{
while(await timer.WaitForNextTickAsync(ct))
{
var now = sw.Elapsed;
var hashes = Interlocked.Read(ref totalHashes);
2026-09-27 17:41:36 +00:00
if(hashes > 0)
{
if(averageStartTime == null)
{
averageStartTime = now;
nextAverageReport = now + TimeSpan.FromMinutes(1);
averageHistory.Add((now, hashes));
}
else if(now >= nextAverageReport)
{
averageHistory.Add((now, hashes));
WriteAverageHashrate(
now,
hashes,
averageHistory,
averageStartTime.Value);
do
{
nextAverageReport += TimeSpan.FromMinutes(1);
}
while(nextAverageReport <= now);
TrimAverageHistory(now, averageHistory);
}
}
2026-09-20 21:24:31 +00:00
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)} " +
2026-09-24 13:48:16 +00:00
$"R:{Interlocked.Read(ref rejectedShares)} " +
$"Blocks:{Interlocked.Read(ref foundBlocks)}",
2026-09-20 21:24:31 +00:00
ConsoleColor.Cyan);
lastHashes = hashes;
lastTime = now;
}
}
finally
{
timer.Dispose();
}
}
2026-09-27 17:41:36 +00:00
private static void WriteAverageHashrate(
TimeSpan now,
long hashes,
List<(TimeSpan Time, long Hashes)> history,
TimeSpan startTime)
{
var avg5 = FormatAverageWindow(
now, hashes, history, startTime, TimeSpan.FromMinutes(5));
var avg15 = FormatAverageWindow(
now, hashes, history, startTime, TimeSpan.FromMinutes(15));
var avg60 = FormatAverageWindow(
now, hashes, history, startTime, TimeSpan.FromMinutes(60));
WriteColor(
$"AVG HASHRATE 5m:{avg5} 15m:{avg15} 60m:{avg60}",
ConsoleColor.DarkCyan);
}
private static string FormatAverageWindow(
TimeSpan now,
long hashes,
List<(TimeSpan Time, long Hashes)> history,
TimeSpan startTime,
TimeSpan window)
{
if(now - startTime < window)
return "warming up";
var cutoff = now - window;
(TimeSpan Time, long Hashes)? sample = null;
foreach(var item in history)
{
if(item.Time <= cutoff)
sample = item;
else
break;
}
if(sample == null)
return "warming up";
var elapsed = (now - sample.Value.Time).TotalSeconds;
if(elapsed <= 0)
return "warming up";
var deltaHashes = hashes - sample.Value.Hashes;
var rate = deltaHashes / elapsed;
return FormatHashrate(rate);
}
private static void TrimAverageHistory(
TimeSpan now,
List<(TimeSpan Time, long Hashes)> history)
{
var keepAfter = now - TimeSpan.FromMinutes(62);
while(history.Count > 2 &&
history[1].Time < keepAfter)
{
history.RemoveAt(0);
}
}
2026-09-20 21:24:31 +00:00
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();
2026-09-27 17:41:36 +00:00
Console.WriteLine("Vexta CPU Miner 0.1.2");
2026-09-20 21:24:31 +00:00
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(
2026-09-27 17:41:36 +00:00
" -t CPU threads (default: auto-detect physical cores)");
2026-09-20 21:24:31 +00:00
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);
}