public async Task StoreUnconfirmedParentsOnSubmitTxAsync() { using CancellationTokenSource cts = new(cancellationTimeout); await RegisterNodesWithServiceAndWaitAsync(cts.Token); Assert.AreEqual(1, zmqService.GetActiveSubscriptions().Count()); // Subscribe invalidtx events var invalidTxDetectedSubscription = EventBus.Subscribe <InvalidTxDetectedEvent>(); // Create and submit first transaction var coin = availableCoins.Dequeue(); var(txHex1, txId1) = CreateNewTransaction(coin, new Money(1000L)); var response = await node0.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex1), true, false, cts.Token); // Create chain based on first transaction with last transaction being submited to mAPI var(lastTxHex, lastTxId, mapiCount) = await CreateUnconfirmedAncestorChainAsync(txHex1, txId1, 100, 0, true, cts.Token); // Check that first tx is in database long?txInternalId1 = await TxRepositoryPostgres.GetTransactionInternalId((new uint256(txId1)).ToBytes()); Assert.IsTrue(txInternalId1.HasValue); Assert.AreNotEqual(0, txInternalId1.Value); }
string RefreseHash(string hash) { var array = HelperTools.HexStringToByteArray(hash); Array.Reverse(array); return(HelperTools.ByteToHexString(array)); }
private async Task <(string, string, int)> CreateUnconfirmedAncestorChainAsync(string txHex1, string txId1, int length, int sendToMAPIRate, bool sendLastToMAPI = false, CancellationToken?cancellationToken = null) { var curTxHex = txHex1; var curTxId = txId1; var mapiTxCount = 0; for (int i = 0; i < length; i++) { Transaction.TryParse(curTxHex, Network.RegTest, out Transaction curTx); var curTxCoin = new Coin(curTx, 0); (curTxHex, curTxId) = CreateNewTransaction(curTxCoin, new Money(1000L)); // Submit every sendToMAPIRate tx to mapi with dsCheck if ((sendToMAPIRate != 0 && i % sendToMAPIRate == 0) || (sendLastToMAPI && i == length - 1)) { var payload = await SubmitTransactionAsync(curTxHex, true, true); Assert.AreEqual(payload.ReturnResult, "success"); mapiTxCount++; } else { _ = await node0.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(curTxHex), true, false, cancellationToken); } } return(curTxHex, curTxId, mapiTxCount); }
private async Task <(string, string)> CatchInMempoolDoubleSpendZMQMessage() { using CancellationTokenSource cts = new CancellationTokenSource(cancellationTimeout); await RegisterNodesWithServiceAndWait(cts.Token); Assert.AreEqual(1, zmqService.GetActiveSubscriptions().Count()); // Subscribe invalidtx events var invalidTxDetectedSubscription = eventBus.Subscribe <InvalidTxDetectedEvent>(); // 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)); // Transactions should not be the same Assert.AreNotEqual(txHex1, txHex2); // Send first transaction using MAPI var payload = await SubmitTransactionAsync(txHex1); Assert.AreEqual(payload.ReturnResult, "success"); // Send second transaction using RPC try { _ = await node0.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex2), true, false, cts.Token); } catch (Exception rpcException) { // Double spend will throw txn-mempool-conflict exception Assert.AreEqual("258: txn-mempool-conflict", rpcException.Message); } // InvalidTx event should be fired var invalidTxEvent = await invalidTxDetectedSubscription.ReadAsync(cts.Token); Assert.AreEqual(InvalidTxRejectionCodes.TxMempoolConflict, invalidTxEvent.Message.RejectionCode); Assert.AreEqual(txId2, invalidTxEvent.Message.TxId); Assert.IsNotNull(invalidTxEvent.Message.CollidedWith, "bitcoind did not return CollidedWith"); Assert.AreEqual(1, invalidTxEvent.Message.CollidedWith.Length); Assert.AreEqual(txId1, invalidTxEvent.Message.CollidedWith[0].TxId); WaitUntilEventBusIsIdle(); // Check if callback was received var calls = Callback.Calls; Assert.AreEqual(1, calls.Length); var callback = HelperTools.JSONDeserialize <JSONEnvelopeViewModelGet>(calls[0].request) .ExtractPayload <CallbackNotificationDoubleSpendViewModel>(); Assert.AreEqual(CallbackReason.DoubleSpendAttempt, callback.CallbackReason); Assert.AreEqual(new uint256(txId1), new uint256(callback.CallbackTxId)); Assert.AreEqual(new uint256(txId2), new uint256(callback.CallbackPayload.DoubleSpendTxId)); return(txHex1, txHex2); }
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 RemovedFromMempoolEventAsync(RemovedFromMempoolEvent e) { if (e.Message.Reason == RemovedFromMempoolMessage.Reasons.CollisionInBlockTx) { var removedTxId = new uint256(e.Message.TxId).ToBytes(); var txWithDSCheck = (await txRepository.GetTxsForDSCheckAsync(new[] { removedTxId }, false)).ToArray(); if (txWithDSCheck.Any()) { // Try to insert the block into DB. If block is already present in DB nothing will be done await blockParser.NewBlockDiscoveredAsync(new NewBlockDiscoveredEvent() { CreationDate = clock.UtcNow(), BlockHash = e.Message.BlockHash }); foreach (var tx in txWithDSCheck) { await txRepository.InsertBlockDoubleSpendAsync( tx.TxInternalId, new uint256(e.Message.BlockHash).ToBytes(), new uint256(e.Message.CollidedWith.TxId).ToBytes(), HelperTools.HexStringToByteArray(e.Message.CollidedWith.Hex)); var notificationEvent = new NewNotificationEvent() { CreationDate = clock.UtcNow(), NotificationType = CallbackReason.DoubleSpend, TransactionId = tx.TxExternalIdBytes }; eventBus.Publish(notificationEvent); } } } }
public async Task RejectSubmitWithWithParamCNTooBig() { await Nodes.CreateNodeAsync(new Node("node1", 0, "mocked", "mocked", null, null)); await QueryReturnPositiveAsync(); IList <(string, string)> queryParams = new List <(string, string)> { ("txid", txC0Hash), ("ctxid", txC1Hash), ("n", "0"), ("cn", "10") }; var bytes = HelperTools.HexStringToByteArray(txC1Hex); var reqContent = new ByteArrayContent(bytes); reqContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); var response = await Post <string>(PrepareQueryParams(MapiServer.ApiDSSubmit, queryParams), Client, reqContent, HttpStatusCode.BadRequest); var responseString = await response.httpResponse.Content.ReadAsStringAsync(); Assert.IsTrue(responseString.Contains("banned")); Assert.IsTrue(banList.IsHostBanned("localhost")); }
/// <summary> /// Replaces currently known transactions with a set of new transactions /// </summary> /// <param name="data"></param> public void SetUpTransaction(params string[] data) { transactions.Clear(); foreach (var tx in data) { AddKnownTransaction(HelperTools.HexStringToByteArray(tx)); } }
public async Task With2NodesOnlyOneDoubleSpendShouldBeSent() { using CancellationTokenSource cts = new(cancellationTimeout); // 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)); // Transactions should not be the same Assert.AreNotEqual(txHex1, txHex2); // Send first transaction using MAPI var payload = await SubmitTransactionAsync(txHex1, false, true); // start another node and connect the nodes // then wait for the new node to sync up before sending a DS tx var node1 = StartBitcoind(1, new BitcoindProcess[] { node0 }); await SyncNodesBlocksAsync(cts.Token, node0, node1); Assert.AreEqual(1, await node1.RpcClient.GetConnectionCountAsync()); await node1.RpcClient.DisconnectNodeAsync(node0.Host, node0.P2Port); do { await Task.Delay(100); } while ((await node1.RpcClient.GetConnectionCountAsync()) > 0); // Send second transaction _ = await node1.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex2), true, false, cts.Token); await node1.RpcClient.GenerateAsync(1); await node1.RpcClient.AddNodeAsync(node0.Host, node0.P2Port); do { await Task.Delay(100); } while ((await node1.RpcClient.GetConnectionCountAsync()) == 0); // We are sleeping here for a second to make sure that after the nodes were reconnected // there wasn't any additional notification sent because of node1 await Task.Delay(1000); var notifications = await TxRepositoryPostgres.GetNotificationsForTestsAsync(); foreach (var notification in notifications) { loggerTest.LogInformation($"NotificationType: {notification.NotificationType}; TxId: {notification.TxInternalId}"); } Assert.AreEqual(1, notifications.Length); }
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 async Task NotifyMempoolDSForAllTxWithDsCheckInChainAsync() { using CancellationTokenSource cts = new(cancellationTimeout); await RegisterNodesWithServiceAndWaitAsync(cts.Token); Assert.AreEqual(1, zmqService.GetActiveSubscriptions().Count()); // Subscribe invalidtx events var invalidTxDetectedSubscription = EventBus.Subscribe <InvalidTxDetectedEvent>(); // Create and submit first transaction var coin = availableCoins.Dequeue(); var(txHex1, txId1) = CreateNewTransaction(coin, new Money(1000L)); var response = await node0.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex1), true, false, cts.Token); // Create chain based on first transaction with every 10th transaction being submited to mAPI var(lastTxHex, lastTxId, mapiCount) = await CreateUnconfirmedAncestorChainAsync(txHex1, txId1, 100, 10, false, cts.Token); // Create ds transaction Transaction.TryParse(txHex1, Network.RegTest, out Transaction dsTx); var dsTxCoin = new Coin(dsTx, 0); var(txHexDs, txIdDs) = CreateNewTransaction(dsTxCoin, new Money(500L)); // Send transaction using RPC try { _ = await node0.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHexDs), true, false, cts.Token); } catch (Exception rpcException) { // Double spend will throw txn-mempool-conflict exception Assert.AreEqual("258: txn-mempool-conflict", rpcException.Message); } // InvalidTx event should be fired var invalidTxEvent = await invalidTxDetectedSubscription.ReadAsync(cts.Token); Assert.AreEqual(InvalidTxRejectionCodes.TxMempoolConflict, invalidTxEvent.Message.RejectionCode); WaitUntilEventBusIsIdle(); // Check if correct number of callbacks was received var calls = Callback.Calls; Assert.AreEqual(mapiCount, calls.Length); var callback = HelperTools.JSONDeserialize <JSONEnvelopeViewModel>(calls[0].request) .ExtractPayload <CallbackNotificationDoubleSpendViewModel>(); Assert.AreEqual(CallbackReason.DoubleSpendAttempt, callback.CallbackReason); Assert.AreEqual(-1, callback.BlockHeight); }
public void TestInitialize() { Initialize(mockedServices: true); ApiKeyAuthentication = AppSettings.RestAdminAPIKey; rpcClientFactoryMock = server.Services.GetRequiredService <IRpcClientFactory>() as RpcClientFactoryMock; if (rpcClientFactoryMock != null) { rpcClientFactoryMock.AddKnownBlock(0, HelperTools.HexStringToByteArray(TestBase.genesisBlock)); rpcClientFactoryMock.Reset(); // remove calls that are used to test node connection when adding a new node } }
public async Task SubmitSameTransactioMultipleTimesAsync() { using CancellationTokenSource cts = new(cancellationTimeout); var(txHex1, txId1) = CreateNewTransaction(); // mAPI, mAPI var(txHex2, txId2) = CreateNewTransaction(); // mAPI, RPC var(txHex3, txId3) = CreateNewTransaction(); // RPC, mAPI var tx1_payload1 = await SubmitTransactionAsync(txHex1); var tx1_payload2 = await SubmitTransactionAsync(txHex1); Assert.AreEqual(tx1_payload1.ReturnResult, "success"); Assert.AreEqual(tx1_payload2.ReturnResult, "failure"); Assert.AreEqual(tx1_payload2.ResultDescription, "Transaction already known"); var tx2_payload1 = await SubmitTransactionAsync(txHex2); Assert.AreEqual(tx2_payload1.ReturnResult, "success"); var tx2_result2 = await Assert.ThrowsExceptionAsync <RpcException>( () => node0.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex2), true, false, cts.Token), "Transaction already in the mempool"); var tx3_result1 = await node0.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex3), true, false, cts.Token); var tx3_payload2 = await SubmitTransactionAsync(txHex3); Assert.AreEqual(tx3_result1, txId3); Assert.AreEqual(tx3_payload2.ReturnResult, "failure"); Assert.AreEqual(tx3_payload2.ResultDescription, "Transaction already in the mempool"); // Mine block and than resend all 3 transactions using mAPI var generatedBlock = await GenerateBlockAndWaitForItTobeInsertedInDBAsync(); var tx1_payload3 = await SubmitTransactionAsync(txHex1); var tx2_payload3 = await SubmitTransactionAsync(txHex2); var tx3_payload3 = await SubmitTransactionAsync(txHex3); Assert.AreEqual(tx1_payload3.ReturnResult, "failure"); Assert.AreEqual(tx1_payload3.ResultDescription, "Transaction already known"); Assert.AreEqual(tx2_payload3.ReturnResult, "failure"); Assert.AreEqual(tx2_payload3.ResultDescription, "Transaction already known"); Assert.AreEqual(tx3_payload3.ReturnResult, "failure"); Assert.AreEqual(tx3_payload3.ResultDescription, "Missing inputs"); Assert.IsNull(tx3_payload3.ConflictedWith); }
public async Task DoubleSpendMempoolCheckCleanUp() { //arrange cleanUpTxService.Pause(); var cleanUpTxTriggeredSubscription = eventBus.Subscribe <CleanUpTxTriggeredEvent>(); (List <Tx> txList, uint256 firstBlockHash) = await CreateAndInsertTxWithMempoolAsync(dsCheckMempool : true); WaitUntilEventBusIsIdle(); var doubleSpendTx = Transaction.Parse(Tx2Hex, Network.Main); List <byte[]> dsTxId = new List <byte[]> { doubleSpendTx.GetHash().ToBytes() }; var txsWithDSCheck = (await TxRepositoryPostgres.GetTxsForDSCheckAsync(dsTxId, true)).ToArray(); var txPayload = HelperTools.HexStringToByteArray(tx2Hex); foreach (var dsTx in txsWithDSCheck) { await TxRepositoryPostgres.InsertMempoolDoubleSpendAsync( dsTx.TxInternalId, dsTx.TxExternalIdBytes, txPayload); } WaitUntilEventBusIsIdle(); var doubleSpends = (await TxRepositoryPostgres.GetTxsToSendMempoolDSNotificationsAsync()).ToList(); Assert.AreEqual(1, doubleSpends.Count()); foreach (var txDoubleSpend in doubleSpends) { await TxRepositoryPostgres.SetNotificationSendDateAsync(CallbackReason.DoubleSpendAttempt, txDoubleSpend.TxInternalId, -1, txDoubleSpend.DoubleSpendTxId, MockedClock.UtcNow); } doubleSpends = (await TxRepositoryPostgres.GetTxsToSendMempoolDSNotificationsAsync()).ToList(); Assert.AreEqual(0, doubleSpends.Count()); using (MockedClock.NowIs(DateTime.UtcNow.AddDays(cleanUpTxAfterDays))) { await ResumeAndWaitForCleanup(cleanUpTxTriggeredSubscription); // check if everything in db was cleared await CheckBlockNotPresentInDb(firstBlockHash); await CheckTxListNotPresentInDbAsync(txList); } }
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)); }
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 override void Initialize(bool mockedServices = false, IEnumerable <KeyValuePair <string, string> > overridenSettings = null) { base.Initialize(mockedServices, overridenSettings); // setup repositories NodeRepository = server.Services.GetRequiredService <INodeRepository>() as NodeRepositoryPostgres; TxRepositoryPostgres = server.Services.GetRequiredService <ITxRepository>() as TxRepositoryPostgres; FeeQuoteRepository = server.Services.GetRequiredService <IFeeQuoteRepository>() as FeeQuoteRepositoryPostgres; MinerId = server.Services.GetRequiredService <IMinerId>(); // setup common services Nodes = server.Services.GetRequiredService <INodes>(); BlockChainInfo = server.Services.GetRequiredService <IBlockChainInfo>(); RpcMultiClient = server.Services.GetRequiredService <IRpcMultiClient>(); EventBus = server.Services.GetRequiredService <IEventBus>(); rpcClientFactoryMock = server.Services.GetRequiredService <IRpcClientFactory>() as RpcClientFactoryMock; feeQuoteRepositoryMock = server.Services.GetRequiredService <IFeeQuoteRepository>() as FeeQuoteRepositoryMock; FeeQuoteRepositoryMock.quoteExpiryMinutes = quoteExpiryMinutes; if (rpcClientFactoryMock != null) { rpcClientFactoryMock.SetUpTransaction( txC3Hex, txC2Hex, txZeroFeeHex, txZeroFeeInput1Hex, txZeroFeeInput2Hex, tx2Hex, tx2Input1Hex, tx2Input2Hex); rpcClientFactoryMock.AddKnownBlock(0, HelperTools.HexStringToByteArray(genesisBlock)); rpcClientFactoryMock.Reset(); // remove calls that are used to test node connection when adding a new node } }
public async Task AllAncestorsAreNotInDBForSecondMAPITxIfChainContainsOtherTxsAsync() { using CancellationTokenSource cts = new(cancellationTimeout); await RegisterNodesWithServiceAndWaitAsync(cts.Token); Assert.AreEqual(1, zmqService.GetActiveSubscriptions().Count()); // Subscribe invalidtx events var invalidTxDetectedSubscription = EventBus.Subscribe <InvalidTxDetectedEvent>(); // Create and submit first transaction var coin = availableCoins.Dequeue(); var(txHex1, txId1) = CreateNewTransaction(coin, new Money(1000L)); var response = await node0.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex1), true, false, cts.Token); // Create chain based on first transaction with last transaction being submited to mAPI var(lastTxHex, lastTxId, mapiCount) = await CreateUnconfirmedAncestorChainAsync(txHex1, txId1, 50, 0, true, cts.Token); // Create another transaction through RPC (lastTxHex, lastTxId, mapiCount) = await CreateUnconfirmedAncestorChainAsync(lastTxHex, lastTxId, 1, 0, false, cts.Token); // Create another transaction but don't submit it Transaction.TryParse(lastTxHex, Network.RegTest, out Transaction lastTx); var curTxCoin = new Coin(lastTx, 0); var(curTxHex, curTxId) = CreateNewTransaction(curTxCoin, new Money(1000L)); // Validate that inputs are not already in the database Transaction.TryParse(curTxHex, Network.RegTest, out Transaction curTx); foreach (var txInput in curTx.Inputs) { var prevOut = await txRepository.GetPrevOutAsync(txInput.PrevOut.Hash.ToBytes(), txInput.PrevOut.N); Assert.IsNull(prevOut); } }
public async Task SubmitTxsWithOneThatCausesDS() { using CancellationTokenSource cts = new(cancellationTimeout); // 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(txHex3, txId3) = CreateNewTransaction(); // Transactions should not be the same Assert.AreNotEqual(txHex1, txHex2); Assert.AreNotEqual(txHex1, txHex3); Assert.AreNotEqual(txHex2, txHex3); // Send first transaction _ = await node0.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex1), true, false, cts.Token); // Send second and third transaction using MAPI var payload = await SubmitTransactionsAsync(new string[] { txHex2, txHex3 }); // Should have one failure Assert.AreEqual(1, payload.FailureCount); // Check transactions Assert.AreEqual(2, payload.Txs.Length); var tx2 = payload.Txs.First(t => t.Txid == txId2); var tx3 = payload.Txs.First(t => t.Txid == txId3); // Tx2 should fail and Tx3 should succeed Assert.AreEqual("failure", tx2.ReturnResult); Assert.AreEqual("success", tx3.ReturnResult); // Tx2 should be conflicted transaction for Tx2 Assert.AreEqual(1, tx2.ConflictedWith.Length); Assert.AreEqual(txId1, tx2.ConflictedWith.First().Txid); }
public async Task SubmitTxThatCausesDS() { using CancellationTokenSource cts = new(cancellationTimeout); // 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)); // Transactions should not be the same Assert.AreNotEqual(txHex1, txHex2); // Send first transaction _ = await node0.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex1), true, false, cts.Token); // Send second transaction using MAPI var payload = await SubmitTransactionAsync(txHex2); Assert.AreEqual("failure", payload.ReturnResult); Assert.AreEqual(1, payload.ConflictedWith.Length); Assert.AreEqual(txId1, payload.ConflictedWith.First().Txid); }
public async Task <byte[]> GetBlockByHeightAsBytesAsync(long blockHeight, CancellationToken?token = null) { var response = await RequestAsyncWithRetry <string>(token, "getblockbyheight", null, blockHeight, 0); return(HelperTools.HexStringToByteArray(response)); }
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 RejectSubmit4InvalidParamsAsync() { var bytes = HelperTools.HexStringToByteArray(txC0Hex); var reqContent = new ByteArrayContent(bytes); reqContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); // We should get banned because we didn't set query parameters var(_, httpResponse) = await Post <string>(MapiServer.ApiDSSubmit, Client, reqContent, HttpStatusCode.BadRequest); string responseString = await httpResponse.Content.ReadAsStringAsync(); Assert.IsTrue(responseString.Contains("banned")); Assert.IsTrue(banList.IsHostBanned("localhost")); IList <(string, string)> queryParams = new List <(string, string)>(); // We should get banned because we didn't set 'ctxid' query parameter banList.RemoveFromBanList("localhost"); queryParams.Add(("txid", "a")); await Post <string>(PrepareQueryParams(MapiServer.ApiDSSubmit, queryParams), Client, reqContent, HttpStatusCode.BadRequest); Assert.IsTrue(responseString.Contains("banned")); Assert.IsTrue(banList.IsHostBanned("localhost")); // We should get banned because we didn't set valid txId query parameter banList.RemoveFromBanList("localhost"); queryParams.Add(("ctxid", "a")); await Post <string>(PrepareQueryParams(MapiServer.ApiDSSubmit, queryParams), Client, reqContent, HttpStatusCode.BadRequest); Assert.IsTrue(responseString.Contains("banned")); Assert.IsTrue(banList.IsHostBanned("localhost")); // We should get banned because we didn't set valid ctxId query parameter banList.RemoveFromBanList("localhost"); queryParams.Clear(); queryParams.Add(("txid", txC0Hash)); queryParams.Add(("ctxid", "a")); await Post <string>(PrepareQueryParams(MapiServer.ApiDSSubmit, queryParams), Client, reqContent, HttpStatusCode.BadRequest); Assert.IsTrue(responseString.Contains("banned")); Assert.IsTrue(banList.IsHostBanned("localhost")); // We should not get banned because txid == ctxid, but banscore should be increased by 10 banList.RemoveFromBanList("localhost"); queryParams.Clear(); queryParams.Add(("txid", txC0Hash)); queryParams.Add(("ctxid", txC0Hash)); await Post <string>(PrepareQueryParams(MapiServer.ApiDSSubmit, queryParams), Client, reqContent, HttpStatusCode.BadRequest); Assert.IsFalse(banList.IsHostBanned("localhost")); Assert.AreEqual(banList.ReturnBanScore("localhost"), HostBanList.WarningScore); // We should not get banned because 'n' is not set, but banscore should be increased by 10 banList.RemoveFromBanList("localhost"); //await QueryReturnPositive(); queryParams.Clear(); queryParams.Add(("txid", txC0Hash)); queryParams.Add(("ctxid", txC1Hash)); await Post <string>(PrepareQueryParams(MapiServer.ApiDSSubmit, queryParams), Client, reqContent, HttpStatusCode.BadRequest); Assert.IsFalse(banList.IsHostBanned("localhost")); Assert.AreEqual(banList.ReturnBanScore("localhost"), HostBanList.WarningScore); // We should not get banned because 'cn' is not set, but banscore should be increased by 10 banList.RemoveFromBanList("localhost"); queryParams.Clear(); queryParams.Add(("txid", txC0Hash)); queryParams.Add(("ctxid", txC1Hash)); queryParams.Add(("n", "0")); await Post <string>(PrepareQueryParams(MapiServer.ApiDSSubmit, queryParams), Client, reqContent, HttpStatusCode.BadRequest); Assert.IsFalse(banList.IsHostBanned("localhost")); Assert.AreEqual(banList.ReturnBanScore("localhost"), HostBanList.WarningScore); // We should not get banned because we didn't query for transaction, but banscore should be increased by 10 banList.RemoveFromBanList("localhost"); queryParams.Clear(); queryParams.Add(("txid", txC0Hash)); queryParams.Add(("ctxid", txC1Hash)); queryParams.Add(("n", "0")); queryParams.Add(("cn", "0")); (_, httpResponse) = await Post <string>(PrepareQueryParams(MapiServer.ApiDSSubmit, queryParams), Client, reqContent, HttpStatusCode.BadRequest); responseString = await httpResponse.Content.ReadAsStringAsync(); Assert.IsTrue(responseString.Contains("banned")); Assert.IsTrue(banList.IsHostBanned("localhost")); }
public async Task InvalidTxDetectedAsync(InvalidTxDetectedEvent e) { if (e.Message.RejectionCode == InvalidTxRejectionCodes.TxMempoolConflict || e.Message.RejectionCode == InvalidTxRejectionCodes.TxDoubleSpendDetected) { if (e.Message.CollidedWith != null && e.Message.CollidedWith.Length > 0) { var collisionTxList = e.Message.CollidedWith.Select(t => new uint256(t.TxId).ToBytes()); var txsWithDSCheck = (await txRepository.GetTxsForDSCheckAsync(collisionTxList, true)).ToArray(); if (txsWithDSCheck.Any()) { var dsTxId = new uint256(e.Message.TxId).ToBytes(); var dsTxPayload = string.IsNullOrEmpty(e.Message.Hex) ? Array.Empty <byte>() : HelperTools.HexStringToByteArray(e.Message.Hex); foreach (var tx in txsWithDSCheck) { var inserted = await txRepository.InsertMempoolDoubleSpendAsync( tx.TxInternalId, dsTxId, dsTxPayload); if (inserted == 0) { return; } var notificationData = new NotificationData { TxExternalId = tx.TxExternalIdBytes, DoubleSpendTxId = dsTxId, CallbackUrl = tx.CallbackUrl, CallbackEncryption = tx.CallbackEncryption, CallbackToken = tx.CallbackToken, TxInternalId = tx.TxInternalId, BlockHeight = -1, BlockInternalId = -1, BlockHash = null }; var notificationEvent = new NewNotificationEvent() { CreationDate = clock.UtcNow(), NotificationType = CallbackReason.DoubleSpendAttempt, TransactionId = tx.TxExternalIdBytes, NotificationData = notificationData }; eventBus.Publish(notificationEvent); } } } } }
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 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); }
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 <byte[]> GetRawTransactionAsBytesAsync(string txId, CancellationToken?token = null) { return(HelperTools.HexStringToByteArray(await RequestAsyncWithRetry <string>(token, "getrawtransaction", null, txId, false))); }
public async Task MultipleInputsWithDS() { using CancellationTokenSource cts = new(30000); // startup another node and link it to the first node node1 = StartBitcoind(1, new BitcoindProcess[] { node0 }); var syncTask = SyncNodesBlocksAsync(cts.Token, node0, node1); StartupLiveMAPI(); var coin0 = availableCoins.Dequeue(); var coin2 = availableCoins.Dequeue(); var tx1 = CreateDS_OP_RETURN_Tx(new Coin[] { coin0, availableCoins.Dequeue(), coin2, availableCoins.Dequeue() }, 0, 2); var tx1Hex = tx1.ToHex(); var tx1Id = tx1.GetHash().ToString(); var payload = await SubmitTransactions(new string[] { tx1Hex }); loggerTest.LogInformation($"Submiting {tx1Id} with dsCheck enabled"); var httpResponse = await PerformRequestAsync(Client, HttpMethod.Get, MapiServer.ApiDSQuery + "/" + tx1Id); // Wait for tx to be propagated to node 1 before submiting a doublespend tx to node 1 await WaitForTxToBeAcceptedToMempool(node1, tx1Id, cts.Token); // Create double spend tx and submit it to node 1 var(txHex2, txId2) = CreateNewTransaction(coin0, new Money(1000L)); await syncTask; loggerTest.LogInformation($"Submiting {txId2} with doublespend"); await Assert.ThrowsExceptionAsync <RpcException>(async() => await node1.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex2), true, false)); // Wait for a bit for node and Live mAPI to process all events await Task.Delay(3000); loggerTest.LogInformation("Retrieving notification data"); var notifications = await TxRepositoryPostgres.GetNotificationsForTestsAsync(); Assert.AreEqual(1, notifications.Length); Assert.AreEqual(txId2, new uint256(notifications.Single().DoubleSpendTxId).ToString()); //Create another DS tx which should not trigger another notification var(txHex3, txId3) = CreateNewTransaction(coin2, new Money(5000L)); loggerTest.LogInformation($"Submiting {txId3} with doublespend"); await Assert.ThrowsExceptionAsync <RpcException>(async() => await node1.RpcClient.SendRawTransactionAsync(HelperTools.HexStringToByteArray(txHex3), true, false)); await Task.Delay(3000); notifications = await TxRepositoryPostgres.GetNotificationsForTestsAsync(); Assert.AreEqual(1, notifications.Length); await StopMAPI(); }