diff --git a/GetworkStratumProxy.ConsoleApp/Program.cs b/GetworkStratumProxy.ConsoleApp/Program.cs index e062525..29955fe 100644 --- a/GetworkStratumProxy.ConsoleApp/Program.cs +++ b/GetworkStratumProxy.ConsoleApp/Program.cs @@ -33,10 +33,12 @@ private static async Task OptionParseOkAsync(CommandLineOptions options) }; using (BaseNode pollingNode = new PollingNode(options.RpcUri, options.PollInterval)) - using (IProxy proxy = new EthProxy(pollingNode, options.StratumIPAddress, options.StratumPort)) + using (IProxy ethProxy = new EthProxy(pollingNode, options.StratumIPAddress, options.StratumPort)) + using (IProxy nicehashProxy = new NicehashProxy(pollingNode, options.StratumIPAddress, options.StratumPort + 1)) { pollingNode.Start(); - proxy.Start(); + ethProxy.Start(); + nicehashProxy.Start(); while (IsRunning) { diff --git a/GetworkStratumProxy/Constants.cs b/GetworkStratumProxy/Constants.cs index f53faad..c0fb721 100644 --- a/GetworkStratumProxy/Constants.cs +++ b/GetworkStratumProxy/Constants.cs @@ -1,7 +1,36 @@ -namespace GetworkStratumProxy +using Nethereum.Hex.HexTypes; +using System.Numerics; + +namespace GetworkStratumProxy { internal class Constants { public const int WorkHeaderCharactersPrefixCount = 10; + + private static readonly BigInteger MaxTarget = BigInteger.Pow(16, 64) - 1; + private const long BaseDifficultyOfOne = 4294967296; + + public static decimal GetDifficultyFromTarget(HexBigInteger currentTarget) + { + var calculatedDifficulty = MaxTarget / currentTarget.Value; + return (decimal)calculatedDifficulty / BaseDifficultyOfOne; + } + + public static HexBigInteger GetTargetFromDifficulty(decimal difficulty) + { + var calculatedDifficulty = difficulty * BaseDifficultyOfOne; + return new HexBigInteger(MaxTarget / (BigInteger)calculatedDifficulty); + } + + public static decimal GetDifficultySize(HexBigInteger currentTarget) + { + var targetDiff = GetDifficultyFromTarget(currentTarget); + return BaseDifficultyOfOne * targetDiff; + } + + public static decimal GetDifficultySize(decimal difficulty) + { + return BaseDifficultyOfOne * difficulty; + } } } diff --git a/GetworkStratumProxy/Proxy/Client/NicehashProxyClient.cs b/GetworkStratumProxy/Proxy/Client/NicehashProxyClient.cs index 6128c2c..6a21d7f 100644 --- a/GetworkStratumProxy/Proxy/Client/NicehashProxyClient.cs +++ b/GetworkStratumProxy/Proxy/Client/NicehashProxyClient.cs @@ -1,17 +1,156 @@ -using System; +using GetworkStratumProxy.Extension; +using GetworkStratumProxy.Rpc; +using GetworkStratumProxy.Rpc.Nicehash; +using Nethereum.Hex.HexTypes; +using Nethereum.RPC.Eth.Mining; +using StreamJsonRpc; +using System; +using System.IO; using System.Net.Sockets; +using System.Security.Cryptography; +using System.Threading.Tasks; namespace GetworkStratumProxy.Proxy.Client { public sealed class NicehashProxyClient : BaseProxyClient { - public NicehashProxyClient(TcpClient tcpClient) : base(tcpClient) + public const string ProtocolVersion = "EthereumStratum/1.0.0"; + + private IEthGetWork GetWorkService { get; set; } + private IEthSubmitWork SubmitWorkService { get; set; } + + private HexBigInteger CurrentJobId { get; set; } = new HexBigInteger(0); + private bool XNSub { get; set; } = false; + + private event EventHandler CanAcceptWorkEvent; + + private EthWork CurrentWork; + + private decimal PreviousDifficulty { get; set; } = -1; + + public NicehashProxyClient(TcpClient tcpClient, IEthGetWork getWorkService, IEthSubmitWork submitWorkService) : base(tcpClient) { + var networkStream = TcpClient.GetStream(); + BackgroundWorkWriter = new StreamWriter(networkStream); + + GetWorkService = getWorkService; + SubmitWorkService = submitWorkService; + + CanAcceptWorkEvent += (o, e) => + { + // TO-DO: very hacky workaround, not a surefire fix for race condition + System.Threading.Thread.Sleep(1000); // Hold this event so authorise can go through + // Fix for when miner subscribes but does not get work refresh triggered + string[] ethGetWorkRaw = getWorkService.SendRequestAsync().Result; + var ethWork = new EthWork(ethGetWorkRaw); + NewJobNotificationEvent(null, ethWork); + }; } - public override void Dispose() + /// + /// Blocking listen and respond to EthProxy RPC messages. + /// + internal async Task StartListeningAsync() + { + using var networkStream = TcpClient.GetStream(); + using var formatter = new JsonMessageFormatter { ProtocolVersion = new Version(1, 0) }; + using var handler = new NewLineDelimitedMessageHandler(networkStream, networkStream, formatter); + using var jsonRpc = new JsonRpc(handler, this); + + jsonRpc.StartListening(); + await jsonRpc.Completion; + ConsoleHelper.Log(GetType().Name, $"RPC service stopped for {Endpoint}", LogLevel.Debug); + } + + internal void NewJobNotificationEvent(object sender, EthWork e) + { + if (StratumState.HasFlag(StratumState.Authorised) && StratumState.HasFlag(StratumState.Subscribed)) + { + CurrentWork = e; + + // e[] = { headerHash, seedHash, Target } + string headerHash = CurrentWork.Header.HexValue; + string seedHash = CurrentWork.Seed.HexValue; + string target = CurrentWork.Target.HexValue; + bool clearJobQueue = true; + + decimal difficulty = Constants.GetDifficultyFromTarget(new HexBigInteger(target)); + if (PreviousDifficulty == -1 || difficulty != PreviousDifficulty) + { + var setDifficultyNotification = new SetDifficultyNotification(difficulty); + ConsoleHelper.Log(GetType().Name, $"Setting mining difficulty " + + $"({target}) to {Endpoint}", LogLevel.Information); + Notify(setDifficultyNotification); + PreviousDifficulty = difficulty; + } + + var miningNotifyNotification = new MiningNotifyNotification(CurrentJobId.HexValue, seedHash, headerHash, clearJobQueue); + ConsoleHelper.Log(GetType().Name, $"Sending job " + + $"({headerHash[..Constants.WorkHeaderCharactersPrefixCount]}...) to {Endpoint}", LogLevel.Information); + Notify(miningNotifyNotification); + + CurrentJobId = (CurrentJobId.Value + 1).ToHexBigInteger(); + } + } + + [JsonRpcMethod("mining.subscribe")] + public object[] Subscribe(string minerName, string protocol) + { + ConsoleHelper.Log(GetType().Name, $"Miner {Endpoint} subscribe ({minerName}-{protocol})", LogLevel.Debug); + if (protocol != ProtocolVersion) + { + throw new Exception($"Unsupported protocol \"{protocol}\""); + } + + // No login handler therefore always successful + StratumState |= StratumState.Subscribed; + ConsoleHelper.Log(GetType().Name, $"Miner {Endpoint} successfully subscribed", LogLevel.Information); + + byte[] connectionIdBytes = new byte[16]; + RandomNumberGenerator.Fill(connectionIdBytes); + string connectionId = Convert.ToHexString(connectionIdBytes); + + string[] miningParams = new string[] { "mining.notify", connectionId, ProtocolVersion }; + string extraNonce = ""; // Let the client infer their own nonce + + object[] response = new object[] { miningParams, extraNonce }; + ConsoleHelper.Log(GetType().Name, $"Sending subscribe response to {Endpoint}", LogLevel.Debug); + return response; + } + + [JsonRpcMethod("mining.authorize")] + public bool Authorise(string username, string password) + { + ConsoleHelper.Log(GetType().Name, $"Miner {Endpoint} login ({username}:{password})", LogLevel.Debug); + + // No login handler therefore always successful + StratumState |= StratumState.Authorised; + ConsoleHelper.Log(GetType().Name, $"Miner {Endpoint} logged in successfully", LogLevel.Information); + + ConsoleHelper.Log(GetType().Name, $"Sending login response to {Endpoint}", LogLevel.Debug); + + CanAcceptWorkEvent(null, null); // Trigger sending of work + return StratumState.HasFlag(StratumState.Authorised) && + StratumState.HasFlag(StratumState.Subscribed); + } + + [JsonRpcMethod("mining.extranonce.subscribe")] + public bool ExtraNonceSubscribe() + { + ConsoleHelper.Log(GetType().Name, $"Miner {Endpoint} requested XNSUB", LogLevel.Debug); + XNSub = true; + return true; + } + + [JsonRpcMethod("mining.submit")] + public bool Submit(string minerUser, string extraNonce, string foundNonce) { throw new NotImplementedException(); } + + public override void Dispose() + { + TcpClient.Dispose(); + } } } diff --git a/GetworkStratumProxy/Proxy/NicehashProxy.cs b/GetworkStratumProxy/Proxy/NicehashProxy.cs index 3e088de..b5f4488 100644 --- a/GetworkStratumProxy/Proxy/NicehashProxy.cs +++ b/GetworkStratumProxy/Proxy/NicehashProxy.cs @@ -1,6 +1,6 @@ -using GetworkStratumProxy.Node; +using GetworkStratumProxy.Extension; +using GetworkStratumProxy.Node; using GetworkStratumProxy.Proxy.Client; -using System; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; @@ -14,12 +14,29 @@ public class NicehashProxy : BaseProxy public NicehashProxy(BaseNode node, IPAddress address, int port) : base(node, address, port) { - throw new NotImplementedException(); + + } + + protected override async Task BeginClientSessionAsync(TcpClient client) + { + var endpoint = client.Client.RemoteEndPoint; + using NicehashProxyClient proxyClient = GetClientOrNew(client); + Node.NewWorkReceived += proxyClient.NewJobNotificationEvent; // Subscribe to new jobs + await proxyClient.StartListeningAsync(); // Blocking listen + Node.NewWorkReceived -= proxyClient.NewJobNotificationEvent; // Unsubscribe + ConsoleHelper.Log(GetType().Name, $"Client {endpoint} unsubscribed from jobs", LogLevel.Information); } - protected override Task BeginClientSessionAsync(TcpClient client) + private NicehashProxyClient GetClientOrNew(TcpClient tcpClient) { - throw new NotImplementedException(); + if (!Clients.TryGetValue(tcpClient.Client.RemoteEndPoint, out NicehashProxyClient nicehashProxyClient)) + { + // Remote endpoint not registered, add new client + ConsoleHelper.Log(GetType().Name, $"Registered new client {tcpClient.Client.RemoteEndPoint}", LogLevel.Debug); + nicehashProxyClient = new NicehashProxyClient(tcpClient, Node.Web3.Eth.Mining.GetWork, Node.Web3.Eth.Mining.SubmitWork); + Clients.TryAdd(tcpClient.Client.RemoteEndPoint, nicehashProxyClient); + } + return nicehashProxyClient; } } } diff --git a/GetworkStratumProxy/Rpc/JsonRpcMessage.cs b/GetworkStratumProxy/Rpc/JsonRpcMessage.cs index 3c2b63c..288af80 100644 --- a/GetworkStratumProxy/Rpc/JsonRpcMessage.cs +++ b/GetworkStratumProxy/Rpc/JsonRpcMessage.cs @@ -5,9 +5,11 @@ namespace GetworkStratumProxy.Rpc public abstract class JsonRpcMessage { [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public int? Id { get; set; } [JsonPropertyName("jsonrpc")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string JsonRpc { get; set; } } } diff --git a/GetworkStratumProxy/Rpc/Nicehash/JsonRpcNotification.cs b/GetworkStratumProxy/Rpc/Nicehash/JsonRpcNotification.cs new file mode 100644 index 0000000..97de456 --- /dev/null +++ b/GetworkStratumProxy/Rpc/Nicehash/JsonRpcNotification.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace GetworkStratumProxy.Rpc.Nicehash +{ + public abstract class JsonRpcNotification : JsonRpcResponse + { + [JsonPropertyName("method")] + public string Method { get; set; } + + [JsonPropertyName("params")] + public object[] Params { get; set; } + + public JsonRpcNotification() + { + Id = null; + } + } +} diff --git a/GetworkStratumProxy/Rpc/Nicehash/MiningNotifyNotification.cs b/GetworkStratumProxy/Rpc/Nicehash/MiningNotifyNotification.cs new file mode 100644 index 0000000..7c5d143 --- /dev/null +++ b/GetworkStratumProxy/Rpc/Nicehash/MiningNotifyNotification.cs @@ -0,0 +1,20 @@ +using Nethereum.Hex.HexTypes; + +namespace GetworkStratumProxy.Rpc.Nicehash +{ + public sealed class MiningNotifyNotification : JsonRpcNotification + { + public MiningNotifyNotification(string jobId, string seedHash, string headerHash, bool clearJobQueue) + { + Method = "mining.notify"; + + Params = new object[] + { + jobId.Replace("0x", ""), + new HexBigInteger(seedHash).HexValue.Replace("0x", ""), + new HexBigInteger(headerHash).HexValue.Replace("0x", ""), + clearJobQueue + }; + } + } +} diff --git a/GetworkStratumProxy/Rpc/Nicehash/SetDifficultyNotification.cs b/GetworkStratumProxy/Rpc/Nicehash/SetDifficultyNotification.cs new file mode 100644 index 0000000..74fa41a --- /dev/null +++ b/GetworkStratumProxy/Rpc/Nicehash/SetDifficultyNotification.cs @@ -0,0 +1,11 @@ +namespace GetworkStratumProxy.Rpc.Nicehash +{ + public sealed class SetDifficultyNotification : JsonRpcNotification + { + public SetDifficultyNotification(decimal difficulty) + { + Method = "mining.set_difficulty"; + Params = new object[] { difficulty }; + } + } +}