예제 #1
0
    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();
    }
예제 #2
0
    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();
    }
예제 #3
0
    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);
    }
예제 #4
0
    /// <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]);
예제 #5
0
    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;
    }
예제 #6
0
    protected JobManagerBase(IComponentContext ctx, IMessageBus messageBus)
    {
        Contract.RequiresNonNull(ctx);
        Contract.RequiresNonNull(messageBus);

        this.ctx        = ctx;
        this.messageBus = messageBus;
    }
예제 #7
0
    public SOLOPaymentScheme(
        IShareRepository shareRepo,
        IBalanceRepository balanceRepo)
    {
        Contract.RequiresNonNull(shareRepo);
        Contract.RequiresNonNull(balanceRepo);

        this.shareRepo   = shareRepo;
        this.balanceRepo = balanceRepo;
    }
예제 #8
0
    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);
    }
예제 #10
0
    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;
        }
    }
예제 #11
0
    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;
    }
예제 #12
0
    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;
    }
예제 #13
0
    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];
    }
예제 #14
0
    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;
    }
예제 #15
0
    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 !
        };
    }
예제 #16
0
    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();
    }
예제 #17
0
    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));
    }
예제 #18
0
    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;
    }
예제 #19
0
    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();
    }
예제 #20
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);
    }
예제 #21
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);
        }
    }
예제 #22
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());
    }
예제 #23
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());
    }
예제 #24
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;
        }

        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);
            }
        }
    }
예제 #25
0
    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());
    }
예제 #26
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);
    }