예제 #1
0
        public async Task CatchMempoolAndBlockDoubleSpendMessages()
        {
            var txs = await CatchInMempoolDoubleSpendZMQMessage();

            var tx1   = HelperTools.ParseBytesToTransaction(HelperTools.HexStringToByteArray(txs.Item1));
            var tx2   = HelperTools.ParseBytesToTransaction(HelperTools.HexStringToByteArray(txs.Item2));
            var txId1 = tx1.GetHash().ToString();
            var txId2 = tx2.GetHash().ToString();

            await MineNextBlockAsync(new[] { tx2 });

            var mempoolTxs2 = await rpcClient0.GetRawMempool();

            // Tx should no longer be in mempool
            Assert.IsFalse(mempoolTxs2.Contains(txId1), "Submitted tx1 should not be found in mempool");
            WaitUntilEventBusIsIdle();

            var calls = Callback.Calls;

            Assert.AreEqual(2, calls.Length);
            var callbackDS = HelperTools.JSONDeserialize <JSONEnvelopeViewModel>(calls[1].request)
                             .ExtractPayload <CallbackNotificationDoubleSpendViewModel>();

            Assert.AreEqual(CallbackReason.DoubleSpend, callbackDS.CallbackReason);
            Assert.AreEqual(new uint256(txId1), new uint256(callbackDS.CallbackTxId));
            Assert.AreEqual(new uint256(txId2), new uint256(callbackDS.CallbackPayload.DoubleSpendTxId));
        }
        public async Task CatchDSOfMempoolAncestorTxByBlockTxAsync()
        {
            using CancellationTokenSource cts = new(cancellationTimeout);

            await RegisterNodesWithServiceAndWaitAsync(cts.Token);

            Assert.AreEqual(1, zmqService.GetActiveSubscriptions().Count());

            // Create two transactions from same input
            var coin = availableCoins.Dequeue();

            var(txHex1, txId1) = CreateNewTransaction(coin, new Money(1000L));
            var(txHex2, txId2) = CreateNewTransaction(coin, new Money(500L));


            var tx2 = HelperTools.ParseBytesToTransaction(HelperTools.HexStringToByteArray(txHex2));

            // Transactions should not be the same
            Assert.AreNotEqual(txHex1, txHex2);

            // Submit transaction
            var response = await node0.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex1), true, false, cts.Token);

            // Create chain based on first transaction with last transaction being sent to mAPI
            var(lastTxHex, lastTxId, mapiCount) = await CreateUnconfirmedAncestorChainAsync(txHex1, txId1, 100, 0, true, cts.Token);

            var mempoolTxs = await rpcClient0.GetRawMempool();

            // Transactions should be in mempool
            Assert.IsTrue(mempoolTxs.Contains(txId1), "Submitted tx1 not found in mempool");

            Assert.AreEqual(0, Callback.Calls.Length);

            // Mine a new block containing tx2
            await MineNextBlockAsync(new[] { tx2 });

            var mempoolTxs2 = await rpcClient0.GetRawMempool();

            // Tx should no longer be in mempool
            Assert.IsFalse(mempoolTxs2.Contains(txId1), "Submitted tx1 should not be found in mempool");
            WaitUntilEventBusIsIdle();

            var calls = Callback.Calls;

            Assert.AreEqual(1, calls.Length);

            var callback = HelperTools.JSONDeserialize <JSONEnvelopeViewModel>(calls[0].request)
                           .ExtractPayload <CallbackNotificationDoubleSpendViewModel>();

            Assert.AreEqual(CallbackReason.DoubleSpend, callback.CallbackReason);
            Assert.AreEqual(new uint256(lastTxId), new uint256(callback.CallbackTxId));
            Assert.AreEqual(new uint256(txId2), new uint256(callback.CallbackPayload.DoubleSpendTxId));
        }
예제 #3
0
 public static bool TryParseTransaction(byte[] transaction, out Transaction result)
 {
     try
     {
         result = HelperTools.ParseBytesToTransaction(transaction);
         return(true);
     }
     catch (Exception)
     {
         result = null;
         return(false);
     }
 }
예제 #4
0
        public async Task CatchDoubleSpendOfMempoolTxByBlockTx()
        {
            // Create two transactions from same input
            var coin = availableCoins.Dequeue();

            var(txHex1, txId1) = CreateNewTransaction(coin, new Money(1000L));
            var(txHex2, txId2) = CreateNewTransaction(coin, new Money(500L));


            var tx2 = HelperTools.ParseBytesToTransaction(HelperTools.HexStringToByteArray(txHex2));

            // Transactions should not be the same
            Assert.AreNotEqual(txHex1, txHex2);

            // Send first transaction using mAPI
            var payload = await SubmitTransactionAsync(txHex1, true, true);

            Assert.AreEqual("success", payload.ReturnResult);

            var mempoolTxs = await rpcClient0.GetRawMempool();

            // Transactions should be in mempool
            Assert.IsTrue(mempoolTxs.Contains(txId1), "Submitted tx1 not found in mempool");

            Assert.AreEqual(0, Callback.Calls.Length);

            // Mine a new block containing tx2
            await MineNextBlockAsync(new[] { tx2 });

            var mempoolTxs2 = await rpcClient0.GetRawMempool();

            // Tx should no longer be in mempool
            Assert.IsFalse(mempoolTxs2.Contains(txId1), "Submitted tx1 should not be found in mempool");
            WaitUntilEventBusIsIdle();

            var calls = Callback.Calls;

            Assert.AreEqual(1, calls.Length);

            var callback = HelperTools.JSONDeserialize <JSONEnvelopeViewModel>(calls[0].request)
                           .ExtractPayload <CallbackNotificationDoubleSpendViewModel>();

            Assert.AreEqual(CallbackReason.DoubleSpend, callback.CallbackReason);
            Assert.AreEqual(new uint256(txId1), new uint256(callback.CallbackTxId));
            Assert.AreEqual(new uint256(txId2), new uint256(callback.CallbackPayload.DoubleSpendTxId));
        }
예제 #5
0
        public Tx CreateNewTx(string txHash, string txHex, bool merkleProof, string merkleFormat, bool dsCheck)
        {
            var tx = new Tx
            {
                DSCheck      = dsCheck,
                MerkleProof  = merkleProof,
                MerkleFormat = merkleFormat,
                ReceivedAt   = MockedClock.UtcNow,
                TxExternalId = new uint256(txHash),
                TxPayload    = HelperTools.HexStringToByteArray(txHex)
            };
            var transaction = HelperTools.ParseBytesToTransaction(tx.TxPayload);

            tx.TxIn = transaction.Inputs.AsIndexedInputs().Select(x => new TxInput
            {
                N        = x.Index,
                PrevN    = x.PrevOut.N,
                PrevTxId = x.PrevOut.Hash.ToBytes()
            }).ToList();

            return(tx);
        }
예제 #6
0
        public async Task <SubmitTransactionsResponse> SubmitTransactionsAsync(IEnumerable <SubmitTransaction> requestEnum, UserAndIssuer user)
        {
            var request = requestEnum.ToArray();

            logger.LogInformation($"Processing {request.Length} incoming transactions");
            // Take snapshot of current metadata and use use it for all transactions
            var info           = blockChainInfo.GetInfo();
            var currentMinerId = await minerId.GetCurrentMinerIdAsync();

            var consolidationParameters = info.ConsolidationTxParameters;

            // Use the same quotes for all transactions in single request
            var quotes = feeQuoteRepository.GetValidFeeQuotesByIdentity(user).ToArray();

            if (quotes == null || !quotes.Any())
            {
                throw new Exception("No fee quotes available");
            }

            var responses            = new List <SubmitTransactionOneResponse>();
            var transactionsToSubmit = new List <(string transactionId, SubmitTransaction transaction, bool allowhighfees, bool dontCheckFees)>();
            int failureCount         = 0;

            IDictionary <uint256, byte[]> allTxs = new Dictionary <uint256, byte[]>();

            foreach (var oneTx in request)
            {
                if ((oneTx.RawTx == null || oneTx.RawTx.Length == 0) && string.IsNullOrEmpty(oneTx.RawTxString))
                {
                    AddFailureResponse(null, $"{nameof(SubmitTransaction.RawTx)} is required", ref responses);

                    failureCount++;
                    continue;
                }

                if (oneTx.RawTx == null)
                {
                    try
                    {
                        oneTx.RawTx = HelperTools.HexStringToByteArray(oneTx.RawTxString);
                    }
                    catch (Exception ex)
                    {
                        AddFailureResponse(null, ex.Message, ref responses);

                        failureCount++;
                        continue;
                    }
                }
                uint256 txId       = Hashes.DoubleSHA256(oneTx.RawTx);
                string  txIdString = txId.ToString();

                if (allTxs.ContainsKey(txId))
                {
                    AddFailureResponse(txIdString, "Transaction with this id occurs more than once within request", ref responses);

                    failureCount++;
                    continue;
                }

                var vc     = new ValidationContext(oneTx);
                var errors = oneTx.Validate(vc);
                if (errors.Count() > 0)
                {
                    AddFailureResponse(txIdString, string.Join(",", errors.Select(x => x.ErrorMessage)), ref responses);

                    failureCount++;
                    continue;
                }
                allTxs.Add(txId, oneTx.RawTx);
                bool okToMine  = false;
                bool okToRelay = false;
                if (await txRepository.TransactionExistsAsync(txId.ToBytes()))
                {
                    AddFailureResponse(txIdString, "Transaction already known", ref responses);

                    failureCount++;
                    continue;
                }

                Transaction    transaction    = null;
                CollidedWith[] colidedWith    = {};
                Exception      exception      = null;
                string[]       prevOutsErrors = { };
                try
                {
                    transaction = HelperTools.ParseBytesToTransaction(oneTx.RawTx);

                    if (transaction.IsCoinBase)
                    {
                        throw new ExceptionWithSafeErrorMessage("Invalid transaction - coinbase transactions are not accepted");
                    }
                    var(sumPrevOuputs, prevOuts) = await CollectPreviousOuputs(transaction, new ReadOnlyDictionary <uint256, byte[]>(allTxs), rpcMultiClient);

                    prevOutsErrors = prevOuts.Where(x => !string.IsNullOrEmpty(x.Error)).Select(x => x.Error).ToArray();
                    colidedWith    = prevOuts.Where(x => x.CollidedWith != null).Select(x => x.CollidedWith).ToArray();

                    if (IsConsolidationTxn(transaction, consolidationParameters, prevOuts))
                    {
                        (okToMine, okToRelay) = (true, true);
                    }
                    else
                    {
                        foreach (var feeQuote in quotes)
                        {
                            var(okToMineTmp, okToRelayTmp) =
                                CheckFees(transaction, oneTx.RawTx.LongLength, sumPrevOuputs, feeQuote);
                            if (GetCheckFeesValue(okToMineTmp, okToRelayTmp) > GetCheckFeesValue(okToMine, okToRelay))
                            {
                                // save best combination
                                (okToMine, okToRelay) = (okToMineTmp, okToRelayTmp);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    exception = ex;
                }

                if (exception != null || colidedWith.Any() || transaction == null || prevOutsErrors.Any())
                {
                    var oneResponse = new SubmitTransactionOneResponse
                    {
                        Txid         = txIdString,
                        ReturnResult = ResultCodes.Failure,
                        // Include non null ConflictedWith only if a collision has been detected
                        ConflictedWith = !colidedWith.Any() ? null : colidedWith.Select(
                            x => new SubmitTransactionConflictedTxResponse
                        {
                            Txid = x.TxId,
                            Size = x.Size,
                            Hex  = x.Hex,
                        }).ToArray()
                    };

                    if (transaction is null)
                    {
                        oneResponse.ResultDescription = "Can not parse transaction";
                    }
                    else if (exception is ExceptionWithSafeErrorMessage)
                    {
                        oneResponse.ResultDescription = exception.Message;
                    }
                    else if (exception != null)
                    {
                        oneResponse.ResultDescription = "Error fetching inputs";
                    }
                    else // colidedWith !=null and there is no exception or prevOutsErrors is not empty
                    {
                        // return "Missing inputs" regardless of error returned from gettxouts (which is usually "missing")
                        oneResponse.ResultDescription = "Missing inputs";
                    }
                    logger.LogError($"Can not calculate fee for {txIdString}. Error: {oneResponse.ResultDescription} Exception: {exception?.ToString() ?? ""}");


                    responses.Add(oneResponse);
                    failureCount++;
                    continue;
                }

                // Transactions  was successfully analyzed
                if (!okToMine && !okToRelay)
                {
                    AddFailureResponse(txIdString, "Not enough fees", ref responses);

                    failureCount++;
                }
                else
                {
                    bool allowHighFees = false;
                    bool dontcheckfee  = okToMine;

                    oneTx.TransactionInputs = transaction.Inputs.AsIndexedInputs().Select(x => new TxInput
                    {
                        N        = x.Index,
                        PrevN    = x.PrevOut.N,
                        PrevTxId = x.PrevOut.Hash.ToBytes()
                    }).ToList();
                    transactionsToSubmit.Add((txIdString, oneTx, allowHighFees, dontcheckfee));
                }
            }

            RpcSendTransactions rpcResponse;

            Exception submitException = null;

            if (transactionsToSubmit.Any())
            {
                // Submit all collected transactions in one call
                try
                {
                    rpcResponse = await rpcMultiClient.SendRawTransactionsAsync(
                        transactionsToSubmit.Select(x => (x.transaction.RawTx, x.allowhighfees, x.dontCheckFees))
                        .ToArray());
                }
                catch (Exception ex)
                {
                    submitException = ex;
                    rpcResponse     = null;
                }
            }
            else
            {
                // Simulate empty response
                rpcResponse = new RpcSendTransactions();
            }


            // Initialize common fields
            var result = new SubmitTransactionsResponse
            {
                Timestamp = clock.UtcNow(),
                MinerId   = currentMinerId,
                CurrentHighestBlockHash   = info.BestBlockHash,
                CurrentHighestBlockHeight = info.BestBlockHeight,
                // TxSecondMempoolExpiry
                // Remaining of the fields are initialized bellow
            };

            if (submitException != null)
            {
                var unableToSubmit = transactionsToSubmit.Select(x =>
                                                                 new SubmitTransactionOneResponse
                {
                    Txid              = x.transactionId,
                    ReturnResult      = ResultCodes.Failure,
                    ResultDescription = "Error while submitting transactions to the node" // do not expose detailed error message. It might contain internal IPS etc
                });

                logger.LogError($"Error while submitting transactions to the node {submitException}");
                responses.AddRange(unableToSubmit);
                result.Txs          = responses.ToArray();
                result.FailureCount = result.Txs.Length; // all of the transactions have failed

                return(result);
            }
            else // submitted without error
            {
                var(submitFailureCount, transformed) = TransformRpcResponse(rpcResponse,
                                                                            transactionsToSubmit.Select(x => x.transactionId).ToArray());
                responses.AddRange(transformed);
                result.Txs          = responses.ToArray();
                result.FailureCount = failureCount + submitFailureCount;

                var successfullTxs = transactionsToSubmit.Where(x => transformed.Any(y => y.ReturnResult == ResultCodes.Success && y.Txid == x.transactionId));
                await txRepository.InsertTxsAsync(successfullTxs.Select(x => new Tx
                {
                    CallbackToken = x.transaction.CallbackToken,
                    CallbackUrl = x.transaction.CallbackUrl,
                    CallbackEncryption = x.transaction.CallbackEncryption,
                    DSCheck = x.transaction.DsCheck,
                    MerkleProof = x.transaction.MerkleProof,
                    TxExternalId = new uint256(x.transactionId),
                    TxPayload = x.transaction.RawTx,
                    ReceivedAt = clock.UtcNow(),
                    TxIn = x.transaction.TransactionInputs
                }).ToList());

                return(result);
            }
        }
예제 #7
0
        public async Task <ActionResult> SubmitDSAsync([FromQuery] string txId, [FromQuery] int?n, [FromQuery] string cTxId, [FromQuery] int?cn)
        {
            logger.LogInformation($"SubmitDSAsync call received for txid:'{txId}', n:'{n}', cTxId:'{cTxId}', cn:'{cn}'");
            // Set response header here that we are interested in DS submit again in case of any error
            this.Response.Headers.Add(DSHeader, "1");
            if (string.IsNullOrEmpty(txId))
            {
                return(AddBanScoreAndReturnResult("'txid' must not be null or empty.", ""));
            }

            if (string.IsNullOrEmpty(cTxId))
            {
                return(AddBanScoreAndReturnResult("'ctxid' must not be null or empty.", txId));
            }

            if (!uint256.TryParse(txId, out uint256 uTxId))
            {
                return(AddBanScoreAndReturnResult("Invalid 'txid' format.", txId));
            }

            if (!uint256.TryParse(cTxId, out uint256 ucTxId))
            {
                return(AddBanScoreAndReturnResult("Invalid 'ctxid' format.", txId));
            }

            if (txId == cTxId)
            {
                return(AddBanScoreAndReturnResult("'ctxid' parameter must not be the same as 'txid'.", txId, HostBanList.WarningScore));
            }

            if (n == null || n < 0)
            {
                return(AddBanScoreAndReturnResult("'n' must be equal or greater than 0.", txId, HostBanList.WarningScore));
            }

            if (cn == null || cn < 0)
            {
                return(AddBanScoreAndReturnResult("'cn' must be equal or greater than 0.", txId, HostBanList.WarningScore));
            }

            if (!transactionRequestsCheck.WasTransactionIdQueried(Request.Host.Host, uTxId))
            {
                return(AddBanScoreAndReturnResult("Submitted transactionId was not queried before making a call to submit, or it was already submitted.", txId));
            }

            var tx = (await txRepository.GetTxsForDSCheckAsync(new byte[][] { uTxId.ToBytes() }, true)).SingleOrDefault();

            if (tx == null)
            {
                return(AddBanScoreAndReturnResult($"There is no transaction waiting for double-spend notification with given transaction id '{txId}'.", txId, HostBanList.WarningScore));
            }

            if (n > tx.OrderderInputs.Length)
            {
                return(AddBanScoreAndReturnResult($"'n' parameter must not be greater than total number of inputs.", txId, HostBanList.WarningScore));
            }

            transactionRequestsCheck.LogKnownTransactionId(Request.Host.Host, uTxId);

            byte[] dsTxBytes;
            using (var ms = new MemoryStream())
            {
                await Request.Body.CopyToAsync(ms);

                dsTxBytes = ms.ToArray();
            }

            if (dsTxBytes.Length == 0)
            {
                return(AddBanScoreAndReturnResult("Proof must not be empty.", txId));
            }

            if (Hashes.DoubleSHA256(dsTxBytes) != ucTxId)
            {
                return(AddBanScoreAndReturnResult("Double-spend transaction does not match the 'ctxid' parameter.", txId));
            }

            Transaction dsTx;

            try
            {
                dsTx = HelperTools.ParseBytesToTransaction(dsTxBytes);
            }
            catch (Exception)
            {
                return(AddBanScoreAndReturnResult("'dsProof' is invalid.", txId));
            }

            if (cn > dsTx.Inputs.Count)
            {
                return(AddBanScoreAndReturnResult($"'cn' parameter must not be greater than total number of inputs.", txId));
            }

            var dsTxIn = dsTx.Inputs[cn.Value];
            var txIn   = tx.OrderderInputs[n.Value];

            if (!(new uint256(txIn.PrevTxId) == dsTxIn.PrevOut.Hash &&
                  txIn.PrevN == dsTxIn.PrevOut.N))
            {
                return(AddBanScoreAndReturnResult("Transaction marked as double-spend does not spend same inputs as original transaction.", txId));
            }

            logger.LogInformation($"Double spend checks completed successfully for '{txId}' and '{cTxId}'. Verifying script.");

            var scripts = new List <(string Tx, int N)>()
            {
                (dsTx.ToHex(), cn.Value)
            }.AsEnumerable();

            // We take single result, because we will be sending only 1 tx at a time
            var verified = (await rpcMultiClient.VerifyScriptAsync(true, appSettings.DSScriptValidationTimeoutSec, scripts)).Single();

            if (verified.Result != "ok")
            {
                return(AddBanScoreAndReturnResult($"Invalid proof script. Reason: '{verified.Description}'.", cTxId));
            }

            logger.LogInformation($"Successfully verified script for transaction '{cTxId}'. Inserting notification data into database");
            transactionRequestsCheck.RemoveQueriedTransactionId(Request.Host.Host, uTxId);

            var inserted = await txRepository.InsertMempoolDoubleSpendAsync(
                tx.TxInternalId,
                dsTx.GetHash(Const.NBitcoinMaxArraySize).ToBytes(),
                dsTxBytes);

            if (inserted > 0)
            {
                var notificationData = new NotificationData
                {
                    TxExternalId       = uTxId.ToBytes(),
                    DoubleSpendTxId    = ucTxId.ToBytes(),
                    CallbackEncryption = tx.CallbackEncryption,
                    CallbackToken      = tx.CallbackToken,
                    CallbackUrl        = tx.CallbackUrl,
                    TxInternalId       = tx.TxInternalId,
                    BlockHeight        = -1,
                    BlockInternalId    = -1,
                    BlockHash          = null
                };

                eventBus.Publish(new Domain.Models.Events.NewNotificationEvent
                {
                    CreationDate     = clock.UtcNow(),
                    NotificationData = notificationData,
                    NotificationType = CallbackReason.DoubleSpendAttempt,
                    TransactionId    = uTxId.ToBytes()
                });
                logger.LogInformation($"Inserted notification push data into database for '{txId}'.");
            }
            // Submit was successfull we set the x-bsv-dsnt to 0, to signal the node we are not interested in this DS anymore
            this.Response.Headers[DSHeader] = "0";

            return(Ok());
        }
예제 #8
0
        public async Task CatchDoubleSpendOfBlockTxByBlockTx()
        {
            // Create two transactions from same input
            var coin = availableCoins.Dequeue();

            var(txHex1, _)     = CreateNewTransaction(coin, new Money(1000L));
            var(txHex2, txId2) = CreateNewTransaction(coin, new Money(500L));


            var tx1 = HelperTools.ParseBytesToTransaction(HelperTools.HexStringToByteArray(txHex1));
            var tx2 = HelperTools.ParseBytesToTransaction(HelperTools.HexStringToByteArray(txHex2));

            // Transactions should not be the same
            Assert.AreNotEqual(txHex1, txHex2);

            var parentBlockHash = await rpcClient0.GetBestBlockHashAsync();

            var parentBlockHeight = (await rpcClient0.GetBlockHeaderAsync(parentBlockHash)).Height;

            // Send first transaction using mAPI - we want to get DS notification for it
            var payload = await SubmitTransactionAsync(txHex1, true, true);

            Assert.AreEqual(payload.ReturnResult, "success");

            // Mine a new block containing tx1
            var b1Hash = (await rpcClient0.GenerateAsync(1)).Single();


            loggerTest.LogInformation($"Block b1 {b1Hash} was mined containing tx1 {tx1.GetHash()}");
            WaitUntilEventBusIsIdle();

            var calls = Callback.Calls;

            Assert.AreEqual(1, calls.Length);
            var signedJSON   = HelperTools.JSONDeserialize <SignedPayloadViewModel>(calls[0].request);
            var notification = HelperTools.JSONDeserialize <CallbackNotificationViewModelBase>(signedJSON.Payload);

            Assert.AreEqual(CallbackReason.MerkleProof, notification.CallbackReason);

            // Mine sibling block to b1 - without any additional transaction
            var(b2, _) = await MineNextBlockAsync(Array.Empty <Transaction>(), false, parentBlockHash);

            loggerTest.LogInformation($"Block b2 {b2.Header.GetHash()} was mined with only coinbase transaction");

            // Mine a child block to b2, containing tx2. This will create a longer chain and we should be notified about doubleSpend
            var(b3, _) = await MineNextBlockAsync(new [] { tx2 }, true, b2, parentBlockHeight + 2);

            loggerTest.LogInformation($"Block b3 {b3.Header.GetHash()} was mined with a ds transaction tx2 {tx2.GetHash()}");

            // Check if b3 was accepted
            var currentBestBlock = await rpcClient0.GetBestBlockHashAsync();

            Assert.AreEqual(b3.GetHash().ToString(), currentBestBlock, "b3 was not activated");
            WaitUntilEventBusIsIdle();


            calls = Callback.Calls;
            Assert.AreEqual(2, calls.Length);
            signedJSON = HelperTools.JSONDeserialize <SignedPayloadViewModel>(calls[1].request);
            var dsNotification = HelperTools.JSONDeserialize <CallbackNotificationDoubleSpendViewModel>(signedJSON.Payload);

            Assert.AreEqual(CallbackReason.DoubleSpend, dsNotification.CallbackReason);
            Assert.AreEqual(txId2, dsNotification.CallbackPayload.DoubleSpendTxId);
        }
예제 #9
0
        public async Task <SubmitTransactionsResponse> SubmitTransactionsAsync(IEnumerable <SubmitTransaction> requestEnum, UserAndIssuer user)
        {
            var request = requestEnum.ToArray();

            logger.LogInformation($"Processing {request.Length} incoming transactions");
            // Take snapshot of current metadata and use use it for all transactions
            var info = await blockChainInfo.GetInfoAsync();

            var currentMinerId = await minerId.GetCurrentMinerIdAsync();

            var consolidationParameters = info.ConsolidationTxParameters;

            // Use the same quotes for all transactions in single request
            var quotes = feeQuoteRepository.GetValidFeeQuotesByIdentity(user).ToArray();

            if (quotes == null || !quotes.Any())
            {
                throw new Exception("No fee quotes available");
            }

            var responses            = new List <SubmitTransactionOneResponse>();
            var transactionsToSubmit = new List <(string transactionId, SubmitTransaction transaction, bool allowhighfees, bool dontCheckFees, bool listUnconfirmedAncestors, Dictionary <string, object> config)>();
            int failureCount         = 0;

            IDictionary <uint256, byte[]> allTxs = new Dictionary <uint256, byte[]>();

            foreach (var oneTx in request)
            {
                if (!string.IsNullOrEmpty(oneTx.MerkleFormat) && !MerkleFormat.ValidFormats.Any(x => x == oneTx.MerkleFormat))
                {
                    AddFailureResponse(null, $"Invalid merkle format {oneTx.MerkleFormat}. Supported formats: {String.Join(",", MerkleFormat.ValidFormats)}.", ref responses);

                    failureCount++;
                    continue;
                }

                if ((oneTx.RawTx == null || oneTx.RawTx.Length == 0) && string.IsNullOrEmpty(oneTx.RawTxString))
                {
                    AddFailureResponse(null, $"{nameof(SubmitTransaction.RawTx)} is required", ref responses);

                    failureCount++;
                    continue;
                }

                if (oneTx.RawTx == null)
                {
                    try
                    {
                        oneTx.RawTx = HelperTools.HexStringToByteArray(oneTx.RawTxString);
                    }
                    catch (Exception ex)
                    {
                        AddFailureResponse(null, ex.Message, ref responses);

                        failureCount++;
                        continue;
                    }
                }
                uint256 txId       = Hashes.DoubleSHA256(oneTx.RawTx);
                string  txIdString = txId.ToString();
                logger.LogInformation($"Processing transaction: { txIdString }");

                if (oneTx.MerkleProof && (appSettings.DontParseBlocks.Value || appSettings.DontInsertTransactions.Value))
                {
                    AddFailureResponse(txIdString, $"Transaction requires merkle proof notification but this instance of mAPI does not support callbacks", ref responses);

                    failureCount++;
                    continue;
                }

                if (oneTx.DsCheck && (appSettings.DontParseBlocks.Value || appSettings.DontInsertTransactions.Value))
                {
                    AddFailureResponse(txIdString, $"Transaction requires double spend notification but this instance of mAPI does not support callbacks", ref responses);

                    failureCount++;
                    continue;
                }

                if (allTxs.ContainsKey(txId))
                {
                    AddFailureResponse(txIdString, "Transaction with this id occurs more than once within request", ref responses);

                    failureCount++;
                    continue;
                }

                var vc     = new ValidationContext(oneTx);
                var errors = oneTx.Validate(vc);
                if (errors.Any())
                {
                    AddFailureResponse(txIdString, string.Join(",", errors.Select(x => x.ErrorMessage)), ref responses);

                    failureCount++;
                    continue;
                }
                allTxs.Add(txId, oneTx.RawTx);
                bool okToMine  = false;
                bool okToRelay = false;
                Dictionary <string, object> policies = null;
                if (await txRepository.TransactionExistsAsync(txId.ToBytes()))
                {
                    AddFailureResponse(txIdString, "Transaction already known", ref responses);

                    failureCount++;
                    continue;
                }

                Transaction    transaction    = null;
                CollidedWith[] colidedWith    = Array.Empty <CollidedWith>();
                Exception      exception      = null;
                string[]       prevOutsErrors = Array.Empty <string>();
                try
                {
                    transaction = HelperTools.ParseBytesToTransaction(oneTx.RawTx);

                    if (transaction.IsCoinBase)
                    {
                        throw new ExceptionWithSafeErrorMessage("Invalid transaction - coinbase transactions are not accepted");
                    }
                    var(sumPrevOuputs, prevOuts) = await CollectPreviousOuputs(transaction, new ReadOnlyDictionary <uint256, byte[]>(allTxs), rpcMultiClient);

                    prevOutsErrors = prevOuts.Where(x => !string.IsNullOrEmpty(x.Error)).Select(x => x.Error).ToArray();
                    colidedWith    = prevOuts.Where(x => x.CollidedWith != null).Select(x => x.CollidedWith).ToArray();
                    logger.LogInformation($"CollectPreviousOuputs for {txIdString} returned { prevOuts.Length } prevOuts ({prevOutsErrors.Length } prevOutsErrors, {colidedWith.Length} colidedWith).");

                    if (appSettings.CheckFeeDisabled.Value || IsConsolidationTxn(transaction, consolidationParameters, prevOuts))
                    {
                        logger.LogInformation($"{txIdString}: appSettings.CheckFeeDisabled { appSettings.CheckFeeDisabled }");
                        (okToMine, okToRelay) = (true, true);
                    }
                    else
                    {
                        logger.LogInformation($"Starting with CheckFees calculation for {txIdString} and { quotes.Length} quotes.");
                        foreach (var feeQuote in quotes)
                        {
                            var(okToMineTmp, okToRelayTmp) =
                                CheckFees(transaction, oneTx.RawTx.LongLength, sumPrevOuputs, feeQuote);
                            if (GetCheckFeesValue(okToMineTmp, okToRelayTmp) > GetCheckFeesValue(okToMine, okToRelay))
                            {
                                // save best combination
                                (okToMine, okToRelay, policies) = (okToMineTmp, okToRelayTmp, feeQuote.PoliciesDict);
                            }
                        }
                        logger.LogInformation($"Finished with CheckFees calculation for {txIdString} and { quotes.Length} quotes: { (okToMine, okToRelay, policies == null ? "" : string.Join(";", policies.Select(x => x.Key + "=" + x.Value)) )}.");
                    }
                }
                catch (Exception ex)
                {
                    exception = ex;
                }

                if (exception != null || colidedWith.Any() || transaction == null || prevOutsErrors.Any())
                {
                    var oneResponse = new SubmitTransactionOneResponse
                    {
                        Txid         = txIdString,
                        ReturnResult = ResultCodes.Failure,
                        // Include non null ConflictedWith only if a collision has been detected
                        ConflictedWith = !colidedWith.Any() ? null : colidedWith.Select(
                            x => new SubmitTransactionConflictedTxResponse
                        {
                            Txid = x.TxId,
                            Size = x.Size,
                            Hex  = x.Hex,
                        }).ToArray()
                    };

                    if (transaction is null)
                    {
                        oneResponse.ResultDescription = "Can not parse transaction";
                    }
                    else if (exception is ExceptionWithSafeErrorMessage)
                    {
                        oneResponse.ResultDescription = exception.Message;
                    }
                    else if (exception != null)
                    {
                        oneResponse.ResultDescription = "Error fetching inputs";
                    }
                    else if (oneResponse.ConflictedWith != null && oneResponse.ConflictedWith.Any(c => c.Txid == oneResponse.Txid))
                    {
                        oneResponse.ResultDescription = "Transaction already in the mempool";
                        oneResponse.ConflictedWith    = null;
                    }
                    else
                    {
                        // return "Missing inputs" regardless of error returned from gettxouts (which is usually "missing")
                        oneResponse.ResultDescription = "Missing inputs";
                    }
                    logger.LogError($"Can not calculate fee for {txIdString}. Error: {oneResponse.ResultDescription} Exception: {exception?.ToString() ?? ""}");


                    responses.Add(oneResponse);
                    failureCount++;
                    continue;
                }

                // Transactions  was successfully analyzed
                if (!okToMine && !okToRelay)
                {
                    AddFailureResponse(txIdString, "Not enough fees", ref responses);

                    failureCount++;
                }
                else
                {
                    bool allowHighFees            = false;
                    bool dontcheckfee             = okToMine;
                    bool listUnconfirmedAncestors = false;

                    oneTx.TransactionInputs = transaction.Inputs.AsIndexedInputs().Select(x => new TxInput
                    {
                        N        = x.Index,
                        PrevN    = x.PrevOut.N,
                        PrevTxId = x.PrevOut.Hash.ToBytes()
                    }).ToList();
                    if (oneTx.DsCheck)
                    {
                        foreach (TxInput txInput in oneTx.TransactionInputs)
                        {
                            var prevOut = await txRepository.GetPrevOutAsync(txInput.PrevTxId, txInput.PrevN);

                            if (prevOut == null)
                            {
                                listUnconfirmedAncestors = true;
                                break;
                            }
                        }
                    }
                    transactionsToSubmit.Add((txIdString, oneTx, allowHighFees, dontcheckfee, listUnconfirmedAncestors, policies));
                }
            }

            logger.LogInformation($"TransactionsToSubmit: { transactionsToSubmit.Count }: { string.Join("; ", transactionsToSubmit.Select(x => x.transactionId))} ");

            RpcSendTransactions rpcResponse;

            Exception submitException = null;

            if (transactionsToSubmit.Any())
            {
                // Submit all collected transactions in one call
                try
                {
                    rpcResponse = await rpcMultiClient.SendRawTransactionsAsync(
                        transactionsToSubmit.Select(x => (x.transaction.RawTx, x.allowhighfees, x.dontCheckFees, x.listUnconfirmedAncestors, x.config))
                        .ToArray());
                }
                catch (Exception ex)
                {
                    submitException = ex;
                    rpcResponse     = null;
                }
            }
            else
            {
                // Simulate empty response
                rpcResponse = new RpcSendTransactions();
            }


            // Initialize common fields
            var result = new SubmitTransactionsResponse
            {
                Timestamp = clock.UtcNow(),
                MinerId   = currentMinerId,
                CurrentHighestBlockHash   = info.BestBlockHash,
                CurrentHighestBlockHeight = info.BestBlockHeight,
                // TxSecondMempoolExpiry
                // Remaining of the fields are initialized bellow
            };

            if (submitException != null)
            {
                var unableToSubmit = transactionsToSubmit.Select(x =>
                                                                 new SubmitTransactionOneResponse
                {
                    Txid              = x.transactionId,
                    ReturnResult      = ResultCodes.Failure,
                    ResultDescription = "Error while submitting transactions to the node" // do not expose detailed error message. It might contain internal IPS etc
                });

                logger.LogError($"Error while submitting transactions to the node {submitException}");
                responses.AddRange(unableToSubmit);
                result.Txs          = responses.ToArray();
                result.FailureCount = result.Txs.Length; // all of the transactions have failed

                return(result);
            }
            else // submitted without error
            {
                var(submitFailureCount, transformed) = TransformRpcResponse(rpcResponse,
                                                                            transactionsToSubmit.Select(x => x.transactionId).ToArray());
                responses.AddRange(transformed);
                result.Txs          = responses.ToArray();
                result.FailureCount = failureCount + submitFailureCount;


                if (!appSettings.DontInsertTransactions.Value)
                {
                    var successfullTxs = transactionsToSubmit.Where(x => transformed.Any(y => y.ReturnResult == ResultCodes.Success && y.Txid == x.transactionId));
                    logger.LogInformation($"Starting with InsertTxsAsync: { successfullTxs.Count() }: { string.Join("; ", successfullTxs.Select(x => x.transactionId))} (TransactionsToSubmit: { transactionsToSubmit.Count })");
                    var watch = System.Diagnostics.Stopwatch.StartNew();
                    await txRepository.InsertTxsAsync(successfullTxs.Select(x => new Tx
                    {
                        CallbackToken = x.transaction.CallbackToken,
                        CallbackUrl = x.transaction.CallbackUrl,
                        CallbackEncryption = x.transaction.CallbackEncryption,
                        DSCheck = x.transaction.DsCheck,
                        MerkleProof = x.transaction.MerkleProof,
                        MerkleFormat = x.transaction.MerkleFormat,
                        TxExternalId = new uint256(x.transactionId),
                        TxPayload = x.transaction.RawTx,
                        ReceivedAt = clock.UtcNow(),
                        TxIn = x.transaction.TransactionInputs
                    }).ToList(), false);

                    long unconfirmedAncestorsCount = 0;
                    if (rpcResponse.Unconfirmed != null)
                    {
                        List <Tx> unconfirmedAncestors = new();
                        foreach (var unconfirmed in rpcResponse.Unconfirmed)
                        {
                            unconfirmedAncestors.AddRange(unconfirmed.Ancestors.Select(u => new Tx
                            {
                                TxExternalId = new uint256(u.Txid),
                                ReceivedAt   = clock.UtcNow(),
                                TxIn         = u.Vin.Select(i => new TxInput()
                                {
                                    PrevTxId = (new uint256(i.Txid)).ToBytes(),
                                    PrevN    = i.Vout
                                }).ToList()
                            })
                                                          );
                        }
                        await txRepository.InsertTxsAsync(unconfirmedAncestors, true);

                        unconfirmedAncestorsCount = unconfirmedAncestors.Count;
                    }
                    watch.Stop();
                    logger.LogInformation($"Finished with InsertTxsAsync: { successfullTxs.Count() } found unconfirmedAncestors { unconfirmedAncestorsCount } took {watch.ElapsedMilliseconds} ms.");
                }

                return(result);
            }
        }
        public async Task CatchDSOfBlockAncestorTxByBlockTxAsync()
        {
            using CancellationTokenSource cts = new(cancellationTimeout);

            await RegisterNodesWithServiceAndWaitAsync(cts.Token);

            Assert.AreEqual(1, zmqService.GetActiveSubscriptions().Count());

            // Create two transactions from same input
            var coin = availableCoins.Dequeue();

            var(txHex1, txId1)   = CreateNewTransaction(coin, new Money(1000L));
            var(txHexDS, txIdDS) = CreateNewTransaction(coin, new Money(500L));

            // Subscribe invalidtx events
            var invalidTxDetectedSubscription = EventBus.Subscribe <InvalidTxDetectedEvent>();

            // Submit transactions
            var response = await node0.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex1), true, false, cts.Token);

            // Create chain based on first transaction
            var(lastTxHex, lastTxId, mapiCount) = await CreateUnconfirmedAncestorChainAsync(txHex1, txId1, 100, 0, true, cts.Token);

            var parentBlockHash = await rpcClient0.GetBestBlockHashAsync();

            var parentBlockHeight = (await rpcClient0.GetBlockHeaderAsync(parentBlockHash)).Height;

            // Mine a new block containing mAPI transaction and its whole unconfirmed ancestor chain
            var b1Hash = (await rpcClient0.GenerateAsync(1)).Single();

            WaitUntilEventBusIsIdle();

            var calls = Callback.Calls;

            Assert.AreEqual(1, calls.Length);
            var signedJSON   = HelperTools.JSONDeserialize <SignedPayloadViewModel>(calls[0].request);
            var notification = HelperTools.JSONDeserialize <CallbackNotificationViewModelBase>(signedJSON.Payload);

            Assert.AreEqual(CallbackReason.MerkleProof, notification.CallbackReason);

            // Mine sibling block to b1 - without any additional transaction
            var(b2, _) = await MineNextBlockAsync(Array.Empty <Transaction>(), false, parentBlockHash);

            // Mine a child block to b2, containing txDS. This will create a longer chain and we should be notified about doubleSpend
            var txDS = HelperTools.ParseBytesToTransaction(HelperTools.HexStringToByteArray(txHexDS));

            var(b3, _) = await MineNextBlockAsync(new[] { txDS }, true, b2, parentBlockHeight + 2);

            // Check if b3 was accepted
            var currentBestBlock = await rpcClient0.GetBestBlockHashAsync();

            Assert.AreEqual(b3.GetHash().ToString(), currentBestBlock, "b3 was not activated");
            WaitUntilEventBusIsIdle();

            calls = Callback.Calls;
            Assert.AreEqual(2, calls.Length);
            signedJSON = HelperTools.JSONDeserialize <SignedPayloadViewModel>(calls[1].request);
            var dsNotification = HelperTools.JSONDeserialize <CallbackNotificationDoubleSpendViewModel>(signedJSON.Payload);

            Assert.AreEqual(CallbackReason.DoubleSpend, dsNotification.CallbackReason);
            Assert.AreEqual(txIdDS, dsNotification.CallbackPayload.DoubleSpendTxId);
        }