protected PayoutHandlerBase( IConnectionFactory cf, IMapper mapper, IShareRepository shareRepo, IBlockRepository blockRepo, IBalanceRepository balanceRepo, IPaymentRepository paymentRepo, IMasterClock clock, IMessageBus messageBus) { Contract.RequiresNonNull(cf); Contract.RequiresNonNull(mapper); Contract.RequiresNonNull(shareRepo); Contract.RequiresNonNull(blockRepo); Contract.RequiresNonNull(balanceRepo); Contract.RequiresNonNull(paymentRepo); Contract.RequiresNonNull(clock); Contract.RequiresNonNull(messageBus); this.cf = cf; this.mapper = mapper; this.clock = clock; this.shareRepo = shareRepo; this.blockRepo = blockRepo; this.balanceRepo = balanceRepo; this.paymentRepo = paymentRepo; this.messageBus = messageBus; BuildFaultHandlingPolicy(); }
public ShareRecorder(IConnectionFactory cf, IMapper mapper, JsonSerializerSettings jsonSerializerSettings, IShareRepository shareRepo, IBlockRepository blockRepo, ClusterConfig clusterConfig, IMessageBus messageBus) { Contract.RequiresNonNull(cf, nameof(cf)); Contract.RequiresNonNull(mapper, nameof(mapper)); Contract.RequiresNonNull(shareRepo, nameof(shareRepo)); Contract.RequiresNonNull(blockRepo, nameof(blockRepo)); Contract.RequiresNonNull(jsonSerializerSettings, nameof(jsonSerializerSettings)); Contract.RequiresNonNull(messageBus, nameof(messageBus)); this.cf = cf; this.mapper = mapper; this.jsonSerializerSettings = jsonSerializerSettings; this.messageBus = messageBus; this.clusterConfig = clusterConfig; this.shareRepo = shareRepo; this.blockRepo = blockRepo; pools = clusterConfig.Pools.ToDictionary(x => x.Id, x => x); BuildFaultHandlingPolicy(); ConfigureRecovery(); }
public PayoutManager(IComponentContext ctx, IConnectionFactory cf, IBlockRepository blockRepo, IShareRepository shareRepo, IBalanceRepository balanceRepo, ClusterConfig clusterConfig, 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; this.clusterConfig = clusterConfig; interval = TimeSpan.FromSeconds(clusterConfig.PaymentProcessing.Interval > 0 ? clusterConfig.PaymentProcessing.Interval : 600); }
/// <summary> /// </summary> /// <example> /// python: http://runnable.com/U3jqtyYUmAUxtsSS/bitcoin-block-merkle-root-python /// nodejs: https://github.com/zone117x/node-stratum-pool/blob/master/lib/merkleTree.js#L9 /// </example> /// <param name="hashList"></param> /// <returns></returns> private IList <byte[]> CalculateSteps(IEnumerable <byte[]> hashList) { Contract.RequiresNonNull(hashList); var steps = new List <byte[]>(); var L = new List <byte[]> { null }; L.AddRange(hashList); var startL = 2; var Ll = L.Count; if (Ll > 1) { while (true) { if (Ll == 1) { break; } steps.Add(L[1]); if (Ll % 2 == 1) { L.Add(L[^ 1]);
protected PoolBase(IComponentContext ctx, JsonSerializerSettings serializerSettings, IConnectionFactory cf, IStatsRepository statsRepo, IMapper mapper, IMasterClock clock, IMessageBus messageBus, RecyclableMemoryStreamManager rmsm, NicehashService nicehashService) : base(ctx, messageBus, rmsm, 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)); Contract.RequiresNonNull(nicehashService, nameof(nicehashService)); this.serializerSettings = serializerSettings; this.cf = cf; this.statsRepo = statsRepo; this.mapper = mapper; this.nicehashService = nicehashService; }
protected JobManagerBase(IComponentContext ctx, IMessageBus messageBus) { Contract.RequiresNonNull(ctx); Contract.RequiresNonNull(messageBus); this.ctx = ctx; this.messageBus = messageBus; }
public SOLOPaymentScheme( IShareRepository shareRepo, IBalanceRepository balanceRepo) { Contract.RequiresNonNull(shareRepo); Contract.RequiresNonNull(balanceRepo); this.shareRepo = shareRepo; this.balanceRepo = balanceRepo; }
public CryptonoteJobManager( IComponentContext ctx, IMasterClock clock, IMessageBus messageBus) : base(ctx, messageBus) { Contract.RequiresNonNull(ctx); Contract.RequiresNonNull(clock); Contract.RequiresNonNull(messageBus); this.clock = clock; }
public void Ban(IPAddress address, TimeSpan duration) { Contract.RequiresNonNull(address, nameof(address)); Contract.Requires <ArgumentException>(duration.TotalMilliseconds > 0, $"{nameof(duration)} must not be empty"); // don't ban loopback if (address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback)) { return; } cache.Set(address.ToString(), string.Empty, duration); }
protected async Task PersistPaymentsAsync(Balance[] balances, string[] transactionConfirmations) { Contract.RequiresNonNull(balances); Contract.RequiresNonNull(transactionConfirmations); Contract.Requires <ArgumentException>(balances.Length > 0); Contract.Requires <ArgumentException>(balances.Length == transactionConfirmations.Length); var coin = poolConfig.Template.As <CoinTemplate>(); try { await faultPolicy.ExecuteAsync(async() => { await cf.RunTx(async(con, tx) => { for (var i = 0; i < balances.Length; i++) { var balance = balances[i]; var transactionConfirmation = transactionConfirmations[i]; if (!string.IsNullOrEmpty(transactionConfirmation) && poolConfig.RewardRecipients.All(x => x.Address != balance.Address)) { // record payment var payment = new Payment { PoolId = poolConfig.Id, Coin = coin.Symbol, Address = balance.Address, Amount = balance.Amount, Created = clock.Now, TransactionConfirmationData = transactionConfirmation }; await paymentRepo.InsertAsync(con, tx, payment); } // reset balance logger.Info(() => $"[{LogCategory}] Resetting balance of {balance.Address}"); await balanceRepo.AddAmountAsync(con, tx, poolConfig.Id, balance.Address, -balance.Amount, "Balance reset after payment"); } }); }); } catch (Exception ex) { logger.Error(ex, () => $"[{LogCategory}] Failed to persist the following payments: " + $"{JsonConvert.SerializeObject(balances.Where(x => x.Amount > 0).ToDictionary(x => x.Address, x => x.Amount))}"); throw; } }
public ErgoJobManager( IComponentContext ctx, IMessageBus messageBus, IMasterClock clock, IExtraNonceProvider extraNonceProvider) : base(ctx, messageBus) { Contract.RequiresNonNull(clock, nameof(clock)); Contract.RequiresNonNull(extraNonceProvider, nameof(extraNonceProvider)); this.clock = clock; this.extraNonceProvider = extraNonceProvider; extraNonceSize = 8 - extraNonceProvider.ByteSize; }
public EthereumJobManager( IComponentContext ctx, IMasterClock clock, IMessageBus messageBus, IExtraNonceProvider extraNonceProvider) : base(ctx, messageBus) { Contract.RequiresNonNull(ctx, nameof(ctx)); Contract.RequiresNonNull(clock, nameof(clock)); Contract.RequiresNonNull(messageBus, nameof(messageBus)); Contract.RequiresNonNull(extraNonceProvider, nameof(extraNonceProvider)); this.clock = clock; this.extraNonceProvider = extraNonceProvider; }
public CryptonoteJob(GetBlockTemplateResponse blockTemplate, byte[] instanceId, string jobId, CryptonoteCoinTemplate coin, PoolConfig poolConfig, ClusterConfig clusterConfig, string prevHash, string randomXRealm) { Contract.RequiresNonNull(blockTemplate); Contract.RequiresNonNull(poolConfig); Contract.RequiresNonNull(clusterConfig); Contract.RequiresNonNull(instanceId); Contract.Requires <ArgumentException>(!string.IsNullOrEmpty(jobId)); BlockTemplate = blockTemplate; PrepareBlobTemplate(instanceId); PrevHash = prevHash; RandomXRealm = randomXRealm; hashFunc = hashFuncs[coin.Hash]; }
protected StratumServer( IComponentContext ctx, IMessageBus messageBus, RecyclableMemoryStreamManager rmsm, IMasterClock clock) { Contract.RequiresNonNull(ctx); Contract.RequiresNonNull(messageBus); Contract.RequiresNonNull(rmsm); Contract.RequiresNonNull(clock); this.ctx = ctx; this.messageBus = messageBus; this.rmsm = rmsm; this.clock = clock; }
public RpcClient(DaemonEndpointConfig endPoint, JsonSerializerSettings serializerSettings, IMessageBus messageBus, string poolId) { Contract.RequiresNonNull(serializerSettings, nameof(serializerSettings)); Contract.RequiresNonNull(messageBus, nameof(messageBus)); Contract.Requires <ArgumentException>(!string.IsNullOrEmpty(poolId), $"{nameof(poolId)} must not be empty"); config = endPoint; this.serializerSettings = serializerSettings; this.messageBus = messageBus; this.poolId = poolId; serializer = new JsonSerializer { ContractResolver = serializerSettings.ContractResolver ! }; }
public virtual async Task ConfigureAsync(ClusterConfig cc, PoolConfig pc, CancellationToken ct) { Contract.RequiresNonNull(pc); logger = LogUtil.GetPoolScopedLogger(typeof(ErgoPayoutHandler), pc); poolConfig = pc; clusterConfig = cc; extraPoolPaymentProcessingConfig = pc.PaymentProcessing.Extra.SafeExtensionDataAs <ErgoPaymentProcessingConfigExtra>(); ergoClient = ErgoClientFactory.CreateClient(pc, cc, null); // detect chain var info = await ergoClient.GetNodeInfoAsync(ct); network = ErgoConstants.RegexChain.Match(info.Name).Groups[1].Value.ToLower(); }
public virtual Task ConfigureAsync(ClusterConfig cc, PoolConfig pc, CancellationToken ct) { Contract.RequiresNonNull(pc); poolConfig = pc; clusterConfig = cc; extraPoolConfig = pc.Extra.SafeExtensionDataAs <BitcoinDaemonEndpointConfigExtra>(); extraPoolPaymentProcessingConfig = pc.PaymentProcessing.Extra.SafeExtensionDataAs <BitcoinPoolPaymentProcessingConfigExtra>(); logger = LogUtil.GetPoolScopedLogger(typeof(BitcoinPayoutHandler), pc); var jsonSerializerSettings = ctx.Resolve <JsonSerializerSettings>(); rpcClient = new RpcClient(pc.Daemons.First(), jsonSerializerSettings, messageBus, pc.Id); return(Task.FromResult(true)); }
public ErgoPayoutHandler( 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); Contract.RequiresNonNull(balanceRepo); Contract.RequiresNonNull(paymentRepo); this.ctx = ctx; }
public PROPPaymentScheme( IConnectionFactory cf, IShareRepository shareRepo, IBlockRepository blockRepo, IBalanceRepository balanceRepo) { Contract.RequiresNonNull(cf, nameof(cf)); Contract.RequiresNonNull(shareRepo, nameof(shareRepo)); Contract.RequiresNonNull(blockRepo, nameof(blockRepo)); Contract.RequiresNonNull(balanceRepo, nameof(balanceRepo)); this.cf = cf; this.shareRepo = shareRepo; this.blockRepo = blockRepo; this.balanceRepo = balanceRepo; BuildFaultHandlingPolicy(); }
public override async Task PayoutAsync(IMiningPool pool, Balance[] balances, CancellationToken ct) { Contract.RequiresNonNull(balances); // Shield first if (supportsNativeShielding) { await ShieldCoinbaseAsync(ct); } else { await ShieldCoinbaseEmulatedAsync(ct); } // 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++) { var 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 balanceResponse = await rpcClient.ExecuteAsync <object>(logger, EquihashCommands.ZGetBalance, ct, new object[] { poolExtraConfig.ZAddress, // default account ZMinConfirmations, // only spend funds covered by this many confirmations }); if (balanceResponse.Error != null || (decimal)(double)balanceResponse.Response - TransferFee < pageAmount) { logger.Info(() => $"[{LogCategory}] Insufficient shielded balance for payment of {FormatAmount(pageAmount)}"); return; } logger.Info(() => $"[{LogCategory}] Paying {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 response = await rpcClient.ExecuteAsync <string>(logger, EquihashCommands.ZSendMany, ct, args); if (response.Error == null) { var operationId = response.Response; // check result if (string.IsNullOrEmpty(operationId)) { logger.Error(() => $"[{LogCategory}] {EquihashCommands.ZSendMany} did not return a operation id!"); } else { logger.Info(() => $"[{LogCategory}] Tracking payment operation id: {operationId}"); var continueWaiting = true; while (continueWaiting) { var operationResultResponse = await rpcClient.ExecuteAsync <ZCashAsyncOperationStatus[]>(logger, EquihashCommands.ZGetOperationResult, ct, 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), ct); } } } else { if (response.Error.Code == (int)BitcoinRPCErrorCode.RPC_WALLET_UNLOCK_NEEDED && !didUnlockWallet) { if (!string.IsNullOrEmpty(extraPoolPaymentProcessingConfig?.WalletPassword)) { logger.Info(() => $"[{LogCategory}] Unlocking wallet"); var unlockResponse = await rpcClient.ExecuteAsync <JToken>(logger, BitcoinCommands.WalletPassphrase, ct, new[] { extraPoolPaymentProcessingConfig.WalletPassword, (object)5 // unlock for N seconds }); if (unlockResponse.Error == null) { didUnlockWallet = true; goto tryTransfer; } else { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.WalletPassphrase} returned error: {response.Error.Message} code {response.Error.Code}"); NotifyPayoutFailure(poolConfig.Id, page, $"{BitcoinCommands.WalletPassphrase} returned error: {response.Error.Message} code {response.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: {response.Error.Message} code {response.Error.Code}"); NotifyPayoutFailure(poolConfig.Id, page, $"{EquihashCommands.ZSendMany} returned error: {response.Error.Message} code {response.Error.Code}", null); } } } // lock wallet logger.Info(() => $"[{LogCategory}] Locking wallet"); await rpcClient.ExecuteAsync <JToken>(logger, BitcoinCommands.WalletLock, ct); }
public virtual async Task PayoutAsync(IMiningPool pool, Balance[] balances, CancellationToken ct) { Contract.RequiresNonNull(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; } var balancesTotal = amounts.Sum(x => x.Value); try { logger.Info(() => $"[{LogCategory}] Paying {FormatAmount(balances.Sum(x => x.Amount))} to {balances.Length} addresses"); // get wallet status var status = await ergoClient.GetWalletStatusAsync(ct); if (!status.IsInitialized) { throw new PaymentException($"Wallet is not initialized"); } if (!status.IsUnlocked) { await UnlockWallet(ct); } // get balance var walletBalances = await ergoClient.WalletBalancesAsync(ct); var walletTotal = walletBalances.Balance / ErgoConstants.SmallestUnit; logger.Info(() => $"[{LogCategory}] Current wallet balance is {FormatAmount(walletTotal)}"); // bail if balance does not satisfy payments if (walletTotal < balancesTotal) { logger.Warn(() => $"[{LogCategory}] Wallet balance currently short of {FormatAmount(balancesTotal - walletTotal)}. Will try again."); return; } // validate addresses logger.Info("Validating addresses ..."); foreach (var pair in amounts) { var validity = await Guard(() => ergoClient.CheckAddressValidityAsync(pair.Key, ct)); if (validity == null || !validity.IsValid) { logger.Warn(() => $"Address {pair.Key} is not valid!"); } } // Create request batch var requests = amounts.Select(x => new PaymentRequest { Address = x.Key, Value = (long)(x.Value * ErgoConstants.SmallestUnit), }).ToArray(); var txId = await Guard(() => ergoClient.WalletPaymentTransactionGenerateAndSendAsync(requests, ct), ex => { if (ex is ApiException <ApiError> apiException) { var error = apiException.Result.Detail ?? apiException.Result.Reason; if (error.Contains("reason:")) { error = error.Substring(error.IndexOf("reason:")); } throw new PaymentException($"Payment transaction failed: {error}"); } else { throw ex; } }); if (string.IsNullOrEmpty(txId)) { throw new PaymentException("Payment transaction failed to return a transaction id"); } // payment successful logger.Info(() => $"[{LogCategory}] Payment transaction id: {txId}"); await PersistPaymentsAsync(balances, txId); NotifyPayoutSuccess(poolConfig.Id, balances, new[] { txId }, null); } catch (PaymentException ex) { logger.Error(() => $"[{LogCategory}] {ex.Message}"); NotifyPayoutFailure(poolConfig.Id, balances, ex.Message, null); } finally { await LockWallet(ct); } }
public virtual async Task <Block[]> ClassifyBlocksAsync(IMiningPool pool, Block[] blocks, CancellationToken ct) { Contract.RequiresNonNull(poolConfig); Contract.RequiresNonNull(blocks); if (blocks.Length == 0) { return(blocks); } var coin = poolConfig.Template.As <ErgoCoinTemplate>(); var pageSize = 100; var pageCount = (int)Math.Ceiling(blocks.Length / (double)pageSize); var result = new List <Block>(); var minConfirmations = extraPoolPaymentProcessingConfig?.MinimumConfirmations ?? (network == "mainnet" ? 720 : 72); var minerRewardsPubKey = await ergoClient.MiningReadMinerRewardPubkeyAsync(ct); var minerRewardsAddress = await ergoClient.MiningReadMinerRewardAddressAsync(ct); for (var i = 0; i < pageCount; i++) { // get a page full of blocks var page = blocks .Skip(i * pageSize) .Take(pageSize) .ToArray(); // fetch header ids for blocks in page var headerBatch = page.Select(block => ergoClient.GetFullBlockAtAsync((int)block.BlockHeight, ct)).ToArray(); await Guard(() => Task.WhenAll(headerBatch), ex => logger.Debug(ex)); for (var j = 0; j < page.Length; j++) { var block = page[j]; var headerTask = headerBatch[j]; if (!headerTask.IsCompletedSuccessfully) { if (headerTask.IsFaulted) { logger.Warn(() => $"Failed to fetch block {block.BlockHeight}: {headerTask.Exception?.InnerException?.Message ?? headerTask.Exception?.Message}"); } else { logger.Warn(() => $"Failed to fetch block {block.BlockHeight}: {headerTask.Status.ToString().ToLower()}"); } continue; } var headerIds = headerTask.Result; // fetch blocks var blockBatch = headerIds.Select(x => ergoClient.GetFullBlockByIdAsync(x, ct)).ToArray(); await Guard(() => Task.WhenAll(blockBatch), ex => logger.Debug(ex)); var blockHandled = false; var pkMismatchCount = 0; var nonceMismatchCount = 0; var coinbaseNonWalletTxCount = 0; foreach (var blockTask in blockBatch) { if (blockHandled) { break; } if (!blockTask.IsCompletedSuccessfully) { continue; } var fullBlock = blockTask.Result; // only consider blocks with pow-solution pk matching ours if (fullBlock.Header.PowSolutions.Pk != minerRewardsPubKey.RewardPubKey) { pkMismatchCount++; continue; } // only consider blocks with pow-solution nonce matching what we have on file if (fullBlock.Header.PowSolutions.N != block.TransactionConfirmationData) { nonceMismatchCount++; continue; } var coinbaseWalletTxFound = false; // reset block reward block.Reward = 0; foreach (var blockTx in fullBlock.BlockTransactions.Transactions) { var walletTx = await Guard(() => ergoClient.WalletGetTransactionAsync(blockTx.Id, ct)); var coinbaseOutput = walletTx?.Outputs?.FirstOrDefault(x => x.Address == minerRewardsAddress.RewardAddress); if (coinbaseOutput != null) { coinbaseWalletTxFound = true; block.ConfirmationProgress = Math.Min(1.0d, (double)walletTx.NumConfirmations / minConfirmations); block.Reward += coinbaseOutput.Value / ErgoConstants.SmallestUnit; block.Hash = fullBlock.Header.Id; if (walletTx.NumConfirmations >= minConfirmations) { // matured and spendable coinbase transaction block.Status = BlockStatus.Confirmed; block.ConfirmationProgress = 1; } } } blockHandled = coinbaseWalletTxFound; if (blockHandled) { result.Add(block); if (block.Status == BlockStatus.Confirmed) { logger.Info(() => $"[{LogCategory}] Unlocked block {block.BlockHeight} worth {FormatAmount(block.Reward)}"); messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); } else { messageBus.NotifyBlockConfirmationProgress(poolConfig.Id, block, coin); } } else { coinbaseNonWalletTxCount++; } } if (!blockHandled) { string orphanReason = null; if (pkMismatchCount == blockBatch.Length) { orphanReason = "pk mismatch"; } else if (nonceMismatchCount == blockBatch.Length) { orphanReason = "nonce mismatch"; } else if (coinbaseNonWalletTxCount == blockBatch.Length) { orphanReason = "no related coinbase tx found in wallet"; } if (!string.IsNullOrEmpty(orphanReason)) { block.Status = BlockStatus.Orphaned; block.Reward = 0; result.Add(block); logger.Info(() => $"[{LogCategory}] Block {block.BlockHeight} classified as orphaned due to {orphanReason}"); messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); } } } } return(result.ToArray()); }
public async Task <Block[]> ClassifyBlocksAsync(IMiningPool pool, Block[] blocks, CancellationToken ct) { Contract.RequiresNonNull(poolConfig); Contract.RequiresNonNull(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 latestBlockResponse = await rpcClient.ExecuteAsync <DaemonResponses.Block>(logger, EC.GetBlockByNumber, ct, new[] { (object)"latest", true }); var latestBlockHeight = latestBlockResponse.Response.Height.Value; // execute batch var blockInfos = await FetchBlocks(blockCache, ct, page.Select(block => (long)block.BlockHeight).ToArray()); for (var j = 0; j < blockInfos.Length; j++) { var blockInfo = blockInfos[j]; var block = page[j]; // 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)) { // mature? if (latestBlockHeight - block.BlockHeight >= EthereumConstants.MinConfimations) { var blockHashResponse = await rpcClient.ExecuteAsync <DaemonResponses.Block>(logger, EC.GetBlockByNumber, ct, new[] { (object)block.BlockHeight.ToStringHexWithPrefix(), true }); var blockHash = blockHashResponse.Response.Hash; var baseGas = blockHashResponse.Response.BaseFeePerGas; var gasUsed = blockHashResponse.Response.GasUsed; var burnedFee = (decimal)0; if (extraPoolConfig?.ChainTypeOverride == "Ethereum") { burnedFee = (baseGas * gasUsed / EthereumConstants.Wei); } block.Hash = blockHash; 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, ct) - burnedFee; } 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, ct, 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 RpcRequest(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 rpcClient.ExecuteBatchAsync(logger, ct, 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) { var blockHashUncleResponse = await rpcClient.ExecuteAsync <DaemonResponses.Block>(logger, EC.GetBlockByNumber, ct, new[] { (object)uncle.Height.Value.ToStringHexWithPrefix(), true }); var blockHashUncle = blockHashUncleResponse.Response.Hash; block.Hash = blockHashUncle; 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 > 0.75) { // we've lost this one block.Hash = "0x0"; block.Status = BlockStatus.Orphaned; block.Reward = 0; messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); } } } return(result.ToArray()); }
public virtual async Task PayoutAsync(IMiningPool pool, Balance[] balances, CancellationToken ct) { Contract.RequiresNonNull(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 {FormatAmount(balances.Sum(x => x.Amount))} to {balances.Length} addresses"); object[] args; if (extraPoolPaymentProcessingConfig?.MinersPayTxFees == true) { var identifier = !string.IsNullOrEmpty(clusterConfig.PaymentProcessing?.CoinbaseString) ? clusterConfig.PaymentProcessing.CoinbaseString.Trim() : "Miningcore"; var comment = $"{identifier} 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 rpcClient.ExecuteAsync <string>(logger, BitcoinCommands.SendMany, ct, args); if (result.Error == null) { if (didUnlockWallet) { // lock wallet logger.Info(() => $"[{LogCategory}] Locking wallet"); await rpcClient.ExecuteAsync <JToken>(logger, BitcoinCommands.WalletLock, ct); } // 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}] Payment 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 rpcClient.ExecuteAsync <JToken>(logger, BitcoinCommands.WalletPassphrase, ct, new[] { 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); } } }
public virtual async Task <Block[]> ClassifyBlocksAsync(IMiningPool pool, Block[] blocks, CancellationToken ct) { Contract.RequiresNonNull(poolConfig); Contract.RequiresNonNull(blocks); var coin = poolConfig.Template.As <CoinTemplate>(); var pageSize = 100; var pageCount = (int)Math.Ceiling(blocks.Length / (double)pageSize); var result = new List <Block>(); int minConfirmations; if (coin is BitcoinTemplate bitcoinTemplate) { minConfirmations = extraPoolConfig?.MinimumConfirmations ?? bitcoinTemplate.CoinbaseMinConfimations ?? BitcoinConstants.CoinbaseMinConfimations; } else { minConfirmations = extraPoolConfig?.MinimumConfirmations ?? BitcoinConstants.CoinbaseMinConfimations; } 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 RpcRequest(BitcoinCommands.GetTransaction, new[] { block.TransactionConfirmationData })).ToArray(); // execute batch var results = await rpcClient.ExecuteBatchAsync(logger, ct, 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); logger.Info(() => $"[{LogCategory}] Block {block.BlockHeight} classified as orphaned due to daemon error {cmdResult.Error.Code}"); messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); } 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); logger.Info(() => $"[{LogCategory}] Block {block.BlockHeight} classified as orphaned due to missing tx details"); } else { switch (transactionInfo.Details[0].Category) { case "immature": // update progress 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 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); }