diff --git a/Program.cs b/Program.cs index 5629cbf..4f5bd83 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers.Binary; using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; @@ -44,9 +45,11 @@ internal static class Program private static string? currentSeed; private static long nextSubmitId = 100; + private static long nextExtraNonce2; private static long totalHashes; private static long acceptedShares; private static long rejectedShares; + private static long foundBlocks; private static async Task Main(string[] args) { @@ -178,7 +181,8 @@ internal static class Program Console.WriteLine(); Console.WriteLine( $"Stopped. Accepted: {Interlocked.Read(ref acceptedShares)}, " + - $"Rejected: {Interlocked.Read(ref rejectedShares)}"); + $"Rejected: {Interlocked.Read(ref rejectedShares)}, " + + $"Blocks: {Interlocked.Read(ref foundBlocks)}"); } private static async Task RunSessionAsync( @@ -293,8 +297,11 @@ internal static class Program var height = parameters[0].GetInt64(); var hash = parameters[1].GetString() ?? ""; + var blockCount = + Interlocked.Increment(ref foundBlocks); + WriteColor( - $"*** BLOCK FOUND! Height {height} ***", + $"*** BLOCK FOUND! #{blockCount} Height {height} ***", ConsoleColor.Magenta); WriteColor( @@ -352,8 +359,11 @@ internal static class Program var height = parameters[0].GetInt64(); var hash = parameters[1].GetString() ?? ""; + var blockCount = + Interlocked.Increment(ref foundBlocks); + WriteColor( - $"*** BLOCK FOUND! Height {height} ***", + $"*** BLOCK FOUND! #{blockCount} Height {height} ***", ConsoleColor.Magenta); WriteColor( @@ -558,6 +568,26 @@ internal static class Program cts?.Dispose(); } + 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'); + } + private static MiningJob ParseJob(JsonElement job) { var jobId = job[0].GetString() @@ -586,7 +616,7 @@ internal static class Program var seed = job[9].GetString() ?? throw new Exception("Invalid RandomX seed"); - var extraNonce2 = new string('0', extraNonce2Size * 2); + var extraNonce2 = NextExtraNonce2(); var coinbaseHex = coinbase1 + @@ -652,31 +682,47 @@ internal static class Program tasks[workerIndex] = Task.Run( async () => { +#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}"); + 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 - }; + BinaryPrimitives.WriteUInt32LittleEndian( + headerBytes.AsSpan(76, 4), + nonce); - var headerBytes = header.ToBytes(); - var rxHash = new byte[32]; - - VextaRandomX.CalculateHash( - Realm, - job.Seed, + vmLease.CalculateHash( headerBytes, rxHash); @@ -817,7 +863,8 @@ internal static class Program WriteColor( $"HASHRATE {FormatHashrate(rate)} " + $"A:{Interlocked.Read(ref acceptedShares)} " + - $"R:{Interlocked.Read(ref rejectedShares)}", + $"R:{Interlocked.Read(ref rejectedShares)} " + + $"Blocks:{Interlocked.Read(ref foundBlocks)}", ConsoleColor.Cyan); lastHashes = hashes; @@ -909,7 +956,7 @@ internal static class Program Console.ForegroundColor = ConsoleColor.White; Console.WriteLine(); - Console.WriteLine("Vexta CPU Miner 0.1.0"); + Console.WriteLine("Vexta CPU Miner 0.1.1"); Console.ForegroundColor = ConsoleColor.DarkGray; Console.WriteLine("Copyright (c) 2026 Vexta Project"); diff --git a/VextaRandomX.cs b/VextaRandomX.cs index 10ed411..7400d88 100644 --- a/VextaRandomX.cs +++ b/VextaRandomX.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.InteropServices; @@ -162,6 +163,47 @@ public static unsafe class VextaRandomX private static readonly Dictionary> Realms = new(); + private static SeedContext BuildSeedContext( + ReadOnlySpan key, + RandomXFlags flags, + int vmCount) + { + var context = new GenContext + { + VmCount = vmCount + }; + + var vms = new BlockingCollection(); + + try + { + context.Init(key, flags); + + Parallel.For(0, vmCount, _ => + { + var vm = new RxVm(); + vm.Init(flags, context.Cache, context.Dataset); + vms.Add(vm); + }); + + return new SeedContext + { + Context = context, + Vms = vms + }; + } + catch + { + while(vms.TryTake(out var vm)) + vm.Dispose(); + + vms.Dispose(); + context.Dispose(); + + throw; + } + } + public static void CreateSeed( string realm, string seedHex, @@ -181,29 +223,41 @@ public static unsafe class VextaRandomX if(vmCount == -1) vmCount = Environment.ProcessorCount; - var flags = GetFlags() | RandomXFlags.FullMem; + var key = Convert.FromHexString(seedHex); - var context = new GenContext + var baseFlags = + (GetFlags() | RandomXFlags.FullMem) & + ~RandomXFlags.LargePages; + + SeedContext seed; + + try { - VmCount = vmCount - }; + var hugePageFlags = + baseFlags | RandomXFlags.LargePages; - context.Init(Convert.FromHexString(seedHex), flags); + seed = BuildSeedContext( + key, + hugePageFlags, + vmCount); - var vms = new BlockingCollection(); - - Parallel.For(0, vmCount, _ => + Console.WriteLine("Huge Pages: ENABLED"); + } + catch(Exception ex) { - var vm = new RxVm(); - vm.Init(flags, context.Cache, context.Dataset); - vms.Add(vm); - }); + Console.WriteLine( + $"Huge Pages: unavailable ({ex.Message})"); - seeds[seedHex] = new SeedContext - { - Context = context, - Vms = vms - }; + Console.WriteLine( + "Huge Pages: falling back to normal memory pages."); + + seed = BuildSeedContext( + key, + baseFlags, + vmCount); + } + + seeds[seedHex] = seed; } } @@ -230,6 +284,76 @@ public static unsafe class VextaRandomX seed.Vms.Dispose(); } + public sealed class VmLease : IDisposable + { + private readonly SeedContext seed; + private RxVm? vm; + + private VmLease( + SeedContext seed, + RxVm vm) + { + this.seed = seed; + this.vm = vm; + } + + public void CalculateHash( + ReadOnlySpan data, + Span result) + { + if(result.Length < 32) + throw new ArgumentException( + "RandomX result buffer must be at least 32 bytes.", + nameof(result)); + + var current = vm + ?? throw new ObjectDisposedException(nameof(VmLease)); + + current.CalculateHash(data, result); + } + + public void Dispose() + { + var current = + Interlocked.Exchange(ref vm, null); + + if(current is not null) + seed.Vms.Add(current); + } + + internal static VmLease? Acquire( + string realm, + string seedHex, + CancellationToken cancellationToken) + { + SeedContext? seed = null; + + lock(Realms) + { + if(Realms.TryGetValue(realm, out var seeds)) + seeds.TryGetValue(seedHex, out seed); + } + + if(seed is null) + return null; + + var vm = seed.Vms.Take(cancellationToken); + + return new VmLease(seed, vm); + } + } + + public static VmLease? AcquireVm( + string realm, + string seedHex, + CancellationToken cancellationToken) + { + return VmLease.Acquire( + realm, + seedHex, + cancellationToken); + } + public static void CalculateHash( string realm, string seedHex,