Compare commits
3 changed files with 10 additions and 399 deletions
347
Program.cs
347
Program.cs
|
|
@ -1,14 +1,12 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
|
@ -87,11 +85,10 @@ internal static class Program
|
|||
password = GetArg(args, "-p") ?? "x";
|
||||
|
||||
var threadText = GetArg(args, "-t");
|
||||
var threadsAuto = threadText == null;
|
||||
|
||||
threadCount = threadsAuto
|
||||
? GetPhysicalCoreCount()
|
||||
: int.Parse(threadText!, CultureInfo.InvariantCulture);
|
||||
threadCount = threadText != null
|
||||
? int.Parse(threadText, CultureInfo.InvariantCulture)
|
||||
: Environment.ProcessorCount;
|
||||
|
||||
if(threadCount < 1)
|
||||
throw new ArgumentException("Thread count must be at least 1");
|
||||
|
|
@ -128,12 +125,9 @@ internal static class Program
|
|||
};
|
||||
|
||||
PrintBanner();
|
||||
PrintStartupDiagnostics();
|
||||
Console.WriteLine($"Pool : {host}:{port}");
|
||||
Console.WriteLine($"Worker : {worker}");
|
||||
Console.WriteLine(
|
||||
$"Mining Threads : {threadCount}" +
|
||||
(threadsAuto ? " (auto)" : " (manual)"));
|
||||
Console.WriteLine($"Threads : {threadCount}");
|
||||
Console.WriteLine();
|
||||
|
||||
var statsTask = StatsLoopAsync(shutdown.Token);
|
||||
|
|
@ -820,235 +814,6 @@ internal static class Program
|
|||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
private static async Task StatsLoopAsync(CancellationToken ct)
|
||||
{
|
||||
var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
|
||||
|
|
@ -1058,10 +823,6 @@ internal static class Program
|
|||
var lastTime = sw.Elapsed;
|
||||
var measuring = false;
|
||||
|
||||
var averageHistory = new List<(TimeSpan Time, long Hashes)>();
|
||||
TimeSpan? averageStartTime = null;
|
||||
var nextAverageReport = TimeSpan.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
while(await timer.WaitForNextTickAsync(ct))
|
||||
|
|
@ -1069,34 +830,6 @@ internal static class Program
|
|||
var now = sw.Elapsed;
|
||||
var hashes = Interlocked.Read(ref totalHashes);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if(hashes == 0)
|
||||
{
|
||||
lastHashes = hashes;
|
||||
|
|
@ -1144,74 +877,6 @@ internal static class Program
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatHashrate(double value)
|
||||
{
|
||||
if(value >= 1_000_000)
|
||||
|
|
@ -1291,7 +956,7 @@ internal static class Program
|
|||
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Vexta CPU Miner 0.1.2");
|
||||
Console.WriteLine("Vexta CPU Miner 0.1.1");
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine("Copyright (c) 2026 Vexta Project");
|
||||
|
|
@ -1331,7 +996,7 @@ internal static class Program
|
|||
Console.WriteLine(" -u Vexta address / worker name");
|
||||
Console.WriteLine(" -p Password (default: x)");
|
||||
Console.WriteLine(
|
||||
" -t CPU threads (default: auto-detect physical cores)");
|
||||
" -t CPU threads (default: logical CPU count)");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Ctrl+C stops the miner cleanly.");
|
||||
}
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -20,11 +20,8 @@ Prebuilt binaries are available for:
|
|||
- Vexta RandomX mining
|
||||
- RandomX full-memory mode
|
||||
- Windows x64 and Linux x64 support
|
||||
- Automatic CPU thread selection based on physical cores
|
||||
- Manual CPU thread override with `-t`
|
||||
- Startup diagnostics for CPU, cores, AES, Huge Pages, and RandomX library
|
||||
- Configurable CPU thread count
|
||||
- Local hashrate display
|
||||
- 5 / 15 / 60 minute average hashrate
|
||||
- Accepted / rejected share statistics
|
||||
- Automatic RandomX seed changes
|
||||
- Daemon-confirmed `BLOCK FOUND` notifications when supported by the pool
|
||||
|
|
@ -62,7 +59,7 @@ Example:
|
|||
| `-o` | Stratum pool address |
|
||||
| `-u` | Vexta wallet address, optionally followed by `.worker` |
|
||||
| `-p` | Pool password, normally `x` |
|
||||
| `-t` | Number of CPU mining threads (optional; auto-detects physical cores when omitted) |
|
||||
| `-t` | Number of CPU mining threads |
|
||||
|
||||
## Requirements
|
||||
|
||||
|
|
@ -72,11 +69,6 @@ Example:
|
|||
|
||||
RandomX full-memory mode is enabled.
|
||||
|
||||
On startup, the miner reports detected CPU information, physical and logical CPU counts,
|
||||
AES support, Huge Pages status, RandomX library status, and the selected mining thread count.
|
||||
|
||||
If `-t` is omitted, the miner automatically uses the detected physical core count.
|
||||
|
||||
## Building from Source
|
||||
|
||||
### Requirements
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
|
@ -10,51 +9,6 @@ namespace Vexta.CpuMiner;
|
|||
|
||||
public static unsafe class VextaRandomX
|
||||
{
|
||||
private static IntPtr nativeLibraryHandle = IntPtr.Zero;
|
||||
|
||||
public static string? NativeLibraryPath { get; private set; }
|
||||
|
||||
public static bool NativeLibraryLoaded => nativeLibraryHandle != IntPtr.Zero;
|
||||
|
||||
static VextaRandomX()
|
||||
{
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
var libraryPath = Path.Combine(AppContext.BaseDirectory, "librandomx.so");
|
||||
|
||||
if(!File.Exists(libraryPath))
|
||||
throw new DllNotFoundException(
|
||||
$"RandomX library was not found at '{libraryPath}'.");
|
||||
|
||||
try
|
||||
{
|
||||
nativeLibraryHandle = NativeLibrary.Load(libraryPath);
|
||||
NativeLibraryPath = libraryPath;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
throw new DllNotFoundException(
|
||||
$"Unable to load RandomX library from '{libraryPath}': {ex.Message}",
|
||||
ex);
|
||||
}
|
||||
|
||||
NativeLibrary.SetDllImportResolver(
|
||||
typeof(VextaRandomX).Assembly,
|
||||
(libraryName, assembly, searchPath) =>
|
||||
{
|
||||
if(string.Equals(
|
||||
libraryName,
|
||||
"librandomx",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return nativeLibraryHandle;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum RandomXFlags
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue