Example #1
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;
        }

        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);
        }
    }
Example #2
0
    public virtual async Task <Block[]> ClassifyBlocksAsync(IMiningPool pool, Block[] blocks, CancellationToken ct)
    {
        Contract.RequiresNonNull(poolConfig, nameof(poolConfig));
        Contract.RequiresNonNull(blocks, nameof(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());
    }
Example #3
0
    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);
    }
Example #4
0
    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());
    }
Example #5
0
    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);
    }