public CryptonoteJobManager( IComponentContext ctx, IMasterClock clock, IMessageBus messageBus) : base(ctx, messageBus) { Contract.RequiresNonNull(ctx, nameof(ctx)); Contract.RequiresNonNull(clock, nameof(clock)); Contract.RequiresNonNull(messageBus, nameof(messageBus)); this.clock = clock; using (var rng = RandomNumberGenerator.Create()) { instanceId = new byte[CryptonoteConstants.InstanceIdSize]; rng.GetNonZeroBytes(instanceId); } }
public EthereumPayoutHandler( IComponentContext ctx, IConnectionFactory cf, IMapper mapper, IShareRepository shareRepo, IBlockRepository blockRepo, IBalanceRepository balanceRepo, IPaymentRepository paymentRepo, IMasterClock clock, IMessageBus messageBus) : base(cf, mapper, shareRepo, blockRepo, balanceRepo, paymentRepo, clock, messageBus) { Contract.RequiresNonNull(ctx, nameof(ctx)); Contract.RequiresNonNull(balanceRepo, nameof(balanceRepo)); Contract.RequiresNonNull(paymentRepo, nameof(paymentRepo)); this.ctx = ctx; }
public EthereumJobManager( IComponentContext ctx, IMasterClock clock, IMessageBus messageBus, JsonSerializerSettings serializerSettings) : base(ctx, messageBus) { Contract.RequiresNonNull(ctx, nameof(ctx)); Contract.RequiresNonNull(clock, nameof(clock)); Contract.RequiresNonNull(messageBus, nameof(messageBus)); this.clock = clock; serializer = new JsonSerializer { ContractResolver = serializerSettings.ContractResolver }; }
public virtual async Task StartAsync(CancellationToken ct) { Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); logger.Info(() => $"Starting Pool ..."); try { SetupBanning(clusterConfig); await SetupJobManager(ct); await InitStatsAsync(); if (poolConfig.EnableInternalStratum == true) { var ipEndpoints = poolConfig.Ports.Keys .Select(port => PoolEndpoint2IPEndpoint(port, poolConfig.Ports[port])) .ToArray(); StartListeners(ipEndpoints); } logger.Info(() => $"Pool Online"); OutputPoolInfo(); } catch (PoolStartupAbortException) { // just forward these throw; } catch (TaskCanceledException) { // just forward these throw; } catch (Exception ex) { logger.Error(ex); throw; } }
public virtual Task ConfigureAsync(ClusterConfig clusterConfig, PoolConfig poolConfig) { Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); this.poolConfig = poolConfig; this.clusterConfig = clusterConfig; extraPoolConfig = poolConfig.Extra.SafeExtensionDataAs <BitcoinDaemonEndpointConfigExtra>(); extraPoolPaymentProcessingConfig = poolConfig.PaymentProcessing.Extra.SafeExtensionDataAs <BitcoinPoolPaymentProcessingConfigExtra>(); logger = LogUtil.GetPoolScopedLogger(typeof(BitcoinPayoutHandler), poolConfig); var jsonSerializerSettings = ctx.Resolve <JsonSerializerSettings>(); daemon = new DaemonClient(jsonSerializerSettings, messageBus, clusterConfig.ClusterName ?? poolConfig.PoolName, poolConfig.Id); daemon.Configure(poolConfig.Daemons); return(Task.FromResult(true)); }
public PayoutManager(IComponentContext ctx, IConnectionFactory cf, IBlockRepository blockRepo, IShareRepository shareRepo, IBalanceRepository balanceRepo, IMessageBus messageBus) { Contract.RequiresNonNull(ctx, nameof(ctx)); Contract.RequiresNonNull(cf, nameof(cf)); Contract.RequiresNonNull(blockRepo, nameof(blockRepo)); Contract.RequiresNonNull(shareRepo, nameof(shareRepo)); Contract.RequiresNonNull(balanceRepo, nameof(balanceRepo)); Contract.RequiresNonNull(messageBus, nameof(messageBus)); this.ctx = ctx; this.cf = cf; this.blockRepo = blockRepo; this.shareRepo = shareRepo; this.balanceRepo = balanceRepo; this.messageBus = messageBus; }
protected PoolBase(IComponentContext ctx, JsonSerializerSettings serializerSettings, IConnectionFactory cf, IStatsRepository statsRepo, IMapper mapper, IMasterClock clock, IMessageBus messageBus) : base(ctx, clock) { Contract.RequiresNonNull(ctx, nameof(ctx)); Contract.RequiresNonNull(serializerSettings, nameof(serializerSettings)); Contract.RequiresNonNull(cf, nameof(cf)); Contract.RequiresNonNull(statsRepo, nameof(statsRepo)); Contract.RequiresNonNull(mapper, nameof(mapper)); Contract.RequiresNonNull(clock, nameof(clock)); Contract.RequiresNonNull(messageBus, nameof(messageBus)); this.serializerSettings = serializerSettings; this.cf = cf; this.statsRepo = statsRepo; this.mapper = mapper; this.messageBus = messageBus; }
public double?Update(VarDiffContext ctx, double difficulty, bool isIdleUpdate) { Contract.RequiresNonNull(ctx, nameof(ctx)); lock (ctx) { // Get Current Time var ts = DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1000.0; // For the first time, won't change diff. if (!ctx.LastTs.HasValue) { ctx.LastRtc = ts; ctx.LastTs = ts; ctx.TimeBuffer = new CircularDoubleBuffer(bufferSize); return(null); } var minDiff = options.MinDiff; var maxDiff = options.MaxDiff ?? Math.Max(minDiff, double.MaxValue); // for regtest var sinceLast = ts - ctx.LastTs.Value; // Always calculate the time until now even there is no share submitted. var timeTotal = ctx.TimeBuffer.Sum(); var timeCount = ctx.TimeBuffer.Size; var avg = (timeTotal + sinceLast) / (timeCount + 1); // Once there is a share submitted, store the time into the buffer and update the last time. if (!isIdleUpdate) { ctx.TimeBuffer.PushBack(sinceLast); ctx.LastTs = ts; } // Check if we need to change the difficulty if (ts - ctx.LastRtc < options.RetargetTime || avg >= tMin && avg <= tMax) { return(null); } // Possible New Diff var newDiff = difficulty * options.TargetTime / avg; // Max delta if (options.MaxDelta.HasValue && options.MaxDelta > 0) { var delta = Math.Abs(newDiff - difficulty); if (delta > options.MaxDelta) { if (newDiff > difficulty) { newDiff -= delta - options.MaxDelta.Value; } else if (newDiff < difficulty) { newDiff += delta - options.MaxDelta.Value; } } } // Clamp to valid range if (newDiff < minDiff) { newDiff = minDiff; } if (newDiff > maxDiff) { newDiff = maxDiff; } // RTC if the Diff is changed if (newDiff < difficulty || newDiff > difficulty) { ctx.LastRtc = ts; ctx.LastUpdate = clock.Now; // Due to change of diff, Buffer needs to be cleared ctx.TimeBuffer = new CircularDoubleBuffer(bufferSize); return(newDiff); } } return(null); }
public async Task <Block[]> ClassifyBlocksAsync(Block[] blocks) { Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); Contract.RequiresNonNull(blocks, nameof(blocks)); var coin = poolConfig.Template.As <EthereumCoinTemplate>(); var pageSize = 100; var pageCount = (int)Math.Ceiling(blocks.Length / (double)pageSize); var blockCache = new Dictionary <long, DaemonResponses.Block>(); var result = new List <Block>(); for (var i = 0; i < pageCount; i++) { // get a page full of blocks var page = blocks .Skip(i * pageSize) .Take(pageSize) .ToArray(); // get latest block var latestBlockResponses = await daemon.ExecuteCmdAllAsync <DaemonResponses.Block>(logger, EC.GetBlockByNumber, new[] { (object)"latest", true }); var latestBlockHeight = latestBlockResponses.First(x => x.Error == null && x.Response?.Height != null).Response.Height.Value; // execute batch var blockInfos = await FetchBlocks(blockCache, page.Select(block => (long)block.BlockHeight).ToArray()); for (var j = 0; j < blockInfos.Length; j++) { var blockInfo = blockInfos[j]; var block = page[j]; // extract confirmation data from stored block var mixHash = block.TransactionConfirmationData.Split(":").First(); var nonce = block.TransactionConfirmationData.Split(":").Last(); // update progress block.ConfirmationProgress = Math.Min(1.0d, (double)(latestBlockHeight - block.BlockHeight) / EthereumConstants.MinConfimations); result.Add(block); messageBus.NotifyBlockConfirmationProgress(poolConfig.Id, block, coin); // is it block mined by us? if (string.Equals(blockInfo.Miner, poolConfig.Address, StringComparison.OrdinalIgnoreCase)) { // additional check // NOTE: removal of first character of both sealfields caused by // https://github.com/paritytech/parity/issues/1090 var match = isParity ? true : blockInfo.SealFields[0].Substring(2) == mixHash && blockInfo.SealFields[1].Substring(2) == nonce; // mature? if (match && (latestBlockHeight - block.BlockHeight >= EthereumConstants.MinConfimations)) { block.Status = BlockStatus.Confirmed; block.ConfirmationProgress = 1; block.BlockHeight = (ulong)blockInfo.Height; block.Reward = GetBaseBlockReward(chainType, block.BlockHeight); // base reward block.Type = "block"; if (extraConfig?.KeepUncles == false) { block.Reward += blockInfo.Uncles.Length * (block.Reward / 32); // uncle rewards } if (extraConfig?.KeepTransactionFees == false && blockInfo.Transactions?.Length > 0) { block.Reward += await GetTxRewardAsync(blockInfo); // tx fees } logger.Info(() => $"[{LogCategory}] Unlocked block {block.BlockHeight} worth {FormatAmount(block.Reward)}"); messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); } continue; } // search for a block containing our block as an uncle by checking N blocks in either direction var heightMin = block.BlockHeight - BlockSearchOffset; var heightMax = Math.Min(block.BlockHeight + BlockSearchOffset, latestBlockHeight); var range = new List <long>(); for (var k = heightMin; k < heightMax; k++) { range.Add((long)k); } // execute batch var blockInfo2s = await FetchBlocks(blockCache, range.ToArray()); foreach (var blockInfo2 in blockInfo2s) { // don't give up yet, there might be an uncle if (blockInfo2.Uncles.Length > 0) { // fetch all uncles in a single RPC batch request var uncleBatch = blockInfo2.Uncles.Select((x, index) => new DaemonCmd(EC.GetUncleByBlockNumberAndIndex, new[] { blockInfo2.Height.Value.ToStringHexWithPrefix(), index.ToStringHexWithPrefix() })) .ToArray(); logger.Info(() => $"[{LogCategory}] Fetching {blockInfo2.Uncles.Length} uncles for block {blockInfo2.Height}"); var uncleResponses = await daemon.ExecuteBatchAnyAsync(logger, uncleBatch); logger.Info(() => $"[{LogCategory}] Fetched {uncleResponses.Count(x => x.Error == null && x.Response != null)} uncles for block {blockInfo2.Height}"); var uncle = uncleResponses.Where(x => x.Error == null && x.Response != null) .Select(x => x.Response.ToObject <DaemonResponses.Block>()) .FirstOrDefault(x => string.Equals(x.Miner, poolConfig.Address, StringComparison.OrdinalIgnoreCase)); if (uncle != null) { // mature? if (latestBlockHeight - uncle.Height.Value >= EthereumConstants.MinConfimations) { block.Status = BlockStatus.Confirmed; block.ConfirmationProgress = 1; block.Reward = GetUncleReward(chainType, uncle.Height.Value, blockInfo2.Height.Value); block.BlockHeight = uncle.Height.Value; block.Type = EthereumConstants.BlockTypeUncle; logger.Info(() => $"[{LogCategory}] Unlocked uncle for block {blockInfo2.Height.Value} at height {uncle.Height.Value} worth {FormatAmount(block.Reward)}"); messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); } else { logger.Info(() => $"[{LogCategory}] Got immature matching uncle for block {blockInfo2.Height.Value}. Will try again."); } break; } } } if (block.Status == BlockStatus.Pending && block.ConfirmationProgress > 1 * *) { // we've lost this one block.Status = BlockStatus.Orphaned; block.Reward = 0; messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); } } } return(result.ToArray()); }
public async Task PayoutAsync(Balance[] balances) { Contract.RequiresNonNull(balances, nameof(balances)); var coin = poolConfig.Template.As <CryptonoteCoinTemplate>(); #if !DEBUG // ensure we have peers var infoResponse = await daemon.ExecuteCmdAnyAsync <GetInfoResponse>(logger, CNC.GetInfo); if (infoResponse.Error != null || infoResponse.Response == null || infoResponse.Response.IncomingConnectionsCount + infoResponse.Response.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 paymentId); var addressPrefix = LibCryptonote.DecodeAddress(address); var addressIntegratedPrefix = LibCryptonote.DecodeIntegratedAddress(address); switch (networkType) { case CryptonoteNetworkType.Main: if (addressPrefix != coin.AddressPrefix && addressIntegratedPrefix != coin.AddressPrefixIntegrated) { logger.Warn(() => $"[{LogCategory}] Excluding payment to invalid address {x.Address}"); return(false); } break; case CryptonoteNetworkType.Test: if (addressPrefix != coin.AddressPrefixTestnet && addressIntegratedPrefix != coin.AddressPrefixIntegratedTestnet) { 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 = LibCryptonote.DecodeIntegratedAddress(address); switch (networkType) { case CryptonoteNetworkType.Main: if (addressIntegratedPrefix == coin.AddressPrefixIntegrated) { isIntegratedAddress = true; } break; case CryptonoteNetworkType.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)) { 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)) { break; } } // save wallet await walletDaemon.ExecuteCmdSingleAsync <JToken>(logger, CryptonoteWalletCommands.Store); }
public async Task <Block[]> ClassifyBlocksAsync(Block[] blocks) { Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); Contract.RequiresNonNull(blocks, nameof(blocks)); var coin = poolConfig.Template.As <CryptonoteCoinTemplate>(); var pageSize = 100; var pageCount = (int)Math.Ceiling(blocks.Length / (double)pageSize); var result = new List <Block>(); for (var i = 0; i < pageCount; i++) { // get a page full of blocks var page = blocks .Skip(i * pageSize) .Take(pageSize) .ToArray(); // NOTE: monerod does not support batch-requests for (var j = 0; j < page.Length; j++) { var block = page[j]; var rpcResult = await daemon.ExecuteCmdAnyAsync <GetBlockHeaderResponse>(logger, CryptonoteCommands.GetBlockHeaderByHeight, 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 / CryptonoteConstants.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 >= CryptonoteConstants.PayoutMinBlockConfirmations) { block.Status = BlockStatus.Confirmed; block.ConfirmationProgress = 1; block.Reward = ((decimal)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 (Share Share, string BlobHex) ProcessShare(string nonce, uint workerExtraNonce, string workerHash, StratumClient worker) { Contract.Requires <ArgumentException>(!string.IsNullOrEmpty(nonce), $"{nameof(nonce)} must not be empty"); Contract.Requires <ArgumentException>(!string.IsNullOrEmpty(workerHash), $"{nameof(workerHash)} must not be empty"); Contract.Requires <ArgumentException>(workerExtraNonce != 0, $"{nameof(workerExtraNonce)} must not be empty"); var context = worker.ContextAs <CryptonoteWorkerContext>(); // validate nonce if (!CryptonoteConstants.RegexValidNonce.IsMatch(nonce)) { throw new StratumException(StratumError.MinusOne, "malformed nonce"); } // clone template Span <byte> blob = stackalloc byte[blobTemplate.Length]; blobTemplate.CopyTo(blob); // inject extranonce var extraNonceBytes = BitConverter.GetBytes(workerExtraNonce.ToBigEndian()); extraNonceBytes.CopyTo(blob.Slice(BlockTemplate.ReservedOffset, extraNonceBytes.Length)); // inject nonce var nonceBytes = nonce.HexToByteArray(); nonceBytes.CopyTo(blob.Slice(CryptonoteConstants.BlobNonceOffset, nonceBytes.Length)); // convert var blobConverted = LibCryptonote.ConvertBlob(blob, blobTemplate.Length); if (blobConverted == null) { throw new StratumException(StratumError.MinusOne, "malformed blob"); } // determine variant CryptonightVariant variant = CryptonightVariant.VARIANT_0; if (coin.HashVariant != 0) { variant = (CryptonightVariant)coin.HashVariant; } else { switch (coin.Hash) { case CryptonightHashType.Normal: variant = (blobConverted[0] >= 10) ? CryptonightVariant.VARIANT_4 : ((blobConverted[0] >= 8) ? CryptonightVariant.VARIANT_2 : ((blobConverted[0] == 7) ? CryptonightVariant.VARIANT_1 : CryptonightVariant.VARIANT_0)); break; case CryptonightHashType.Lite: variant = CryptonightVariant.VARIANT_1; break; case CryptonightHashType.Heavy: variant = CryptonightVariant.VARIANT_0; break; } } // hash it Span <byte> headerHash = stackalloc byte[32]; hashFunc(blobConverted, headerHash, variant, 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(CryptonoteConstants.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 <byte> blockHash = stackalloc byte[32]; ComputeBlockHash(blobConverted, blockHash); // Fill in block-relevant fields result.IsBlockCandidate = true; result.BlockHash = blockHash.ToHexString(); } return(result, blob.ToHexString()); }
public void Init(BlockTemplate blockTemplate, string jobId, PoolConfig poolConfig, BitcoinPoolConfigExtra extraPoolConfig, ClusterConfig clusterConfig, IMasterClock clock, IDestination poolAddressDestination, Network network, bool isPoS, double shareMultiplier, IHashAlgorithm coinbaseHasher, IHashAlgorithm headerHasher, IHashAlgorithm blockHasher) { Contract.RequiresNonNull(blockTemplate, nameof(blockTemplate)); Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); Contract.RequiresNonNull(clusterConfig, nameof(clusterConfig)); Contract.RequiresNonNull(clock, nameof(clock)); Contract.RequiresNonNull(poolAddressDestination, nameof(poolAddressDestination)); Contract.RequiresNonNull(coinbaseHasher, nameof(coinbaseHasher)); Contract.RequiresNonNull(headerHasher, nameof(headerHasher)); Contract.RequiresNonNull(blockHasher, nameof(blockHasher)); Contract.Requires <ArgumentException>(!string.IsNullOrEmpty(jobId), $"{nameof(jobId)} must not be empty"); this.poolConfig = poolConfig; coin = poolConfig.Template.As <BitcoinTemplate>(); networkParams = coin.GetNetwork(network.NetworkType); txVersion = coin.CoinbaseTxVersion; this.network = network; this.clock = clock; this.poolAddressDestination = poolAddressDestination; BlockTemplate = blockTemplate; JobId = jobId; Difficulty = new Target(new NBitcoin.BouncyCastle.Math.BigInteger(BlockTemplate.Target, 16)).Difficulty; extraNoncePlaceHolderLength = BitcoinConstants.ExtranoncePlaceHolderLength; this.isPoS = isPoS; this.shareMultiplier = shareMultiplier; txComment = !string.IsNullOrEmpty(extraPoolConfig?.CoinbaseTxComment) ? extraPoolConfig.CoinbaseTxComment : coin.CoinbaseTxComment; if (coin.HasMasterNodes) { masterNodeParameters = BlockTemplate.Extra.SafeExtensionDataAs <MasterNodeBlockTemplateExtra>(); if (!string.IsNullOrEmpty(masterNodeParameters.CoinbasePayload)) { txVersion = 3; var txType = 5; txVersion = txVersion + ((uint)(txType << 16)); } } if (coin.HasPayee) { payeeParameters = BlockTemplate.Extra.SafeExtensionDataAs <PayeeBlockTemplateExtra>(); } this.coinbaseHasher = coinbaseHasher; this.headerHasher = headerHasher; this.blockHasher = blockHasher; if (!string.IsNullOrEmpty(BlockTemplate.Target)) { blockTargetValue = new uint256(BlockTemplate.Target); } else { var tmp = new Target(BlockTemplate.Bits.HexToByteArray()); blockTargetValue = tmp.ToUInt256(); } previousBlockHashReversedHex = BlockTemplate.PreviousBlockhash .HexToByteArray() .ReverseByteOrder() .ToHexString(); BuildMerkleBranches(); BuildCoinbase(); jobParams = new object[] { JobId, previousBlockHashReversedHex, coinbaseInitialHex, coinbaseFinalHex, merkleBranchesHex, BlockTemplate.Version.ToStringHex8(), BlockTemplate.Bits, BlockTemplate.CurTime.ToStringHex8(), false }; }
public override async Task PayoutAsync(Balance[] balances) { Contract.RequiresNonNull(balances, nameof(balances)); // Shield first if (supportsNativeShielding) { await ShieldCoinbaseAsync(); } else { await ShieldCoinbaseEmulatedAsync(); } var didUnlockWallet = false; // send in batches with no more than 50 recipients to avoid running into tx size limits var pageSize = 50; var pageCount = (int)Math.Ceiling(balances.Length / (double)pageSize); for (var i = 0; i < pageCount; i++) { didUnlockWallet = false; // get a page full of balances var page = balances .Skip(i * pageSize) .Take(pageSize) .ToArray(); // build args var amounts = page .Where(x => x.Amount > 0) .Select(x => new ZSendManyRecipient { Address = x.Address, Amount = Math.Round(x.Amount, 8) }) .ToList(); if (amounts.Count == 0) { return; } var pageAmount = amounts.Sum(x => x.Amount); // check shielded balance var balanceResult = await daemon.ExecuteCmdSingleAsync <object>(logger, EquihashCommands.ZGetBalance, new object[] { poolExtraConfig.ZAddress, // default account ZMinConfirmations, // only spend funds covered by this many confirmations }); if (balanceResult.Error != null || (decimal)(double)balanceResult.Response - TransferFee < pageAmount) { logger.Info(() => $"[{LogCategory}] Insufficient shielded balance for payment of {FormatAmount(pageAmount)}"); return; } logger.Info(() => $"[{LogCategory}] Paying out {FormatAmount(pageAmount)} to {page.Length} addresses"); var args = new object[] { poolExtraConfig.ZAddress, // default account amounts, // addresses and associated amounts ZMinConfirmations, // only spend funds covered by this many confirmations TransferFee }; // send command tryTransfer: var result = await daemon.ExecuteCmdSingleAsync <string>(logger, EquihashCommands.ZSendMany, args); if (result.Error == null) { var operationId = result.Response; // check result if (string.IsNullOrEmpty(operationId)) { logger.Error(() => $"[{LogCategory}] {EquihashCommands.ZSendMany} did not return a operation id!"); } else { logger.Info(() => $"[{LogCategory}] Tracking payout operation id: {operationId}"); var continueWaiting = true; while (continueWaiting) { var operationResultResponse = await daemon.ExecuteCmdSingleAsync <ZCashAsyncOperationStatus[]>(logger, EquihashCommands.ZGetOperationResult, new object[] { new object[] { operationId } }); if (operationResultResponse.Error == null && operationResultResponse.Response?.Any(x => x.OperationId == operationId) == true) { var operationResult = operationResultResponse.Response.First(x => x.OperationId == operationId); if (!Enum.TryParse(operationResult.Status, true, out ZOperationStatus status)) { logger.Error(() => $"Unrecognized operation status: {operationResult.Status}"); break; } switch (status) { case ZOperationStatus.Success: var txId = operationResult.Result?.Value <string>("txid") ?? string.Empty; logger.Info(() => $"[{LogCategory}] {EquihashCommands.ZSendMany} completed with transaction id: {txId}"); await PersistPaymentsAsync(page, txId); NotifyPayoutSuccess(poolConfig.Id, page, new[] { txId }, null); continueWaiting = false; continue; case ZOperationStatus.Cancelled: case ZOperationStatus.Failed: logger.Error(() => $"{EquihashCommands.ZSendMany} failed: {operationResult.Error.Message} code {operationResult.Error.Code}"); NotifyPayoutFailure(poolConfig.Id, page, $"{EquihashCommands.ZSendMany} failed: {operationResult.Error.Message} code {operationResult.Error.Code}", null); continueWaiting = false; continue; } } logger.Info(() => $"[{LogCategory}] Waiting for completion: {operationId}"); await Task.Delay(TimeSpan.FromSeconds(10)); } } } else { if (result.Error.Code == (int)BitcoinRPCErrorCode.RPC_WALLET_UNLOCK_NEEDED && !didUnlockWallet) { if (!string.IsNullOrEmpty(extraPoolPaymentProcessingConfig?.WalletPassword)) { logger.Info(() => $"[{LogCategory}] Unlocking wallet"); var unlockResult = await daemon.ExecuteCmdSingleAsync <JToken>(logger, BitcoinCommands.WalletPassphrase, new[] { (object)extraPoolPaymentProcessingConfig.WalletPassword, (object)5 // unlock for N seconds }); if (unlockResult.Error == null) { didUnlockWallet = true; goto tryTransfer; } else { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.WalletPassphrase} returned error: {result.Error.Message} code {result.Error.Code}"); NotifyPayoutFailure(poolConfig.Id, page, $"{BitcoinCommands.WalletPassphrase} returned error: {result.Error.Message} code {result.Error.Code}", null); break; } } else { logger.Error(() => $"[{LogCategory}] Wallet is locked but walletPassword was not configured. Unable to send funds."); NotifyPayoutFailure(poolConfig.Id, page, $"Wallet is locked but walletPassword was not configured. Unable to send funds.", null); break; } } else { logger.Error(() => $"[{LogCategory}] {EquihashCommands.ZSendMany} returned error: {result.Error.Message} code {result.Error.Code}"); NotifyPayoutFailure(poolConfig.Id, page, $"{EquihashCommands.ZSendMany} returned error: {result.Error.Message} code {result.Error.Code}", null); } } } // lock wallet logger.Info(() => $"[{LogCategory}] Locking wallet"); await daemon.ExecuteCmdSingleAsync <JToken>(logger, BitcoinCommands.WalletLock); }
public virtual async Task <Block[]> ClassifyBlocksAsync(Block[] blocks) { Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); Contract.RequiresNonNull(blocks, nameof(blocks)); var coin = poolConfig.Template.As <CoinTemplate>(); var pageSize = 100; var pageCount = (int)Math.Ceiling(blocks.Length / (double)pageSize); var result = new List <Block>(); for (var i = 0; i < pageCount; i++) { // get a page full of blocks var page = blocks .Skip(i * pageSize) .Take(pageSize) .ToArray(); // build command batch (block.TransactionConfirmationData is the hash of the blocks coinbase transaction) var batch = page.Select(block => new DaemonCmd(BitcoinCommands.GetTransaction, new[] { block.TransactionConfirmationData })).ToArray(); // execute batch var results = await daemon.ExecuteBatchAnyAsync(logger, batch); for (var j = 0; j < results.Length; j++) { var cmdResult = results[j]; var transactionInfo = cmdResult.Response?.ToObject <Transaction>(); var block = page[j]; // check error if (cmdResult.Error != null) { // Code -5 interpreted as "orphaned" if (cmdResult.Error.Code == -5) { block.Status = BlockStatus.Orphaned; block.Reward = 0; result.Add(block); } else { logger.Warn(() => $"[{LogCategory}] Daemon reports error '{cmdResult.Error.Message}' (Code {cmdResult.Error.Code}) for transaction {page[j].TransactionConfirmationData}"); } } // missing transaction details are interpreted as "orphaned" else if (transactionInfo?.Details == null || transactionInfo.Details.Length == 0) { block.Status = BlockStatus.Orphaned; block.Reward = 0; result.Add(block); } else { switch (transactionInfo.Details[0].Category) { case "immature": // update progress var minConfirmations = extraPoolConfig?.MinimumConfirmations ?? BitcoinConstants.CoinbaseMinConfimations; block.ConfirmationProgress = Math.Min(1.0d, (double)transactionInfo.Confirmations / minConfirmations); block.Reward = transactionInfo.Amount; // update actual block-reward from coinbase-tx result.Add(block); messageBus.NotifyBlockConfirmationProgress(poolConfig.Id, block, coin); break; case "generate": // matured and spendable coinbase transaction block.Status = BlockStatus.Confirmed; block.ConfirmationProgress = 1; block.Reward = transactionInfo.Amount; // update actual block-reward from coinbase-tx result.Add(block); logger.Info(() => $"[{LogCategory}] Unlocked block {block.BlockHeight} worth {FormatAmount(block.Reward)}"); messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); break; default: logger.Info(() => $"[{LogCategory}] Block {block.BlockHeight} classified as orphaned. Category: {transactionInfo.Details[0].Category}"); block.Status = BlockStatus.Orphaned; block.Reward = 0; result.Add(block); messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); break; } } } } return(result.ToArray()); }
public virtual async Task PayoutAsync(Balance[] balances) { Contract.RequiresNonNull(balances, nameof(balances)); // build args var amounts = balances .Where(x => x.Amount > 0) .ToDictionary(x => x.Address, x => Math.Round(x.Amount, 4)); if (amounts.Count == 0) { return; } logger.Info(() => $"[{LogCategory}] Paying out {FormatAmount(balances.Sum(x => x.Amount))} to {balances.Length} addresses"); object[] args; if (extraPoolPaymentProcessingConfig?.MinersPayTxFees == true) { var comment = (poolConfig.PoolName ?? clusterConfig.ClusterName ?? "PoolCore").Trim() + " Payment"; var subtractFeesFrom = amounts.Keys.ToArray(); if (!poolConfig.Template.As <BitcoinTemplate>().HasMasterNodes) { args = new object[] { string.Empty, // default account amounts, // addresses and associated amounts 1, // only spend funds covered by this many confirmations comment, // tx comment subtractFeesFrom // distribute transaction fee equally over all recipients }; } else { args = new object[] { string.Empty, // default account amounts, // addresses and associated amounts 1, // only spend funds covered by this many confirmations false, // Whether to add confirmations to transactions locked via InstantSend comment, // tx comment subtractFeesFrom, // distribute transaction fee equally over all recipients false, // use_is: Send this transaction as InstantSend false, // Use anonymized funds only }; } } else { args = new object[] { string.Empty, // default account amounts, // addresses and associated amounts }; } var didUnlockWallet = false; // send command tryTransfer: var result = await daemon.ExecuteCmdSingleAsync <string>(logger, BitcoinCommands.SendMany, args, new JsonSerializerSettings()); if (result.Error == null) { if (didUnlockWallet) { // lock wallet logger.Info(() => $"[{LogCategory}] Locking wallet"); await daemon.ExecuteCmdSingleAsync <JToken>(logger, BitcoinCommands.WalletLock); } // check result var txId = result.Response; if (string.IsNullOrEmpty(txId)) { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.SendMany} did not return a transaction id!"); } else { logger.Info(() => $"[{LogCategory}] Payout transaction id: {txId}"); } await PersistPaymentsAsync(balances, txId); NotifyPayoutSuccess(poolConfig.Id, balances, new[] { txId }, null); } else { if (result.Error.Code == (int)BitcoinRPCErrorCode.RPC_WALLET_UNLOCK_NEEDED && !didUnlockWallet) { if (!string.IsNullOrEmpty(extraPoolPaymentProcessingConfig?.WalletPassword)) { logger.Info(() => $"[{LogCategory}] Unlocking wallet"); var unlockResult = await daemon.ExecuteCmdSingleAsync <JToken>(logger, BitcoinCommands.WalletPassphrase, new[] { (object)extraPoolPaymentProcessingConfig.WalletPassword, (object)5 // unlock for N seconds }); if (unlockResult.Error == null) { didUnlockWallet = true; goto tryTransfer; } else { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.WalletPassphrase} returned error: {result.Error.Message} code {result.Error.Code}"); } } else { logger.Error(() => $"[{LogCategory}] Wallet is locked but walletPassword was not configured. Unable to send funds."); } } else { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.SendMany} returned error: {result.Error.Message} code {result.Error.Code}"); NotifyPayoutFailure(poolConfig.Id, balances, $"{BitcoinCommands.SendMany} returned error: {result.Error.Message} code {result.Error.Code}", null); } } }