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 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 async Task CallbackReceivedAsync(string path, IHeaderDictionary headers, byte[] data) { string host = ""; if (headers.TryGetValue("Host", out var hostValues)) { host = hostValues[0].Split(":")[0]; // chop off port } if (callbackHostConfig != null) { if (!callbackHostConfig.TryGetValue(host, out var hostConfig)) { // Retry with empty string that represents the default host callbackHostConfig.TryGetValue("", out hostConfig); } if (hostConfig != null) { if (hostConfig.CallbackFailurePercent > 0) { if (rnd.Next(0, 100) < hostConfig.CallbackFailurePercent) { stats.IncrementSimulatedCallbackErrors(); throw new Exception("Stress test tool intentionally failing callback"); } } if (hostConfig.MinCallbackDelayMs != null || hostConfig.MaxCallbackDelayMs != null) { // If only one value is present then copy the value from the other one int min = (int)(hostConfig.MinCallbackDelayMs ?? hostConfig.MaxCallbackDelayMs); int max = (int)(hostConfig.MaxCallbackDelayMs ?? hostConfig.MinCallbackDelayMs); int delay = rnd.Next(Math.Min(min, max), Math.Max(min, max)); await Task.Delay(delay); } } } // assume that responses are signed // TODO: decrypting is not currently supported var payload = HelperTools.JSONDeserialize <SignedPayloadViewModel>(Encoding.UTF8.GetString(data)) .Payload; var notification = HelperTools.JSONDeserialize <CallbackNotificationViewModelBase>(payload); stats.IncrementCallbackReceived(host, new uint256(notification.CallbackTxId)); }
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 async Task SubmitTransactionAndWaitForProof2() { var(txHex, txId) = CreateNewTransaction(); var payload = await SubmitTransactionAsync(txHex, merkleProof : true, merkleFormat : MerkleFormat.TSC); Assert.AreEqual(payload.ReturnResult, "success"); // Try to fetch tx from the node var txFromNode = await rpcClient0.GetRawTransactionAsBytesAsync(txId); Assert.AreEqual(txHex, HelperTools.ByteToHexString(txFromNode)); Assert.AreEqual(0, Callback.Calls.Length); var notificationEventSubscription = EventBus.Subscribe <NewNotificationEvent>(); // This is not absolutely necessary, since we ar waiting for NotificationEvent too, but it helps // with troubleshooting: var generatedBlock = await GenerateBlockAndWaitForItTobeInsertedInDBAsync(); loggerTest.LogInformation($"Generated block {generatedBlock} should contain our transaction"); await WaitForEventBusEventAsync(notificationEventSubscription, $"Waiting for merkle notification event for tx {txId}", (evt) => evt.NotificationType == CallbackReason.MerkleProof && new uint256(evt.TransactionId) == new uint256(txId) ); WaitUntilEventBusIsIdle(); // Check if callback was received Assert.AreEqual(1, Callback.Calls.Length); // Verify that it parses merkleproof2 var callback = HelperTools.JSONDeserialize <JSONEnvelopeViewModel>(Callback.Calls[0].request) .ExtractPayload <CallbackNotificationMerkeProof2ViewModel>(); Assert.AreEqual(CallbackReason.MerkleProof, callback.CallbackReason); // Validate callback var blockHeader = BlockHeader.Parse(callback.CallbackPayload.Target, Network.RegTest); Assert.AreEqual(generatedBlock, blockHeader.GetHash()); Assert.AreEqual(new uint256(txId), new uint256(callback.CallbackTxId)); Assert.AreEqual(new uint256(txId), new uint256(callback.CallbackPayload.TxOrId)); }
private static Dictionary <string, object> GetPoliciesDict(string json) { return(HelperTools.JSONDeserialize <Dictionary <string, object> >(json)); }
static async Task SendTransactionsBatch(IEnumerable <string> transactions, HttpClient client, Stats stats, string url, string callbackUrl, string callbackToken, string callbackEncryption) { var query = new List <string>(); string doCallbacks = string.IsNullOrEmpty(callbackUrl) ? "false" : "true"; query.Add($"defaultDsCheck={doCallbacks}"); query.Add($"defaultMerkleProof={doCallbacks}"); if (!string.IsNullOrEmpty(callbackUrl)) { query.Add("defaultCallbackUrl=" + WebUtility.UrlEncode(callbackUrl)); if (!string.IsNullOrEmpty(callbackToken)) { query.Add("defaultCallbackToken=" + WebUtility.UrlEncode(callbackToken)); } if (!string.IsNullOrEmpty(callbackEncryption)) { query.Add("defaultCallbackEncryption=" + WebUtility.UrlEncode(callbackEncryption)); } } string queryString = string.Join("&", query.ToArray()); var ub = new UriBuilder(url); if (ub.Query.Length == 0) { ub.Query = queryString; // automatically adds ? at the beginning } else { ub.Query = ub.Query.Substring(1) + "&" + queryString; // remove leading ? it is added back automatically } string urlWithParams = ub.Uri.ToString(); string callbackHost = ""; if (!string.IsNullOrEmpty(callbackUrl)) { callbackHost = new Uri(callbackUrl).Host; } // We currently submit through REST interface., We could also use binary interface var request = transactions.Select(t => new SubmitTransactionViewModel { RawTx = t, // All other parameters are passed in query string CallbackUrl = null, CallbackToken = null, CallbackEncryption = null, MerkleProof = null, DsCheck = null }).ToArray(); var requestString = HelperTools.JSONSerialize(request, false); var response = await client.PostAsync(urlWithParams, new StringContent(requestString, new UTF8Encoding(false), MediaTypeNames.Application.Json)); var responseAsString = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { Console.WriteLine($"Error while submitting transaction request {responseAsString}"); stats.IncrementRequestErrors(); } else { var rEnvelope = HelperTools.JSONDeserialize <SignedPayloadViewModel>(responseAsString); var r = HelperTools.JSONDeserialize <SubmitTransactionsResponseViewModel>(rEnvelope.Payload); int printLimit = 10; var errorItems = r.Txs.Where(t => t.ReturnResult != "success").ToArray(); var okItems = r.Txs.Where(t => t.ReturnResult == "success").ToArray(); stats.AddRequestTxFailures(callbackHost, errorItems.Select(x => new uint256(x.Txid))); stats.AddOkSubmited(callbackHost, okItems.Select(x => new uint256(x.Txid))); var errors = errorItems .Select(t => t.Txid + " " + t.ReturnResult + " " + t.ResultDescription).ToArray(); var limitedErrors = string.Join(Environment.NewLine, errors.Take(printLimit)); if (errors.Any()) { Console.WriteLine($"Error while submitting transactions. Printing up to {printLimit} out of {errors.Length} errors : {limitedErrors}"); } } }
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); }