diff --git a/.gitignore b/.gitignore index c318eebe2..0bf0b0de5 100644 --- a/.gitignore +++ b/.gitignore @@ -205,4 +205,10 @@ fabric.properties # End of https://www.toptal.com/developers/gitignore/api/rider .idea -.fake \ No newline at end of file +.fake +/nbproject/ +/*/*/*/*.a +/*/*/*/*/*/*/*/*.o +/*/*/*/*/*/*/*/*/*.o +/*/*/*/*/*/*/*/*/*/*.o +/*/*/*/*/*/*/*/*/*/*.o diff --git a/src/Miningcore/AutofacModule.cs b/src/Miningcore/AutofacModule.cs index 8b70800ad..ef2b595b9 100644 --- a/src/Miningcore/AutofacModule.cs +++ b/src/Miningcore/AutofacModule.cs @@ -3,6 +3,7 @@ using Miningcore.Api; using Miningcore.Banning; using Miningcore.Blockchain.Bitcoin; +using Miningcore.Blockchain.Conceal; using Miningcore.Blockchain.Cryptonote; using Miningcore.Blockchain.Equihash; using Miningcore.Blockchain.Ethereum; @@ -147,7 +148,12 @@ protected override void Load(ContainerBuilder builder) // Bitcoin and family builder.RegisterType(); + + ////////////////////// + // Conceal + builder.RegisterType(); + ////////////////////// // Cryptonote diff --git a/src/Miningcore/Blockchain/Conceal/ConcealConstants.cs b/src/Miningcore/Blockchain/Conceal/ConcealConstants.cs new file mode 100644 index 000000000..d4237a065 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/ConcealConstants.cs @@ -0,0 +1,60 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using Org.BouncyCastle.Math; + +namespace Miningcore.Blockchain.Conceal; + +public enum ConcealNetworkType +{ + Main = 1, + Test +} + +public static class ConcealConstants +{ + public const string WalletDaemonCategory = "wallet"; + + public const string DaemonRpcLocation = "json_rpc"; + public const string DaemonRpcGetInfoLocation = "getinfo"; + public const int ConcealRpcMethodNotFound = -32601; + public const int PaymentIdHexLength = 64; + public const decimal SmallestUnit = 1000000; + public static readonly Regex RegexValidNonce = new("^[0-9a-f]{8}$", RegexOptions.Compiled); + + public static readonly BigInteger Diff1 = new("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16); + public static readonly System.Numerics.BigInteger Diff1b = System.Numerics.BigInteger.Parse("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", NumberStyles.HexNumber); + + public const int PayoutMinBlockConfirmations = 10; + + public const int InstanceIdSize = 4; + public const int ExtraNonceSize = 4; + + // NOTE: for whatever strange reason only reserved_size -1 can be used, + // the LAST byte MUST be zero or nothing works + public const int ReserveSize = ExtraNonceSize + InstanceIdSize + 1; + + // Offset to nonce in block blob + public const int BlobNonceOffset = 39; + + public const decimal StaticTransactionFeeReserve = 0.001m; // in conceal +} + +public static class ConcealCommands +{ + public const string GetInfo = "getinfo"; + public const string GetLastBlockHeader = "getlastblockheader"; + public const string GetBlockTemplate = "getblocktemplate"; + public const string SubmitBlock = "submitblock"; + public const string GetBlockHeaderByHash = "getblockheaderbyhash"; + public const string GetBlockHeaderByHeight = "getblockheaderbyheight"; +} + +public static class ConcealWalletCommands +{ + public const string GetBalance = "getBalance"; + public const string GetAddress = "getAddresses"; + public const string SendTransaction = "sendTransaction"; + public const string GetTransactions = "getTransactions"; + public const string SplitIntegratedAddress = "splitIntegrated"; + public const string Save = "save"; +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/ConcealJob.cs b/src/Miningcore/Blockchain/Conceal/ConcealJob.cs new file mode 100644 index 000000000..c37df5618 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/ConcealJob.cs @@ -0,0 +1,192 @@ +using Miningcore.Blockchain.Conceal.DaemonResponses; +using Miningcore.Configuration; +using Miningcore.Extensions; +using Miningcore.Native; +using Miningcore.Stratum; +using Miningcore.Util; +using Org.BouncyCastle.Math; +using static Miningcore.Native.Cryptonight.Algorithm; +using Contract = Miningcore.Contracts.Contract; + +namespace Miningcore.Blockchain.Conceal; + +public class ConcealJob +{ + public ConcealJob(GetBlockTemplateResponse blockTemplate, byte[] instanceId, string jobId, + ConcealCoinTemplate coin, PoolConfig poolConfig, ClusterConfig clusterConfig, string prevHash) + { + Contract.RequiresNonNull(blockTemplate); + Contract.RequiresNonNull(poolConfig); + Contract.RequiresNonNull(clusterConfig); + Contract.RequiresNonNull(instanceId); + Contract.Requires(!string.IsNullOrEmpty(jobId)); + + BlockTemplate = blockTemplate; + PrepareBlobTemplate(instanceId); + PrevHash = prevHash; + + hashFunc = hashFuncs[coin.Hash]; + } + + protected delegate void HashFunc(ReadOnlySpan data, Span result, ulong height); + + protected static readonly Dictionary hashFuncs = new() + { + { CryptonightHashType.CryptonightCCX, (data, result, height) => Cryptonight.CryptonightHash(data, result, CN_CCX, height) }, + { CryptonightHashType.CryptonightGPU, (data, result, height) => Cryptonight.CryptonightHash(data, result, CN_GPU, height) }, + }; + + private byte[] blobTemplate; + private int extraNonce; + private readonly HashFunc hashFunc; + + private void PrepareBlobTemplate(byte[] instanceId) + { + blobTemplate = BlockTemplate.Blob.HexToByteArray(); + + // inject instanceId + instanceId.CopyTo(blobTemplate, BlockTemplate.ReservedOffset + ConcealConstants.ExtraNonceSize); + } + + private string EncodeBlob(uint workerExtraNonce) + { + Span blob = stackalloc byte[blobTemplate.Length]; + blobTemplate.CopyTo(blob); + + // inject extranonce (big-endian) at the beginning of the reserved area + var bytes = BitConverter.GetBytes(workerExtraNonce.ToBigEndian()); + bytes.CopyTo(blob[BlockTemplate.ReservedOffset..]); + + return CryptonoteBindings.ConvertBlob(blob, blobTemplate.Length).ToHexString(); + } + + private string EncodeTarget(double difficulty, int size = 4) + { + var diff = BigInteger.ValueOf((long) (difficulty * 255d)); + var quotient = ConcealConstants.Diff1.Divide(diff).Multiply(BigInteger.ValueOf(255)); + var bytes = quotient.ToByteArray().AsSpan(); + Span padded = stackalloc byte[32]; + + var padLength = padded.Length - bytes.Length; + + if(padLength > 0) + bytes.CopyTo(padded.Slice(padLength, bytes.Length)); + + padded = padded[..size]; + padded.Reverse(); + + return padded.ToHexString(); + } + + private void ComputeBlockHash(ReadOnlySpan blobConverted, Span result) + { + // blockhash is computed from the converted blob data prefixed with its length + Span block = stackalloc byte[blobConverted.Length + 1]; + block[0] = (byte) blobConverted.Length; + blobConverted.CopyTo(block[1..]); + + CryptonoteBindings.CryptonightHashFast(block, result); + } + + #region API-Surface + + public string PrevHash { get; } + public GetBlockTemplateResponse BlockTemplate { get; } + + public void PrepareWorkerJob(ConcealWorkerJob workerJob, out string blob, out string target) + { + workerJob.Height = BlockTemplate.Height; + workerJob.ExtraNonce = (uint) Interlocked.Increment(ref extraNonce); + + if(extraNonce < 0) + extraNonce = 0; + + blob = EncodeBlob(workerJob.ExtraNonce); + target = EncodeTarget(workerJob.Difficulty); + } + + public (Share Share, string BlobHex) ProcessShare(string nonce, uint workerExtraNonce, string workerHash, StratumConnection worker) + { + Contract.Requires(!string.IsNullOrEmpty(nonce)); + Contract.Requires(!string.IsNullOrEmpty(workerHash)); + Contract.Requires(workerExtraNonce != 0); + + var context = worker.ContextAs(); + + // validate nonce + if(!ConcealConstants.RegexValidNonce.IsMatch(nonce)) + throw new StratumException(StratumError.MinusOne, "malformed nonce"); + + // clone template + Span blob = stackalloc byte[blobTemplate.Length]; + blobTemplate.CopyTo(blob); + + // inject extranonce + var bytes = BitConverter.GetBytes(workerExtraNonce.ToBigEndian()); + bytes.CopyTo(blob[BlockTemplate.ReservedOffset..]); + + // inject nonce + bytes = nonce.HexToByteArray(); + bytes.CopyTo(blob[ConcealConstants.BlobNonceOffset..]); + + // convert + var blobConverted = CryptonoteBindings.ConvertBlob(blob, blobTemplate.Length); + if(blobConverted == null) + throw new StratumException(StratumError.MinusOne, "malformed blob"); + + // hash it + Span headerHash = stackalloc byte[32]; + hashFunc(blobConverted, headerHash, BlockTemplate.Height); + + var headerHashString = headerHash.ToHexString(); + if(headerHashString != workerHash) + throw new StratumException(StratumError.MinusOne, "bad hash"); + + // check difficulty + var headerValue = headerHash.ToBigInteger(); + var shareDiff = (double) new BigRational(ConcealConstants.Diff1b, headerValue); + var stratumDifficulty = context.Difficulty; + var ratio = shareDiff / stratumDifficulty; + var isBlockCandidate = shareDiff >= BlockTemplate.Difficulty; + + // test if share meets at least workers current difficulty + if(!isBlockCandidate && ratio < 0.99) + { + // check if share matched the previous difficulty from before a vardiff retarget + if(context.VarDiff?.LastUpdate != null && context.PreviousDifficulty.HasValue) + { + ratio = shareDiff / context.PreviousDifficulty.Value; + + if(ratio < 0.99) + throw new StratumException(StratumError.LowDifficultyShare, $"low difficulty share ({shareDiff})"); + + // use previous difficulty + stratumDifficulty = context.PreviousDifficulty.Value; + } + + else + throw new StratumException(StratumError.LowDifficultyShare, $"low difficulty share ({shareDiff})"); + } + + var result = new Share + { + BlockHeight = BlockTemplate.Height, + Difficulty = stratumDifficulty, + }; + + if(isBlockCandidate) + { + // Compute block hash + Span blockHash = stackalloc byte[32]; + ComputeBlockHash(blobConverted, blockHash); + + // Fill in block-relevant fields + result.IsBlockCandidate = true; + result.BlockHash = blockHash.ToHexString(); + } + + return (result, blob.ToHexString()); + } + + #endregion // API-Surface +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/ConcealJobManager.cs b/src/Miningcore/Blockchain/Conceal/ConcealJobManager.cs new file mode 100644 index 000000000..075514320 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/ConcealJobManager.cs @@ -0,0 +1,676 @@ +using static System.Array; +using System.Globalization; +using System.Reactive; +using System.Reactive.Linq; +using System.Security.Cryptography; +using System.Text; +using Autofac; +using Miningcore.Blockchain.Bitcoin; +using Miningcore.Blockchain.Conceal.Configuration; +using Miningcore.Blockchain.Conceal.DaemonRequests; +using Miningcore.Blockchain.Conceal.DaemonResponses; +using Miningcore.Blockchain.Conceal.StratumRequests; +using Miningcore.Configuration; +using Miningcore.Extensions; +using Miningcore.JsonRpc; +using Miningcore.Messaging; +using Miningcore.Mining; +using Miningcore.Native; +using Miningcore.Notifications.Messages; +using Miningcore.Rest; +using Miningcore.Rpc; +using Miningcore.Stratum; +using Miningcore.Time; +using Miningcore.Util; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NLog; +using Contract = Miningcore.Contracts.Contract; +using static Miningcore.Util.ActionUtils; + +namespace Miningcore.Blockchain.Conceal; + +public class ConcealJobManager : JobManagerBase +{ + public ConcealJobManager( + IComponentContext ctx, + IMasterClock clock, + IHttpClientFactory httpClientFactory, + IMessageBus messageBus) : + base(ctx, messageBus) + { + Contract.RequiresNonNull(ctx); + Contract.RequiresNonNull(clock); + Contract.RequiresNonNull(messageBus); + + this.clock = clock; + this.httpClientFactory = httpClientFactory; + } + + private byte[] instanceId; + private DaemonEndpointConfig[] daemonEndpoints; + private IHttpClientFactory httpClientFactory; + private SimpleRestClient restClient; + private RpcClient rpc; + private RpcClient walletRpc; + private readonly IMasterClock clock; + private ConcealNetworkType networkType; + private ConcealPoolConfigExtra extraPoolConfig; + private ulong poolAddressBase58Prefix; + private DaemonEndpointConfig[] walletDaemonEndpoints; + private ConcealCoinTemplate coin; + + protected async Task UpdateJob(CancellationToken ct, string via = null, string json = null) + { + try + { + var response = string.IsNullOrEmpty(json) ? await GetBlockTemplateAsync(ct) : GetBlockTemplateFromJson(json); + + // may happen if daemon is currently not connected to peers + if(response.Error != null) + { + logger.Warn(() => $"Unable to update job. Daemon responded with: {response.Error.Message} Code {response.Error.Code}"); + return false; + } + + var blockTemplate = response.Response; + var job = currentJob; + var newHash = blockTemplate.Blob.HexToByteArray().AsSpan().Slice(7, 32).ToHexString(); + + var isNew = job == null || newHash != job.PrevHash; + + if(isNew) + { + messageBus.NotifyChainHeight(poolConfig.Id, blockTemplate.Height, poolConfig.Template); + + if(via != null) + logger.Info(() => $"Detected new block {blockTemplate.Height} [{via}]"); + else + logger.Info(() => $"Detected new block {blockTemplate.Height}"); + + // init job + job = new ConcealJob(blockTemplate, instanceId, NextJobId(), coin, poolConfig, clusterConfig, newHash); + currentJob = job; + + // update stats + BlockchainStats.LastNetworkBlockTime = clock.Now; + BlockchainStats.BlockHeight = job.BlockTemplate.Height; + BlockchainStats.NetworkDifficulty = job.BlockTemplate.Difficulty; + BlockchainStats.NextNetworkTarget = ""; + BlockchainStats.NextNetworkBits = ""; + } + + else + { + if(via != null) + logger.Debug(() => $"Template update {blockTemplate.Height} [{via}]"); + else + logger.Debug(() => $"Template update {blockTemplate.Height}"); + } + + return isNew; + } + + catch(OperationCanceledException) + { + // ignored + } + + catch(Exception ex) + { + logger.Error(ex, () => $"Error during {nameof(UpdateJob)}"); + } + + return false; + } + + private async Task> GetBlockTemplateAsync(CancellationToken ct) + { + var request = new GetBlockTemplateRequest + { + WalletAddress = poolConfig.Address, + ReserveSize = ConcealConstants.ReserveSize + }; + + return await rpc.ExecuteAsync(logger, ConcealCommands.GetBlockTemplate, ct, request); + } + + private RpcResponse GetBlockTemplateFromJson(string json) + { + var result = JsonConvert.DeserializeObject(json); + + return new RpcResponse(result.ResultAs()); + } + + private async Task ShowDaemonSyncProgressAsync(CancellationToken ct) + { + var info = await restClient.Get(ConcealConstants.DaemonRpcGetInfoLocation, ct); + + if(info.Status != "OK") + { + var lowestHeight = info.Height; + + var totalBlocks = info.TargetHeight; + var percent = (double) lowestHeight / totalBlocks * 100; + + logger.Info(() => $"Daemon has downloaded {percent:0.00}% of blockchain from {info.OutgoingConnectionsCount} peers"); + } + } + + private async Task UpdateNetworkStatsAsync(CancellationToken ct) + { + try + { + var coin = poolConfig.Template.As(); + var info = await restClient.Get(ConcealConstants.DaemonRpcGetInfoLocation, ct); + + if(info.Status != "OK") + logger.Warn(() => $"Error(s) refreshing network stats..."); + + if(info.Status == "OK") + { + BlockchainStats.NetworkHashrate = info.TargetHeight > 0 ? (double) info.Difficulty / coin.DifficultyTarget : 0; + BlockchainStats.ConnectedPeers = info.OutgoingConnectionsCount + info.IncomingConnectionsCount; + } + } + + catch(Exception e) + { + logger.Error(e); + } + } + + private async Task SubmitBlockAsync(Share share, string blobHex, string blobHash) + { + var response = await rpc.ExecuteAsync(logger, ConcealCommands.SubmitBlock, CancellationToken.None, new[] { blobHex }); + + if(response.Error != null || response?.Response?.Status != "OK") + { + var error = response.Error?.Message ?? response.Response?.Status; + + logger.Warn(() => $"Block {share.BlockHeight} [{blobHash[..6]}] submission failed with: {error}"); + messageBus.SendMessage(new AdminNotification("Block submission failed", $"Pool {poolConfig.Id} {(!string.IsNullOrEmpty(share.Source) ? $"[{share.Source.ToUpper()}] " : string.Empty)}failed to submit block {share.BlockHeight}: {error}")); + return false; + } + + return true; + } + + #region API-Surface + + public IObservable Blocks { get; private set; } + + public ConcealCoinTemplate Coin => coin; + + public override void Configure(PoolConfig pc, ClusterConfig cc) + { + Contract.RequiresNonNull(pc); + Contract.RequiresNonNull(cc); + + logger = LogUtil.GetPoolScopedLogger(typeof(JobManagerBase), pc); + poolConfig = pc; + clusterConfig = cc; + extraPoolConfig = pc.Extra.SafeExtensionDataAs(); + coin = pc.Template.As(); + + var NetworkTypeSpecified = !string.IsNullOrEmpty(extraPoolConfig.NetworkTypeSpecified) ? extraPoolConfig.NetworkTypeSpecified : "testnet"; + + switch(NetworkTypeSpecified.ToLower()) + { + case "mainnet": + networkType = ConcealNetworkType.Main; + break; + case "testnet": + networkType = ConcealNetworkType.Test; + break; + default: + throw new PoolStartupException($"Unsupport net type '{NetworkTypeSpecified}'", poolConfig.Id); + } + + // extract standard daemon endpoints + daemonEndpoints = pc.Daemons + .Where(x => string.IsNullOrEmpty(x.Category)) + .Select(x => + { + if(string.IsNullOrEmpty(x.HttpPath)) + x.HttpPath = ConcealConstants.DaemonRpcLocation; + + return x; + }) + .ToArray(); + + if(cc.PaymentProcessing?.Enabled == true && pc.PaymentProcessing?.Enabled == true) + { + // extract wallet daemon endpoints + walletDaemonEndpoints = pc.Daemons + .Where(x => x.Category?.ToLower() == ConcealConstants.WalletDaemonCategory) + .Select(x => + { + if(string.IsNullOrEmpty(x.HttpPath)) + x.HttpPath = ConcealConstants.DaemonRpcLocation; + + return x; + }) + .ToArray(); + + if(walletDaemonEndpoints.Length == 0) + throw new PoolStartupException("Wallet-RPC daemon is not configured (Daemon configuration for conceal-pools require an additional entry of category \'wallet' pointing to the wallet daemon)", pc.Id); + } + + ConfigureDaemons(); + } + + public bool ValidateAddress(string address) + { + if(string.IsNullOrEmpty(address)) + return false; + + var addressPrefix = CryptonoteBindings.DecodeAddress(address); + var addressIntegratedPrefix = CryptonoteBindings.DecodeIntegratedAddress(address); + var coin = poolConfig.Template.As(); + + switch(networkType) + { + case ConcealNetworkType.Main: + if(addressPrefix != coin.AddressPrefix) + return false; + break; + + case ConcealNetworkType.Test: + if(addressPrefix != coin.AddressPrefixTestnet) + return false; + break; + } + + return true; + } + + public BlockchainStats BlockchainStats { get; } = new(); + + public void PrepareWorkerJob(ConcealWorkerJob workerJob, out string blob, out string target) + { + blob = null; + target = null; + + var job = currentJob; + + if(job != null) + { + lock(job) + { + job.PrepareWorkerJob(workerJob, out blob, out target); + } + } + } + + public async ValueTask SubmitShareAsync(StratumConnection worker, + ConcealSubmitShareRequest request, ConcealWorkerJob workerJob, CancellationToken ct) + { + Contract.RequiresNonNull(worker); + Contract.RequiresNonNull(request); + + var context = worker.ContextAs(); + + var job = currentJob; + if(workerJob.Height != job?.BlockTemplate.Height) + throw new StratumException(StratumError.MinusOne, "block expired"); + + // validate & process + var (share, blobHex) = job.ProcessShare(request.Nonce, workerJob.ExtraNonce, request.Hash, worker); + + // enrich share with common data + share.PoolId = poolConfig.Id; + share.IpAddress = worker.RemoteEndpoint.Address.ToString(); + share.Miner = context.Miner; + share.Worker = context.Worker; + share.UserAgent = context.UserAgent; + share.Source = clusterConfig.ClusterName; + share.NetworkDifficulty = job.BlockTemplate.Difficulty; + share.Created = clock.Now; + + // if block candidate, submit & check if accepted by network + if(share.IsBlockCandidate) + { + logger.Info(() => $"Submitting block {share.BlockHeight} [{share.BlockHash[..6]}]"); + + share.IsBlockCandidate = await SubmitBlockAsync(share, blobHex, share.BlockHash); + + if(share.IsBlockCandidate) + { + logger.Info(() => $"Daemon accepted block {share.BlockHeight} [{share.BlockHash[..6]}] submitted by {context.Miner}"); + + OnBlockFound(); + + share.TransactionConfirmationData = share.BlockHash; + } + + else + { + // clear fields that no longer apply + share.TransactionConfirmationData = null; + } + } + + return share; + } + + #endregion // API-Surface + + private static JToken GetFrameAsJToken(byte[] frame) + { + var text = Encoding.UTF8.GetString(frame); + + // find end of message type indicator + var index = text.IndexOf(":"); + + if (index == -1) + return null; + + var json = text.Substring(index + 1); + + return JToken.Parse(json); + } + + #region Overrides + + protected override void ConfigureDaemons() + { + var jsonSerializerSettings = ctx.Resolve(); + + restClient = new SimpleRestClient(httpClientFactory, "http://" + daemonEndpoints.First().Host.ToString() + ":" + daemonEndpoints.First().Port.ToString() + "/"); + rpc = new RpcClient(daemonEndpoints.First(), jsonSerializerSettings, messageBus, poolConfig.Id); + + if(clusterConfig.PaymentProcessing?.Enabled == true && poolConfig.PaymentProcessing?.Enabled == true) + { + // also setup wallet daemon + walletRpc = new RpcClient(walletDaemonEndpoints.First(), jsonSerializerSettings, messageBus, poolConfig.Id); + } + } + + protected override async Task AreDaemonsHealthyAsync(CancellationToken ct) + { + logger.Debug(() => "Checking if conceald daemon is healthy..."); + + // test daemons + var response = await restClient.Get(ConcealConstants.DaemonRpcGetInfoLocation, ct); + if(response.Status != "OK") + { + logger.Debug(() => $"conceald daemon did not responded..."); + return false; + } + + logger.Debug(() => $"{response.Status} - Incoming: {response.IncomingConnectionsCount} - Outgoing: {response.OutgoingConnectionsCount})"); + + if(clusterConfig.PaymentProcessing?.Enabled == true && poolConfig.PaymentProcessing?.Enabled == true) + { + logger.Debug(() => "Checking if walletd daemon is healthy..."); + + // test wallet daemons + //var response2 = await walletRpc.ExecuteAsync(logger, ConcealWalletCommands.GetAddress, ct); + var request2 = new GetBalanceRequest + { + Address = poolConfig.Address + }; + + var response2 = await walletRpc.ExecuteAsync(logger, ConcealWalletCommands.GetBalance, ct, request2); + + if(response2.Error != null) + logger.Debug(() => $"walletd daemon response: {response2.Error.Message} (Code {response2.Error.Code})"); + + return response2.Error == null; + } + + return true; + } + + protected override async Task AreDaemonsConnectedAsync(CancellationToken ct) + { + logger.Debug(() => "Checking if conceald daemon is connected..."); + + var response = await restClient.Get(ConcealConstants.DaemonRpcGetInfoLocation, ct); + + if(response.Status != "OK") + logger.Debug(() => $"conceald daemon is not connected..."); + + if(response.Status == "OK") + logger.Debug(() => $"Peers connected - Incoming: {response.IncomingConnectionsCount} - Outgoing: {response.OutgoingConnectionsCount}"); + + return response.Status == "OK" && + (response.OutgoingConnectionsCount + response.IncomingConnectionsCount) > 0; + } + + protected override async Task EnsureDaemonsSynchedAsync(CancellationToken ct) + { + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5)); + + logger.Debug(() => "Checking if conceald daemon is synched..."); + + var syncPendingNotificationShown = false; + + do + { + var request = new GetBlockTemplateRequest + { + WalletAddress = poolConfig.Address, + ReserveSize = ConcealConstants.ReserveSize + }; + + var response = await rpc.ExecuteAsync(logger, + ConcealCommands.GetBlockTemplate, ct, request); + + if(response.Error != null) + logger.Debug(() => $"conceald daemon response: {response.Error.Message} (Code {response.Error.Code})"); + + var isSynched = response.Error is not {Code: -9}; + + if(isSynched) + { + logger.Info(() => "All daemons synched with blockchain"); + break; + } + + if(!syncPendingNotificationShown) + { + logger.Info(() => "Daemon is still syncing with network. Manager will be started once synced."); + syncPendingNotificationShown = true; + } + + await ShowDaemonSyncProgressAsync(ct); + } while(await timer.WaitForNextTickAsync(ct)); + } + + protected override async Task PostStartInitAsync(CancellationToken ct) + { + SetInstanceId(); + + // coin config + var coin = poolConfig.Template.As(); + var infoResponse = await restClient.Get(ConcealConstants.DaemonRpcGetInfoLocation, ct); + + if(infoResponse.Status != "OK") + throw new PoolStartupException($"Init RPC failed...", poolConfig.Id); + + if(clusterConfig.PaymentProcessing?.Enabled == true && poolConfig.PaymentProcessing?.Enabled == true) + { + //var addressResponse = await walletRpc.ExecuteAsync(logger, ConcealWalletCommands.GetAddress, ct); + var request2 = new GetAddressRequest + { + }; + + var addressResponse = await walletRpc.ExecuteAsync(logger, ConcealWalletCommands.GetAddress, ct, request2); + + // ensure pool owns wallet + //if(clusterConfig.PaymentProcessing?.Enabled == true && addressResponse.Response?.Address != poolConfig.Address) + if(clusterConfig.PaymentProcessing?.Enabled == true && Exists(addressResponse.Response?.Address, element => element == poolConfig.Address) == false) + throw new PoolStartupException($"Wallet-Daemon does not own pool-address '{poolConfig.Address}'", poolConfig.Id); + } + + // address validation + poolAddressBase58Prefix = CryptonoteBindings.DecodeAddress(poolConfig.Address); + if(poolAddressBase58Prefix == 0) + throw new PoolStartupException("Unable to decode pool-address", poolConfig.Id); + + switch(networkType) + { + case ConcealNetworkType.Main: + if(poolAddressBase58Prefix != coin.AddressPrefix) + throw new PoolStartupException($"Invalid pool address prefix. Expected {coin.AddressPrefix}, got {poolAddressBase58Prefix}", poolConfig.Id); + break; + + case ConcealNetworkType.Test: + if(poolAddressBase58Prefix != coin.AddressPrefixTestnet) + throw new PoolStartupException($"Invalid pool address prefix. Expected {coin.AddressPrefixTestnet}, got {poolAddressBase58Prefix}", poolConfig.Id); + break; + } + + // update stats + BlockchainStats.RewardType = "POW"; + BlockchainStats.NetworkType = networkType.ToString(); + + await UpdateNetworkStatsAsync(ct); + + // Periodically update network stats + Observable.Interval(TimeSpan.FromMinutes(1)) + .Select(via => Observable.FromAsync(() => + Guard(()=> UpdateNetworkStatsAsync(ct), + ex=> logger.Error(ex)))) + .Concat() + .Subscribe(); + + SetupJobUpdates(ct); + } + + private void SetInstanceId() + { + instanceId = new byte[ConcealConstants.InstanceIdSize]; + + using(var rng = RandomNumberGenerator.Create()) + { + rng.GetNonZeroBytes(instanceId); + } + + if(clusterConfig.InstanceId.HasValue) + instanceId[0] = clusterConfig.InstanceId.Value; + } + + protected virtual void SetupJobUpdates(CancellationToken ct) + { + var blockSubmission = blockFoundSubject.Synchronize(); + var pollTimerRestart = blockFoundSubject.Synchronize(); + + var triggers = new List> + { + blockSubmission.Select(x => (JobRefreshBy.BlockFound, (string) null)) + }; + + if(extraPoolConfig?.BtStream == null) + { + // collect ports + var zmq = poolConfig.Daemons + .Where(x => !string.IsNullOrEmpty(x.Extra.SafeExtensionDataAs()?.ZmqBlockNotifySocket)) + .ToDictionary(x => x, x => + { + var extra = x.Extra.SafeExtensionDataAs(); + var topic = !string.IsNullOrEmpty(extra.ZmqBlockNotifyTopic.Trim()) ? extra.ZmqBlockNotifyTopic.Trim() : BitcoinConstants.ZmqPublisherTopicBlockHash; + + return (Socket: extra.ZmqBlockNotifySocket, Topic: topic); + }); + + if(zmq.Count > 0) + { + logger.Info(() => $"Subscribing to ZMQ push-updates from {string.Join(", ", zmq.Values)}"); + + var blockNotify = rpc.ZmqSubscribe(logger, ct, zmq) + .Where(msg => + { + bool result = false; + + try + { + var text = Encoding.UTF8.GetString(msg[0].Read()); + + result = text.StartsWith("json-minimal-chain_main:"); + } + + catch + { + } + + if(!result) + msg.Dispose(); + + return result; + }) + .Select(msg => + { + using(msg) + { + var token = GetFrameAsJToken(msg[0].Read()); + + if (token != null) + return token.Value("first_height").ToString(CultureInfo.InvariantCulture); + + // We just take the second frame's raw data and turn it into a hex string. + // If that string changes, we got an update (DistinctUntilChanged) + return msg[0].Read().ToHexString(); + } + }) + .DistinctUntilChanged() + .Select(_ => (JobRefreshBy.PubSub, (string) null)) + .Publish() + .RefCount(); + + pollTimerRestart = Observable.Merge( + blockSubmission, + blockNotify.Select(_ => Unit.Default)) + .Publish() + .RefCount(); + + triggers.Add(blockNotify); + } + + if(poolConfig.BlockRefreshInterval > 0) + { + // periodically update block-template + var pollingInterval = poolConfig.BlockRefreshInterval > 0 ? poolConfig.BlockRefreshInterval : 1000; + + triggers.Add(Observable.Timer(TimeSpan.FromMilliseconds(pollingInterval)) + .TakeUntil(pollTimerRestart) + .Select(_ => (JobRefreshBy.Poll, (string) null)) + .Repeat()); + } + + else + { + // get initial blocktemplate + triggers.Add(Observable.Interval(TimeSpan.FromMilliseconds(1000)) + .Select(_ => (JobRefreshBy.Initial, (string) null)) + .TakeWhile(_ => !hasInitialBlockTemplate)); + } + } + + else + { + triggers.Add(BtStreamSubscribe(extraPoolConfig.BtStream) + .Select(json => (JobRefreshBy.BlockTemplateStream, json)) + .Publish() + .RefCount()); + + // get initial blocktemplate + triggers.Add(Observable.Interval(TimeSpan.FromMilliseconds(1000)) + .Select(_ => (JobRefreshBy.Initial, (string) null)) + .TakeWhile(_ => !hasInitialBlockTemplate)); + } + + Blocks = triggers.Merge() + .Select(x => Observable.FromAsync(() => UpdateJob(ct, x.Via, x.Data))) + .Concat() + .Where(isNew => isNew) + .Do(_ => hasInitialBlockTemplate = true) + .Select(_ => Unit.Default) + .Publish() + .RefCount(); + } + + #endregion // Overrides +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/ConcealPayoutHandler.cs b/src/Miningcore/Blockchain/Conceal/ConcealPayoutHandler.cs new file mode 100644 index 000000000..f7c103b10 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/ConcealPayoutHandler.cs @@ -0,0 +1,497 @@ +using System.Data; +using Autofac; +using AutoMapper; +using Miningcore.Blockchain.Conceal.Configuration; +using Miningcore.Blockchain.Conceal.DaemonRequests; +using Miningcore.Blockchain.Conceal.DaemonResponses; +using Miningcore.Configuration; +using Miningcore.Extensions; +using Miningcore.Messaging; +using Miningcore.Mining; +using Miningcore.Native; +using Miningcore.Payments; +using Miningcore.Persistence; +using Miningcore.Persistence.Model; +using Miningcore.Persistence.Repositories; +using Miningcore.Rest; +using Miningcore.Rpc; +using Miningcore.Time; +using Miningcore.Util; +using Newtonsoft.Json; +using Contract = Miningcore.Contracts.Contract; +using CNC = Miningcore.Blockchain.Conceal.ConcealCommands; +using Newtonsoft.Json.Linq; + +namespace Miningcore.Blockchain.Conceal; + +[CoinFamily(CoinFamily.Conceal)] +public class ConcealPayoutHandler : PayoutHandlerBase, + IPayoutHandler +{ + public ConcealPayoutHandler( + IComponentContext ctx, + IConnectionFactory cf, + IMapper mapper, + IShareRepository shareRepo, + IBlockRepository blockRepo, + IBalanceRepository balanceRepo, + IPaymentRepository paymentRepo, + IMasterClock clock, + IHttpClientFactory httpClientFactory, + IMessageBus messageBus) : + base(cf, mapper, shareRepo, blockRepo, balanceRepo, paymentRepo, clock, messageBus) + { + Contract.RequiresNonNull(ctx); + Contract.RequiresNonNull(balanceRepo); + Contract.RequiresNonNull(paymentRepo); + + this.ctx = ctx; + this.httpClientFactory = httpClientFactory; + } + + private readonly IComponentContext ctx; + private IHttpClientFactory httpClientFactory; + private SimpleRestClient restClient; + private RpcClient rpcClient; + private RpcClient rpcClientWallet; + private ConcealNetworkType? networkType; + private ConcealPoolPaymentProcessingConfigExtra extraConfig; + private ConcealPoolConfigExtra extraPoolConfig; + private bool walletSupportsSendTransaction; + + protected override string LogCategory => "Conceal Payout Handler"; + + private async Task HandleSendTransactionResponseAsync(RpcResponse response, params Balance[] balances) + { + var coin = poolConfig.Template.As(); + + if(response.Error == null) + { + var txHash = response.Response.TxHash; + var txFee = ConcealConstants.StaticTransactionFeeReserve; + + logger.Info(() => $"[{LogCategory}] Payment transaction id: {txHash}, TxFee {FormatAmount(txFee)}"); + + await PersistPaymentsAsync(balances, txHash); + NotifyPayoutSuccess(poolConfig.Id, balances, new[] { txHash }, txFee); + return true; + } + + else + { + logger.Error(() => $"[{LogCategory}] Daemon command '{ConcealWalletCommands.SendTransaction}' returned error: {response.Error.Message} code {response.Error.Code}"); + + NotifyPayoutFailure(poolConfig.Id, balances, $"Daemon command '{ConcealWalletCommands.SendTransaction}' returned error: {response.Error.Message} code {response.Error.Code}", null); + return false; + } + } + + private async Task EnsureBalance(decimal requiredAmount, ConcealCoinTemplate coin, CancellationToken ct) + { + //var response = await rpcClientWallet.ExecuteAsync(logger, ConcealWalletCommands.GetBalance, ct); + var request = new GetBalanceRequest + { + Address = poolConfig.Address + }; + + var response = await rpcClientWallet.ExecuteAsync(logger, ConcealWalletCommands.GetBalance, ct, request); + + if(response.Error != null) + { + logger.Error(() => $"[{LogCategory}] Daemon command '{ConcealWalletCommands.GetBalance}' returned error: {response.Error.Message} code {response.Error.Code}"); + return false; + } + + var balance = Math.Floor(response.Response.Balance / coin.SmallestUnit); + + if(balance < requiredAmount) + { + logger.Info(() => $"[{LogCategory}] {FormatAmount(requiredAmount)} required for payment, but only have {FormatAmount(balance)} available yet. Will try again."); + return false; + } + + logger.Info(() => $"[{LogCategory}] Current balance is {FormatAmount(balance)}"); + return true; + } + + private async Task PayoutBatch(Balance[] balances, CancellationToken ct) + { + var coin = poolConfig.Template.As(); + + // ensure there's enough balance + if(!await EnsureBalance(balances.Sum(x => x.Amount), coin, ct)) + return false; + + // build request + var request = new SendTransactionRequest + { + Addresses = new string[] + { + poolConfig.Address + }, + Transfers = balances + .Where(x => x.Amount > 0) + .Select(x => + { + ExtractAddressAndPaymentId(x.Address, out var address, out _); + + logger.Debug(() => $"[{LogCategory}] [batch] ['address': '{x.Address} - {address}', 'amount': {Math.Floor(x.Amount * coin.SmallestUnit)}]"); + + return new SendTransactionTransfers + { + Address = address, + Amount = (ulong) Math.Floor(x.Amount * coin.SmallestUnit) + }; + }).ToArray(), + ChangeAddress = poolConfig.Address + }; + + if(request.Transfers.Length == 0) + return true; + + logger.Debug(() => $"[{LogCategory}] [batch] RPC data: ['anonymity': {request.Anonymity}, 'fee': {request.Fee}, 'unlockTime': {request.UnlockTime}, 'changeAddress': '{request.ChangeAddress}']"); + logger.Info(() => $"[{LogCategory}] [batch] Paying {FormatAmount(balances.Sum(x => x.Amount))} to {balances.Length} addresses:\n{string.Join("\n", balances.OrderByDescending(x => x.Amount).Select(x => $"{FormatAmount(x.Amount)} to {x.Address}"))}"); + + // send command + var sendTransactionResponse = await rpcClientWallet.ExecuteAsync(logger, ConcealWalletCommands.SendTransaction, ct, request); + + return await HandleSendTransactionResponseAsync(sendTransactionResponse, balances); + } + + private void ExtractAddressAndPaymentId(string input, out string address, out string paymentId) + { + paymentId = null; + var index = input.IndexOf(PayoutConstants.PayoutInfoSeperator); + + if(index != -1) + { + address = input[..index]; + + if(index + 1 < input.Length) + { + paymentId = input[(index + 1)..]; + + // ignore invalid payment ids + if(paymentId.Length != ConcealConstants.PaymentIdHexLength) + paymentId = null; + } + } + + else + address = input; + } + + private async Task PayoutToPaymentId(Balance balance, CancellationToken ct) + { + var coin = poolConfig.Template.As(); + + ExtractAddressAndPaymentId(balance.Address, out var address, out var paymentId); + var isIntegratedAddress = string.IsNullOrEmpty(paymentId); + + // ensure there's enough balance + if(!await EnsureBalance(balance.Amount, coin, ct)) + return false; + + // build request + var request = new SendTransactionRequest + { + Addresses = new string[] + { + poolConfig.Address + }, + Transfers = new[] + { + new SendTransactionTransfers + { + Address = address, + Amount = (ulong) Math.Floor(balance.Amount * coin.SmallestUnit) + } + }, + ChangeAddress = poolConfig.Address + }; + + if(!isIntegratedAddress) + logger.Info(() => $"[{LogCategory}] Paying {FormatAmount(balance.Amount)} to address {balance.Address} with paymentId {paymentId}"); + else + logger.Info(() => $"[{LogCategory}] Paying {FormatAmount(balance.Amount)} to integrated address {balance.Address}"); + + // send command + var result = await rpcClientWallet.ExecuteAsync(logger, ConcealWalletCommands.SendTransaction, ct, request); + + return await HandleSendTransactionResponseAsync(result, balance); + } + + #region IPayoutHandler + + public async Task ConfigureAsync(ClusterConfig cc, PoolConfig pc, CancellationToken ct) + { + Contract.RequiresNonNull(pc); + + poolConfig = pc; + clusterConfig = cc; + extraConfig = pc.PaymentProcessing.Extra.SafeExtensionDataAs(); + extraPoolConfig = pc.Extra.SafeExtensionDataAs(); + + var NetworkTypeSpecified = !string.IsNullOrEmpty(extraPoolConfig.NetworkTypeSpecified) ? extraPoolConfig.NetworkTypeSpecified : "testnet"; + + switch(NetworkTypeSpecified.ToLower()) + { + case "mainnet": + networkType = ConcealNetworkType.Main; + break; + case "testnet": + networkType = ConcealNetworkType.Test; + break; + default: + throw new PoolStartupException($"Unsupport net type '{NetworkTypeSpecified}'", poolConfig.Id); + } + + logger = LogUtil.GetPoolScopedLogger(typeof(ConcealPayoutHandler), pc); + + // configure standard daemon + var jsonSerializerSettings = ctx.Resolve(); + + var daemonEndpoints = pc.Daemons + .Where(x => string.IsNullOrEmpty(x.Category)) + .Select(x => + { + if(string.IsNullOrEmpty(x.HttpPath)) + x.HttpPath = ConcealConstants.DaemonRpcLocation; + + return x; + }) + .ToArray(); + + restClient = new SimpleRestClient(httpClientFactory, "http://" + daemonEndpoints.First().Host.ToString() + ":" + daemonEndpoints.First().Port.ToString() + "/"); + rpcClient = new RpcClient(daemonEndpoints.First(), jsonSerializerSettings, messageBus, pc.Id); + + // configure wallet daemon + var walletDaemonEndpoints = pc.Daemons + .Where(x => x.Category?.ToLower() == ConcealConstants.WalletDaemonCategory) + .Select(x => + { + if(string.IsNullOrEmpty(x.HttpPath)) + x.HttpPath = ConcealConstants.DaemonRpcLocation; + + return x; + }) + .ToArray(); + + rpcClientWallet = new RpcClient(walletDaemonEndpoints.First(), jsonSerializerSettings, messageBus, pc.Id); + + // detect sendTransaction support + var response = await rpcClientWallet.ExecuteAsync(logger, ConcealWalletCommands.SendTransaction, ct); + walletSupportsSendTransaction = response.Error.Code != ConcealConstants.ConcealRpcMethodNotFound; + } + + public async Task ClassifyBlocksAsync(IMiningPool pool, Block[] blocks, CancellationToken ct) + { + Contract.RequiresNonNull(poolConfig); + Contract.RequiresNonNull(blocks); + + var coin = poolConfig.Template.As(); + var pageSize = 100; + var pageCount = (int) Math.Ceiling(blocks.Length / (double) pageSize); + var result = new List(); + + for(var i = 0; i < pageCount; i++) + { + // get a page full of blocks + var page = blocks + .Skip(i * pageSize) + .Take(pageSize) + .ToArray(); + + // NOTE: conceald does not support batch-requests??? + for(var j = 0; j < page.Length; j++) + { + var block = page[j]; + + var rpcResult = await rpcClient.ExecuteAsync(logger, + CNC.GetBlockHeaderByHeight, ct, + new GetBlockHeaderByHeightRequest + { + Height = block.BlockHeight + }); + + if(rpcResult.Error != null) + { + logger.Debug(() => $"[{LogCategory}] Daemon reports error '{rpcResult.Error.Message}' (Code {rpcResult.Error.Code}) for block {block.BlockHeight}"); + continue; + } + + if(rpcResult.Response?.BlockHeader == null) + { + logger.Debug(() => $"[{LogCategory}] Daemon returned no header for block {block.BlockHeight}"); + continue; + } + + var blockHeader = rpcResult.Response.BlockHeader; + + // update progress + block.ConfirmationProgress = Math.Min(1.0d, (double) blockHeader.Depth / ConcealConstants.PayoutMinBlockConfirmations); + result.Add(block); + + messageBus.NotifyBlockConfirmationProgress(poolConfig.Id, block, coin); + + // orphaned? + if(blockHeader.IsOrphaned || blockHeader.Hash != block.TransactionConfirmationData) + { + block.Status = BlockStatus.Orphaned; + block.Reward = 0; + + messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); + continue; + } + + // matured and spendable? + if(blockHeader.Depth >= ConcealConstants.PayoutMinBlockConfirmations) + { + block.Status = BlockStatus.Confirmed; + block.ConfirmationProgress = 1; + block.Reward = (blockHeader.Reward / coin.SmallestUnit) * coin.BlockrewardMultiplier; + + logger.Info(() => $"[{LogCategory}] Unlocked block {block.BlockHeight} worth {FormatAmount(block.Reward)}"); + + messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); + } + } + } + + return result.ToArray(); + } + + public override async Task UpdateBlockRewardBalancesAsync(IDbConnection con, IDbTransaction tx, + IMiningPool pool, Block block, CancellationToken ct) + { + var blockRewardRemaining = await base.UpdateBlockRewardBalancesAsync(con, tx, pool, block, ct); + + // Deduct static reserve for tx fees + blockRewardRemaining -= ConcealConstants.StaticTransactionFeeReserve; + + return blockRewardRemaining; + } + + public async Task PayoutAsync(IMiningPool pool, Balance[] balances, CancellationToken ct) + { + Contract.RequiresNonNull(balances); + + var coin = poolConfig.Template.As(); + +#if !DEBUG // ensure we have peers + var infoResponse = await restClient.Get(CNC.GetInfo, ct); + + if (infoResponse.Status != "OK" || + infoResponse.IncomingConnectionsCount + infoResponse.OutgoingConnectionsCount < 3) + { + logger.Warn(() => $"[{LogCategory}] Payout aborted. Not enough peers (4 required)"); + return; + } +#endif + // validate addresses + balances = balances + .Where(x => + { + ExtractAddressAndPaymentId(x.Address, out var address, out _); + + var addressPrefix = CryptonoteBindings.DecodeAddress(address); + var addressIntegratedPrefix = CryptonoteBindings.DecodeIntegratedAddress(address); + + switch(networkType) + { + case ConcealNetworkType.Main: + if(addressPrefix != coin.AddressPrefix) + { + logger.Warn(() => $"[{LogCategory}] Excluding payment to invalid address: {x.Address}"); + return false; + } + + break; + + case ConcealNetworkType.Test: + if(addressPrefix != coin.AddressPrefixTestnet) + { + logger.Warn(() => $"[{LogCategory}] Excluding payment to invalid address: {x.Address}"); + return false; + } + + break; + } + + return true; + }) + .ToArray(); + + // simple balances first + var simpleBalances = balances + .Where(x => + { + ExtractAddressAndPaymentId(x.Address, out var address, out var paymentId); + + var hasPaymentId = paymentId != null; + var isIntegratedAddress = false; + var addressIntegratedPrefix = CryptonoteBindings.DecodeIntegratedAddress(address); + + switch(networkType) + { + case ConcealNetworkType.Main: + if(addressIntegratedPrefix == coin.AddressPrefixIntegrated) + isIntegratedAddress = true; + break; + + case ConcealNetworkType.Test: + if(addressIntegratedPrefix == coin.AddressPrefixIntegratedTestnet) + isIntegratedAddress = true; + break; + } + + return !hasPaymentId && !isIntegratedAddress; + }) + .OrderByDescending(x => x.Amount) + .ToArray(); + + if(simpleBalances.Length > 0) +#if false + await PayoutBatch(simpleBalances); +#else + { + var maxBatchSize = 15; // going over 15 yields "sv/gamma are too large" + var pageSize = maxBatchSize; + var pageCount = (int) Math.Ceiling((double) simpleBalances.Length / pageSize); + + for(var i = 0; i < pageCount; i++) + { + var page = simpleBalances + .Skip(i * pageSize) + .Take(pageSize) + .ToArray(); + + if(!await PayoutBatch(page, ct)) + break; + } + } +#endif + // balances with paymentIds + var minimumPaymentToPaymentId = extraConfig?.MinimumPaymentToPaymentId ?? poolConfig.PaymentProcessing.MinimumPayment; + + var paymentIdBalances = balances.Except(simpleBalances) + .Where(x => x.Amount >= minimumPaymentToPaymentId) + .ToArray(); + + foreach(var balance in paymentIdBalances) + { + if(!await PayoutToPaymentId(balance, ct)) + break; + } + + // save wallet + await rpcClientWallet.ExecuteAsync(logger, ConcealWalletCommands.Save, ct); + } + + public double AdjustBlockEffort(double effort) + { + return effort; + } + + #endregion // IPayoutHandler +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/ConcealPool.cs b/src/Miningcore/Blockchain/Conceal/ConcealPool.cs new file mode 100644 index 000000000..e8f0d9086 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/ConcealPool.cs @@ -0,0 +1,423 @@ +using System.Globalization; +using System.Reactive; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using Autofac; +using AutoMapper; +using Microsoft.IO; +using Miningcore.Blockchain.Conceal.StratumRequests; +using Miningcore.Blockchain.Conceal.StratumResponses; +using Miningcore.Configuration; +using Miningcore.JsonRpc; +using Miningcore.Messaging; +using Miningcore.Mining; +using Miningcore.Nicehash; +using Miningcore.Notifications.Messages; +using Miningcore.Payments; +using Miningcore.Persistence; +using Miningcore.Persistence.Repositories; +using Miningcore.Stratum; +using Miningcore.Time; +using Newtonsoft.Json; +using static Miningcore.Util.ActionUtils; + +namespace Miningcore.Blockchain.Conceal; + +[CoinFamily(CoinFamily.Conceal)] +public class ConcealPool : PoolBase +{ + public ConcealPool(IComponentContext ctx, + JsonSerializerSettings serializerSettings, + IConnectionFactory cf, + IStatsRepository statsRepo, + IMapper mapper, + IMasterClock clock, + IMessageBus messageBus, + RecyclableMemoryStreamManager rmsm, + NicehashService nicehashService) : + base(ctx, serializerSettings, cf, statsRepo, mapper, clock, messageBus, rmsm, nicehashService) + { + } + + private long currentJobId; + + private ConcealJobManager manager; + private string minerAlgo; + + private async Task OnLoginAsync(StratumConnection connection, Timestamped tsRequest) + { + var request = tsRequest.Value; + var context = connection.ContextAs(); + + if(request.Id == null) + throw new StratumException(StratumError.MinusOne, "missing request id"); + + var loginRequest = request.ParamsAs(); + + if(string.IsNullOrEmpty(loginRequest?.Login)) + throw new StratumException(StratumError.MinusOne, "missing login"); + + // extract worker/miner/paymentid + var split = loginRequest.Login.Split('.'); + context.Miner = split[0].Trim(); + context.Worker = split.Length > 1 ? split[1].Trim() : null; + context.UserAgent = loginRequest.UserAgent?.Trim(); + + var addressToValidate = context.Miner; + + // extract paymentid + var index = context.Miner.IndexOf('#'); + if(index != -1) + { + var paymentId = context.Miner[(index + 1)..].Trim(); + + // validate + if(!string.IsNullOrEmpty(paymentId) && paymentId.Length != ConcealConstants.PaymentIdHexLength) + throw new StratumException(StratumError.MinusOne, "invalid payment id"); + + // re-append to address + addressToValidate = context.Miner[..index].Trim(); + context.Miner = addressToValidate + PayoutConstants.PayoutInfoSeperator + paymentId; + } + + // validate login + var result = manager.ValidateAddress(addressToValidate); + + context.IsSubscribed = result; + context.IsAuthorized = result; + + if(context.IsAuthorized) + { + // extract control vars from password + var passParts = loginRequest.Password?.Split(PasswordControlVarsSeparator); + var staticDiff = GetStaticDiffFromPassparts(passParts); + + // Nicehash support + var nicehashDiff = await GetNicehashStaticMinDiff(context, manager.Coin.Name, manager.Coin.GetAlgorithmName()); + + if(nicehashDiff.HasValue) + { + if(!staticDiff.HasValue || nicehashDiff > staticDiff) + { + logger.Info(() => $"[{connection.ConnectionId}] Nicehash detected. Using API supplied difficulty of {nicehashDiff.Value}"); + + staticDiff = nicehashDiff; + } + + else + logger.Info(() => $"[{connection.ConnectionId}] Nicehash detected. Using miner supplied difficulty of {staticDiff.Value}"); + } + + // Static diff + if(staticDiff.HasValue && + (context.VarDiff != null && staticDiff.Value >= context.VarDiff.Config.MinDiff || + context.VarDiff == null && staticDiff.Value > context.Difficulty)) + { + context.VarDiff = null; // disable vardiff + context.SetDifficulty(staticDiff.Value); + + logger.Info(() => $"[{connection.ConnectionId}] Static difficulty set to {staticDiff.Value}"); + } + + // respond + var loginResponse = new ConcealLoginResponse + { + Id = connection.ConnectionId, + Job = CreateWorkerJob(connection) + }; + + await connection.RespondAsync(loginResponse, request.Id); + + // log association + if(!string.IsNullOrEmpty(context.Worker)) + logger.Info(() => $"[{connection.ConnectionId}] Authorized worker {context.Worker}@{context.Miner}"); + else + logger.Info(() => $"[{connection.ConnectionId}] Authorized miner {context.Miner}"); + } + + else + { + await connection.RespondErrorAsync(StratumError.MinusOne, "invalid login", request.Id); + + if(clusterConfig?.Banning?.BanOnLoginFailure is null or true) + { + logger.Info(() => $"[{connection.ConnectionId}] Banning unauthorized worker {context.Miner} for {loginFailureBanTimeout.TotalSeconds} sec"); + + banManager.Ban(connection.RemoteEndpoint.Address, loginFailureBanTimeout); + + Disconnect(connection); + } + } + } + + private async Task OnGetJobAsync(StratumConnection connection, Timestamped tsRequest) + { + var request = tsRequest.Value; + var context = connection.ContextAs(); + + if(request.Id == null) + throw new StratumException(StratumError.MinusOne, "missing request id"); + + var getJobRequest = request.ParamsAs(); + + // validate worker + if(connection.ConnectionId != getJobRequest?.WorkerId || !context.IsAuthorized) + throw new StratumException(StratumError.MinusOne, "unauthorized"); + + // respond + var job = CreateWorkerJob(connection); + await connection.RespondAsync(job, request.Id); + } + + private ConcealJobParams CreateWorkerJob(StratumConnection connection) + { + var context = connection.ContextAs(); + var job = new ConcealWorkerJob(NextJobId(), context.Difficulty); + + manager.PrepareWorkerJob(job, out var blob, out var target); + + // should never happen + if(string.IsNullOrEmpty(blob) || string.IsNullOrEmpty(blob)) + return null; + + var result = new ConcealJobParams + { + JobId = job.Id, + Blob = blob, + Target = target, + Height = job.Height + }; + + if(!string.IsNullOrEmpty(minerAlgo)) + result.Algorithm = minerAlgo; + + // update context + lock(context) + { + context.AddJob(job); + } + + return result; + } + + private async Task OnSubmitAsync(StratumConnection connection, Timestamped tsRequest, CancellationToken ct) + { + var request = tsRequest.Value; + var context = connection.ContextAs(); + + try + { + if(request.Id == null) + throw new StratumException(StratumError.MinusOne, "missing request id"); + + // check age of submission (aged submissions are usually caused by high server load) + var requestAge = clock.Now - tsRequest.Timestamp.UtcDateTime; + + if(requestAge > maxShareAge) + { + logger.Warn(() => $"[{connection.ConnectionId}] Dropping stale share submission request (server overloaded?)"); + return; + } + + // check request + var submitRequest = request.ParamsAs(); + + // validate worker + if(connection.ConnectionId != submitRequest?.WorkerId || !context.IsAuthorized) + throw new StratumException(StratumError.MinusOne, "unauthorized"); + + // recognize activity + context.LastActivity = clock.Now; + + ConcealWorkerJob job; + + lock(context) + { + var jobId = submitRequest?.JobId; + + if((job = context.FindJob(jobId)) == null) + throw new StratumException(StratumError.MinusOne, "invalid jobid"); + } + + // dupe check + if(!job.Submissions.TryAdd(submitRequest.Nonce, true)) + throw new StratumException(StratumError.MinusOne, "duplicate share"); + + // submit + var share = await manager.SubmitShareAsync(connection, submitRequest, job, ct); + await connection.RespondAsync(new ConcealResponseBase(), request.Id); + + // publish + messageBus.SendMessage(new StratumShare(connection, share)); + + // telemetry + PublishTelemetry(TelemetryCategory.Share, clock.Now - tsRequest.Timestamp.UtcDateTime, true); + + logger.Info(() => $"[{connection.ConnectionId}] Share accepted: D={Math.Round(share.Difficulty, 3)}"); + + // update pool stats + if(share.IsBlockCandidate) + poolStats.LastPoolBlockTime = clock.Now; + + // update client stats + context.Stats.ValidShares++; + + await UpdateVarDiffAsync(connection, false, ct); + } + + catch(StratumException ex) + { + // telemetry + PublishTelemetry(TelemetryCategory.Share, clock.Now - tsRequest.Timestamp.UtcDateTime, false); + + // update client stats + context.Stats.InvalidShares++; + logger.Info(() => $"[{connection.ConnectionId}] Share rejected: {ex.Message} [{context.UserAgent}]"); + + // banning + ConsiderBan(connection, context, poolConfig.Banning); + + throw; + } + } + + private string NextJobId() + { + return Interlocked.Increment(ref currentJobId).ToString(CultureInfo.InvariantCulture); + } + + private async Task OnNewJobAsync() + { + logger.Info(() => "Broadcasting jobs"); + + await Guard(() => ForEachMinerAsync(async (connection, ct) => + { + // send job + var job = CreateWorkerJob(connection); + await connection.NotifyAsync(ConcealStratumMethods.JobNotify, job); + })); + } + + #region Overrides + + protected override async Task SetupJobManager(CancellationToken ct) + { + manager = ctx.Resolve(); + manager.Configure(poolConfig, clusterConfig); + + await manager.StartAsync(ct); + + if(poolConfig.EnableInternalStratum == true) + { + minerAlgo = GetMinerAlgo(); + + disposables.Add(manager.Blocks + .Select(_ => Observable.FromAsync(() => + Guard(OnNewJobAsync, + ex=> logger.Debug(() => $"{nameof(OnNewJobAsync)}: {ex.Message}")))) + .Concat() + .Subscribe(_ => { }, ex => + { + logger.Debug(ex, nameof(OnNewJobAsync)); + })); + + // start with initial blocktemplate + await manager.Blocks.Take(1).ToTask(ct); + } + + else + { + // keep updating NetworkStats + disposables.Add(manager.Blocks.Subscribe()); + } + } + + private string GetMinerAlgo() + { + switch(manager.Coin.Hash) + { + case CryptonightHashType.CryptonightCCX: + return $"cn-ccx"; + + case CryptonightHashType.CryptonightGPU: + return $"cn-gpu"; + } + + return null; + } + + protected override async Task InitStatsAsync(CancellationToken ct) + { + await base.InitStatsAsync(ct); + + blockchainStats = manager.BlockchainStats; + } + + protected override WorkerContextBase CreateWorkerContext() + { + return new ConcealWorkerContext(); + } + + protected override async Task OnRequestAsync(StratumConnection connection, + Timestamped tsRequest, CancellationToken ct) + { + var request = tsRequest.Value; + var context = connection.ContextAs(); + + try + { + switch(request.Method) + { + case ConcealStratumMethods.Login: + await OnLoginAsync(connection, tsRequest); + break; + + case ConcealStratumMethods.GetJob: + await OnGetJobAsync(connection, tsRequest); + break; + + case ConcealStratumMethods.Submit: + await OnSubmitAsync(connection, tsRequest, ct); + break; + + case ConcealStratumMethods.KeepAlive: + // recognize activity + context.LastActivity = clock.Now; + break; + + default: + logger.Debug(() => $"[{connection.ConnectionId}] Unsupported RPC request: {JsonConvert.SerializeObject(request, serializerSettings)}"); + + await connection.RespondErrorAsync(StratumError.Other, $"Unsupported request {request.Method}", request.Id); + break; + } + } + + catch(StratumException ex) + { + await connection.RespondErrorAsync(ex.Code, ex.Message, request.Id, false); + } + } + + public override double HashrateFromShares(double shares, double interval) + { + var result = shares / interval; + return result; + } + + public override double ShareMultiplier => 1; + + protected override async Task OnVarDiffUpdateAsync(StratumConnection connection, double newDiff, CancellationToken ct) + { + await base.OnVarDiffUpdateAsync(connection, newDiff, ct); + + if(connection.Context.ApplyPendingDifficulty()) + { + // re-send job + var job = CreateWorkerJob(connection); + await connection.NotifyAsync(ConcealStratumMethods.JobNotify, job); + } + } + + #endregion // Overrides +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/ConcealStratumMethods.cs b/src/Miningcore/Blockchain/Conceal/ConcealStratumMethods.cs new file mode 100644 index 000000000..c28111c0a --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/ConcealStratumMethods.cs @@ -0,0 +1,29 @@ +namespace Miningcore.Blockchain.Conceal; + +public class ConcealStratumMethods +{ + /// + /// Used to subscribe to work + /// + public const string Login = "login"; + + /// + /// New job notification + /// + public const string JobNotify = "job"; + + /// + /// Get Job request + /// + public const string GetJob = "getjob"; + + /// + /// Submit share request + /// + public const string Submit = "submit"; + + /// + /// Keep alive request + /// + public const string KeepAlive = "keepalived"; +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/ConcealWorkerContext.cs b/src/Miningcore/Blockchain/Conceal/ConcealWorkerContext.cs new file mode 100644 index 000000000..9539d0bfd --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/ConcealWorkerContext.cs @@ -0,0 +1,32 @@ +using Miningcore.Mining; + +namespace Miningcore.Blockchain.Conceal; + +public class ConcealWorkerContext : WorkerContextBase +{ + /// + /// Usually a wallet address + /// NOTE: May include paymentid (seperated by a dot .) + /// + public string Miner { get; set; } + + /// + /// Arbitrary worker identififer for miners using multiple rigs + /// + public string Worker { get; set; } + + private List validJobs { get; } = new(); + + public void AddJob(ConcealWorkerJob job) + { + validJobs.Insert(0, job); + + while(validJobs.Count > 4) + validJobs.RemoveAt(validJobs.Count - 1); + } + + public ConcealWorkerJob FindJob(string jobId) + { + return validJobs.FirstOrDefault(x => x.Id == jobId); + } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/ConcealWorkerJob.cs b/src/Miningcore/Blockchain/Conceal/ConcealWorkerJob.cs new file mode 100644 index 000000000..4bbc82d83 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/ConcealWorkerJob.cs @@ -0,0 +1,19 @@ +using System.Collections.Concurrent; + +namespace Miningcore.Blockchain.Conceal; + +public class ConcealWorkerJob +{ + public ConcealWorkerJob(string jobId, double difficulty) + { + Id = jobId; + Difficulty = difficulty; + } + + public string Id { get; } + public uint Height { get; set; } + public uint ExtraNonce { get; set; } + public double Difficulty { get; set; } + + public readonly ConcurrentDictionary Submissions = new(StringComparer.OrdinalIgnoreCase); +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/Configuration/ConcealDaemonEndpointConfigExtra.cs b/src/Miningcore/Blockchain/Conceal/Configuration/ConcealDaemonEndpointConfigExtra.cs new file mode 100644 index 000000000..721d37430 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/Configuration/ConcealDaemonEndpointConfigExtra.cs @@ -0,0 +1,16 @@ +namespace Miningcore.Blockchain.Conceal.Configuration; + +public class ConcealDaemonEndpointConfigExtra +{ + /// + /// Address of ZeroMQ block notify socket + /// Should match the value of -zmqpubhashblock daemon start parameter + /// + public string ZmqBlockNotifySocket { get; set; } + + /// + /// Optional: ZeroMQ block notify topic + /// Defaults to "hashblock" if left blank + /// + public string ZmqBlockNotifyTopic { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/Configuration/ConcealPoolConfigExtra.cs b/src/Miningcore/Blockchain/Conceal/Configuration/ConcealPoolConfigExtra.cs new file mode 100644 index 000000000..a38f8d73d --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/Configuration/ConcealPoolConfigExtra.cs @@ -0,0 +1,18 @@ +using Miningcore.Configuration; +using Newtonsoft.Json.Linq; + +namespace Miningcore.Blockchain.Conceal.Configuration; + +public class ConcealPoolConfigExtra +{ + /// + /// Blocktemplate stream published via ZMQ + /// + public ZmqPubSubEndpointConfig BtStream { get; set; } + + /// + /// Conceal does not have a RPC method which returns on which network it is operating, so user can specify which one + /// Defaults to `testnet` if not specified + /// + public string NetworkTypeSpecified { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/Configuration/ConcealPoolPaymentProcessingConfigExtra.cs b/src/Miningcore/Blockchain/Conceal/Configuration/ConcealPoolPaymentProcessingConfigExtra.cs new file mode 100644 index 000000000..c77d581f3 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/Configuration/ConcealPoolPaymentProcessingConfigExtra.cs @@ -0,0 +1,6 @@ +namespace Miningcore.Blockchain.Conceal.Configuration; + +public class ConcealPoolPaymentProcessingConfigExtra +{ + public decimal MinimumPaymentToPaymentId { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetAddressRequest.cs b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetAddressRequest.cs new file mode 100644 index 000000000..b8ae737e4 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetAddressRequest.cs @@ -0,0 +1,5 @@ +namespace Miningcore.Blockchain.Conceal.DaemonRequests; + +public class GetAddressRequest +{ +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetBalanceRequest.cs b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetBalanceRequest.cs new file mode 100644 index 000000000..f07412232 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetBalanceRequest.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.DaemonRequests; + +public class GetBalanceRequest +{ + /// + /// (Optional) If address is not specified, returns the balance of the first address in the wallet. + /// + [JsonProperty("address")] + public string Address { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetBlockHeaderByHashRequest.cs b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetBlockHeaderByHashRequest.cs new file mode 100644 index 000000000..1428dd321 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetBlockHeaderByHashRequest.cs @@ -0,0 +1,6 @@ +namespace Miningcore.Blockchain.Conceal.DaemonRequests; + +public class GetBlockHeaderByHashRequest +{ + public string Hash { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetBlockHeaderByHeightRequest.cs b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetBlockHeaderByHeightRequest.cs new file mode 100644 index 000000000..3682b35c1 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetBlockHeaderByHeightRequest.cs @@ -0,0 +1,6 @@ +namespace Miningcore.Blockchain.Conceal.DaemonRequests; + +public class GetBlockHeaderByHeightRequest +{ + public ulong Height { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetBlockTemplateRequest.cs b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetBlockTemplateRequest.cs new file mode 100644 index 000000000..9d24424cc --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetBlockTemplateRequest.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.DaemonRequests; + +public class GetBlockTemplateRequest +{ + /// + /// Address of wallet to receive coinbase transactions if block is successfully mined. + /// + [JsonProperty("wallet_address")] + public string WalletAddress { get; set; } + + [JsonProperty("reserve_size")] + public uint ReserveSize { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetInfoRequest.cs b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetInfoRequest.cs new file mode 100644 index 000000000..5075dc3bb --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetInfoRequest.cs @@ -0,0 +1,5 @@ +namespace Miningcore.Blockchain.Conceal.DaemonRequests; + +public class GetInfoRequest +{ +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetLastBlockHeaderRequest.cs b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetLastBlockHeaderRequest.cs new file mode 100644 index 000000000..6a4715387 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonRequests/GetLastBlockHeaderRequest.cs @@ -0,0 +1,5 @@ +namespace Miningcore.Blockchain.Conceal.DaemonRequests; + +public class GetLastBlockHeaderRequest +{ +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonRequests/SendTransactionRequest.cs b/src/Miningcore/Blockchain/Conceal/DaemonRequests/SendTransactionRequest.cs new file mode 100644 index 000000000..9a783eeb1 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonRequests/SendTransactionRequest.cs @@ -0,0 +1,55 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.DaemonRequests; + +public class SendTransactionTransfers +{ + [JsonProperty("address")] + public string Address { get; set; } + + [JsonProperty("amount")] + public ulong Amount { get; set; } +} + +public class SendTransactionRequest +{ + /// + /// Privacy level (a discrete number from 1 to infinity). Level 5 is recommended + /// + [JsonProperty("anonymity")] + public uint Anonymity { get; set; } = 5; + + /// + /// Transaction fee. The fee in Conceal is fixed at .001 CCX. This parameter should be specified in minimal available CCX units. For example, if your fee is .001 CCX, you should pass it as 1000 + /// + [JsonProperty("fee")] + public ulong Fee { get; set; } = 1000; + + /// + /// (Optional) Height of the block until which transaction is going to be locked for spending (0 to not add a lock) + /// + [JsonProperty("unlockTime")] + public uint UnlockTime { get; set; } = 0; + + /// + /// (Optional) Array of strings, where each string is an address to take the funds from + /// + [JsonProperty("addresses")] + public string[] Addresses { get; set; } + + /// + /// Array of strings and integers, where each string and integer are respectively an address and an amount where the funds are going to + /// + [JsonProperty("transfers")] + public SendTransactionTransfers[] Transfers { get; set; } + + /// + /// (Optional) Valid and existing address in the conceal wallet container used by walletd (Conceal Wallet RPC), it will receive the change of the transaction + /// IMPORTANT RULES: + /// 1: if container contains only 1 address, changeAddress field can be left empty and the change is going to be sent to this address + /// 2: if addresses field contains only 1 address, changeAddress can be left empty and the change is going to be sent to this address + /// 3: in the rest of the cases, changeAddress field is mandatory and must contain an address. + /// + [JsonProperty("changeAddress")] + public string ChangeAddress { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonRequests/SplitIntegratedAddressRequest.cs b/src/Miningcore/Blockchain/Conceal/DaemonRequests/SplitIntegratedAddressRequest.cs new file mode 100644 index 000000000..1a9fa7c7d --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonRequests/SplitIntegratedAddressRequest.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.DaemonRequests; + +public class SplitIntegratedAddressRequest +{ + [JsonProperty("integrated_address")] + public string WalletAddress { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetAddressResponse.cs b/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetAddressResponse.cs new file mode 100644 index 000000000..7f5725d46 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetAddressResponse.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.DaemonResponses; + +public class GetAddressResponse +{ + [JsonProperty("addresses")] + public string[] Address { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetBalanceResponse.cs b/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetBalanceResponse.cs new file mode 100644 index 000000000..af088cf09 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetBalanceResponse.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.DaemonResponses; + +public class GetBalanceResponse +{ + /// + /// Available balance of the specified address + /// + [JsonProperty("availableBalance")] + public decimal Balance { get; set; } + + /// + /// Locked amount of the specified address + /// + [JsonProperty("lockedAmount")] + public decimal LockedBalance { get; set; } + + /// + /// Locked amount of the specified address + /// + [JsonProperty("lockedDepositBalance")] + public decimal LockedDepositBalance { get; set; } + + /// + /// Balance of unlocked deposits that can be withdrawn + /// + [JsonProperty("unlockedDepositBalance")] + public decimal UnlockedDepositBalance { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetBlockHeaderResponse.cs b/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetBlockHeaderResponse.cs new file mode 100644 index 000000000..502e1becd --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetBlockHeaderResponse.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.DaemonResponses; + +public class BlockHeader +{ + public ulong Deposits { get; set; } + public long Difficulty { get; set; } + public long Depth { get; set; } + public uint Height { get; set; } + public string Hash { get; set; } + public string Nonce { get; set; } + public ulong Reward { get; set; } + public ulong Timestamp { get; set; } + + [JsonProperty("major_version")] + public uint MajorVersion { get; set; } + + [JsonProperty("minor_version")] + public uint MinorVersion { get; set; } + + [JsonProperty("prev_hash")] + public string PreviousBlockhash { get; set; } + + [JsonProperty("orphan_status")] + public bool IsOrphaned { get; set; } +} + +public class GetBlockHeaderResponse +{ + [JsonProperty("block_header")] + public BlockHeader BlockHeader { get; set; } + + public string Status { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetBlockTemplateResponse.cs b/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetBlockTemplateResponse.cs new file mode 100644 index 000000000..d54318749 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetBlockTemplateResponse.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.DaemonResponses; + +public class GetBlockTemplateResponse +{ + [JsonProperty("blocktemplate_blob")] + public string Blob { get; set; } + + public long Difficulty { get; set; } + public uint Height { get; set; } + + [JsonProperty("reserved_offset")] + public int ReservedOffset { get; set; } + + public string Status { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetInfoResponse.cs b/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetInfoResponse.cs new file mode 100644 index 000000000..ea65843f6 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonResponses/GetInfoResponse.cs @@ -0,0 +1,131 @@ +using System.Text.Json.Serialization; + +namespace Miningcore.Blockchain.Conceal.DaemonResponses; + +public record GetInfoResponse +{ + /// + /// Number of alternative blocks to main chain. + /// + [JsonPropertyName("alt_blocks_count")] + public int AltBlocksCount { get; set; } + + /// + /// ??? + /// + [JsonPropertyName("block_major_version")] + public int BlockMajorVersion { get; set; } + + /// + /// ??? + /// + [JsonPropertyName("block_minor_version")] + public int BlockMinorVersion { get; set; } + + /// + /// ??? + /// + [JsonPropertyName("connections")] + public string[] Connections { get; set; } + + /// + /// Network difficulty(analogous to the strength of the network) + /// + [JsonPropertyName("difficulty")] + public ulong Difficulty { get; set; } + + /// + /// Address dedicated to smartnode rewards + /// + [JsonPropertyName("fee_address")] + public string FeeAddress { get; set; } + + /// + /// Total amount deposit + /// + [JsonPropertyName("full_deposit_amount")] + public ulong FullAmountDeposit { get; set; } + + /// + /// Grey Peerlist Size + /// + [JsonPropertyName("grey_peerlist_size")] + public int GreyPeerlistSize { get; set; } + + /// + /// The height of the next block in the chain. + /// + [JsonPropertyName("height")] + public uint TargetHeight { get; set; } + + /// + /// Number of peers connected to and pulling from your node. + /// + [JsonPropertyName("incoming_connections_count")] + public int IncomingConnectionsCount { get; set; } + + /// + /// Last block difficulty + /// + [JsonPropertyName("last_block_difficulty")] + public ulong LastBlockDifficulty { get; set; } + + /// + /// Last block reward + /// + [JsonPropertyName("last_block_reward")] + public ulong LastBlockReward { get; set; } + + /// + /// Last block timestamp + /// + [JsonPropertyName("last_block_timestamp")] + public ulong LastBlockTimestamp { get; set; } + + /// + /// Current length of longest chain known to daemon. + /// + [JsonPropertyName("last_known_block_index")] + public uint Height { get; set; } + + /// + /// Number of peers that you are connected to and getting information from. + /// + [JsonPropertyName("outgoing_connections_count")] + public int OutgoingConnectionsCount { get; set; } + + /// + /// General RPC error code. "OK" means everything looks good. + /// + public string Status { get; set; } + + /// + /// Hash of the highest block in the chain. + /// + [JsonPropertyName("top_block_hash")] + public string TopBlockHash { get; set; } + + /// + /// Total number of non-coinbase transaction in the chain. + /// + [JsonPropertyName("tx_count")] + public uint TransactionCount { get; set; } + + /// + /// Number of transactions that have been broadcast but not included in a block. + /// + [JsonPropertyName("tx_pool_size")] + public uint TransactionPoolSize { get; set; } + + /// + /// Version + /// + [JsonPropertyName("version")] + public string Version { get; set; } + + /// + /// White Peerlist Size + /// + [JsonPropertyName("white_peerlist_size")] + public uint WhitePeerlistSize { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonResponses/SendTransactionResponse.cs b/src/Miningcore/Blockchain/Conceal/DaemonResponses/SendTransactionResponse.cs new file mode 100644 index 000000000..61ac6361a --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonResponses/SendTransactionResponse.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.DaemonResponses; + +public class SendTransactionResponse +{ + /// + /// Publically searchable transaction hash + /// + [JsonProperty("transactionHash")] + public string TxHash { get; set; } + + public string Status { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonResponses/SplitIntegratedAddressResponse.cs b/src/Miningcore/Blockchain/Conceal/DaemonResponses/SplitIntegratedAddressResponse.cs new file mode 100644 index 000000000..4525ce68a --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonResponses/SplitIntegratedAddressResponse.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.DaemonResponses; + +public class SplitIntegratedAddressResponse +{ + [JsonProperty("address")] + public string StandardAddress { get; set; } + + [JsonProperty("payment_id")] + public string Payment { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/DaemonResponses/SubmitResponse.cs b/src/Miningcore/Blockchain/Conceal/DaemonResponses/SubmitResponse.cs new file mode 100644 index 000000000..e69ff7ceb --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/DaemonResponses/SubmitResponse.cs @@ -0,0 +1,6 @@ +namespace Miningcore.Blockchain.Conceal.DaemonResponses; + +public class SubmitResponse +{ + public string Status { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/StratumRequests/ConcealGetJobRequest.cs b/src/Miningcore/Blockchain/Conceal/StratumRequests/ConcealGetJobRequest.cs new file mode 100644 index 000000000..9be4e2d50 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/StratumRequests/ConcealGetJobRequest.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.StratumRequests; + +public class ConcealGetJobRequest +{ + [JsonProperty("id")] + public string WorkerId { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/StratumRequests/ConcealLoginRequest.cs b/src/Miningcore/Blockchain/Conceal/StratumRequests/ConcealLoginRequest.cs new file mode 100644 index 000000000..5ba640914 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/StratumRequests/ConcealLoginRequest.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.StratumRequests; + +public class ConcealLoginRequest +{ + [JsonProperty("login")] + public string Login { get; set; } + + [JsonProperty("pass")] + public string Password { get; set; } + + [JsonProperty("agent")] + public string UserAgent { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/StratumRequests/ConcealSubmitShareRequest.cs b/src/Miningcore/Blockchain/Conceal/StratumRequests/ConcealSubmitShareRequest.cs new file mode 100644 index 000000000..aa469ac1f --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/StratumRequests/ConcealSubmitShareRequest.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.StratumRequests; + +public class ConcealSubmitShareRequest +{ + [JsonProperty("id")] + public string WorkerId { get; set; } + + [JsonProperty("job_id")] + public string JobId { get; set; } + + public string Nonce { get; set; } + + [JsonProperty("result")] + public string Hash { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/StratumResponses/ConcealLoginResponse.cs b/src/Miningcore/Blockchain/Conceal/StratumResponses/ConcealLoginResponse.cs new file mode 100644 index 000000000..a6e1c483d --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/StratumResponses/ConcealLoginResponse.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace Miningcore.Blockchain.Conceal.StratumResponses; + +public class ConcealJobParams +{ + [JsonProperty("job_id")] + public string JobId { get; set; } + + public string Blob { get; set; } + public string Target { get; set; } + + [JsonProperty("algo")] + public string Algorithm { get; set; } + + /// + /// Introduced for CNv4 (aka CryptonightR) + /// + public ulong Height { get; set; } +} + +public class ConcealLoginResponse : ConcealResponseBase +{ + public string Id { get; set; } = "1"; + public ConcealJobParams Job { get; set; } +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Conceal/StratumResponses/ConcealResponseBase.cs b/src/Miningcore/Blockchain/Conceal/StratumResponses/ConcealResponseBase.cs new file mode 100644 index 000000000..4425a9118 --- /dev/null +++ b/src/Miningcore/Blockchain/Conceal/StratumResponses/ConcealResponseBase.cs @@ -0,0 +1,6 @@ +namespace Miningcore.Blockchain.Conceal.StratumResponses; + +public class ConcealResponseBase +{ + public string Status { get; set; } = "OK"; +} \ No newline at end of file diff --git a/src/Miningcore/Blockchain/Cryptonote/CryptonoteJob.cs b/src/Miningcore/Blockchain/Cryptonote/CryptonoteJob.cs index cfb505ad6..a0ce6bdf3 100644 --- a/src/Miningcore/Blockchain/Cryptonote/CryptonoteJob.cs +++ b/src/Miningcore/Blockchain/Cryptonote/CryptonoteJob.cs @@ -35,26 +35,26 @@ public CryptonoteJob(GetBlockTemplateResponse blockTemplate, byte[] instanceId, { { CryptonightHashType.RandomX, (realm, seedHex, data, result, _) => RandomX.CalculateHash(realm, seedHex, data, result) }, { CryptonightHashType.RandomARQ, (realm, seedHex, data, result, _) => RandomARQ.CalculateHash(realm, seedHex, data, result) }, - { CryptonightHashType.Crytonight0, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_0, height) }, - { CryptonightHashType.Crytonight1, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_1, height) }, - { CryptonightHashType.Crytonight2, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_2, height) }, - { CryptonightHashType.CrytonightHalf, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_HALF, height) }, - { CryptonightHashType.CrytonightDouble, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_DOUBLE, height) }, - { CryptonightHashType.CrytonightR, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_R, height) }, - { CryptonightHashType.CrytonightRTO, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_RTO, height) }, - { CryptonightHashType.CrytonightRWZ, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_RWZ, height) }, - { CryptonightHashType.CrytonightZLS, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_ZLS, height) }, - { CryptonightHashType.CrytonightCCX, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_CCX, height) }, - { CryptonightHashType.CrytonightGPU, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_GPU, height) }, - { CryptonightHashType.CrytonightFast, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_FAST, height) }, - { CryptonightHashType.CrytonightXAO, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_XAO, height) }, + { CryptonightHashType.Cryptonight0, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_0, height) }, + { CryptonightHashType.Cryptonight1, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_1, height) }, + { CryptonightHashType.Cryptonight2, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_2, height) }, + { CryptonightHashType.CryptonightHalf, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_HALF, height) }, + { CryptonightHashType.CryptonightDouble, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_DOUBLE, height) }, + { CryptonightHashType.CryptonightR, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_R, height) }, + { CryptonightHashType.CryptonightRTO, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_RTO, height) }, + { CryptonightHashType.CryptonightRWZ, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_RWZ, height) }, + { CryptonightHashType.CryptonightZLS, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_ZLS, height) }, + { CryptonightHashType.CryptonightCCX, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_CCX, height) }, + { CryptonightHashType.CryptonightGPU, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_GPU, height) }, + { CryptonightHashType.CryptonightFast, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_FAST, height) }, + { CryptonightHashType.CryptonightXAO, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_XAO, height) }, { CryptonightHashType.Ghostrider, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, GHOSTRIDER_RTM, height) }, - { CryptonightHashType.CrytonightLite0, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_LITE_0, height) }, - { CryptonightHashType.CrytonightLite1, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_LITE_1, height) }, - { CryptonightHashType.CrytonightHeavy, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_HEAVY_0, height) }, - { CryptonightHashType.CrytonightHeavyXHV, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_HEAVY_XHV, height) }, - { CryptonightHashType.CrytonightHeavyTube, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_HEAVY_TUBE, height) }, - { CryptonightHashType.CrytonightPico, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_PICO_0, height) }, + { CryptonightHashType.CryptonightLite0, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_LITE_0, height) }, + { CryptonightHashType.CryptonightLite1, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_LITE_1, height) }, + { CryptonightHashType.CryptonightHeavy, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_HEAVY_0, height) }, + { CryptonightHashType.CryptonightHeavyXHV, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_HEAVY_XHV, height) }, + { CryptonightHashType.CryptonightHeavyTube, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_HEAVY_TUBE, height) }, + { CryptonightHashType.CryptonightPico, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, CN_PICO_0, height) }, { CryptonightHashType.ArgonCHUKWA, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, AR2_CHUKWA, height) }, { CryptonightHashType.ArgonCHUKWAV2, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, AR2_CHUKWA_V2, height) }, { CryptonightHashType.ArgonWRKZ, (_, _, data, result, height) => Cryptonight.CryptonightHash(data, result, AR2_WRKZ, height) }, diff --git a/src/Miningcore/Configuration/ClusterConfig.cs b/src/Miningcore/Configuration/ClusterConfig.cs index d430976d1..0fa3c07ad 100644 --- a/src/Miningcore/Configuration/ClusterConfig.cs +++ b/src/Miningcore/Configuration/ClusterConfig.cs @@ -22,6 +22,9 @@ public enum CoinFamily [EnumMember(Value = "equihash")] Equihash, + [EnumMember(Value = "conceal")] + Conceal, + [EnumMember(Value = "cryptonote")] Cryptonote, @@ -129,6 +132,7 @@ public abstract partial class CoinTemplate { {CoinFamily.Bitcoin, typeof(BitcoinTemplate)}, {CoinFamily.Equihash, typeof(EquihashCoinTemplate)}, + {CoinFamily.Conceal, typeof(ConcealCoinTemplate)}, {CoinFamily.Cryptonote, typeof(CryptonoteCoinTemplate)}, {CoinFamily.Ethereum, typeof(EthereumCoinTemplate)}, {CoinFamily.Ergo, typeof(ErgoCoinTemplate)}, @@ -322,6 +326,12 @@ public partial class EquihashNetworkParams public bool UseBitcoinPayoutHandler { get; set; } } +public enum ConcealSubfamily +{ + [EnumMember(Value = "none")] + None, +} + public enum CryptonoteSubfamily { [EnumMember(Value = "none")] @@ -337,64 +347,64 @@ public enum CryptonightHashType RandomARQ, [EnumMember(Value = "cn0")] - Crytonight0, + Cryptonight0, [EnumMember(Value = "cn1")] - Crytonight1, + Cryptonight1, [EnumMember(Value = "cn2")] - Crytonight2, + Cryptonight2, [EnumMember(Value = "cn-half")] - CrytonightHalf, + CryptonightHalf, [EnumMember(Value = "cn-double")] - CrytonightDouble, + CryptonightDouble, [EnumMember(Value = "cn-r")] - CrytonightR, + CryptonightR, [EnumMember(Value = "cn-rto")] - CrytonightRTO, + CryptonightRTO, [EnumMember(Value = "cn-rwz")] - CrytonightRWZ, + CryptonightRWZ, [EnumMember(Value = "cn-zls")] - CrytonightZLS, + CryptonightZLS, [EnumMember(Value = "cn-ccx")] - CrytonightCCX, + CryptonightCCX, [EnumMember(Value = "cn-gpu")] - CrytonightGPU, + CryptonightGPU, [EnumMember(Value = "cn-fast")] - CrytonightFast, + CryptonightFast, [EnumMember(Value = "cn-xao")] - CrytonightXAO, + CryptonightXAO, [EnumMember(Value = "gr")] Ghostrider, [EnumMember(Value = "cn_lite0")] - CrytonightLite0, + CryptonightLite0, [EnumMember(Value = "cn_lite1")] - CrytonightLite1, + CryptonightLite1, [EnumMember(Value = "cn_heavy")] - CrytonightHeavy, + CryptonightHeavy, [EnumMember(Value = "cn_heavy_xhv")] - CrytonightHeavyXHV, + CryptonightHeavyXHV, [EnumMember(Value = "cn_heavy_tube")] - CrytonightHeavyTube, + CryptonightHeavyTube, [EnumMember(Value = "cn_pico")] - CrytonightPico, + CryptonightPico, [EnumMember(Value = "argon_chukwa")] ArgonCHUKWA, @@ -406,6 +416,69 @@ public enum CryptonightHashType ArgonWRKZ, } +public partial class ConcealCoinTemplate : CoinTemplate +{ + [JsonProperty(Order = -7, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(ConcealSubfamily.None)] + [JsonConverter(typeof(StringEnumConverter), true)] + public ConcealSubfamily Subfamily { get; set; } + + /// + /// Broader Cryptonight hash family + /// + [JsonConverter(typeof(StringEnumConverter), true)] + [JsonProperty(Order = -5)] + public CryptonightHashType Hash { get; set; } + + /// + /// Set to 0 for automatic selection from blobtemplate + /// + [JsonProperty(Order = -4, DefaultValueHandling = DefaultValueHandling.Include)] + public int HashVariant { get; set; } + + /// + /// Conceal network hashrate = `Difficulty / DifficultyTarget` + /// See: parameter -> DIFFICULTY_TARGET in src/CryptoNoteConfig.h + /// + public ulong DifficultyTarget { get; set; } + + /// + /// Smallest unit for Blockreward formatting + /// + public decimal SmallestUnit { get; set; } + + /// + /// Prefix of a valid address + /// See: parameter -> CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX in src/CryptoNoteConfig.h + /// + public ulong AddressPrefix { get; set; } + + /// + /// Prefix of a valid testnet-address + /// See: parameter -> CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX in src/CryptoNoteConfig.h + /// + public ulong AddressPrefixTestnet { get; set; } + + /// + /// Prefix of a valid integrated address + /// See: parameter -> CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX in src/CryptoNoteConfig.h + /// + public ulong AddressPrefixIntegrated { get; set; } + + /// + /// Prefix of a valid integrated testnet-address + /// See: parameter -> CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX in src/CryptoNoteConfig.h + /// + public ulong AddressPrefixIntegratedTestnet { get; set; } + + /// + /// Fraction of block reward, the pool really gets to keep + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(1.0d)] + public decimal BlockrewardMultiplier { get; set; } +} + public partial class CryptonoteCoinTemplate : CoinTemplate { [JsonProperty(Order = -7, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] diff --git a/src/Miningcore/Configuration/ClusterConfigExtensions.cs b/src/Miningcore/Configuration/ClusterConfigExtensions.cs index 687d672f1..d7c783306 100644 --- a/src/Miningcore/Configuration/ClusterConfigExtensions.cs +++ b/src/Miningcore/Configuration/ClusterConfigExtensions.cs @@ -146,6 +146,24 @@ public override string GetAlgorithmName() #endregion } +public partial class ConcealCoinTemplate +{ + #region Overrides of CoinTemplate + + public override string GetAlgorithmName() + { +// switch(Hash) +// { +// case CryptonightHashType.RandomX: +// return "RandomX"; +// } + + return Hash.ToString(); + } + + #endregion +} + public partial class CryptonoteCoinTemplate { #region Overrides of CoinTemplate diff --git a/src/Miningcore/coins.json b/src/Miningcore/coins.json index fa4a37586..abc3eb588 100644 --- a/src/Miningcore/coins.json +++ b/src/Miningcore/coins.json @@ -4239,6 +4239,27 @@ "subAddressPrefixStagenet": 36, "explorerBlockLink": "https://www.exploremonero.com/block/$height$", "explorerTxLink": "https://www.exploremonero.com/transaction/{0}" + }, + "conceal": { + "name": "Conceal", + "canonicalName": "Conceal", + "symbol": "CCX", + "family": "conceal", + "website": "https://conceal.network/", + "market": "", + "twitter": "https://twitter.com/ConcealNetwork", + "telegram": "https://t.me/concealnetworkusers", + "discord": "http://discord.conceal.network/", + "hash": "cn-gpu", + "hashVariant": 0, + "difficultyTarget": 120, + "smallestUnit": 1000000, + "addressPrefix": 31444, + "addressPrefixTestnet": 31444, + "addressPrefixIntegrated": 31444, + "addressPrefixIntegratedTestnet": 31444, + "explorerBlockLink": "https://explorer.conceal.network/index.html?hash=$hash$#blockchain_block", + "explorerTxLink": "https://explorer.conceal.network/index.html?hash={0}#blockchain_transaction" }, "callisto": { "name": "Callisto Network",