diff --git a/Dockerfile b/Dockerfile index 58961330b..0075e3412 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0-jammy as BUILDER WORKDIR /app RUN apt-get update && \ - apt-get -y install cmake ninja-build build-essential libssl-dev pkg-config libboost-all-dev libsodium-dev libzmq5n libzmq3-dev golang-go libgmp-dev + apt-get -y install cmake ninja-build build-essential libssl-dev pkg-config libboost-all-dev libsodium-dev libzmq5 libzmq3-dev golang-go libgmp-dev COPY . . WORKDIR /app/src/Miningcore RUN dotnet publish -c Release --framework net6.0 -o ../../build diff --git a/src/Miningcore/Blockchain/Alephium/AlephiumConstants.cs b/src/Miningcore/Blockchain/Alephium/AlephiumConstants.cs index 6af843268..88de2a68a 100644 --- a/src/Miningcore/Blockchain/Alephium/AlephiumConstants.cs +++ b/src/Miningcore/Blockchain/Alephium/AlephiumConstants.cs @@ -19,6 +19,9 @@ public static class AlephiumConstants public const uint ShareMultiplier = 1; // ALPH smallest unit is called PHI: https://wiki.alephium.org/glossary#gas-price public const decimal SmallestUnit = 1000000000000000000; + + public const string BlockTypeUncle = "uncle"; + public const string BlockTypeBlock = "block"; // Socket miner API public const int MessageHeaderSize = 4; // 4 bytes body length diff --git a/src/Miningcore/Blockchain/Alephium/AlephiumJobManager.cs b/src/Miningcore/Blockchain/Alephium/AlephiumJobManager.cs index d71022d72..11bcccb9a 100644 --- a/src/Miningcore/Blockchain/Alephium/AlephiumJobManager.cs +++ b/src/Miningcore/Blockchain/Alephium/AlephiumJobManager.cs @@ -331,33 +331,30 @@ private async Task UpdateJob(CancellationToken ct, string via = null, Alep validJobs.RemoveAt(validJobs.Count - 1); } - if(isNew) - { - if(via != null) - logger.Info(() => $"Detected new block {job.BlockTemplate.Height} on chain[{job.BlockTemplate.ChainIndex}] [{via}]"); - else - logger.Info(() => $"Detected new block {job.BlockTemplate.Height} on chain[{job.BlockTemplate.ChainIndex}]"); - - // update stats - if ((job.BlockTemplate.Height - 1) > BlockchainStats.BlockHeight) - { - // update stats - BlockchainStats.LastNetworkBlockTime = clock.Now; - BlockchainStats.BlockHeight = job.BlockTemplate.Height - 1; - } - } - + if(via != null) + logger.Info(() => $"Detected new block {job.BlockTemplate.Height} on chain[{job.BlockTemplate.ChainIndex}] [{via}]"); else + logger.Info(() => $"Detected new block {job.BlockTemplate.Height} on chain[{job.BlockTemplate.ChainIndex}]"); + + // update stats + if ((job.BlockTemplate.Height - 1) > BlockchainStats.BlockHeight) { - if(via != null) - logger.Debug(() => $"Template update {job.BlockTemplate.Height} on chain[{job.BlockTemplate.ChainIndex}] [{via}]"); - else - logger.Debug(() => $"Template update {job.BlockTemplate.Height} on chain[{job.BlockTemplate.ChainIndex}]"); + // update stats + BlockchainStats.LastNetworkBlockTime = clock.Now; + BlockchainStats.BlockHeight = job.BlockTemplate.Height - 1; } currentJob = job; } + else + { + if(via != null) + logger.Debug(() => $"Template update {job.BlockTemplate.Height} on chain[{job.BlockTemplate.ChainIndex}] [{via}]"); + else + logger.Debug(() => $"Template update {job.BlockTemplate.Height} on chain[{job.BlockTemplate.ChainIndex}]"); + } + return isNew; } @@ -632,6 +629,7 @@ protected override async Task PostStartInitAsync(CancellationToken ct) network = "mainnet"; break; case 1: + case 7: network = "testnet"; break; case 4: @@ -694,6 +692,9 @@ protected override async Task AreDaemonsHealthyAsync(CancellationToken ct) protected override async Task AreDaemonsConnectedAsync(CancellationToken ct) { + var infosChainParams = await Guard(() => rpc.GetInfosChainParamsAsync(ct), + ex=> logger.Debug(ex)); + var info = await Guard(() => rpc.GetInfosInterCliquePeerInfoAsync(ct), ex=> logger.Debug(ex)); @@ -704,7 +705,10 @@ protected override async Task AreDaemonsConnectedAsync(CancellationToken c if(!string.IsNullOrEmpty(nodeInfo?.BuildInfo.ReleaseVersion)) BlockchainStats.NodeVersion = nodeInfo?.BuildInfo.ReleaseVersion; - return info?.Count > 0; + if(infosChainParams?.NetworkId != 7) + return info?.Count > 0; + + return true; } protected override async Task EnsureDaemonsSynchedAsync(CancellationToken ct) diff --git a/src/Miningcore/Blockchain/Alephium/AlephiumPayoutHandler.cs b/src/Miningcore/Blockchain/Alephium/AlephiumPayoutHandler.cs index 580c40823..f93bfbf0d 100644 --- a/src/Miningcore/Blockchain/Alephium/AlephiumPayoutHandler.cs +++ b/src/Miningcore/Blockchain/Alephium/AlephiumPayoutHandler.cs @@ -71,6 +71,7 @@ public virtual async Task ConfigureAsync(ClusterConfig cc, PoolConfig pc, Cancel network = "mainnet"; break; case 1: + case 7: network = "testnet"; break; case 4: @@ -105,107 +106,156 @@ public virtual async Task ClassifyBlocksAsync(IMiningPool pool, Block[] for(var j = 0; j < page.Length; j++) { var block = page[j]; - + + List blockRewardTransactions; + BlockEntry blockInfo; + var isBlockInMainChain = await Guard(() => alephiumClient.GetBlockflowIsBlockInMainChainAsync((string) block.Hash, ct), ex=> logger.Debug(ex)); - // We lost that battle + // Starting with Rhone-Upgrade - https://docs.alephium.org/integration/mining/#rhone-upgrade - "Ghost" uncles are now a thing on ALPH + // When a Block is not found in main chain, we must check now if it could be a "ghost" uncle if(!isBlockInMainChain) { - result.Add(block); - - block.Status = BlockStatus.Orphaned; - block.Reward = 0; - - logger.Info(() => $"[{LogCategory}] Block {block.BlockHeight} classified as orphaned because it's not the chain"); + block.Type = AlephiumConstants.BlockTypeUncle; + + // get uncle block info + blockInfo = await Guard(() => alephiumClient.UncleHashAsync((string) block.Hash, ct), + ex=> logger.Debug(ex)); + + // Dang, not even a "ghost" uncle, we definitely lost that battle :'( + if(blockInfo == null) + { + result.Add(block); + + block.Status = BlockStatus.Orphaned; + block.Reward = 0; + + logger.Info(() => $"[{LogCategory}] Block {block.BlockHeight} [{block.Hash}] classified as orphaned because it's not the chain and not even a 'ghost' uncle"); + + messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); + + continue; + } + else + { + logger.Debug(() => $"[{LogCategory}] Block {block.BlockHeight} [{block.Hash}] is a possible ghost uncle. It contains {blockInfo.Transactions.Count} transaction(s)"); - messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); - continue; + // we only need the transaction(s) related to the block reward + blockRewardTransactions = blockInfo.Transactions + .Where(x => x.Unsigned.Inputs.Count < 1) + .ToList(); + } } else { + block.Type = AlephiumConstants.BlockTypeBlock; + // get block info - var blockInfo = await Guard(() => alephiumClient.HashAsync((string) block.Hash, ct), + blockInfo = await Guard(() => alephiumClient.HashAsync((string) block.Hash, ct), ex=> logger.Debug(ex)); - logger.Debug(() => $"[{LogCategory}] Block {block.BlockHeight} contains {blockInfo.Transactions.Count} transaction(s)"); + logger.Debug(() => $"[{LogCategory}] Block {block.BlockHeight} [{block.Hash}] contains {blockInfo.Transactions.Count} transaction(s)"); + + // we only need the transaction(s) related to the block reward + blockRewardTransactions = blockInfo.Transactions + .Where(x => x.Unsigned.Inputs.Count < 1) + .ToList(); + } + + logger.Debug(() => $"[{LogCategory}] Block {block.BlockHeight} [{block.Hash}] contains {blockRewardTransactions.Count} transaction(s) related to the block reward"); + // Money time + if(blockRewardTransactions.Count > 0) + { // get wallet miner's addresses var walletMinersAddresses = await Guard(() => alephiumClient.GetMinersAddressesAsync(ct), ex=> logger.Debug(ex)); - // we only need the transaction(s) related to the block reward - var blockRewardTransactions = blockInfo.Transactions - .Where(x => x.Unsigned.Inputs.Count < 1) - .ToList(); + // We only need the transaction(s) for our wallet miner's addresses + blockRewardTransactions = blockRewardTransactions + .Where(x => + { + var fixedOutputs = x.Unsigned.FixedOutputs + .Where(y => walletMinersAddresses.Addresses.Contains(y.Address)) + .ToList(); - logger.Debug(() => $"[{LogCategory}] Block {block.BlockHeight} contains {blockRewardTransactions.Count} transaction(s) related to the block reward"); + return fixedOutputs.Count > 0; + }) + .ToList(); - // update real blockHeight from chain if necessary - //if (block.BlockHeight != blockInfo.Height) - //block.BlockHeight = blockInfo.Height; + logger.Debug(() => $"[{LogCategory}] Block {block.BlockHeight} [{block.Hash}] contains {blockRewardTransactions.Count} transaction(s) related to our wallet miner's addresses"); - // update progress - // Two block confirmations methods are available: - // 1) ALPH default lock mechanism: All of the mined coins are locked for N minutes (up to +8 hours on mainnet, very short on testnet) - // 2) Mining pool operator provides a custom block rewards lock time, this method must be ONLY USE ON TESTNET in order to mimic MAINNET - if(extraPoolPaymentProcessingConfig?.BlockRewardsLockTime == null) + if(blockRewardTransactions.Count > 0) { - logger.Info(() => $"[{LogCategory}] Block {block.BlockHeight} uses the default block reward lock mechanism for minimum confirmations calculation"); - - decimal transactionsLockTime = 0; - int totalTransactionsLockTime = 0; - foreach (var blockTransactionLockTime in blockRewardTransactions) + // update progress + // Two block confirmations methods are available: + // 1) ALPH default lock mechanism: All of the mined coins are locked for N minutes (up to +8 hours on mainnet, very short on testnet) + // 2) Mining pool operator provides a custom block rewards lock time, this method must be ONLY USE ON TESTNET in order to mimic MAINNET + if(extraPoolPaymentProcessingConfig?.BlockRewardsLockTime == null) { - foreach (var unsignedLockTimeFixedOutputs in blockTransactionLockTime.Unsigned.FixedOutputs) + logger.Info(() => $"[{LogCategory}] Block {block.BlockHeight} [{block.Hash}] uses the default block reward lock mechanism for minimum confirmations calculation"); + + decimal transactionsLockTime = 0; + int totalTransactionsLockTime = 0; + foreach (var blockTransactionLockTime in blockRewardTransactions) { - // We only need the transaction(s) for our wallet miner's addresses - if(walletMinersAddresses.Addresses.Contains(unsignedLockTimeFixedOutputs.Address)) + foreach (var unsignedLockTimeFixedOutputs in blockTransactionLockTime.Unsigned.FixedOutputs) { transactionsLockTime += (decimal) unsignedLockTimeFixedOutputs.LockTime; totalTransactionsLockTime += 1; } } - } - if(totalTransactionsLockTime > 0) - transactionsLockTime /= totalTransactionsLockTime; + if(totalTransactionsLockTime > 0) + transactionsLockTime /= totalTransactionsLockTime; - block.ConfirmationProgress = Math.Min(1.0d, (double) ((AlephiumUtils.UnixTimeStampForApi(clock.Now) - blockInfo.Timestamp) / (transactionsLockTime - blockInfo.Timestamp))); - } - else - { - logger.Info(() => $"[{LogCategory}] Block {block.BlockHeight} uses a custom [{network}] block rewards lock time: [{extraPoolPaymentProcessingConfig?.BlockRewardsLockTime}] minute(s)"); + block.ConfirmationProgress = Math.Min(1.0d, (double) ((AlephiumUtils.UnixTimeStampForApi(clock.Now) - blockInfo.Timestamp) / (transactionsLockTime - blockInfo.Timestamp))); + } + else + { + logger.Info(() => $"[{LogCategory}] Block {block.BlockHeight} [{block.Hash}] uses a custom [{network}] block rewards lock time: [{extraPoolPaymentProcessingConfig?.BlockRewardsLockTime}] minute(s)"); - block.ConfirmationProgress = Math.Min(1.0d, (double) ((AlephiumUtils.UnixTimeStampForApi(clock.Now) - blockInfo.Timestamp) / ((decimal) extraPoolPaymentProcessingConfig?.BlockRewardsLockTime * 60 * 1000))); - } + block.ConfirmationProgress = Math.Min(1.0d, (double) ((AlephiumUtils.UnixTimeStampForApi(clock.Now) - blockInfo.Timestamp) / ((decimal) extraPoolPaymentProcessingConfig?.BlockRewardsLockTime * 60 * 1000))); + } - result.Add(block); + result.Add(block); - messageBus.NotifyBlockConfirmationProgress(poolConfig.Id, block, coin); + messageBus.NotifyBlockConfirmationProgress(poolConfig.Id, block, coin); - // matured and spendable? - if(block.ConfirmationProgress >= 1) - { - block.Status = BlockStatus.Confirmed; - block.ConfirmationProgress = 1; + // matured and spendable? + if(block.ConfirmationProgress >= 1) + { + block.Status = BlockStatus.Confirmed; + block.ConfirmationProgress = 1; - // reset block reward - block.Reward = 0; + // reset block reward + block.Reward = 0; - foreach (var blockTransaction in blockRewardTransactions) - { - foreach (var unsignedFixedOutputs in blockTransaction.Unsigned.FixedOutputs) + foreach (var blockTransaction in blockRewardTransactions) { - // We only need the transaction(s) for our wallet miner's addresses - if(walletMinersAddresses.Addresses.Contains(unsignedFixedOutputs.Address)) + foreach (var unsignedFixedOutputs in blockTransaction.Unsigned.FixedOutputs) + { block.Reward += AlephiumUtils.ConvertNumberFromApi(unsignedFixedOutputs.AttoAlphAmount) / AlephiumConstants.SmallestUnit; + } } + + logger.Info(() => $"[{LogCategory}] Unlocked block {block.BlockHeight} [{block.Hash}] worth {FormatAmount(block.Reward)}"); + messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); } - logger.Info(() => $"[{LogCategory}] Unlocked block {block.BlockHeight} worth {FormatAmount(block.Reward)}"); - messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); + continue; } } + + // If we end here that only means that we definitely lost that battle :'( + result.Add(block); + + block.Status = BlockStatus.Orphaned; + block.Reward = 0; + + logger.Info(() => $"[{LogCategory}] Block {block.BlockHeight} [{block.Hash}] classified as orphaned because it's not the chain"); + + messageBus.NotifyBlockUnlocked(poolConfig.Id, block, coin); } } @@ -215,7 +265,20 @@ public virtual async Task ClassifyBlocksAsync(IMiningPool pool, Block[] public virtual async Task PayoutAsync(IMiningPool pool, Balance[] balances, CancellationToken ct) { Contract.RequiresNonNull(balances); + + var infosChainParams = await Guard(() => alephiumClient.GetInfosChainParamsAsync(ct)); + + var info = await Guard(() => alephiumClient.GetInfosInterCliquePeerInfoAsync(ct)); + if(infosChainParams?.NetworkId != 7) + { + if(info?.Count < 1) + { + logger.Warn(() => $"[{LogCategory}] Payout aborted. Not enough peer(s)"); + return; + } + } + // build args var amounts = balances .Where(x => x.Amount > 0) diff --git a/src/Miningcore/Blockchain/Alephium/RPC/AlephiumClient.cs b/src/Miningcore/Blockchain/Alephium/RPC/AlephiumClient.cs index 2b8038384..d3ae55c60 100644 --- a/src/Miningcore/Blockchain/Alephium/RPC/AlephiumClient.cs +++ b/src/Miningcore/Blockchain/Alephium/RPC/AlephiumClient.cs @@ -9910,6 +9910,138 @@ public virtual async System.Threading.Tasks.Task HashAsync(string bl } } + /// + /// Get a mainchain block by ghost uncle hash + /// + /// A server side error occurred. + public virtual System.Threading.Tasks.Task UncleHashAsync(string ghost_uncle_hash) + { + return UncleHashAsync(ghost_uncle_hash, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get a mainchain block by ghost uncle hash + /// + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task UncleHashAsync(string ghost_uncle_hash, System.Threading.CancellationToken cancellationToken) + { + if (ghost_uncle_hash == null) + throw new System.ArgumentNullException("ghost_uncle_hash"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/blockflow/main-chain-block-by-ghost-uncle/{ghost_uncle_hash}"); + urlBuilder_.Replace("{ghost_uncle_hash}", System.Uri.EscapeDataString(ConvertToString(ghost_uncle_hash, System.Globalization.CultureInfo.InvariantCulture))); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + await PrepareRequestAsync(client_, request_, urlBuilder_, cancellationToken).ConfigureAwait(false); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + await PrepareRequestAsync(client_, request_, url_, cancellationToken).ConfigureAwait(false); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + await ProcessResponseAsync(client_, response_, cancellationToken).ConfigureAwait(false); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new AlephiumApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 400) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new AlephiumApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new AlephiumApiException("BadRequest", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 401) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new AlephiumApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new AlephiumApiException("Unauthorized", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 404) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new AlephiumApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new AlephiumApiException("NotFound", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 500) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new AlephiumApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new AlephiumApiException("InternalServerError", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 503) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new AlephiumApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new AlephiumApiException("ServiceUnavailable", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new AlephiumApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + /// /// Get a block and events with hash ///