예제 #1
0
 public void AttachPool(IMiningPool pool)
 {
     lock (pools)
     {
         pools.Add(pool);
     }
 }
예제 #2
0
    public async Task UpdateBalancesAsync(IDbConnection con, IDbTransaction tx, IMiningPool pool, IPayoutHandler payoutHandler,
                                          Block block, decimal blockReward, CancellationToken ct)
    {
        var poolConfig = pool.Config;

        // calculate rewards
        var rewards         = new Dictionary <string, decimal>();
        var shareCutOffDate = CalculateRewards(block, blockReward, rewards, ct);

        // update balances
        foreach (var address in rewards.Keys)
        {
            var amount = rewards[address];

            if (amount > 0)
            {
                logger.Info(() => $"Adding {payoutHandler.FormatAmount(amount)} to balance of {address} for block {block.BlockHeight}");

                await balanceRepo.AddAmountAsync(con, tx, poolConfig.Id, address, amount, $"Reward for block {block.BlockHeight}");
            }
        }

        // delete discarded shares
        if (shareCutOffDate.HasValue)
        {
            var cutOffCount = await shareRepo.CountSharesByMinerAsync(con, tx, poolConfig.Id, block.Miner, ct);

            if (cutOffCount > 0)
            {
                logger.Info(() => $"Deleting {cutOffCount} discarded shares for {block.Miner}");

                await shareRepo.DeleteSharesByMinerAsync(con, tx, poolConfig.Id, block.Miner, ct);
            }
        }
    }
예제 #3
0
 public static void NotifyPoolStatus(this IMessageBus messageBus, IMiningPool pool, PoolStatus status)
 {
     messageBus.SendMessage(new PoolStatusNotification
     {
         Pool   = pool,
         Status = status
     });
 }
예제 #4
0
        public async Task UpdateBalancesAsync(IDbConnection con, IDbTransaction tx, IMiningPool pool, IPayoutHandler payoutHandler, Block block, decimal blockReward, CancellationToken ct)
        {
            var poolConfig   = pool.Config;
            var payoutConfig = poolConfig.PaymentProcessing.PayoutSchemeConfig;

            // PPLNS window (see https://bitcointalk.org/index.php?topic=39832)
            var window = payoutConfig?.ToObject <Config>()?.Factor ?? 2.0m;

            // calculate rewards
            var shares          = new Dictionary <string, double>();
            var rewards         = new Dictionary <string, decimal>();
            var shareCutOffDate = await CalculateRewardsAsync(pool, payoutHandler, window, block, blockReward, shares, rewards, ct);

            // update balances
            foreach (var address in rewards.Keys)
            {
                var amount = rewards[address];

                if (amount > 0)
                {
                    logger.Info(() => $"Adding {payoutHandler.FormatAmount(amount)} to balance of {address} for {FormatUtil.FormatQuantity(shares[address])} ({shares[address]}) shares for block {block.BlockHeight}");
                    await balanceRepo.AddAmountAsync(con, tx, poolConfig.Id, address, amount, $"Reward for {FormatUtil.FormatQuantity(shares[address])} shares for block {block.BlockHeight}");
                }
            }

            // delete discarded shares
            if (shareCutOffDate.HasValue)
            {
                var cutOffCount = await shareRepo.CountSharesBeforeCreatedAsync(con, tx, poolConfig.Id, shareCutOffDate.Value);

                if (cutOffCount > 0)
                {
                    await LogDiscardedSharesAsync(poolConfig, block, shareCutOffDate.Value);

#if !DEBUG
                    logger.Info(() => $"Deleting {cutOffCount} discarded shares before {shareCutOffDate.Value:O}");
                    await shareRepo.DeleteSharesBeforeCreatedAsync(con, tx, poolConfig.Id, shareCutOffDate.Value);
#endif
                }
            }

            // diagnostics
            var totalShareCount = shares.Values.ToList().Sum(x => new decimal(x));
            var totalRewards    = rewards.Values.ToList().Sum(x => x);

            if (totalRewards > 0)
            {
                logger.Info(() => $"{FormatUtil.FormatQuantity((double) totalShareCount)} ({Math.Round(totalShareCount, 2)}) shares contributed to a total payout of {payoutHandler.FormatAmount(totalRewards)} ({totalRewards / blockReward * 100:0.00}% of block reward) to {rewards.Keys.Count} addresses");
            }
        }
예제 #5
0
        public static PoolInfo ToPoolInfo(this IMiningPool pool, IMapper mapper)
        {
            var poolInfo = mapper.Map <PoolInfo>(pool.Config);

            poolInfo.PoolStats    = pool.PoolStats;
            poolInfo.NetworkStats = pool.NetworkStats;

            // pool wallet link
            CoinMetaData.AddressInfoLinks.TryGetValue(pool.Config.Coin.Type, out var addressInfobaseUrl);
            if (!string.IsNullOrEmpty(addressInfobaseUrl))
            {
                poolInfo.AddressInfoLink = string.Format(addressInfobaseUrl, poolInfo.Address);
            }

            // pool fees
            poolInfo.PoolFeePercent = (float)pool.Config.RewardRecipients
                                      .Sum(x => x.Percentage);

            return(poolInfo);
        }
예제 #6
0
        public async Task PayoutAsync(IMiningPool pool, Balance[] balances, CancellationToken ct)
        {
            // ensure we have peers
            var infoResponse = await daemon.ExecuteCmdSingleAsync <string>(logger, EC.GetPeerCount, ct);

            if (networkType == EthereumNetworkType.Mainnet &&
                (infoResponse.Error != null || string.IsNullOrEmpty(infoResponse.Response) ||
                 infoResponse.Response.IntegralFromHex <int>() < EthereumConstants.MinPayoutPeerCount))
            {
                logger.Warn(() => $"[{LogCategory}] Payout aborted. Not enough peers (4 required)");
                return;
            }

            var txHashes = new List <string>();

            foreach (var balance in balances)
            {
                try
                {
                    var txHash = await PayoutAsync(balance, ct);

                    txHashes.Add(txHash);
                }

                catch (Exception ex)
                {
                    logger.Error(ex);

                    NotifyPayoutFailure(poolConfig.Id, new[] { balance }, ex.Message, null);
                }
            }

            if (txHashes.Any())
            {
                NotifyPayoutSuccess(poolConfig.Id, balances, txHashes.ToArray(), null);
            }
        }
예제 #7
0
    public virtual async Task <decimal> UpdateBlockRewardBalancesAsync(IDbConnection con, IDbTransaction tx, IMiningPool pool, Block block, CancellationToken ct)
    {
        var blockRewardRemaining = block.Reward;

        // Distribute funds to configured reward recipients
        foreach (var recipient in poolConfig.RewardRecipients.Where(x => x.Percentage > 0))
        {
            var amount  = block.Reward * (recipient.Percentage / 100.0m);
            var address = recipient.Address;

            blockRewardRemaining -= amount;

            // skip transfers from pool wallet to pool wallet
            if (address != poolConfig.Address)
            {
                logger.Info(() => $"Adding {FormatAmount(amount)} to balance of {address}");
                await balanceRepo.AddAmountAsync(con, tx, poolConfig.Id, address, amount, $"Reward for block {block.BlockHeight}");
            }
        }

        return(blockRewardRemaining);
    }
예제 #8
0
        public void AttachPool(IMiningPool pool)
        {
            pools[pool.Config.Id] = new PoolContext(pool, LogUtil.GetPoolScopedLogger(typeof(ShareRecorder), pool.Config));

            pool.Shares.Subscribe(x => { queue.Add(x.Share); });
        }
예제 #9
0
    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);
        }
    }
예제 #10
0
    public virtual Task CalculateBlockEffortAsync(IMiningPool pool, Block block, double accumulatedBlockShareDiff, CancellationToken ct)
    {
        block.Effort = accumulatedBlockShareDiff * ErgoConstants.ShareMultiplier / block.NetworkDifficulty;

        return(Task.FromResult(true));
    }
예제 #11
0
    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 PoolContext(IMiningPool pool, ILogger logger)
 {
     Pool   = pool;
     Logger = logger;
 }
 public void AttachPool(IMiningPool pool)
 {
     pools[pool.Config.Id] = new PoolContext(pool, LogUtil.GetPoolScopedLogger(typeof(ShareRecorder), pool.Config));
 }
예제 #14
0
        public static PoolInfo ToPoolInfo(this PoolConfig poolConfig, IMapper mapper, Persistence.Model.PoolStats stats, IMiningPool pool)
        {
            var poolInfo = mapper.Map <PoolInfo>(poolConfig);

            poolInfo.PoolStats    = mapper.Map <PoolStats>(stats);
            poolInfo.NetworkStats = pool?.NetworkStats ?? mapper.Map <BlockchainStats>(stats);

            // pool wallet link
            var addressInfobaseUrl = poolConfig.Template.ExplorerAccountLink;

            if (!string.IsNullOrEmpty(addressInfobaseUrl))
            {
                poolInfo.AddressInfoLink = string.Format(addressInfobaseUrl, poolInfo.Address);
            }

            // pool fees
            poolInfo.PoolFeePercent = (float)poolConfig.RewardRecipients.Sum(x => x.Percentage);

            // strip security critical stuff
            if (poolInfo.PaymentProcessing.Extra != null)
            {
                var extra = poolInfo.PaymentProcessing.Extra;

                extra.StripValue(nameof(EthereumPoolPaymentProcessingConfigExtra.CoinbasePassword));
                extra.StripValue(nameof(AionPoolPaymentExtraConfig.AccountPassword));
                extra.StripValue(nameof(AionPoolPaymentExtraConfig.PrivateKey));
                extra.StripValue(nameof(AionPoolPaymentExtraConfig.SendTransactionsUsingPrivateKey));
            }

            return(poolInfo);
        }
예제 #15
0
 public void AttachPool(IMiningPool pool)
 {
     pools[pool.Config.Id] = pool;
 }
예제 #16
0
        public override async Task PayoutAsync(IMiningPool pool, Balance[] balances, CancellationToken ct)
        {
            Contract.RequiresNonNull(balances, nameof(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[]
                            {
                                (object)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);
        }
예제 #17
0
        public async Task <Block[]> ClassifyBlocksAsync(IMiningPool pool, Block[] blocks, CancellationToken ct)
        {
            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, ct, 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, 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 blockHashResponses = await daemon.ExecuteCmdAllAsync <DaemonResponses.Block>(logger, EC.GetBlockByNumber, ct,
                                                                                                             new[] { (object)block.BlockHeight.ToStringHexWithPrefix(), true });

                            var blockHash = blockHashResponses.First(x => x.Error == null && x.Response?.Hash != null).Response.Hash;

                            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); // 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, 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 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, 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 blockHashUncleResponses = await daemon.ExecuteCmdAllAsync <DaemonResponses.Block>(logger, EC.GetBlockByNumber, ct,
                                                                                                                          new[] { (object)uncle.Height.Value.ToStringHexWithPrefix(), true });

                                    var blockHashUncle = blockHashUncleResponses.First(x => x.Error == null && x.Response?.Hash != null).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());
        }
예제 #18
0
        public override async Task <decimal> 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 -= EthereumConstants.StaticTransactionFeeReserve;

            return(blockRewardRemaining);
        }
예제 #19
0
        public Task CalculateBlockEffortAsync(IMiningPool pool, Block block, double accumulatedBlockShareDiff, CancellationToken ct)
        {
            block.Effort = accumulatedBlockShareDiff / block.NetworkDifficulty;

            return(Task.FromResult(true));
        }
예제 #20
0
        public virtual async Task PayoutAsync(IMiningPool pool, Balance[] balances, CancellationToken ct)
        {
            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 {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,

                        // workaround for https://bitcoin.stackexchange.com/questions/102508/bitcoin-cli-sendtoaddress-error-fallbackfee-is-disabled-wait-a-few-blocks-or-en
                        // using bitcoin regtest
                        //true,
                        //null,
                        //"unset",
                        //"1"
                    };
                }

                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[]
                        {
                            (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);
                }
            }
        }
예제 #21
0
        public virtual async Task <Block[]> ClassifyBlocksAsync(IMiningPool pool, Block[] blocks, CancellationToken ct)
        {
            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>();
            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());
        }
예제 #22
0
 public void AttachPool(IMiningPool pool)
 {
     pool.Shares.Subscribe(x => { queue.Add(x.Share); });
 }
예제 #23
0
    public static PoolInfo ToPoolInfo(this PoolConfig poolConfig, IMapper mapper, Persistence.Model.PoolStats stats, IMiningPool pool)
    {
        var poolInfo = mapper.Map <PoolInfo>(poolConfig);

        poolInfo.PoolStats    = mapper.Map <PoolStats>(stats);
        poolInfo.NetworkStats = pool?.NetworkStats ?? mapper.Map <BlockchainStats>(stats);

        // pool wallet link
        var addressInfobaseUrl = poolConfig.Template.ExplorerAccountLink;

        if (!string.IsNullOrEmpty(addressInfobaseUrl))
        {
            poolInfo.AddressInfoLink = string.Format(addressInfobaseUrl, poolInfo.Address);
        }

        // pool fees
        poolInfo.PoolFeePercent = poolConfig.RewardRecipients != null ? (float)poolConfig.RewardRecipients.Sum(x => x.Percentage) : 0;

        // strip security critical stuff
        if (poolInfo.PaymentProcessing.Extra != null)
        {
            var extra = poolInfo.PaymentProcessing.Extra;

            extra.StripValue(nameof(ErgoPaymentProcessingConfigExtra.WalletPassword));
        }

        if (poolInfo.Ports != null)
        {
            foreach (var port in poolInfo.Ports.Keys)
            {
                var portInfo = poolInfo.Ports[port];

                portInfo.TlsPfxFile     = null;
                portInfo.TlsPfxPassword = null;
            }
        }
        return(poolInfo);
    }