void ExecuteAndCheckSendTransactions(string[] txsHex, RpcSendTransactions expected, RpcSendTransactions node0Response, RpcSendTransactions node1Response) { rpcClientFactoryMock.SetUpPredefinedResponse( ("umockNode0:sendrawtransactions", node0Response), ("umockNode1:sendrawtransactions", node1Response)); var c = new RpcMultiClient(new MockNodes(2), rpcClientFactoryMock, NullLogger <RpcMultiClient> .Instance); var r = c.SendRawTransactionsAsync( txsHex.Select(x => (HelperTools.HexStringToByteArray(x), true, true)).ToArray()).Result; Assert.AreEqual(JsonSerializer.Serialize(expected), JsonSerializer.Serialize(r)); }
void TestMixedTxC1(RpcSendTransactions node0Response, RpcSendTransactions node1Response) { ExecuteAndCheckSendTransactions(new[] { txC1Hex }, new RpcSendTransactions { Known = new string[0], Evicted = new string[0], Invalid = new[] { new RpcSendTransactions.RpcInvalidTx() { Txid = txC1Hash, RejectReason = "Mixed results" } } }, node0Response, node1Response); }
public void SendRawTransationsInvalid() { // Empty response means, that everything was accepted var invalidResponse = new RpcSendTransactions { Known = new string[0], Evicted = new string[0], Invalid = new[] { new RpcSendTransactions.RpcInvalidTx { Txid = txC1Hash } } }; ExecuteAndCheckSendTransactions( new [] { txC1Hex }, invalidResponse, invalidResponse, invalidResponse); }
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 static (int failureCount, SubmitTransactionOneResponse[] responses) TransformRpcResponse(RpcSendTransactions rpcResponse, string[] allSubmitedTxIds) { // Track which transaction was already processed, so that we only return one response per txid: var processed = new Dictionary <string, object>(StringComparer.InvariantCulture); int failed = 0; var respones = new List <SubmitTransactionOneResponse>(); if (rpcResponse.Invalid != null) { foreach (var invalid in rpcResponse.Invalid) { if (processed.TryAdd(invalid.Txid, null)) { respones.Add(new SubmitTransactionOneResponse { Txid = invalid.Txid, ReturnResult = ResultCodes.Failure, ResultDescription = (invalid.RejectCode + " " + invalid.RejectReason).Trim(), ConflictedWith = invalid.CollidedWith?.Select(t => new SubmitTransactionConflictedTxResponse { Txid = t.Txid, Size = t.Size, Hex = t.Hex } ).ToArray() }); failed++; } } } if (rpcResponse.Evicted != null) { foreach (var evicted in rpcResponse.Evicted) { if (processed.TryAdd(evicted, null)) { respones.Add(new SubmitTransactionOneResponse { Txid = evicted, ReturnResult = ResultCodes.Failure, ResultDescription = "evicted" // This only happens if mempool is full and contain no P2P transactions (which have low priority) }); failed++; } } } if (rpcResponse.Known != null) { foreach (var known in rpcResponse.Known) { if (processed.TryAdd(known, null)) { respones.Add(new SubmitTransactionOneResponse { Txid = known, ReturnResult = ResultCodes.Success, ResultDescription = "Already known" }); } } } // If a transaction is not present in response, then it was successfully accepted as a new transaction foreach (var txId in allSubmitedTxIds) { if (!processed.ContainsKey(txId)) { respones.Add(new SubmitTransactionOneResponse { Txid = txId, ReturnResult = ResultCodes.Success, }); } } return(failed, respones.ToArray()); }
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); } }