967 lines
28 KiB
C#
967 lines
28 KiB
C#
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);
|
|
}
|