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);
        }
Example #2
0
        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);
        }
Example #4
0
        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);
        }