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);
                    }
                }
            }
        }
Example #7
0
        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"));
        }
Example #8
0
 /// <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);
        }
Example #12
0
        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);
        }
Example #14
0
        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));
        }
Example #17
0
        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);
        }
Example #18
0
        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);
        }
Example #22
0
        public async Task <byte[]> GetBlockByHeightAsBytesAsync(long blockHeight, CancellationToken?token = null)
        {
            var response = await RequestAsyncWithRetry <string>(token, "getblockbyheight", null, blockHeight, 0);

            return(HelperTools.HexStringToByteArray(response));
        }
Example #23
0
        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);
            }
        }
Example #24
0
        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);
        }
Example #28
0
        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);
            }
        }
Example #29
0
 public async Task <byte[]> GetRawTransactionAsBytesAsync(string txId, CancellationToken?token = null)
 {
     return(HelperTools.HexStringToByteArray(await RequestAsyncWithRetry <string>(token, "getrawtransaction", null, txId, false)));
 }
Example #30
0
        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();
        }