Complete sources for a monero webminer.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1165 lines
47 KiB

// The MIT License (MIT)
// Copyright (c) 2018-2019 - the webminerpool developer
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Fleck;
using TinyJson;
using JsonData = System.Collections.Generic.Dictionary<string, object>;
namespace Server {
public class Client {
public PoolConnection PoolConnection;
public IWebSocketConnection WebSocket;
public string Pool = string.Empty;
public string Login;
public string Password;
public bool GotHandshake;
public bool GotPoolInfo;
public DateTime Created;
public DateTime LastPoolJobTime;
public string LastTarget = string.Empty;
public string UserId;
public int NumChecked = 0;
public double Fee = DevDonation.DonationLevel;
public int Version = 1;
}
public class JobInfo {
public string JobId;
public string InnerId;
public string Blob;
public string Target;
public int Variant;
public string Algo;
public CcHashset<string> Solved;
public bool DevJob;
}
public class Job {
public string Blob;
public string Target;
public int Variant;
public string JobId;
public string Algo;
public DateTime Age = DateTime.MinValue;
}
public struct Credentials {
public string Pool;
public string Login;
public string Password;
}
class MainClass {
[DllImport ("libhash.so", CallingConvention = CallingConvention.StdCall)]
static extern IntPtr hash_cn (string hex, int lite, int variant);
[DllImport ("libhash.so", CallingConvention = CallingConvention.StdCall)]
static extern IntPtr hash_free (IntPtr ptr);
public const string SEP = "<-|->";
public const string RegexIsHex = "^[a-fA-F0-9]+$";
public const string RegexIsXMR = "[a-zA-Z|\\d]{95}";
public const int JobCacheSize = (int) 1e5;
private static bool libHashAvailable = false;
private static PoolList PoolList;
// time to connect to a pool in seconds
private const int GraceConnectionTime = 16;
// server logic every x seconds
private const int HeartbeatRate = 10;
// after that job-age we do not forward dev jobs
private const int TimeDevJobsAreOld = 600;
// in seconds, pool is not sending new jobs
private const int PoolTimeout = 60 * 12;
// for the statistics shown every heartbeat
private const int SpeedAverageOverXHeartbeats = 10;
// try not to kill ourselfs
private const int MaxHashChecksPerHeartbeat = 40;
// so we can keep an eye on the memory
private const int ForceGCEveryXHeartbeat = 40;
// save statistics
private const int SaveStatisticsEveryXHeartbeat = 40;
// mining with the same credentials (pool, login, password)
// results in connections beeing "bundled" to a single connection
// seen by the pool. that can result in large difficulties and
// hashrate fluctuations. this parameter sets the number of clients
// in one batch, e.g. for BatchSize = 100 and 1000 clients
// there will be 10 pool connections.
public const int BatchSize = 200;
private static int Heartbeats = 0;
private static int HashesCheckedThisHeartbeat = 0;
private static long totalHashes = 0;
private static long totalDevHashes = 0;
private static long exceptionCounter = 0;
private static bool saveLoginIdsNextHeartbeat = false;
private static CcDictionary<Guid, Client> clients = new CcDictionary<Guid, Client> ();
private static CcDictionary<string, JobInfo> jobInfos = new CcDictionary<string, JobInfo> ();
private static CcDictionary<string, long> statistics = new CcDictionary<string, long> ();
private static CcDictionary<string, Credentials> loginids = new CcDictionary<string, Credentials> ();
private static CcDictionary<string, int> credentialSpamProtector = new CcDictionary<string, int> ();
private static CcHashset<Client> slaves = new CcHashset<Client> ();
private static CcQueue<string> jobQueue = new CcQueue<string> ();
private static Job devJob = new Job ();
static Client ourself;
private static UInt32 HexToUInt32 (String hex) {
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
bytes[i / 2] = Convert.ToByte (hex.Substring (i, 2), 16);
return BitConverter.ToUInt32 (bytes, 0);;
}
private static bool CheckHashTarget (string target, string result) {
// first check if result meets target
string ourtarget = result.Substring (56, 8);
if (HexToUInt32 (ourtarget) >= HexToUInt32 (target))
return false;
else
return true;
}
//private static object hashLocker = new object ();
private static bool CheckHash (string blob, string algo, int variant, string nonce, string target, string result, bool fullcheck) {
// first check if result meets target
string ourtarget = result.Substring (56, 8);
if (HexToUInt32 (ourtarget) >= HexToUInt32 (target))
return false;
if (libHashAvailable && fullcheck) {
// recalculate the hash
string parta = blob.Substring (0, 78);
string partb = blob.Substring (86, blob.Length - 86);
// hashlib should be thread safe. If you encounter problems
// (mono crashing with sigsev)
// a workaround is to uncomment the lock.
//lock (hashLocker) {
IntPtr pStr;
if (algo == "cn") pStr = hash_cn (parta + nonce + partb, 0, variant);
else pStr = hash_cn (parta + nonce + partb, 1, variant);
string ourresult = Marshal.PtrToStringAnsi (pStr);
hash_free (pStr);
if (ourresult != result) return false;
//}
}
return true;
}
private static void PoolDisconnectCallback (Client client, string reason) {
DisconnectClient (client, reason);
}
private static void PoolErrorCallback (Client client, JsonData msg) {
if (msg["error"] == null) {
// looks good
string forward = "{\"identifier\":\"" + "hashsolved" + "\"}\n";
client.WebSocket.Send (forward);
Console.WriteLine ("{0}: solved job", client.WebSocket.ConnectionInfo.Id);
} else {
if (client != ourself) {
string forward = "{\"identifier\":\"" + "error" +
"\",\"param\":\"" + "pool rejected hash" + "\"}\n";
client.WebSocket.Send (forward);
Console.WriteLine ("{0}: got a pool rejection", client.WebSocket.ConnectionInfo.Id);
}
}
}
private static bool IsCompatible(string blob, int variant, int clientVersion)
{
// clientVersion < 5 is not allowed to connect anymore.
// clientVersion 5 does support variant 0 and 1
// clientVersion 6 does support 0,1 and >1
// blobVersion7 = cn1, blobVersionX = cn2 if X > 7
if (clientVersion > 5) return true;
else
{
if (variant == -1)
{
bool iscn2 = false;
try { iscn2 = (HexToUInt32(blob.Substring(0, 2) + "000000") > 7); } catch { }
if (iscn2) return false;
}
else if (variant > 1)
{
return false;
}
return true;
}
}
private static void PoolReceiveCallback (Client client, JsonData msg, CcHashset<string> hashset) {
string jobId = Guid.NewGuid ().ToString ("N");
client.LastPoolJobTime = DateTime.Now;
// Todo: This can be done easier/nicer.
JobInfo ji = new JobInfo
{
JobId = jobId,
Blob = msg["blob"].GetString(),
Target = msg["target"].GetString(),
InnerId = msg["job_id"].GetString(),
Algo = msg["algo"].GetString(),
Solved = hashset,
DevJob = (client == ourself)
};
if (!int.TryParse (msg["variant"].GetString (), out ji.Variant)) { ji.Variant = -1; }
jobInfos.TryAdd (jobId, ji); // Todo: We can combine these two datastructures
jobQueue.Enqueue (jobId);
if (client == ourself) {
devJob.Blob = ji.Blob;
devJob.JobId = jobId;
devJob.Age = DateTime.Now;
devJob.Algo = ji.Algo;
devJob.Target = ji.Target;
devJob.Variant = ji.Variant;
List<Client> slavelist = new List<Client> (slaves.Values);
foreach (Client slave in slavelist) {
bool compatible = IsCompatible(devJob.Blob, devJob.Variant, slave.Version);
if (!compatible) continue;
string newtarget;
string forward;
if (string.IsNullOrEmpty (slave.LastTarget)) {
newtarget = devJob.Target;
} else {
uint diff1 = HexToUInt32 (slave.LastTarget);
uint diff2 = HexToUInt32 (devJob.Target);
if (diff1 > diff2)
newtarget = slave.LastTarget;
else
newtarget = devJob.Target;
}
forward = "{\"identifier\":\"" + "job" +
"\",\"job_id\":\"" + devJob.JobId +
"\",\"algo\":\"" + devJob.Algo.ToLower () +
"\",\"variant\":" + devJob.Variant.ToString () +
",\"blob\":\"" + devJob.Blob +
"\",\"target\":\"" + newtarget + "\"}\n";
slave.WebSocket.Send (forward);
Console.WriteLine ("Sending job to slave {0}", slave.WebSocket.ConnectionInfo.Id);
}
} else {
// forward this to the websocket!
string forward = string.Empty;
bool tookdev = false;
if (Random2.NextDouble () < client.Fee) {
if (((DateTime.Now - devJob.Age).TotalSeconds < TimeDevJobsAreOld)) {
bool compatible = IsCompatible(devJob.Blob, devJob.Variant, client.Version);
if (compatible) {
// okay, do not send devjob.Target, but
// the last difficulty
string newtarget = string.Empty;
if (string.IsNullOrEmpty (client.LastTarget)) {
newtarget = devJob.Target;
} else {
uint diff1 = HexToUInt32 (client.LastTarget);
uint diff2 = HexToUInt32 (devJob.Target);
if (diff1 > diff2)
newtarget = client.LastTarget;
else
newtarget = devJob.Target;
}
forward = "{\"identifier\":\"" + "job" +
"\",\"job_id\":\"" + devJob.JobId +
"\",\"algo\":\"" + devJob.Algo.ToLower () +
"\",\"variant\":" + devJob.Variant.ToString () +
",\"blob\":\"" + devJob.Blob +
"\",\"target\":\"" + newtarget + "\"}\n";
tookdev = true;
}
}
}
if (!tookdev) {
bool compatible = IsCompatible(ji.Blob, ji.Variant, client.Version);
if (!compatible) return;
forward = "{\"identifier\":\"" + "job" +
"\",\"job_id\":\"" + jobId +
"\",\"algo\":\"" + msg["algo"].GetString ().ToLower () +
"\",\"variant\":" + msg["variant"].GetString () +
",\"blob\":\"" + msg["blob"].GetString () +
"\",\"target\":\"" + msg["target"].GetString () + "\"}\n";
client.LastTarget = msg["target"].GetString ();
}
if (tookdev) {
if (!slaves.Contains (client)) slaves.TryAdd (client);
Console.WriteLine ("Send dev job!");
} else {
slaves.TryRemove (client);
}
client.WebSocket.Send (forward);
Console.WriteLine ("{0}: got job from pool", client.WebSocket.ConnectionInfo.Id);
}
}
public static void RemoveClient (Guid guid) {
Client client;
if (!clients.TryRemove (guid, out client)) return;
slaves.TryRemove (client);
try {
var wsoc = client.WebSocket as WebSocketConnection;
if (wsoc != null) wsoc.CloseSocket ();
} catch { }
try { client.WebSocket.Close (); } catch { }
if (client.PoolConnection != null)
PoolConnectionFactory.Close (client);
}
public static void DisconnectClient (Client client, string reason) {
if (client.WebSocket.IsAvailable) {
string msg = "{\"identifier\":\"" + "error" +
"\",\"param\":\"" + reason + "\"}\n";
System.Threading.Tasks.Task t = client.WebSocket.Send (msg);
t.ContinueWith ((prevTask) => {
prevTask.Wait ();
RemoveClient (client.WebSocket.ConnectionInfo.Id);
});
} else {
RemoveClient (client.WebSocket.ConnectionInfo.Id);
}
}
public static void DisconnectClient (Guid guid, string reason) {
Client client = clients[guid];
DisconnectClient (client, reason);
}
private static void CreateOurself()
{
ourself = new Client();
ourself.Login = DevDonation.DevAddress;
ourself.Pool = DevDonation.DevPoolUrl;
ourself.Created = ourself.LastPoolJobTime = DateTime.Now;
ourself.Password = DevDonation.DevPoolPwd;
ourself.WebSocket = new EmptyWebsocket();
clients.TryAdd(Guid.Empty, ourself);
ourself.PoolConnection = PoolConnectionFactory.CreatePoolConnection(ourself,
DevDonation.DevPoolUrl, DevDonation.DevPoolPort, DevDonation.DevAddress, DevDonation.DevPoolPwd);
ourself.PoolConnection.DefaultAlgorithm = "cn";
ourself.PoolConnection.DefaultVariant = -1;
}
private static bool CheckLibHash (string input, string expected,
int lite, int variant, out Exception ex) {
string hashedResult = string.Empty;
try {
IntPtr pStr = hash_cn (input, lite, variant);
hashedResult = Marshal.PtrToStringAnsi (pStr);
hash_free (pStr);
} catch (Exception e) {
ex = e;
return false;
}
if (hashedResult != expected) {
ex = new Exception ("Hash function returned wrong hash");
return false;
}
ex = null;
return true;
}
private static void ExcessiveHashTest () {
Parallel.For (0, 100, (i) => {
string testStr = new string ('1', 151) + '3';
IntPtr ptr = hash_cn (testStr, 1, 0);
string str = Marshal.PtrToStringAnsi (ptr);
hash_free (ptr);
Console.WriteLine (i.ToString () + " " + str);
});
}
public static void Main (string[] args) {
//ExcessiveHashTest(); return;
CConsole.ColorInfo (() => {
#if (DEBUG)
Console.WriteLine ("[{0}] webminerpool server started - DEBUG MODE", DateTime.Now);
#else
Console.WriteLine ("[{0}] webminerpool server started", DateTime.Now);
#endif
double devfee = (new Client ()).Fee;
if (devfee > double.Epsilon)
Console.WriteLine ("Developer fee of {0}% enabled. Thank You.", (devfee * 100.0d).ToString ("F1"));
Console.WriteLine ();
});
try {
PoolList = PoolList.LoadFromFile ("pools.json");
}
catch(Exception ex) {
CConsole.ColorAlert (() => Console.WriteLine("Could not load pool list from pools.json: {0}", ex.Message));
return;
}
CConsole.ColorInfo (() => Console.WriteLine ("Loaded {0} pools from pools.json.", PoolList.Count));
Exception exception = null;
libHashAvailable = true;
// cn
libHashAvailable &= CheckLibHash ("6465206f6d6e69627573206475626974616e64756d",
"2f8e3df40bd11f9ac90c743ca8e32bb391da4fb98612aa3b6cdc639ee00b31f5",
0, 0, out exception);
// cn_v1
libHashAvailable &= CheckLibHash("38274c97c45a172cfc97679870422e3a1ab0784960c60514d816271415c306ee3a3ed1a77e31f6a885c3cb",
"ed082e49dbd5bbe34a3726a0d1dad981146062b39d36d62c71eb1ed8ab49459b",
0, 1, out exception);
// cn_v2
libHashAvailable &= CheckLibHash("5468697320697320612074657374205468697320697320612074657374205468697320697320612074657374",
"353fdc068fd47b03c04b9431e005e00b68c2168a3cc7335c8b9b308156591a4f",
0, 2, out exception);
// cn_lite
libHashAvailable &= CheckLibHash("6465206f6d6e69627573206475626974616e64756d",
"1b73647a792df8724ce28fddc1e4b6f348dc39e6aa47c434fe400cec98ec2b91",
1, 0, out exception);
// cn_lite_v1
libHashAvailable &= CheckLibHash("38274c97c45a172cfc97679870422e3a1ab0784960c60514d816271415c306ee3a3ed1a77e31f6a885c3cb",
"4e785376ed2733262d83cc25321a9d0003f5395315de919acf1b97f0a84fbd2d",
1, 1, out exception);
// cn_lite_v2 (speculative)
libHashAvailable &= CheckLibHash("5468697320697320612074657374205468697320697320612074657374205468697320697320612074657374",
"49c95241af3bd74e78f473936ac36214bbc386a36869f9406b5da16aa0ee4b06",
1, 2, out exception);
if (!libHashAvailable) CConsole.ColorWarning (() =>
Console.WriteLine ("libhash.so is not available. Checking user submitted hashes disabled.")
);
PoolConnectionFactory.RegisterCallbacks (PoolReceiveCallback, PoolErrorCallback, PoolDisconnectCallback);
if (File.Exists ("statistics.dat")) {
try {
statistics.Clear ();
string[] lines = File.ReadAllLines ("statistics.dat");
foreach (string line in lines) {
string[] statisticsdata = line.Split (new string[] { SEP }, StringSplitOptions.None);
string statid = statisticsdata[1];
long statnum = 0;
long.TryParse (statisticsdata[0], out statnum);
statistics.TryAdd (statid, statnum);
}
} catch (Exception ex) {
CConsole.ColorAlert (() =>
Console.WriteLine ("Error while reading statistics: {0}", ex));
}
}
if (File.Exists ("logins.dat")) {
try {
loginids.Clear ();
string[] lines = File.ReadAllLines ("logins.dat");
foreach (string line in lines) {
string[] logindata = line.Split (new string[] { SEP }, StringSplitOptions.None);
Credentials cred = new Credentials
{
Pool = logindata[1],
Login = logindata[2],
Password = logindata[3]
};
loginids.TryAdd (logindata[0], cred);
}
} catch (Exception ex) {
CConsole.ColorAlert (() =>
Console.WriteLine ("Error while reading logins: {0}", ex));
}
}
X509Certificate2 cert = null;
try { cert = new X509Certificate2 ("certificate.pfx", "miner"); } catch (Exception e) { exception = e; cert = null; }
bool certAvailable = (cert != null);
if (!certAvailable)
CConsole.ColorWarning (() => Console.WriteLine ("SSL certificate could not be loaded. Secure connection disabled."));
WebSocketServer server;
string localAddr = (certAvailable ? "wss://" : "ws://") + "0.0.0.0:8181";
server = new WebSocketServer (localAddr);
server.Certificate = cert;
FleckLog.LogAction = (level, message, ex) => {
switch (level) {
case LogLevel.Debug:
#if (DEBUG)
Console.WriteLine ("FLECK (Debug): " + message);
#endif
break;
case LogLevel.Error:
if (ex != null && !string.IsNullOrEmpty (ex.Message)) {
CConsole.ColorAlert (() => Console.WriteLine ("FLECK: " + message + " " + ex.Message));
exceptionCounter++;
if ((exceptionCounter % 200) == 0) {
Helper.WriteTextAsyncWrapper ("fleck_error.txt", ex.ToString ());
}
} else Console.WriteLine ("FLECK: " + message);
break;
case LogLevel.Warn:
if (ex != null && !string.IsNullOrEmpty (ex.Message)) {
Console.WriteLine ("FLECK: " + message + " " + ex.Message);
exceptionCounter++;
if ((exceptionCounter % 200) == 0) {
Helper.WriteTextAsyncWrapper ("fleck_warn.txt", ex.ToString ());
}
} else Console.WriteLine ("FLECK: " + message);
break;
default:
Console.WriteLine ("FLECK: " + message);
break;
}
};
server.RestartAfterListenError = true;
server.ListenerSocket.NoDelay = false;
server.Start (socket => {
socket.OnOpen = () => {
string ipadr = string.Empty;
try { ipadr = socket.ConnectionInfo.ClientIpAddress; } catch { }
Client client = new Client ();
client.WebSocket = socket;
client.Created = client.LastPoolJobTime = DateTime.Now;
Guid guid = socket.ConnectionInfo.Id;
clients.TryAdd (guid, client);
Console.WriteLine ("{0}: connected with ip {1}", guid, ipadr);
};
socket.OnClose = () => {
Guid guid = socket.ConnectionInfo.Id;
RemoveClient (socket.ConnectionInfo.Id);
Console.WriteLine (guid + ": closed");
};
socket.OnError = error => {
Guid guid = socket.ConnectionInfo.Id;
RemoveClient (socket.ConnectionInfo.Id);
Console.WriteLine (guid + ": unexpected close");
};
socket.OnMessage = message => {
string ipadr = string.Empty;
try { ipadr = socket.ConnectionInfo.ClientIpAddress; } catch { }
Guid guid = socket.ConnectionInfo.Id;
if (message.Length > 3000) {
RemoveClient (guid); // that can't be valid, do not even try to parse
}
JsonData msg = message.FromJson<JsonData> ();
if (msg == null || !msg.ContainsKey ("identifier")) return;
Client client = null;
// in very rare occasions, we get interference with onopen()
// due to async code. wait a second and retry.
for (int tries = 0; tries < 4; tries++) {
if (clients.TryGetValue (guid, out client)) break;
Task.Run (async delegate { await Task.Delay (TimeSpan.FromSeconds (1)); }).Wait ();
}
if (client == null) {
// famous comment: this should not happen
RemoveClient (guid);
return;
}
string identifier = (string) msg["identifier"];
if (identifier == "handshake") {
if (client.GotHandshake) {
// no merci for malformed data.
DisconnectClient (client, "Handshake already performed.");
return;
}
client.GotHandshake = true;
if (msg.ContainsKey ("version")) {
int.TryParse (msg["version"].GetString (), out client.Version);
}
if (client.Version < 5) {
DisconnectClient (client, "Client version too old.");
return;
}
if (client.Version < 6) {
CConsole.ColorWarning (() => Console.WriteLine ("Warning: Outdated client connected. Make sure to update the clients"));
}
if (msg.ContainsKey ("loginid")) {
string loginid = msg["loginid"].GetString ();
if (loginid.Length != 36 && loginid.Length != 32) {
Console.WriteLine ("Invalid LoginId!");
DisconnectClient (client, "Invalid loginid.");
return;
}
Credentials crdts;
if (!loginids.TryGetValue (loginid, out crdts)) {
Console.WriteLine ("Unregistered LoginId! {0}", loginid);
DisconnectClient (client, "Loginid not registered!");
return;
}
client.Login = crdts.Login;
client.Password = crdts.Password;
client.Pool = crdts.Pool;
} else if (msg.ContainsKey ("login") && msg.ContainsKey ("password") && msg.ContainsKey ("pool")) {
client.Login = msg["login"].GetString ();
client.Password = msg["password"].GetString ();
client.Pool = msg["pool"].GetString ();
} else {
// no merci for malformed data.
Console.WriteLine ("Malformed handshake");
DisconnectClient (client, "Login, password and pool have to be specified.");
return;
}
client.UserId = string.Empty;
if (msg.ContainsKey ("userid")) {
string uid = msg["userid"].GetString ();
if (uid.Length > 200) { RemoveClient (socket.ConnectionInfo.Id); return; }
client.UserId = uid;
}
Console.WriteLine ("{0}: handshake - {1}, {2}", guid, client.Pool,
(client.Login.Length > 8 ? client.Login.Substring (0, 8) + "..." : client.Login));
if (!string.IsNullOrEmpty (ipadr)) Firewall.Update (ipadr, Firewall.UpdateEntry.Handshake);
PoolInfo pi;
if (!PoolList.TryGetPool(client.Pool, out pi)) {
// we dont have that pool?
DisconnectClient (client, "pool not known");
return;
}
// if pools have some default password
if (client.Password == "") client.Password = pi.EmptyPassword;
client.PoolConnection = PoolConnectionFactory.CreatePoolConnection (
client, pi.Url, pi.Port, client.Login, client.Password);
client.PoolConnection.DefaultAlgorithm = pi.DefaultAlgorithm;
client.PoolConnection.DefaultVariant = pi.DefaultVariant;
} else if (identifier == "solved") {
if (!client.GotHandshake) {
// no merci
RemoveClient (socket.ConnectionInfo.Id);
return;
}
Console.WriteLine ("{0}: reports solved hash", guid);
new Task (() => {
if (!msg.ContainsKey ("job_id") ||
!msg.ContainsKey ("nonce") ||
!msg.ContainsKey ("result")) {
// no merci for malformed data.
RemoveClient (guid);
return;
}
string jobid = msg["job_id"].GetString ();
JobInfo ji;
if (!jobInfos.TryGetValue (jobid, out ji)) {
// this job id is not known to us
Console.WriteLine ("Job unknown!");
return;
}
string reportedNonce = msg["nonce"].GetString ();
string reportedResult = msg["result"].GetString ();
if (ji.Solved.Contains (reportedNonce.ToLower ())) {
Console.WriteLine ("Nonce collision!");
return;
}
if (reportedNonce.Length != 8 || (!Regex.IsMatch (reportedNonce, RegexIsHex))) {
DisconnectClient (client, "nonce malformed");
return;
}
if (reportedResult.Length != 64 || (!Regex.IsMatch (reportedResult, RegexIsHex))) {
DisconnectClient (client, "result malformed");
return;
}
double prob = ((double) HexToUInt32 (ji.Target)) / ((double) 0xffffffff);
long howmanyhashes = ((long) (1.0 / prob));
totalHashes += howmanyhashes;
if (ji.DevJob) {
// that was an "dev" job. could be that the target does not match
if (!CheckHashTarget (ji.Target, reportedResult)) {
Console.WriteLine ("Hash does not reach our target difficulty.");
return;
}
totalDevHashes += howmanyhashes;
}
// default chance to get hash-checked is 10%
double chanceForACheck = 0.1;
// check new clients more often, but prevent that to happen the first 30s the server is running
if (Heartbeats > 3 && client.NumChecked < 9) chanceForACheck = 1.0 - 0.1 * client.NumChecked;
bool performFullCheck = (Random2.NextDouble () < chanceForACheck && HashesCheckedThisHeartbeat < MaxHashChecksPerHeartbeat);
if (performFullCheck) {
client.NumChecked++;
HashesCheckedThisHeartbeat++;
}
bool validHash = CheckHash (ji.Blob, ji.Algo, ji.Variant, reportedNonce, ji.Target, reportedResult, performFullCheck);
if (!validHash) {
CConsole.ColorWarning (() =>
Console.WriteLine ("{0} got disconnected for WRONG hash.", client.WebSocket.ConnectionInfo.Id.ToString ()));
if (!string.IsNullOrEmpty (ipadr)) Firewall.Update (ipadr, Firewall.UpdateEntry.WrongHash);
RemoveClient (client.WebSocket.ConnectionInfo.Id);
} else {
if (performFullCheck)
Console.WriteLine ("{0}: got hash-checked", client.WebSocket.ConnectionInfo.Id.ToString ());
if (!string.IsNullOrEmpty (ipadr)) Firewall.Update (ipadr, Firewall.UpdateEntry.SolvedJob);
ji.Solved.TryAdd (reportedNonce.ToLower ());
if (client.UserId != string.Empty) {
long currentstat = 0;
bool exists = statistics.TryGetValue (client.UserId, out currentstat);
if (exists) statistics[client.UserId] = currentstat + howmanyhashes;
else statistics.TryAdd (client.UserId, howmanyhashes);
}
if (!ji.DevJob) client.PoolConnection.Hashes += howmanyhashes;
Client jiClient = client;
if (ji.DevJob) jiClient = ourself;
string msg1 = "{\"id\":\"" + jiClient.PoolConnection.PoolId +
"\",\"job_id\":\"" + ji.InnerId +
"\",\"nonce\":\"" + msg["nonce"].GetString () +
"\",\"result\":\"" + msg["result"].GetString () +
"\"}";
string msg0 = "{\"method\":\"" + "submit" +
"\",\"params\":" + msg1 +
",\"id\":\"" + "1" + "\"}\n"; // TODO: check the "1"
jiClient.PoolConnection.Send (jiClient, msg0);
}
}).Start ();
} else if (identifier == "poolinfo") {
if (!client.GotPoolInfo) {
client.GotPoolInfo = true;
client.WebSocket.Send (PoolList.JsonPools);
}
} else
if (identifier == "register") {
string registerip = string.Empty;
try { registerip = client.WebSocket.ConnectionInfo.ClientIpAddress; } catch { };
if (string.IsNullOrEmpty (registerip)) { DisconnectClient (guid, "Unknown error."); return; }
int registeredThisSession = 0;
if (credentialSpamProtector.TryGetValue (registerip, out registeredThisSession)) {
registeredThisSession++;
credentialSpamProtector[registerip] = registeredThisSession;
} else {
credentialSpamProtector.TryAdd (registerip, 0);
}
if (registeredThisSession > 10) {
DisconnectClient (guid, "Too many registrations. You need to wait.");
return;
}
if (!msg.ContainsKey ("login") ||
!msg.ContainsKey ("password") ||
!msg.ContainsKey ("pool")) {
// no merci for malformed data.
DisconnectClient (guid, "Login, password and pool have to be specified!");
return;
}
// everything seems to be okay
Credentials crdts = new Credentials ();
crdts.Login = msg["login"].GetString ();
crdts.Pool = msg["pool"].GetString ();
crdts.Password = msg["password"].GetString ();
PoolInfo pi;
if (!PoolList.TryGetPool (crdts.Pool, out pi)) {
// we dont have that pool?
DisconnectClient (client, "Pool not known!");
return;
}
bool loginok = false;
try { loginok = Regex.IsMatch (crdts.Login, RegexIsXMR); } catch { }
if (!loginok) {
DisconnectClient (client, "Not a valid address.");
return;
}
if (crdts.Password.Length > 120) {
DisconnectClient (client, "Password too long.");
return;
}
string newloginguid = Guid.NewGuid ().ToString ("N");
loginids.TryAdd (newloginguid, crdts);
string smsg = "{\"identifier\":\"" + "registered" +
"\",\"loginid\":\"" + newloginguid +
"\"}";
client.WebSocket.Send (smsg);
Console.WriteLine ("Client registered!");
saveLoginIdsNextHeartbeat = true;
} else if (identifier == "userstats") {
if (!msg.ContainsKey ("userid")) return;
Console.WriteLine ("Userstat request");
string uid = msg["userid"].GetString ();
long hashn = 0;
statistics.TryGetValue (uid, out hashn);
string smsg = "{\"identifier\":\"" + "userstats" +
"\",\"userid\":\"" + uid +
"\",\"value\":" + hashn.ToString () + "}\n";
client.WebSocket.Send (smsg);
}
};
});
bool running = true;
double totalSpeed = 0, totalDevSpeed = 0;
while (running) {
Heartbeats++;
Firewall.Heartbeat (Heartbeats);
try {
if (Heartbeats % SaveStatisticsEveryXHeartbeat == 0) {
CConsole.ColorInfo (() => Console.WriteLine ("Saving statistics."));
StringBuilder sb = new StringBuilder ();
foreach (var stat in statistics) {
sb.AppendLine (stat.Value.ToString () + SEP + stat.Key);
}
File.WriteAllText ("statistics.dat", sb.ToString ().TrimEnd ('\r', '\n'));
}
} catch (Exception ex) {
CConsole.ColorAlert (() => Console.WriteLine ("Error saving statistics.dat: {0}", ex));
}
try {
if (saveLoginIdsNextHeartbeat) {
saveLoginIdsNextHeartbeat = false;
CConsole.ColorInfo (() => Console.WriteLine ("Saving logins."));
StringBuilder sb = new StringBuilder ();
foreach (var lins in loginids) {
sb.AppendLine (lins.Key + SEP + lins.Value.Pool + SEP + lins.Value.Login + SEP + lins.Value.Password);
}
File.WriteAllText ("logins.dat", sb.ToString ().TrimEnd ('\r', '\n'));
}
} catch (Exception ex) {
CConsole.ColorAlert (() => Console.WriteLine ("Error saving logins.dat: {0}", ex));
}
try {
Task.Run (async delegate { await Task.Delay (TimeSpan.FromSeconds (HeartbeatRate)); }).Wait ();
if (Heartbeats % SpeedAverageOverXHeartbeats == 0) {
totalSpeed = (double) totalHashes / (double) (HeartbeatRate * SpeedAverageOverXHeartbeats);
totalDevSpeed = (double) totalDevHashes / (double) (HeartbeatRate * SpeedAverageOverXHeartbeats);
totalHashes = 0;
totalDevHashes = 0;
}
CConsole.ColorInfo (() =>
Console.WriteLine ("[{0}] heartbeat; connections client/pool: {1}/{2}; jobqueue: {3}k; speed: {4}kH/s",
DateTime.Now.ToString (),
clients.Count,
PoolConnectionFactory.Connections.Count,
((double) jobQueue.Count / 1000.0d).ToString ("F1"),
((double) totalSpeed / 1000.0d).ToString ("F1")));
while (jobQueue.Count > JobCacheSize) {
string deq;
if (jobQueue.TryDequeue (out deq)) {
jobInfos.TryRemove (deq);
}
}
DateTime now = DateTime.Now;
List<PoolConnection> pcc = new List<PoolConnection> (PoolConnectionFactory.Connections.Values);
foreach (PoolConnection pc in pcc) {
PoolConnectionFactory.CheckPoolConnection (pc);
}
List<Client> cc = new List<Client> (clients.Values);
foreach (Client c in cc) {
try {
if ((now - c.Created).TotalSeconds > GraceConnectionTime) {
if (c.PoolConnection == null || c.PoolConnection.TcpClient == null) DisconnectClient (c, "timeout.");
else if (!c.PoolConnection.TcpClient.Connected) DisconnectClient (c, "lost pool connection.");
else if ((now - c.LastPoolJobTime).TotalSeconds > PoolTimeout) {
DisconnectClient (c, "pool is not sending new jobs.");
}
}
} catch { RemoveClient (c.WebSocket.ConnectionInfo.Id); }
}
if (clients.ContainsKey (Guid.Empty)) {
if (clients.Count == 1)
RemoveClient (Guid.Empty);
} else {
// we removed ourself because we got disconnected from the pool
// make us alive again!
if (clients.Count > 4 && DevDonation.DonationLevel > double.Epsilon) {
CConsole.ColorWarning (() =>
Console.WriteLine ("disconnected from dev pool. trying to reconnect."));
devJob = new Job ();
CreateOurself ();
}
}
HashesCheckedThisHeartbeat = 0;
if (Heartbeats % ForceGCEveryXHeartbeat == 0) {
CConsole.ColorInfo (() => {
Console.WriteLine ("Garbage collection. Currently using {0} MB.", Math.Round (((double) (GC.GetTotalMemory (false)) / 1024 / 1024)));
DateTime tbc = DateTime.Now;
// trust me, I am a professional
GC.Collect (GC.MaxGeneration, GCCollectionMode.Forced); // DON'T DO THIS!!!
Console.WriteLine ("Garbage collected in {0} ms. Currently using {1} MB ({2} clients).", (DateTime.Now - tbc).Milliseconds,
Math.Round (((double) (GC.GetTotalMemory (false)) / 1024 / 1024)), clients.Count);
});
}
} catch (Exception ex) {
CConsole.ColorAlert (() =>
Console.WriteLine ("Exception caught in the main loop ! {0}", ex));
}
}
}
}
}