private async Task <bool> ProcessMatureBlockDepositsAsync(SerializableResult <List <MaturedBlockDepositsModel> > matureBlockDepositsResult) { // "Value"'s count will be 0 if we are using NewtonSoft's serializer, null if using .Net Core 3's serializer. if (matureBlockDepositsResult.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); } // 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.Trace(deposit.ToString()); } } // If we received a portion of blocks we can ask for new portion without any delay. RecordLatestMatureDepositsResult result = await this.crossChainTransferStore.RecordLatestMatureDepositsAsync(matureBlockDepositsResult.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.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); }
/// <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); this.logger.LogDebug("Request model created: {0}:{1}, {2}:{3}.", nameof(model.BlockHeight), model.BlockHeight, nameof(model.MaxBlocksToSend), model.MaxBlocksToSend); // Ask for blocks. IList <MaturedBlockDepositsModel> matureBlockDeposits = await this.federationGatewayClient.GetMaturedBlockDepositsAsync(model, cancellationToken).ConfigureAwait(false); bool delayRequired = true; if (matureBlockDeposits != null) { // Log what we've received. foreach (MaturedBlockDepositsModel maturedBlockDeposit in matureBlockDeposits) { // 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 (matureBlockDeposits.Count > 0) { RecordLatestMatureDepositsResult result = await this.store.RecordLatestMatureDepositsAsync(matureBlockDeposits).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); } } else { this.logger.LogDebug("Failed to fetch matured block deposits from counter chain node! {0} doesn't respond!", this.federationGatewayClient.EndpointUrl); } return(delayRequired); }
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); }