private int?DetermineApplicableRetrievalHeight(DepositRetrievalType retrievalType, int retrieveFromHeight, out string message) { message = string.Empty; int applicableMaturityHeight; if (retrievalType == DepositRetrievalType.Small) { applicableMaturityHeight = this.consensusTip.Height - this.federatedPegSettings.MinimumConfirmationsSmallDeposits; } else if (retrievalType == DepositRetrievalType.Normal) { applicableMaturityHeight = this.consensusTip.Height - this.federatedPegSettings.MinimumConfirmationsNormalDeposits; } else if (retrievalType == DepositRetrievalType.Distribution) { applicableMaturityHeight = this.consensusTip.Height - this.federatedPegSettings.MinimumConfirmationsDistributionDeposits; } else { applicableMaturityHeight = this.consensusTip.Height - this.federatedPegSettings.MinimumConfirmationsLargeDeposits; } if (retrieveFromHeight > applicableMaturityHeight) { message = string.Format("The submitted block height of {0} is not mature enough for '{1}' deposits, blocks below {2} can be returned.", retrieveFromHeight, retrievalType, applicableMaturityHeight); return(null); } this.logger.LogDebug("Blocks will be inspected for '{0}' deposits from height {1}.", retrievalType, applicableMaturityHeight); return(applicableMaturityHeight); }
public Deposit(uint256 id, DepositRetrievalType retrievalType, Money amount, string targetAddress, int blockNumber, uint256 blockHash) { this.Id = id; this.RetrievalType = retrievalType; this.Amount = amount; this.TargetAddress = targetAddress; this.BlockNumber = blockNumber; this.BlockHash = blockHash; }
private List <MaturedBlockDepositsModel> RetrieveDepositsFromHeight(DepositRetrievalType retrievalType, int applicableHeight, int retrieveFromHeight) { List <ChainedHeader> applicableHeaders = RetrieveApplicableHeaders(applicableHeight, retrieveFromHeight); var depositBlockModels = new List <MaturedBlockDepositsModel>(); // Half of the timeout, we will also need time to convert it to json. using (var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(RestApiClientBase.TimeoutSeconds / 2))) { for (int headerIndex = 0; headerIndex < applicableHeaders.Count; headerIndex += 100) { List <ChainedHeader> currentHeaders = applicableHeaders.GetRange(headerIndex, Math.Min(100, applicableHeaders.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); break; } MaturedBlockDepositsModel depositBlockModel = this.depositExtractor.ExtractBlockDeposits(chainedHeaderBlock, retrievalType); if (depositBlockModel != null && depositBlockModel.Deposits != null) { this.logger.LogDebug("{0} '{1}' deposits extracted at block '{2}'", depositBlockModel.Deposits.Count, retrievalType, chainedHeaderBlock.ChainedHeader); if (depositBlockModel.Deposits.Any()) { depositBlockModels.Add(depositBlockModel); } } if (depositBlockModels.Count >= MaturedBlocksSyncManager.MaxBlocksToRequest || depositBlockModels.SelectMany(d => d.Deposits).Count() >= int.MaxValue) { this.logger.LogDebug("Stopping matured blocks collection, thresholds reached; {0}={1}, numberOfDeposits={2}", nameof(depositBlockModels), depositBlockModels.Count, depositBlockModels.SelectMany(d => d.Deposits).Count()); break; } if (cancellationToken.IsCancellationRequested) { this.logger.LogDebug("Stopping matured blocks collection, the request is taking too long, sending what has been collected."); break; } } } } return(depositBlockModels); }
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 IDeposit ExtractDepositFromTransaction(Transaction transaction, int blockHeight, uint256 blockHash, DepositRetrievalType depositRetrievalType) { // Coinbase transactions can't have deposits. if (transaction.IsCoinBase) { return(null); } // Deposits have a certain structure. if (transaction.Outputs.Count != ExpectedNumberOfOutputsNoChange && transaction.Outputs.Count != ExpectedNumberOfOutputsChange) { return(null); } var depositsToMultisig = transaction.Outputs.Where(output => output.ScriptPubKey == this.depositScript && output.Value >= FederatedPegSettings.CrossChainTransferMinimum).ToList(); if (!depositsToMultisig.Any()) { return(null); } if (!this.opReturnDataReader.TryGetTargetAddress(transaction, out string targetAddress)) { return(null); } // Check if this deposit is intended for distribution to the miners. This is identified by a specific destination address in the deposit OP_RETURN. // A distribution deposit is otherwise exactly the same as a regular deposit transaction. if (targetAddress == StraxCoinstakeRule.CirrusDummyAddress && depositRetrievalType != DepositRetrievalType.Distribution) { // Distribution transactions are special and take precedence over all the other types. return(null); } this.logger.LogDebug("Processing a received deposit transaction of type {0} with address: {1}. Transaction hash: {2}.", depositRetrievalType, targetAddress, transaction.GetHash()); return(new Deposit(transaction.GetHash(), depositRetrievalType, depositsToMultisig.Sum(o => o.Value), targetAddress, blockHeight, blockHash)); }
/// <inheritdoc /> public IReadOnlyList <IDeposit> ExtractDepositsFromBlock(Block block, int blockHeight, DepositRetrievalType depositRetrievalType) { var deposits = new List <IDeposit>(); // If it's an empty block (i.e. only the coinbase transaction is present), there's no deposits inside. if (block.Transactions.Count <= 1) { return(deposits); } uint256 blockHash = block.GetHash(); foreach (Transaction transaction in block.Transactions) { IDeposit deposit = this.ExtractDepositFromTransaction(transaction, blockHeight, blockHash, depositRetrievalType); if (deposit == null) { continue; } if (depositRetrievalType == DepositRetrievalType.Small && deposit.Amount <= this.federatedPegSettings.SmallDepositThresholdAmount) { deposits.Add(deposit); continue; } if (depositRetrievalType == DepositRetrievalType.Normal && deposit.Amount > this.federatedPegSettings.SmallDepositThresholdAmount && deposit.Amount <= this.federatedPegSettings.NormalDepositThresholdAmount) { deposits.Add(deposit); continue; } if (depositRetrievalType == DepositRetrievalType.Large && deposit.Amount > this.federatedPegSettings.NormalDepositThresholdAmount) { deposits.Add(deposit); continue; } if (depositRetrievalType == DepositRetrievalType.Distribution) { deposits.Add(deposit); } } return(deposits); }
/// <inheritdoc /> public MaturedBlockDepositsModel ExtractBlockDeposits(ChainedHeaderBlock blockToExtractDepositsFrom, DepositRetrievalType depositRetrievalType) { Guard.NotNull(blockToExtractDepositsFrom, nameof(blockToExtractDepositsFrom)); var maturedBlockModel = new MaturedBlockInfoModel() { BlockHash = blockToExtractDepositsFrom.ChainedHeader.HashBlock, BlockHeight = blockToExtractDepositsFrom.ChainedHeader.Height, BlockTime = blockToExtractDepositsFrom.ChainedHeader.Header.Time }; IReadOnlyList <IDeposit> deposits = ExtractDepositsFromBlock(blockToExtractDepositsFrom.Block, blockToExtractDepositsFrom.ChainedHeader.Height, depositRetrievalType); return(new MaturedBlockDepositsModel(maturedBlockModel, deposits)); }
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); }
/// <inheritdoc /> public IDeposit ExtractDepositFromTransaction(Transaction transaction, int blockHeight, uint256 blockHash, DepositRetrievalType depositRetrievalType) { // Coinbases can't have deposits. if (transaction.IsCoinBase) { return(null); } // Deposits have a certain structure. if (transaction.Outputs.Count != ExpectedNumberOfOutputsNoChange && transaction.Outputs.Count != ExpectedNumberOfOutputsChange) { return(null); } var depositsToMultisig = transaction.Outputs.Where(output => output.ScriptPubKey == this.depositScript && output.Value >= FederatedPegSettings.CrossChainTransferMinimum).ToList(); if (!depositsToMultisig.Any()) { return(null); } if (!this.opReturnDataReader.TryGetTargetAddress(transaction, out string targetAddress)) { return(null); } this.logger.LogDebug("Processing a received deposit transaction with address: {0}. Transaction hash: {1}.", targetAddress, transaction.GetHash()); return(new Deposit(transaction.GetHash(), depositRetrievalType, depositsToMultisig.Sum(o => o.Value), targetAddress, blockHeight, blockHash)); }
/// <inheritdoc /> public IReadOnlyList <IDeposit> ExtractDepositsFromBlock(Block block, int blockHeight, DepositRetrievalType depositRetrievalType) { var deposits = new List <IDeposit>(); // If it's an empty block, there's no deposits inside. if (block.Transactions.Count <= 1) { return(deposits); } uint256 blockHash = block.GetHash(); foreach (Transaction transaction in block.Transactions) { IDeposit deposit = this.ExtractDepositFromTransaction(transaction, blockHeight, blockHash, depositRetrievalType); if (deposit != null) { if (depositRetrievalType == DepositRetrievalType.Faster && deposit.Amount <= this.federatedPegSettings.FasterDepositThresholdAmount) { deposits.Add(deposit); } if (depositRetrievalType == DepositRetrievalType.Normal && deposit.Amount > this.federatedPegSettings.FasterDepositThresholdAmount) { deposits.Add(deposit); } } } return(deposits); }