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)); }
public static bool TryParseTransaction(byte[] transaction, out Transaction result) { try { result = HelperTools.ParseBytesToTransaction(transaction); return(true); } catch (Exception) { result = null; return(false); } }
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)); }
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); }
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); } }
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()); }
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); }
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); }