public async Task DummyRegistration(string originWalletName, string originWalletPassword)
        {
            // TODO: Move this functionality into the tests
            var token = new List <byte>();

            // Server ID
            token.AddRange(Encoding.ASCII.GetBytes("".PadRight(34)));

            // IPv4 address
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);

            // IPv6 address
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);

            // Onion address
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);
            token.Add(0x00);

            // Port number
            byte[] portNumber = BitConverter.GetBytes(37123);
            token.Add(portNumber[0]);
            token.Add(portNumber[1]);

            // RSA sig length
            byte[] rsaLength = BitConverter.GetBytes(256);
            token.Add(rsaLength[0]);
            token.Add(rsaLength[1]);

            // RSA signature
            byte[] rsaSig = new byte[256];
            token.AddRange(rsaSig);

            // ECDSA sig length
            byte[] ecdsaLength = BitConverter.GetBytes(128);
            token.Add(ecdsaLength[0]);
            token.Add(ecdsaLength[1]);

            // ECDSA signature
            byte[] ecdsaSig = new byte[128];
            token.AddRange(ecdsaSig);

            // Configuration hash
            token.AddRange(Encoding.ASCII.GetBytes("aa4e984c5655a677716539acc8cbc0ce29331429"));

            // Finally add protocol byte and computed length to beginning of header
            byte[] protocolVersionByte = BitConverter.GetBytes(254);
            byte[] headerLength        = BitConverter.GetBytes(token.Count);

            token.Insert(0, protocolVersionByte[0]);
            token.Insert(1, headerLength[0]);
            token.Insert(2, headerLength[1]);

            Money outputValue = new Money(0.0001m, MoneyUnit.BTC);

            Transaction sendTx = new Transaction();

            // Recognisable string used to tag the transaction within the blockchain
            byte[] bytes = Encoding.UTF8.GetBytes("BREEZE_REGISTRATION_MARKER");
            sendTx.Outputs.Add(new TxOut()
            {
                Value        = outputValue,
                ScriptPubKey = TxNullDataTemplate.Instance.GenerateScriptPubKey(bytes)
            });

            // Add each data-encoding PubKey as a TxOut
            foreach (PubKey pubKey in BlockChainDataConversions.BytesToPubKeys(token.ToArray()))
            {
                TxOut destTxOut = new TxOut()
                {
                    Value        = outputValue,
                    ScriptPubKey = pubKey.ScriptPubKey
                };

                sendTx.Outputs.Add(destTxOut);
            }

            HdAccount highestAcc = null;

            foreach (HdAccount account in this.walletManager.GetAccounts(originWalletName))
            {
                if (highestAcc == null)
                {
                    highestAcc = account;
                }

                if (account.GetSpendableAmount().ConfirmedAmount > highestAcc.GetSpendableAmount().ConfirmedAmount)
                {
                    highestAcc = account;
                }
            }

            // This fee rate is primarily for regtest, testnet and mainnet have actual estimators that work
            FeeRate feeRate = new FeeRate(new Money(10000, MoneyUnit.Satoshi));
            WalletAccountReference  accountRef     = new WalletAccountReference(originWalletName, highestAcc.Name);
            List <Recipient>        recipients     = new List <Recipient>();
            TransactionBuildContext txBuildContext = new TransactionBuildContext(accountRef, recipients);

            txBuildContext.WalletPassword   = originWalletPassword;
            txBuildContext.OverrideFeeRate  = feeRate;
            txBuildContext.Sign             = true;
            txBuildContext.MinConfirmations = 0;

            this.walletTransactionHandler.FundTransaction(txBuildContext, sendTx);

            this.logger.LogDebug("Trying to broadcast transaction: " + sendTx.GetHash());

            await this.broadcasterManager.BroadcastTransactionAsync(sendTx).ConfigureAwait(false);

            var bcResult = this.broadcasterManager.GetTransaction(sendTx.GetHash()).State;

            switch (bcResult)
            {
            case Stratis.Bitcoin.Broadcasting.State.Broadcasted:
            case Stratis.Bitcoin.Broadcasting.State.Propagated:
                this.logger.LogDebug("Broadcasted transaction: " + sendTx.GetHash());
                break;

            case Stratis.Bitcoin.Broadcasting.State.ToBroadcast:
                // Wait for propagation
                var waited = TimeSpan.Zero;
                var period = TimeSpan.FromSeconds(1);
                while (TimeSpan.FromSeconds(21) > waited)
                {
                    // Check BroadcasterManager for broadcast success
                    var transactionEntry = this.broadcasterManager.GetTransaction(sendTx.GetHash());
                    if (transactionEntry != null &&
                        transactionEntry.State == Stratis.Bitcoin.Broadcasting.State.Propagated)
                    {
                        // TODO: This is cluttering up the console, only need to log it once
                        this.logger.LogDebug("Propagated transaction: " + sendTx.GetHash());
                    }
                    await Task.Delay(period).ConfigureAwait(false);

                    waited += period;
                }
                break;

            case Stratis.Bitcoin.Broadcasting.State.CantBroadcast:
                // Do nothing
                break;
            }

            this.logger.LogDebug("Uncertain if transaction was propagated: " + sendTx.GetHash());
        }
Beispiel #2
0
        public void TestMultiClientWithoutTor(int numClients)
        {
            // Workaround for segwit not correctly activating
            Network.RegTest.Consensus.BIP9Deployments[BIP9Deployments.Segwit] = new BIP9DeploymentsParameters(1, 0, DateTime.Now.AddDays(50).ToUnixTimestamp());

            NodeBuilder builder = NodeBuilder.Create(version: "0.15.1");

            CoreNode coreNode = GetCoreNode(builder);

            coreNode.Start();

            // Replicate portions of BreezeServer's Program.cs. Maybe refactor it into a class/function in future
            var serviceProvider = new ServiceCollection()
                                  .AddLogging()
                                  .AddSingleton <Breeze.BreezeServer.Services.ITumblerService, Breeze.BreezeServer.Services.TumblerService>()
                                  .BuildServiceProvider();

            serviceProvider
            .GetService <ILoggerFactory>()
            .AddConsole(LogLevel.Debug);

            // Skip the registration code - that can be tested separately

            string configPath = Path.Combine(coreNode.DataFolder, "breeze.conf");

            File.WriteAllLines(configPath, this.breezeServerConfig);

            BreezeConfiguration config = new BreezeConfiguration(configPath);

            var    coreRpc             = coreNode.CreateRPCClient();
            string ntbServerConfigPath = Path.Combine(coreNode.DataFolder, "server.config");

            File.WriteAllLines(ntbServerConfigPath, GetNTBServerConfig(coreRpc));

            // We need to start up the masternode prior to creating the SBFN instance so that
            // we have the URI available for starting the TumbleBit feature
            // TODO: Also need to see if NTB interactive console interferes with later parts of the test
            new Thread(delegate()
            {
                Thread.CurrentThread.IsBackground = true;
                // By instantiating the TumblerService directly the registration logic is skipped
                var tumbler = serviceProvider.GetService <Breeze.BreezeServer.Services.ITumblerService>();
                tumbler.StartTumbler(config, false, "server.config", Path.GetFullPath(coreNode.DataFolder), false);
            }).Start();

            // Wait for URI file to be written out by the TumblerService
            while (!File.Exists(Path.Combine(coreNode.DataFolder, "uri.txt")))
            {
                Thread.Sleep(1000);
            }

            Console.WriteLine("* URI file detected *");
            Thread.Sleep(5000);

            var serverAddress = File.ReadAllText(Path.Combine(coreNode.DataFolder, "uri.txt"));

            // Not used for this test
            ConfigurationOptionWrapper <object> registrationStoreDirectory = new ConfigurationOptionWrapper <object>("RegistrationStoreDirectory", "");

            // Force SBFN to use the temporary hidden service to connect to the server
            ConfigurationOptionWrapper <object> masternodeUri = new ConfigurationOptionWrapper <object>("MasterNodeUri", serverAddress);

            ConfigurationOptionWrapper <object>[] configurationOptions = { registrationStoreDirectory, masternodeUri };

            List <CoreNode> clientNodes = new List <CoreNode>();

            int apiPortNum = 37229;

            for (int i = 0; i < numClients; i++)
            {
                var temp = builder.CreateStratisPowNode(false, fullNodeBuilder =>
                {
                    fullNodeBuilder
                    .UsePowConsensus()
                    .UseBlockStore()
                    .UseMempool()
                    .UseBlockNotification()
                    .UseTransactionNotification()
                    .AddMining()
                    .UseWallet()
                    .UseWatchOnlyWallet()
                    .UseApi()
                    .AddRPC()
                    .UseTumbleBit(configurationOptions);
                });

                temp.ConfigParameters.AddOrReplace("apiuri", $"http://localhost:{apiPortNum}");

                clientNodes.Add(temp);

                apiPortNum++;
            }

            foreach (var node in clientNodes)
            {
                node.Start();
            }

            // Create the source and destination wallets for nodes
            for (int i = 0; i < numClients; i++)
            {
                var wm1 = clientNodes[i].FullNode.NodeService <IWalletManager>() as WalletManager;
                wm1.CreateWallet("TumbleBit1", $"alice{i}");
                wm1.CreateWallet("TumbleBit1", $"bob{i}");
            }

            // Mined coins only mature after 100 blocks on regtest
            // Additionally, we need to force Segwit to activate in order for NTB to work correctly
            coreRpc.Generate(450);

            while (coreRpc.GetBlockCount() < 450)
            {
                Thread.Sleep(100);
            }

            for (int i = 0; i < numClients; i++)
            {
                coreRpc.AddNode(clientNodes[i].Endpoint, false);
                var rpc = clientNodes[i].CreateRPCClient();
                rpc.AddNode(coreNode.Endpoint, false);

                for (int j = 0; j < numClients; j++)
                {
                    if (i != j)
                    {
                        rpc.AddNode(clientNodes[j].Endpoint, false);
                    }
                }
            }

            for (int i = 0; i < numClients; i++)
            {
                var wm1          = clientNodes[i].FullNode.NodeService <IWalletManager>() as WalletManager;
                var destination1 = wm1.GetUnusedAddress(new WalletAccountReference($"alice{i}", "account 0"));
                coreRpc.SendToAddress(BitcoinAddress.Create(destination1.Address, Network.RegTest), new Money(5.0m, MoneyUnit.BTC));
            }

            clientNodes[0].FullNode.Settings.Logger.LogInformation("Waiting for transactions to propagate and finalise");
            Thread.Sleep(5000);

            coreRpc.Generate(1);

            // Wait for SBFN clients to sync with the core node
            foreach (var node in clientNodes)
            {
                TestHelper.WaitLoop(() => node.CreateRPCClient().GetBestBlockHash() == coreRpc.GetBestBlockHash());
            }

            // Test implementation note: the coins do not seem to immediately appear in the wallet.
            // This is possibly some sort of race condition between the wallet manager and block generation/sync.
            // This extra delay seems to ensure that the coins are definitely in the wallet by the time the
            // transaction count gets logged to the console below.

            // Wait instead of generating a block
            Thread.Sleep(5000);

            for (int i = 0; i < numClients; i++)
            {
                var wm1 = clientNodes[i].FullNode.NodeService <IWalletManager>() as WalletManager;
                //logger1.LogError($"({i}) Number of wallet transactions: " + wm1.GetSpendableTransactionsInWallet($"alice{i}").Count());

                // Connect each client to server and start tumbling
                using (HttpClient client = new HttpClient())
                {
                    client.DefaultRequestHeaders.Accept.Clear();
                    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                    var apiSettings1   = clientNodes[i].FullNode.NodeService <ApiSettings>();
                    var connectContent = new StringContent(new ConnectRequest {
                        OriginWalletName = $"alice{i}"
                    }.ToString(), Encoding.UTF8, "application/json");
                    var connectResponse = client.PostAsync(apiSettings1.ApiUri + "api/TumbleBit/connect", connectContent).GetAwaiter().GetResult();
                    var tumbleContent   = new StringContent(new TumbleRequest {
                        OriginWalletName = $"alice{i}", OriginWalletPassword = "******", DestinationWalletName = $"bob{i}"
                    }.ToString(), Encoding.UTF8, "application/json");
                    var tumbleResponse = client.PostAsync(apiSettings1.ApiUri + "api/TumbleBit/tumble", tumbleContent).GetAwaiter().GetResult();

                    // Note that the TB client takes about 30 seconds to completely start up, as it has to check the server parameters and RSA key proofs
                }

                clientNodes[i].FullNode.Settings.Logger.LogInformation($"Client ({i}) About to start tumbling loop");
            }

            while (true)
            {
                for (int i = 0; i < numClients; i++)
                {
                    clientNodes[i].FullNode.Settings.Logger.LogInformation($"Wallet {i} balance height: " + clientNodes[i].FullNode.Chain.Height);

                    var wm1 = clientNodes[i].FullNode.NodeService <IWalletManager>() as WalletManager;

                    HdAccount alice1 = wm1.GetWalletByName($"alice{i}").GetAccountByCoinType("account 0", (CoinType)Network.RegTest.Consensus.CoinType);

                    clientNodes[i].FullNode.Settings.Logger.LogInformation($"(A{i}) Confirmed: " + alice1.GetSpendableAmount().ConfirmedAmount.ToString());
                    clientNodes[i].FullNode.Settings.Logger.LogInformation($"(A{i}) Unconfirmed: " + alice1.GetSpendableAmount().UnConfirmedAmount.ToString());

                    HdAccount bob1 = wm1.GetWalletByName($"bob{i}").GetAccountByCoinType("account 0", (CoinType)Network.RegTest.Consensus.CoinType);

                    clientNodes[i].FullNode.Settings.Logger.LogInformation($"(B{i}) Confirmed: " + bob1.GetSpendableAmount().ConfirmedAmount.ToString());
                    clientNodes[i].FullNode.Settings.Logger.LogInformation($"(B{i}) Unconfirmed: " + bob1.GetSpendableAmount().UnConfirmedAmount.ToString());

                    clientNodes[i].FullNode.Settings.Logger.LogInformation("===");
                }

                coreRpc.Generate(1);

                // Try to ensure the invalid phase error does not occur
                // (seems to occur when the server has not yet processed a new block and the client has)
                //TestHelper.WaitLoop(() => rpc1.GetBestBlockHash() == coreRpc.GetBestBlockHash());
                //TestHelper.WaitLoop(() => rpc2.GetBestBlockHash() == coreRpc.GetBestBlockHash());

                /*var mempool = node1.FullNode.NodeService<MempoolManager>();
                 * var mempoolTx = mempool.GetMempoolAsync().Result;
                 * if (mempoolTx.Count > 0)
                 * {
                 *  Console.WriteLine("--- Mempool contents ---");
                 *  foreach (var tx in mempoolTx)
                 *  {
                 *      var hex = mempool.GetTransaction(tx).Result;
                 *      Console.WriteLine(tx + " ->");
                 *      Console.WriteLine(hex);
                 *      Console.WriteLine("---");
                 *  }
                 * }*/

                Thread.Sleep(20000);
            }

            if (builder != null)
            {
                builder.Dispose();
            }
        }
        public void TestWithTor()
        {
            // Workaround for segwit not correctly activating
            Network.RegTest.Consensus.BIP9Deployments[BIP9Deployments.Segwit] = new BIP9DeploymentsParameters(1, 0, DateTime.Now.AddDays(50).ToUnixTimestamp());

            using (NodeBuilder builder = NodeBuilder.Create(version: "0.15.1"))
            {
                HttpClient client = null;

                var coreNode = builder.CreateNode(false);

                // This line has no effect currently as the changes to the config get overwritten
                coreNode.ConfigParameters.AddOrReplace("printtoconsole", "0");

                coreNode.ConfigParameters.AddOrReplace("debug", "1");
                //coreNode.ConfigParameters.AddOrReplace("prematurewitness", "1");
                //coreNode.ConfigParameters.AddOrReplace("walletprematurewitness", "1");
                coreNode.ConfigParameters.AddOrReplace("rpcworkqueue", "100");

                coreNode.Start();

                // Replicate portions of BreezeServer's Program.cs. Maybe refactor it into a class/function in future
                var serviceProvider = new ServiceCollection()
                                      .AddLogging()
                                      .AddSingleton <Breeze.BreezeServer.Services.ITumblerService, Breeze.BreezeServer.Services.TumblerService>()
                                      .BuildServiceProvider();

                serviceProvider
                .GetService <ILoggerFactory>()
                .AddConsole(LogLevel.Debug);

                // Skip the registration code - that can be tested separately

                string   configPath         = Path.Combine(coreNode.DataFolder, "breeze.conf");
                string[] breezeServerConfig =
                {
                    "network=regtest", // Only the network setting is currently used from this file
                    "rpc.user=dummy",
                    "rpc.password=dummy",
                    "rpc.url=http://127.0.0.1:26174/",
                    "breeze.ipv4=127.0.0.1",
                    "breeze.ipv6=2001:0db8:85a3:0000:0000:8a2e:0370:7334",
                    "breeze.onion=0123456789ABCDEF",
                    "breeze.port=37123",
                    "breeze.regtxfeevalue=10000",
                    "breeze.regtxoutputvalue=1000",
                    "tumbler.url=http://127.0.0.1:37123/api/v1/",
                    "tumbler.rsakeyfile=/Users/username/.ntumblebitserver/RegTest/Tumbler.pem",
                    "tumbler.ecdsakeyaddress=TVwRFmEKRCnQAgShf3QshBjp1Tmucm1e87"
                };
                File.WriteAllLines(configPath, breezeServerConfig);

                BreezeConfiguration config = new BreezeConfiguration(configPath);

                var      coreRpc             = coreNode.CreateRPCClient();
                string   ntbServerConfigPath = Path.Combine(coreNode.DataFolder, "server.config");
                string[] ntbServerConfig     =
                {
                    "regtest=1",
                    "rpc.url=http://127.0.0.1:" + coreRpc.Address.Port + "/",
                    "rpc.user="******"rpc.password="******"cycle=kotori",
                    "tor.enabled=true",
                    "tor.server=127.0.0.1:9051" // We assume for now that tor has been manually started
                };

                File.WriteAllLines(ntbServerConfigPath, ntbServerConfig);

                // We need to start up the masternode prior to creating the SBFN instance so that
                // we have the URI available for starting the TumbleBit feature
                // TODO: Also need to see if NTB interactive console interferes with later parts of the test
                new Thread(delegate()
                {
                    Thread.CurrentThread.IsBackground = true;
                    // By instantiating the TumblerService directly the registration logic is skipped
                    var tumbler = serviceProvider.GetService <Breeze.BreezeServer.Services.ITumblerService>();
                    tumbler.StartTumbler(config, false, "server.config", Path.GetFullPath(coreNode.DataFolder), false);
                }).Start();

                // Wait for URI file to be written out by the TumblerService
                while (!File.Exists(Path.Combine(coreNode.DataFolder, "uri.txt")))
                {
                    Thread.Sleep(1000);
                }

                Console.WriteLine("* URI file detected *");
                Thread.Sleep(5000);

                var serverAddress = File.ReadAllText(Path.Combine(coreNode.DataFolder, "uri.txt"));

                // Not used for this test
                ConfigurationOptionWrapper <object> registrationStoreDirectory = new ConfigurationOptionWrapper <object>("RegistrationStoreDirectory", Path.Combine(coreNode.DataFolder, "registrationHistory.json"));

                // Force SBFN to connect to the server
                ConfigurationOptionWrapper <object>   masternodeUri         = new ConfigurationOptionWrapper <object>("MasterNodeUri", serverAddress);
                ConfigurationOptionWrapper <object>   torOption             = new ConfigurationOptionWrapper <object>("Tor", true);
                ConfigurationOptionWrapper <object>   tumblerProtocolOption = new ConfigurationOptionWrapper <object>("TumblerProtocol", TumblerProtocolType.Tcp);
                ConfigurationOptionWrapper <object>   useDummyAddressOption = new ConfigurationOptionWrapper <object>("UseDummyAddress", false);
                ConfigurationOptionWrapper <object>[] configurationOptions  = { registrationStoreDirectory, masternodeUri, torOption, tumblerProtocolOption, useDummyAddressOption };

                // Logging for NTB client code
                ConsoleLoggerProcessor loggerProcessor = new ConsoleLoggerProcessor();
                Logs.Configure(new FuncLoggerFactory(i => new CustomerConsoleLogger(i, Logs.SupportDebug(true), false, loggerProcessor)));

                CoreNode node1 = builder.CreateStratisPowNode(true, fullNodeBuilder =>
                {
                    fullNodeBuilder
                    .UsePowConsensus()
                    .UseBlockStore()
                    .UseMempool()
                    .UseBlockNotification()
                    .UseTransactionNotification()
                    .AddMining()
                    .UseWallet()
                    .UseWatchOnlyWallet()
                    .UseApi()
                    .AddRPC()
                    .UseTumbleBit(configurationOptions);
                });

                var apiSettings = node1.FullNode.NodeService <ApiSettings>();

                NLog.Config.LoggingConfiguration config1 = LogManager.Configuration;
                var folder = Path.Combine(node1.DataFolder, "Logs");

                var tbTarget = new FileTarget();
                tbTarget.Name             = "tumblebit";
                tbTarget.FileName         = Path.Combine(folder, "tumblebit.txt");
                tbTarget.ArchiveFileName  = Path.Combine(folder, "tb-${date:universalTime=true:format=yyyy-MM-dd}.txt");
                tbTarget.ArchiveNumbering = ArchiveNumberingMode.Sequence;
                tbTarget.ArchiveEvery     = FileArchivePeriod.Day;
                tbTarget.MaxArchiveFiles  = 7;
                tbTarget.Layout           = "[${longdate:universalTime=true} ${threadid}${mdlc:item=id}] ${level:uppercase=true}: ${callsite} ${message}";
                tbTarget.Encoding         = Encoding.UTF8;

                var ruleTb = new LoggingRule("*", NLog.LogLevel.Debug, tbTarget);
                config1.LoggingRules.Add(ruleTb);

                config1.AddTarget(tbTarget);

                // Apply new rules.
                LogManager.ReconfigExistingLoggers();

                //node1.NotInIBD();

                // Create the source and destination wallets
                var wm1 = node1.FullNode.NodeService <IWalletManager>() as WalletManager;
                //var wm2 = node2.FullNode.NodeService<IWalletManager>() as WalletManager;
                wm1.CreateWallet("TumbleBit1", "alice");
                wm1.CreateWallet("TumbleBit1", "bob");

                // Mined coins only mature after 100 blocks on regtest
                // Additionally, we need to force Segwit to activate in order for NTB to work correctly
                coreRpc.Generate(450);

                var rpc1 = node1.CreateRPCClient();
                //var rpc2 = node2.CreateRPCClient();

                coreRpc.AddNode(node1.Endpoint, false);
                rpc1.AddNode(coreNode.Endpoint, false);

                var amount      = new Money(5.0m, MoneyUnit.BTC);
                var destination = wm1.GetUnusedAddress(new WalletAccountReference("alice", "account 0"));

                coreRpc.SendToAddress(BitcoinAddress.Create(destination.Address, Network.RegTest), amount);

                Console.WriteLine("Waiting for transaction to propagate and finalise");
                Thread.Sleep(5000);

                coreRpc.Generate(1);

                // Wait for SBFN to sync with the core node
                TestHelper.WaitLoop(() => rpc1.GetBestBlockHash() == coreRpc.GetBestBlockHash());

                // Test implementation note: the coins do not seem to immediately appear in the wallet.
                // This is possibly some sort of race condition between the wallet manager and block generation/sync.
                // This extra delay seems to ensure that the coins are definitely in the wallet by the time the
                // transaction count gets logged to the console below.

                // Wait instead of generating a block
                Thread.Sleep(5000);

                //var log = node1.FullNode.NodeService<ILogger>();
                Console.WriteLine("Number of wallet transactions: " + wm1.GetSpendableTransactionsInWallet("alice").Count());

                // Connect to server and start tumbling
                using (client = new HttpClient())
                {
                    client.DefaultRequestHeaders.Accept.Clear();
                    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                    // Sample returned output
                    // {"tumbler":"ctb://<onionaddress>.onion?h=<confighash>","denomination":"0.01000000","fee":"0.00010000","network":"RegTest","estimate":"22200"}
                    var connectContent = new StringContent(new ConnectRequest {
                        OriginWalletName = "alice"
                    }.ToString(), Encoding.UTF8, "application/json");
                    var connectResponse = client.PostAsync(apiSettings.ApiUri + "api/TumbleBit/connect", connectContent).GetAwaiter().GetResult();

                    //Assert.StartsWith("[{\"", connectResponse);

                    var tumbleModel = new TumbleRequest {
                        OriginWalletName = "alice", OriginWalletPassword = "******", DestinationWalletName = "bob"
                    };
                    var tumbleContent = new StringContent(tumbleModel.ToString(), Encoding.UTF8, "application/json");

                    var tumbleResponse = client.PostAsync(apiSettings.ApiUri + "api/TumbleBit/tumble", tumbleContent).GetAwaiter().GetResult();

                    // Note that the TB client takes about 30 seconds to completely start up, as it has to check the server parameters and
                    // RSA key proofs

                    //Assert.StartsWith("[{\"", tumbleResponse);
                }

                HdAccount alice = null;
                HdAccount bob   = null;
                // TODO: Move forward specific numbers of blocks and check interim states? TB tests already do that
                for (int i = 0; i < 80; i++)
                {
                    Console.WriteLine("Wallet balance height: " + node1.FullNode.Chain.Height);

                    alice = wm1.GetWalletByName("alice").GetAccountByCoinType("account 0", (CoinType)Network.RegTest.Consensus.CoinType);

                    Console.WriteLine("(A) Confirmed: " + alice.GetSpendableAmount().ConfirmedAmount.ToString());
                    Console.WriteLine("(A) Unconfirmed: " + alice.GetSpendableAmount().UnConfirmedAmount.ToString());

                    bob = wm1.GetWalletByName("bob").GetAccountByCoinType("account 0", (CoinType)Network.RegTest.Consensus.CoinType);

                    Console.WriteLine("(B) Confirmed: " + bob.GetSpendableAmount().ConfirmedAmount.ToString());
                    Console.WriteLine("(B) Unconfirmed: " + bob.GetSpendableAmount().UnConfirmedAmount.ToString());

                    coreRpc.Generate(1);
                    builder.SyncNodes();

                    // Try to ensure the invalid phase error does not occur
                    // (seems to occur when the server has not yet processed a new block and the client has)
                    TestHelper.WaitLoop(() => rpc1.GetBestBlockHash() == coreRpc.GetBestBlockHash());

                    var mempool   = node1.FullNode.NodeService <MempoolManager>();
                    var mempoolTx = mempool.GetMempoolAsync().Result;
                    if (mempoolTx.Count > 0)
                    {
                        Console.WriteLine("--- Mempool contents ---");
                        foreach (var tx in mempoolTx)
                        {
                            var hex = mempool.GetTransaction(tx).Result;
                            Console.WriteLine(tx + " ->");
                            Console.WriteLine(hex);
                            Console.WriteLine("---");
                        }
                    }

                    Thread.Sleep(20000);
                }

                // Check destination wallet for tumbled coins
                Assert.True(alice.GetSpendableAmount().ConfirmedAmount < new Money(5.0m, MoneyUnit.BTC));
                Assert.True(bob.GetSpendableAmount().ConfirmedAmount > new Money(0.0035m, MoneyUnit.BTC));

                // TODO: Need to amend TumblerService so that it can be shut down within the test

                if (client != null)
                {
                    client.Dispose();
                    client = null;
                }
            }
        }