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);
        }
Example #5
0
        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));
 }
Example #9
0
        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);
        }