/// <summary>Asks for blocks from another gateway node and then processes them.</summary> /// <returns><c>true</c> if delay between next time we should ask for blocks is required; <c>false</c> otherwise.</returns> protected async Task <bool> SyncDepositsAsync() { // First ensure that the federation wallet is active. if (!this.federationWalletManager.IsFederationWalletActive()) { this.logger.LogInformation("The CCTS will start processing deposits once the federation wallet has been activated."); return(true); } // Then ensure that the node is out of IBD. if (this.initialBlockDownloadState.IsInitialBlockDownload()) { this.logger.LogInformation("The CCTS will start processing deposits once the node is out of IBD."); return(true); } // Then ensure that the federation wallet is synced with the chain. if (!this.federationWalletManager.IsSyncedWithChain()) { this.logger.LogInformation($"The CCTS will start processing deposits once the federation wallet is synced with the chain; height {this.federationWalletManager.WalletTipHeight}"); return(true); } this.logger.LogInformation($"Requesting deposits from counterchain node."); SerializableResult <List <MaturedBlockDepositsModel> > matureBlockDeposits = await this.federationGatewayClient.GetMaturedBlockDepositsAsync(this.crossChainTransferStore.NextMatureDepositHeight, this.nodeLifetime.ApplicationStopping).ConfigureAwait(false); if (matureBlockDeposits == null) { this.logger.LogDebug("Failed to fetch normal deposits from counter chain node; {0} didn't respond.", this.federationGatewayClient.EndpointUrl); return(true); } if (matureBlockDeposits.Value == null) { this.logger.LogDebug("Failed to fetch normal deposits from counter chain node; {0} didn't reply with any deposits; Message: {1}", this.federationGatewayClient.EndpointUrl, matureBlockDeposits.Message ?? "none"); return(true); } return(await ProcessMatureBlockDepositsAsync(matureBlockDeposits).ConfigureAwait(false)); }
public void RetrieveDeposits_ReturnsSmallAndNormalDeposits_Scenario2() { // Create a "chain" of 30 blocks. this.blocks = ChainedHeadersHelper.CreateConsecutiveHeadersAndBlocks(30, true, this.mainChainNetwork); // Add 6 normal deposits to block 11 through to 16. for (int i = 11; i < 17; i++) { this.blocks[i].Block.AddTransaction(new Transaction()); CreateDepositTransaction(this.targetAddress, this.blocks[i].Block, Money.Coins(i), this.opReturnBytes); } // Add 4 small deposits to blocks 5 through to 9 (the amounts are less than 10). for (int i = 5; i < 9; i++) { this.blocks[i].Block.AddTransaction(new Transaction()); CreateDepositTransaction(this.targetAddress, this.blocks[i].Block, Money.Coins(i), this.opReturnBytes); } this.consensusManager.GetBlockData(Arg.Any<List<uint256>>()).Returns(delegate (CallInfo info) { var hashes = (List<uint256>)info[0]; return hashes.Select((hash) => this.blocks.Single(x => x.ChainedHeader.HashBlock == hash)).ToArray(); }); this.consensusManager.Tip.Returns(this.blocks.Last().ChainedHeader); var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings); SerializableResult<List<MaturedBlockDepositsModel>> depositsResult = maturedBlocksProvider.RetrieveDeposits(5); // Total deposits Assert.Equal(10, depositsResult.Value.SelectMany(b => b.Deposits).Count()); // Normal Deposits Assert.Equal(6, depositsResult.Value.SelectMany(b => b.Deposits).Where(d => d.RetrievalType == DepositRetrievalType.Normal).Count()); // Small Deposits Assert.Equal(4, depositsResult.Value.SelectMany(b => b.Deposits).Where(d => d.RetrievalType == DepositRetrievalType.Small).Count()); }
public void RetrieveDeposits_ReturnsFasterAndNormalDeposits_Scenario3() { // Create a "chain" of 30 blocks. List <ChainedHeaderBlock> blocks = ChainedHeadersHelper.CreateConsecutiveHeadersAndBlocks(30, null, true); // Add 6 faster deposits to blocks 8 through to 13 (the amounts are less than 10). for (int i = 8; i <= 13; i++) { blocks[i].Block.AddTransaction(new Transaction()); CreateDepositTransaction(this.targetAddress, blocks[i].Block, Money.Coins(8), this.opReturnBytes); } // Add 5 normal deposits to block 11 through to 15. for (int i = 11; i <= 15; i++) { blocks[i].Block.AddTransaction(new Transaction()); CreateDepositTransaction(this.targetAddress, blocks[i].Block, Money.Coins(i), this.opReturnBytes); } this.consensusManager.GetBlockData(Arg.Any <List <uint256> >()).Returns(delegate(CallInfo info) { var hashes = (List <uint256>)info[0]; return(hashes.Select((hash) => blocks.Single(x => x.ChainedHeader.HashBlock == hash)).ToArray()); }); this.consensusManager.Tip.Returns(blocks.Last().ChainedHeader); var depositExtractor = new DepositExtractor(this.loggerFactory, this.federatedPegSettings, this.opReturnDataReader); var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings, this.loggerFactory); SerializableResult <List <MaturedBlockDepositsModel> > depositsResult = maturedBlocksProvider.RetrieveDeposits(5); // Total deposits Assert.Equal(11, depositsResult.Value.SelectMany(b => b.Deposits).Count()); // Faster Deposits Assert.Equal(6, depositsResult.Value.SelectMany(b => b.Deposits).Where(d => d.RetrievalType == DepositRetrievalType.Faster).Count()); // Normal Deposits Assert.Equal(5, depositsResult.Value.SelectMany(b => b.Deposits).Where(d => d.RetrievalType == DepositRetrievalType.Normal).Count()); }
public IActionResult GetMaturedBlockDeposits([FromBody] MaturedBlockRequestModel blockRequest) { Guard.NotNull(blockRequest, nameof(blockRequest)); if (!this.ModelState.IsValid) { IEnumerable <string> errors = this.ModelState.Values.SelectMany(e => e.Errors.Select(m => m.ErrorMessage)); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Formatting error", string.Join(Environment.NewLine, errors))); } try { SerializableResult <List <MaturedBlockDepositsModel> > depositsResult = this.maturedBlocksProvider.GetMaturedDeposits(blockRequest.BlockHeight, blockRequest.MaxBlocksToSend); return(this.Json(depositsResult)); } catch (Exception e) { this.logger.LogDebug("Exception thrown calling /api/FederationGateway/{0}: {1}.", FederationGatewayRouteEndPoint.GetMaturedBlockDeposits, e.Message); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, $"Could not re-sync matured block deposits: {e.Message}", e.ToString())); } }
public async Task BlocksAreRequestedIfThereIsSomethingToRequestAsync() { this.crossChainTransferStore.NextMatureDepositHeight.Returns(5); this.crossChainTransferStore.RecordLatestMatureDepositsAsync(null).ReturnsForAnyArgs(new RecordLatestMatureDepositsResult().Succeeded()); var models = new List <MaturedBlockDepositsModel>() { new MaturedBlockDepositsModel(new MaturedBlockInfoModel(), new List <IDeposit>()) }; var result = SerializableResult <List <MaturedBlockDepositsModel> > .Ok(models); this.federationGatewayClient.GetMaturedBlockDepositsAsync(0).ReturnsForAnyArgs(Task.FromResult(result)); bool delayRequired = await this.syncManager.ExposedSyncBatchOfBlocksAsync(); // Delay shouldn't be required because a non-empty list was provided. Assert.False(delayRequired); // Now provide empty list. result = SerializableResult <List <MaturedBlockDepositsModel> > .Ok(new List <MaturedBlockDepositsModel>() { }); this.federationGatewayClient.GetMaturedBlockDepositsAsync(0).ReturnsForAnyArgs(Task.FromResult(result)); bool delayRequired2 = await this.syncManager.ExposedSyncBatchOfBlocksAsync(); // Delay is required because an empty list was provided. Assert.True(delayRequired2); // Now provide null. result = SerializableResult <List <MaturedBlockDepositsModel> > .Ok(null as List <MaturedBlockDepositsModel>); this.federationGatewayClient.GetMaturedBlockDepositsAsync(0).ReturnsForAnyArgs(Task.FromResult(result)); bool delayRequired3 = await this.syncManager.ExposedSyncBatchOfBlocksAsync(); // Delay is required because a null list was provided. Assert.True(delayRequired3); }
public void GetMaturedBlocksReturnsDeposits() { List <ChainedHeader> headers = ChainedHeadersHelper.CreateConsecutiveHeaders(10, null, true); foreach (ChainedHeader chainedHeader in headers) { chainedHeader.Block = new Block(chainedHeader.Header); } var blocks = new List <ChainedHeaderBlock>(headers.Count); foreach (ChainedHeader chainedHeader in headers) { blocks.Add(new ChainedHeaderBlock(chainedHeader.Block, chainedHeader)); } ChainedHeader tip = headers.Last(); this.consensusManager.GetBlockData(Arg.Any <List <uint256> >()).Returns(delegate(CallInfo info) { List <uint256> hashes = (List <uint256>)info[0]; return(hashes.Select((hash) => blocks.Single(x => x.ChainedHeader.HashBlock == hash)).ToArray()); }); uint zero = 0; this.depositExtractor.MinimumDepositConfirmations.Returns(info => zero); this.depositExtractor.ExtractBlockDeposits(null).ReturnsForAnyArgs(new MaturedBlockDepositsModel(new MaturedBlockInfoModel(), new List <IDeposit>())); this.consensusManager.Tip.Returns(tip); // Makes every block a matured block. var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, this.depositExtractor, this.loggerFactory); SerializableResult <List <MaturedBlockDepositsModel> > depositsResult = maturedBlocksProvider.GetMaturedDeposits(0, 10); // Expect the number of matured deposits to equal the number of blocks. Assert.Equal(10, depositsResult.Value.Count); }
public void GetMaturedBlocksReturnsDeposits() { this.blocks = ChainedHeadersHelper.CreateConsecutiveHeadersAndBlocks(10, true, this.mainChainNetwork); ChainedHeader tip = this.blocks.Last().ChainedHeader; IFederatedPegSettings federatedPegSettings = Substitute.For<IFederatedPegSettings>(); federatedPegSettings.MinimumConfirmationsNormalDeposits.Returns(0); var deposits = new List<IDeposit>() { new Deposit(new uint256(0), DepositRetrievalType.Normal, 100, "test", 0, new uint256(1)) }; // Set the first block up to return 100 normal deposits. IDepositExtractor depositExtractor = Substitute.For<IDepositExtractor>(); depositExtractor.ExtractDepositsFromBlock(this.blocks.First().Block, this.blocks.First().ChainedHeader.Height, new[] { DepositRetrievalType.Normal }).ReturnsForAnyArgs(deposits); this.consensusManager.Tip.Returns(tip); // Makes every block a matured block. var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, federatedPegSettings); SerializableResult<List<MaturedBlockDepositsModel>> depositsResult = maturedBlocksProvider.RetrieveDeposits(0); Assert.Equal(11, depositsResult.Value.Count); }
public void RetrieveDeposits_ReturnsLargeDeposits_Scenario6() { // Create a "chain" of 20 blocks. this.blocks = ChainedHeadersHelper.CreateConsecutiveHeadersAndBlocks(20, true, this.mainChainNetwork); // Add 4 small deposits to blocks 5 through to 8 (the amounts are less than 10). for (int i = 5; i <= 8; i++) { this.blocks[i].Block.AddTransaction(new Transaction()); CreateDepositTransaction(this.targetAddress, this.blocks[i].Block, Money.Coins(i), this.opReturnBytes); } // Add 6 normal deposits to block 11 through to 16. for (int i = 11; i <= 16; i++) { this.blocks[i].Block.AddTransaction(new Transaction()); CreateDepositTransaction(this.targetAddress, this.blocks[i].Block, Money.Coins(i), this.opReturnBytes); } // Add 6 large deposits to block 11 through to 16. for (int i = 11; i <= 16; i++) { this.blocks[i].Block.AddTransaction(new Transaction()); CreateDepositTransaction(this.targetAddress, this.blocks[i].Block, Money.Coins((long)this.federatedPegSettings.NormalDepositThresholdAmount + 1), this.opReturnBytes); } this.consensusManager.Tip.Returns(this.blocks.Last().ChainedHeader); var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings); SerializableResult <List <MaturedBlockDepositsModel> > depositsResult = maturedBlocksProvider.RetrieveDeposits(10); // Total deposits Assert.Equal(4, depositsResult.Value.SelectMany(b => b.Deposits).Count()); }
public void RetrieveDeposits_ReturnsSmallAndNormalDeposits_Scenario3() { // Create a "chain" of 30 blocks. this.blocks = ChainedHeadersHelper.CreateConsecutiveHeadersAndBlocks(30, true, this.mainChainNetwork); // Add 6 small deposits to blocks 8 through to 13 (the amounts are less than 10). for (int i = 8; i <= 13; i++) { this.blocks[i].Block.AddTransaction(new Transaction()); CreateDepositTransaction(this.targetAddress, this.blocks[i].Block, Money.Coins(8), this.opReturnBytes); } // Add 5 normal deposits to block 11 through to 15. for (int i = 11; i <= 15; i++) { this.blocks[i].Block.AddTransaction(new Transaction()); CreateDepositTransaction(this.targetAddress, this.blocks[i].Block, Money.Coins(i), this.opReturnBytes); } this.consensusManager.Tip.Returns(this.blocks.Last().ChainedHeader); var depositExtractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader); var maturedBlocksProvider = new MaturedBlocksProvider(this.consensusManager, depositExtractor, this.federatedPegSettings); SerializableResult <List <MaturedBlockDepositsModel> > depositsResult = maturedBlocksProvider.RetrieveDeposits(5); // Total deposits Assert.Equal(11, depositsResult.Value.SelectMany(b => b.Deposits).Count()); // Small Deposits Assert.Equal(6, depositsResult.Value.SelectMany(b => b.Deposits).Where(d => d.RetrievalType == DepositRetrievalType.Small).Count()); // Normal Deposits Assert.Equal(5, depositsResult.Value.SelectMany(b => b.Deposits).Where(d => d.RetrievalType == DepositRetrievalType.Normal).Count()); }
public async Task ReturnsNullIfCounterChainNodeIsOfflineAsync() { SerializableResult <List <MaturedBlockDepositsModel> > result = await this.client.GetMaturedBlockDepositsAsync(100); Assert.Null(result); }
private void RetrieveDeposits(DepositRetrievalType retrievalType, int retrieveFromHeight, StringBuilder messageBuilder, SerializableResult <List <MaturedBlockDepositsModel> > result) { var retrieveUpToHeight = DetermineApplicableRetrievalHeight(retrievalType, retrieveFromHeight, out string message); if (retrieveUpToHeight == null) { this.logger.LogDebug(message); messageBuilder.AppendLine(message); } else { List <MaturedBlockDepositsModel> deposits = RetrieveDepositsFromHeight(retrievalType, retrieveFromHeight, retrieveUpToHeight.Value); if (deposits.Any()) { result.Value.AddRange(deposits); } } }
/// <inheritdoc /> public async Task <SerializableResult <List <MaturedBlockDepositsModel> > > RetrieveDepositsAsync(int maturityHeight) { if (this.consensusManager.Tip == null) { return(SerializableResult <List <MaturedBlockDepositsModel> > .Fail("Consensus is not ready to provide blocks (it is un-initialized or still starting up).")); } var result = new SerializableResult <List <MaturedBlockDepositsModel> > { Value = new List <MaturedBlockDepositsModel>(), Message = "" }; // If we're asked for blocks beyond the tip then let the caller know that there are no new blocks available. if (maturityHeight > this.consensusManager.Tip.Height) { return(result); } int maxConfirmations = this.retrievalTypeConfirmations.Values.Max(); int startHeight = maturityHeight - maxConfirmations; // Determine the first block to extract deposits for. ChainedHeader firstToProcess = this.consensusManager.Tip.GetAncestor(maturityHeight); for (ChainedHeader verifyBlock = firstToProcess?.Previous; verifyBlock != null && verifyBlock.Height >= startHeight; verifyBlock = verifyBlock.Previous) { if (!this.deposits.TryGetValue(verifyBlock.Height, out BlockDeposits blockDeposits) || blockDeposits.BlockHash != verifyBlock.HashBlock) { firstToProcess = verifyBlock; } } var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(RestApiClientBase.TimeoutSeconds / 2)); // Process the blocks after the previous block until the last available block or time expires. foreach (ChainedHeaderBlock chainedHeaderBlock in this.consensusManager.GetBlocksAfterBlock(firstToProcess?.Previous, MaturedBlocksBatchSize, cancellationToken)) { // Find all deposits in the given block. await RecordBlockDepositsAsync(chainedHeaderBlock, this.retrievalTypeConfirmations).ConfigureAwait(false); // Don't process blocks below the requested maturity height. if (chainedHeaderBlock.ChainedHeader.Height < maturityHeight) { this.logger.LogDebug("{0} below maturity height of {1}.", chainedHeaderBlock.ChainedHeader, maturityHeight); continue; } var maturedDeposits = new List <IDeposit>(); // Inspect the deposits in the block for each retrieval type (validate against the retrieval type's confirmation requirement). foreach ((DepositRetrievalType retrievalType, int requiredConfirmations) in this.retrievalTypeConfirmations) { // If the block height is more than the required confirmations, then the potential deposits // contained within are valid for the given retrieval type. if (chainedHeaderBlock.ChainedHeader.Height > requiredConfirmations) { maturedDeposits.AddRange(this.RecallBlockDeposits(chainedHeaderBlock.ChainedHeader.Height - requiredConfirmations, retrievalType)); } } this.logger.LogDebug("{0} mature deposits retrieved from block '{1}'.", maturedDeposits.Count, chainedHeaderBlock.ChainedHeader); result.Value.Add(new MaturedBlockDepositsModel(new MaturedBlockInfoModel() { BlockHash = chainedHeaderBlock.ChainedHeader.HashBlock, BlockHeight = chainedHeaderBlock.ChainedHeader.Height, BlockTime = chainedHeaderBlock.ChainedHeader.Header.Time }, maturedDeposits)); // Clean-up. this.deposits.TryRemove(chainedHeaderBlock.ChainedHeader.Height - maxConfirmations, out _); } return(result); }
/// <inheritdoc /> public SerializableResult <List <MaturedBlockDepositsModel> > GetMaturedDeposits(int retrieveFromHeight, int maxBlocks, int maxDeposits = int.MaxValue) { ChainedHeader consensusTip = this.consensusManager.Tip; if (consensusTip == null) { return(SerializableResult <List <MaturedBlockDepositsModel> > .Fail("Consensus is not ready to provide blocks.")); } int matureTipHeight = (consensusTip.Height - (int)this.depositExtractor.MinimumDepositConfirmations); if (retrieveFromHeight > matureTipHeight) { this.logger.LogTrace("(-)[RETRIEVEFROMBLOCK_HIGHER_THAN_MATUREDTIP]:{0}={1},{2}={3}", nameof(retrieveFromHeight), retrieveFromHeight, nameof(matureTipHeight), matureTipHeight); return(SerializableResult <List <MaturedBlockDepositsModel> > .Fail(string.Format(RetrieveBlockHeightHigherThanMaturedTipMessage, retrieveFromHeight, matureTipHeight))); } var maturedBlockDepositModels = new List <MaturedBlockDepositsModel>(); // Half of the timeout. We will also need time to convert it to json. int maxTimeCollectionCanTakeMs = RestApiClientBase.TimeoutMs / 2; var cancellation = new CancellationTokenSource(maxTimeCollectionCanTakeMs); int maxBlockHeight = Math.Min(matureTipHeight, retrieveFromHeight + maxBlocks - 1); var headers = new List <ChainedHeader>(); ChainedHeader header = consensusTip.GetAncestor(maxBlockHeight); for (int i = maxBlockHeight; i >= retrieveFromHeight; i--) { headers.Add(header); header = header.Previous; } headers.Reverse(); int numberOfDeposits = 0; for (int headerIndex = 0; headerIndex < headers.Count; headerIndex += 100) { List <ChainedHeader> currentHeaders = headers.GetRange(headerIndex, Math.Min(100, headers.Count - headerIndex)); var hashes = currentHeaders.Select(h => h.HashBlock).ToList(); ChainedHeaderBlock[] blocks = this.consensusManager.GetBlockData(hashes); foreach (ChainedHeaderBlock chainedHeaderBlock in blocks) { if (chainedHeaderBlock?.Block?.Transactions == null) { this.logger.LogDebug(UnableToRetrieveBlockDataFromConsensusMessage, chainedHeaderBlock.ChainedHeader); this.logger.LogTrace("(-)[BLOCKDATA_MISSING_FROM_CONSENSUS]"); return(SerializableResult <List <MaturedBlockDepositsModel> > .Ok(maturedBlockDepositModels, string.Format(UnableToRetrieveBlockDataFromConsensusMessage, chainedHeaderBlock.ChainedHeader))); } MaturedBlockDepositsModel maturedBlockDepositModel = this.depositExtractor.ExtractBlockDeposits(chainedHeaderBlock); if (maturedBlockDepositModel.Deposits != null && maturedBlockDepositModel.Deposits.Count > 0) { this.logger.LogDebug("{0} deposits extracted at block {1}", maturedBlockDepositModel.Deposits.Count, chainedHeaderBlock.ChainedHeader); } maturedBlockDepositModels.Add(maturedBlockDepositModel); numberOfDeposits += maturedBlockDepositModel.Deposits?.Count ?? 0; if (maturedBlockDepositModels.Count >= maxBlocks || numberOfDeposits >= maxDeposits) { this.logger.LogDebug("Stopping matured blocks collection, thresholds reached; {0}={1}, {2}={3}", nameof(maturedBlockDepositModels), maturedBlockDepositModels.Count, nameof(numberOfDeposits), numberOfDeposits); return(SerializableResult <List <MaturedBlockDepositsModel> > .Ok(maturedBlockDepositModels)); } if (cancellation.IsCancellationRequested) { this.logger.LogDebug("Stopping matured blocks collection, the request is taking too long. Sending what has been collected."); return(SerializableResult <List <MaturedBlockDepositsModel> > .Ok(maturedBlockDepositModels)); } } } return(SerializableResult <List <MaturedBlockDepositsModel> > .Ok(maturedBlockDepositModels)); }
private async Task <bool> ProcessMatureBlockDepositsAsync(SerializableResult <List <MaturedBlockDepositsModel> > matureBlockDeposits) { // "Value"'s count will be 0 if we are using NewtonSoft's serializer, null if using .Net Core 3's serializer. if (matureBlockDeposits.Value.Count == 0) { this.logger.LogDebug("Considering ourselves fully synced since no blocks were received."); // If we've received nothing we assume we are at the tip and should flush. // Same mechanic as with syncing headers protocol. await this.crossChainTransferStore.SaveCurrentTipAsync().ConfigureAwait(false); return(true); } this.logger.LogInformation("Processing {0} matured blocks.", matureBlockDeposits.Value.Count); // Filter out conversion transactions & also log what we've received for diagnostic purposes. foreach (MaturedBlockDepositsModel maturedBlockDeposit in matureBlockDeposits.Value) { var tempDepositList = new List <IDeposit>(); if (maturedBlockDeposit.Deposits.Count > 0) { this.logger.LogDebug("Matured deposit count for block {0} height {1}: {2}.", maturedBlockDeposit.BlockInfo.BlockHash, maturedBlockDeposit.BlockInfo.BlockHeight, maturedBlockDeposit.Deposits.Count); } foreach (IDeposit potentialConversionTransaction in maturedBlockDeposit.Deposits) { // If this is not a conversion transaction then add it immediately to the temporary list. if (potentialConversionTransaction.RetrievalType != DepositRetrievalType.ConversionSmall && potentialConversionTransaction.RetrievalType != DepositRetrievalType.ConversionNormal && potentialConversionTransaction.RetrievalType != DepositRetrievalType.ConversionLarge) { tempDepositList.Add(potentialConversionTransaction); continue; } if (this.federatedPegSettings.IsMainChain) { this.logger.LogWarning("Conversion transactions do not get actioned by the main chain."); continue; } var interFluxV2MainChainActivationHeight = ((PoAConsensusOptions)this.network.Consensus.Options).InterFluxV2MainChainActivationHeight; if (interFluxV2MainChainActivationHeight != 0 && maturedBlockDeposit.BlockInfo.BlockHeight < interFluxV2MainChainActivationHeight) { this.logger.LogWarning("Conversion transactions '{0}' will not be processed below the main chain activation height of {1}.", potentialConversionTransaction.Id, interFluxV2MainChainActivationHeight); continue; } this.logger.LogInformation("Conversion transaction '{0}' received.", potentialConversionTransaction.Id); ChainedHeader applicableHeader = null; bool conversionExists = false; if (this.conversionRequestRepository.Get(potentialConversionTransaction.Id.ToString()) != null) { this.logger.LogWarning("Conversion transaction '{0}' already exists, ignoring.", potentialConversionTransaction.Id); conversionExists = true; } else { // This should ony happen if the conversion does't exist yet. if (!FindApplicableConversionRequestHeader(maturedBlockDeposit, potentialConversionTransaction, out applicableHeader)) { continue; } } InteropConversionRequestFee interopConversionRequestFee = await this.conversionRequestFeeService.AgreeFeeForConversionRequestAsync(potentialConversionTransaction.Id.ToString(), maturedBlockDeposit.BlockInfo.BlockHeight).ConfigureAwait(false); // If a dynamic fee could not be determined, create a fallback fee. if (interopConversionRequestFee == null || (interopConversionRequestFee != null && interopConversionRequestFee.State != InteropFeeState.AgreeanceConcluded)) { interopConversionRequestFee.Amount = ConversionRequestFeeService.FallBackFee; this.logger.LogWarning($"A dynamic fee for conversion request '{potentialConversionTransaction.Id}' could not be determined, using a fixed fee of {ConversionRequestFeeService.FallBackFee} STRAX."); } if (Money.Satoshis(interopConversionRequestFee.Amount) >= potentialConversionTransaction.Amount) { this.logger.LogWarning("Conversion transaction '{0}' is no longer large enough to cover the fee.", potentialConversionTransaction.Id); continue; } // We insert the fee distribution as a deposit to be processed, albeit with a special address. // Deposits with this address as their destination will be distributed between the multisig members. // Note that it will be actioned immediately as a matured deposit. this.logger.LogInformation("Adding conversion fee distribution for transaction '{0}' to deposit list.", potentialConversionTransaction.Id); // Instead of being a conversion deposit, the fee distribution is translated to its non-conversion equivalent. DepositRetrievalType depositType = DepositRetrievalType.Small; switch (potentialConversionTransaction.RetrievalType) { case DepositRetrievalType.ConversionSmall: depositType = DepositRetrievalType.Small; break; case DepositRetrievalType.ConversionNormal: depositType = DepositRetrievalType.Normal; break; case DepositRetrievalType.ConversionLarge: depositType = DepositRetrievalType.Large; break; } tempDepositList.Add(new Deposit(potentialConversionTransaction.Id, depositType, Money.Satoshis(interopConversionRequestFee.Amount), this.network.ConversionTransactionFeeDistributionDummyAddress, potentialConversionTransaction.TargetChain, potentialConversionTransaction.BlockNumber, potentialConversionTransaction.BlockHash)); if (!conversionExists) { this.logger.LogDebug("Adding conversion request for transaction '{0}' to repository.", potentialConversionTransaction.Id); this.conversionRequestRepository.Save(new ConversionRequest() { RequestId = potentialConversionTransaction.Id.ToString(), RequestType = ConversionRequestType.Mint, Processed = false, RequestStatus = ConversionRequestStatus.Unprocessed, // We do NOT convert to wei here yet. That is done when the minting transaction is submitted on the Ethereum network. Amount = (ulong)(potentialConversionTransaction.Amount - Money.Satoshis(interopConversionRequestFee.Amount)).Satoshi, BlockHeight = applicableHeader.Height, DestinationAddress = potentialConversionTransaction.TargetAddress, DestinationChain = potentialConversionTransaction.TargetChain }); } } maturedBlockDeposit.Deposits = tempDepositList.AsReadOnly(); // Order all non-conversion deposit transactions in the block deterministically. maturedBlockDeposit.Deposits = maturedBlockDeposit.Deposits.OrderBy(x => x.Id, Comparer <uint256> .Create(DeterministicCoinOrdering.CompareUint256)).ToList(); foreach (IDeposit deposit in maturedBlockDeposit.Deposits) { this.logger.LogDebug("Deposit matured: {0}", deposit.ToString()); } } // If we received a portion of blocks we can ask for a new portion without any delay. RecordLatestMatureDepositsResult result = await this.crossChainTransferStore.RecordLatestMatureDepositsAsync(matureBlockDeposits.Value).ConfigureAwait(false); return(!result.MatureDepositRecorded); }
private async Task <bool> ProcessMatureBlockDepositsAsync(SerializableResult <List <MaturedBlockDepositsModel> > matureBlockDeposits) { // "Value"'s count will be 0 if we are using NewtonSoft's serializer, null if using .Net Core 3's serializer. if (matureBlockDeposits.Value.Count == 0) { this.logger.Debug("Considering ourselves fully synced since no blocks were received."); // If we've received nothing we assume we are at the tip and should flush. // Same mechanic as with syncing headers protocol. await this.crossChainTransferStore.SaveCurrentTipAsync().ConfigureAwait(false); return(true); } // Filter out conversion transactions & also log what we've received for diagnostic purposes. foreach (MaturedBlockDepositsModel maturedBlockDeposit in matureBlockDeposits.Value) { foreach (IDeposit conversionTransaction in maturedBlockDeposit.Deposits.Where(d => d.RetrievalType == DepositRetrievalType.ConversionSmall || d.RetrievalType == DepositRetrievalType.ConversionNormal || d.RetrievalType == DepositRetrievalType.ConversionLarge)) { this.logger.Info("Conversion mint transaction " + conversionTransaction + " received in matured blocks."); if (this.conversionRequestRepository.Get(conversionTransaction.Id.ToString()) != null) { this.logger.Info("Conversion mint transaction " + conversionTransaction + " already exists, ignoring."); continue; } // Get the first block on this chain that has a timestamp after the deposit's block time on the counterchain. // This is so that we can assign a block height that the deposit 'arrived' on the sidechain. // TODO: This can probably be made more efficient than looping every time. ChainedHeader header = this.chainIndexer.Tip; bool found = false; while (true) { if (header == this.chainIndexer.Genesis) { break; } if (header.Previous.Header.Time <= maturedBlockDeposit.BlockInfo.BlockTime) { found = true; break; } header = header.Previous; } if (!found) { continue; } this.conversionRequestRepository.Save(new ConversionRequest() { RequestId = conversionTransaction.Id.ToString(), RequestType = ConversionRequestType.Mint, Processed = false, RequestStatus = ConversionRequestStatus.Unprocessed, // We do NOT convert to wei here yet. That is done when the minting transaction is submitted on the Ethereum network. Amount = (ulong)conversionTransaction.Amount.Satoshi, BlockHeight = header.Height, DestinationAddress = conversionTransaction.TargetAddress, DestinationChain = conversionTransaction.TargetChain }); } // Order all other transactions in the block deterministically. maturedBlockDeposit.Deposits = maturedBlockDeposit.Deposits.Where(d => d.RetrievalType != DepositRetrievalType.ConversionSmall && d.RetrievalType != DepositRetrievalType.ConversionNormal && d.RetrievalType != DepositRetrievalType.ConversionLarge).OrderBy(x => x.Id, Comparer <uint256> .Create(DeterministicCoinOrdering.CompareUint256)).ToList(); foreach (IDeposit deposit in maturedBlockDeposit.Deposits) { this.logger.Trace(deposit.ToString()); } } // If we received a portion of blocks we can ask for a new portion without any delay. RecordLatestMatureDepositsResult result = await this.crossChainTransferStore.RecordLatestMatureDepositsAsync(matureBlockDeposits.Value).ConfigureAwait(false); return(!result.MatureDepositRecorded); }
/// <summary>Asks for blocks from another gateway node and then processes them.</summary> /// <returns><c>true</c> if delay between next time we should ask for blocks is required; <c>false</c> otherwise.</returns> /// <exception cref="OperationCanceledException">Thrown when <paramref name="cancellationToken"/> is cancelled.</exception> protected async Task <bool> SyncBatchOfBlocksAsync(CancellationToken cancellationToken = default(CancellationToken)) { int blocksToRequest = 1; // TODO why are we asking for max of 1 block and if it's not suspended then 1000? investigate this logic in maturedBlocksProvider if (!this.store.HasSuspended()) { blocksToRequest = MaxBlocksToRequest; } // API method that provides blocks should't give us blocks that are not mature! var model = new MaturedBlockRequestModel(this.store.NextMatureDepositHeight, blocksToRequest, MaxDepositsToRequest); this.logger.LogDebug("Request model created: {0}:{1}, {2}:{3}.", nameof(model.BlockHeight), model.BlockHeight, nameof(model.MaxBlocksToSend), model.MaxBlocksToSend); // Ask for blocks. SerializableResult <List <MaturedBlockDepositsModel> > matureBlockDepositsResult = await this.federationGatewayClient.GetMaturedBlockDepositsAsync(model, cancellationToken).ConfigureAwait(false); if (matureBlockDepositsResult == null) { this.logger.LogDebug("Failed to fetch matured block deposits from counter chain node; {0} didn't respond.", this.federationGatewayClient.EndpointUrl); this.logger.LogTrace("(-)[COUNTERCHAIN_NODE_NO_RESPONSE]:true"); return(true); } if (matureBlockDepositsResult.Value == null) { this.logger.LogDebug("Failed to fetch matured block deposits from counter chain node; {0} didn't reply with any deposits; Message: {1}", this.federationGatewayClient.EndpointUrl, matureBlockDepositsResult.Message ?? "none"); this.logger.LogTrace("(-)[COUNTERCHAIN_NODE_SENT_NO_DEPOSITS]:true"); return(true); } bool delayRequired = true; // Log what we've received. foreach (MaturedBlockDepositsModel maturedBlockDeposit in matureBlockDepositsResult.Value) { // Order transactions in block deterministically maturedBlockDeposit.Deposits = maturedBlockDeposit.Deposits.OrderBy(x => x.Id, Comparer <uint256> .Create(DeterministicCoinOrdering.CompareUint256)).ToList(); foreach (IDeposit deposit in maturedBlockDeposit.Deposits) { this.logger.LogDebug("New deposit received BlockNumber={0}, TargetAddress='{1}', depositId='{2}', Amount='{3}'.", deposit.BlockNumber, deposit.TargetAddress, deposit.Id, deposit.Amount); } } if (matureBlockDepositsResult.Value.Count > 0) { RecordLatestMatureDepositsResult result = await this.store.RecordLatestMatureDepositsAsync(matureBlockDepositsResult.Value).ConfigureAwait(false); // If we received a portion of blocks we can ask for new portion without any delay. if (result.MatureDepositRecorded) { delayRequired = false; } } else { this.logger.LogDebug("Considering ourselves fully synced since no blocks were received."); // If we've received nothing we assume we are at the tip and should flush. // Same mechanic as with syncing headers protocol. await this.store.SaveCurrentTipAsync().ConfigureAwait(false); } return(delayRequired); }