From f3655d1ad97288c7e418e6a6d8281f46ef43fba9 Mon Sep 17 00:00:00 2001 From: Alexander Schlindwein Date: Wed, 30 Sep 2020 15:20:18 +0200 Subject: [PATCH 1/6] LND: Simple channel closing implementation Adds a `CloseChannel` method to `LndClient` which delegates to the existing `LndSwaggerClient.CloseChannelAsync` method. --- .../CloseChannelRequest.cs | 27 +++++++++++++++++++ .../CloseChannelResponse.cs | 20 ++++++++++++++ src/BTCPayServer.Lightning.LND/LndClient.cs | 15 +++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/BTCPayServer.Lightning.Common/CloseChannelRequest.cs create mode 100644 src/BTCPayServer.Lightning.Common/CloseChannelResponse.cs diff --git a/src/BTCPayServer.Lightning.Common/CloseChannelRequest.cs b/src/BTCPayServer.Lightning.Common/CloseChannelRequest.cs new file mode 100644 index 00000000..b7643c59 --- /dev/null +++ b/src/BTCPayServer.Lightning.Common/CloseChannelRequest.cs @@ -0,0 +1,27 @@ +using System; + +namespace BTCPayServer.Lightning +{ + public class CloseChannelRequest + { + public NodeInfo NodeInfo + { + get; set; + } + public string ChannelPointFundingTxIdStr + { + get; set; + } + public long ChannelPointOutputIndex + { + get; set; + } + public static void AssertIsSane(CloseChannelRequest closeChannelRequest) + { + if (closeChannelRequest == null) + throw new ArgumentNullException(nameof(closeChannelRequest)); + if (closeChannelRequest.ChannelPointFundingTxIdStr == null) + throw new ArgumentNullException(nameof(closeChannelRequest.ChannelPointFundingTxIdStr)); + } + } +} diff --git a/src/BTCPayServer.Lightning.Common/CloseChannelResponse.cs b/src/BTCPayServer.Lightning.Common/CloseChannelResponse.cs new file mode 100644 index 00000000..3a86b15f --- /dev/null +++ b/src/BTCPayServer.Lightning.Common/CloseChannelResponse.cs @@ -0,0 +1,20 @@ +namespace BTCPayServer.Lightning +{ + public enum CloseChannelResult + { + Ok, + Failed, + } + + public class CloseChannelResponse + { + public CloseChannelResponse(CloseChannelResult result) + { + Result = result; + } + public CloseChannelResult Result + { + get; set; + } + } +} diff --git a/src/BTCPayServer.Lightning.LND/LndClient.cs b/src/BTCPayServer.Lightning.LND/LndClient.cs index e59b2b28..8d53a348 100644 --- a/src/BTCPayServer.Lightning.LND/LndClient.cs +++ b/src/BTCPayServer.Lightning.LND/LndClient.cs @@ -427,6 +427,21 @@ async Task ILightningClient.OpenChannel(OpenChannelRequest } } + public async Task CloseChannel(CloseChannelRequest closeChannelRequest, CancellationToken cancellation) + { + CloseChannelRequest.AssertIsSane(closeChannelRequest); + cancellation.ThrowIfCancellationRequested(); + try + { + var result = await this.SwaggerClient.CloseChannelAsync(closeChannelRequest.ChannelPointFundingTxIdStr, closeChannelRequest.ChannelPointOutputIndex, cancellation); + return new CloseChannelResponse(CloseChannelResult.Ok); + } + catch (Exception) + { + return new CloseChannelResponse(CloseChannelResult.Failed); + } + } + async Task ILightningClient.GetDepositAddress() { return BitcoinAddress.Create((await SwaggerClient.NewWitnessAddressAsync()).Address, Network); From 4843ab6e9bc1b0badd7a00116faa7783c45da834 Mon Sep 17 00:00:00 2001 From: "Andres G. Aragoneses" Date: Mon, 5 Oct 2020 17:41:56 +0200 Subject: [PATCH 2/6] Move tests-related file from src/ to tests/ folder In fact its namespace was BTCPayServer.Lightning.Tests so it was simply misplaced. --- {src/BTCPayServer.Lightning.All => tests}/ConnectChannels.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {src/BTCPayServer.Lightning.All => tests}/ConnectChannels.cs (100%) diff --git a/src/BTCPayServer.Lightning.All/ConnectChannels.cs b/tests/ConnectChannels.cs similarity index 100% rename from src/BTCPayServer.Lightning.All/ConnectChannels.cs rename to tests/ConnectChannels.cs From 965633754df96814c62c3130eee31e521f94f663 Mon Sep 17 00:00:00 2001 From: "Andres G. Aragoneses" Date: Mon, 5 Oct 2020 17:43:41 +0200 Subject: [PATCH 3/6] Cosmetic: rename CreateChannel to OpenChannel Because this way it matches better with the LN API, and is easier to find. --- tests/ConnectChannels.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ConnectChannels.cs b/tests/ConnectChannels.cs index b287749a..57430095 100644 --- a/tests/ConnectChannels.cs +++ b/tests/ConnectChannels.cs @@ -44,11 +44,11 @@ public static async Task ConnectAll(RPCClient cashCow, IEnumerable Date: Mon, 5 Oct 2020 17:46:36 +0200 Subject: [PATCH 4/6] Cosmetic(tests): more few renames --- tests/{ConnectChannels.cs => ChannelSetup.cs} | 8 ++++---- tests/CommonTests.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) rename tests/{ConnectChannels.cs => ChannelSetup.cs} (95%) diff --git a/tests/ConnectChannels.cs b/tests/ChannelSetup.cs similarity index 95% rename from tests/ConnectChannels.cs rename to tests/ChannelSetup.cs index 57430095..c6ac1a10 100644 --- a/tests/ConnectChannels.cs +++ b/tests/ChannelSetup.cs @@ -14,9 +14,9 @@ namespace BTCPayServer.Lightning.Tests /// /// Utilities to connect channels on regtest /// - public static class ConnectChannels + public static class ChannelSetup { - static ConnectChannels() + static ChannelSetup() { Logs = NullLogger.Instance; } @@ -26,13 +26,13 @@ public static ILogger Logs set; } /// - /// Create channels from all senders to all receivers while mining on cashCow + /// Open channels from all senders to all receivers while mining on cashCow /// /// The miner and liquidity source /// Senders of payment on Lightning Network /// Receivers of payment on Lightning Network /// - public static async Task ConnectAll(RPCClient cashCow, IEnumerable senders, IEnumerable receivers) + public static async Task OpenAll(RPCClient cashCow, IEnumerable senders, IEnumerable receivers) { if(await cashCow.GetBlockCountAsync() <= cashCow.Network.Consensus.CoinbaseMaturity) { diff --git a/tests/CommonTests.cs b/tests/CommonTests.cs index 1fd6673f..cb6f52b1 100644 --- a/tests/CommonTests.cs +++ b/tests/CommonTests.cs @@ -25,7 +25,7 @@ public CommonTests(ITestOutputHelper helper) Docker = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("IN_DOCKER_CONTAINER")); Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; Logs.LogProvider = new XUnitLogProvider(helper); - ConnectChannels.Logs = Logs.LogProvider.CreateLogger("Tests"); + ChannelSetup.Logs = Logs.LogProvider.CreateLogger("Tests"); } public static bool Docker { get; set; } @@ -271,7 +271,7 @@ private async Task EnsureConnectedToDestinations((string Name, ILightningClient { await Task.WhenAll(WaitServersAreUp($"{test.Name} (Customer)", test.Customer), WaitServersAreUp($"{test.Name} (Merchant)", test.Merchant)); Tests.Logs.Tester.LogInformation($"{test.Name}: Connecting channels..."); - await ConnectChannels.ConnectAll(Tester.CreateRPC(), new[] { test.Customer }, new[] { test.Merchant }); + await ChannelSetup.OpenAll(Tester.CreateRPC(), new[] { test.Customer }, new[] { test.Merchant }); Tests.Logs.Tester.LogInformation($"{test.Name}: Channels connected"); } From aea9e9d9b384c47278acf7b89c9fae106d7950dd Mon Sep 17 00:00:00 2001 From: "Andres G. Aragoneses" Date: Tue, 6 Oct 2020 17:56:05 +0200 Subject: [PATCH 5/6] tests: add a test for closing a channel with LND Not sure if we should add this as the Dispose() method of the test class actually: https://stackoverflow.com/a/33516224/544947 --- .../OpenChannelResponse.cs | 4 +++ src/BTCPayServer.Lightning.LND/LndClient.cs | 4 ++- tests/ChannelSetup.cs | 11 ++++++++ tests/CommonTests.cs | 25 +++++++++++++++++++ tests/Tester.cs | 13 +++++++--- 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/BTCPayServer.Lightning.Common/OpenChannelResponse.cs b/src/BTCPayServer.Lightning.Common/OpenChannelResponse.cs index 3a61b822..c72913fa 100644 --- a/src/BTCPayServer.Lightning.Common/OpenChannelResponse.cs +++ b/src/BTCPayServer.Lightning.Common/OpenChannelResponse.cs @@ -23,5 +23,9 @@ public OpenChannelResult Result { get; set; } + public string FundingTxIdIfAvailable + { + get; set; + } } } diff --git a/src/BTCPayServer.Lightning.LND/LndClient.cs b/src/BTCPayServer.Lightning.LND/LndClient.cs index 8d53a348..d1b9da24 100644 --- a/src/BTCPayServer.Lightning.LND/LndClient.cs +++ b/src/BTCPayServer.Lightning.LND/LndClient.cs @@ -373,7 +373,9 @@ async Task ILightningClient.OpenChannel(OpenChannelRequest req.Sat_per_byte = ((int)openChannelRequest.FeeRate.SatoshiPerByte).ToString(); } var result = await this.SwaggerClient.OpenChannelSyncAsync(req, cancellation); - return new OpenChannelResponse(OpenChannelResult.Ok); + var res = new OpenChannelResponse(OpenChannelResult.Ok); + res.FundingTxIdIfAvailable = result.Funding_txid_str; + return res; } catch(SwaggerException ex) when (ex.AsLNDError() is LndError2 lndError && diff --git a/tests/ChannelSetup.cs b/tests/ChannelSetup.cs index c6ac1a10..bb911b29 100644 --- a/tests/ChannelSetup.cs +++ b/tests/ChannelSetup.cs @@ -48,6 +48,15 @@ public static async Task OpenAll(RPCClient cashCow, IEnumerable availableFundingTxIds + = new Dictionary<(ILightningClient, ILightningClient), string>(); + + internal static string GetFundingTxIdForChannel(ILightningClient sender, ILightningClient dest) + { + return availableFundingTxIds.GetValueOrDefault((sender, dest)); + } + public static async Task OpenChannel(RPCClient cashCow, ILightningClient sender, ILightningClient dest) { var destInfo = await dest.GetInfo(); @@ -115,6 +124,8 @@ public static async Task OpenChannel(RPCClient cashCow, ILightningClient sender, } if(openChannel.Result == OpenChannelResult.Ok) { + if (!String.IsNullOrEmpty(openChannel.FundingTxIdIfAvailable)) + availableFundingTxIds[(sender, dest)] = openChannel.FundingTxIdIfAvailable; // generate one block and a bit more time to confirm channel opening await cashCow.GenerateAsync(1); await WaitLNSynched(cashCow, sender); diff --git a/tests/CommonTests.cs b/tests/CommonTests.cs index cb6f52b1..8a81e234 100644 --- a/tests/CommonTests.cs +++ b/tests/CommonTests.cs @@ -536,5 +536,30 @@ public void CanParseLightningURL() Assert.True(LightningConnectionString.TryParse("type=charge;server=http://api-token:foiewnccewuify@127.0.0.1:54938/;allowinsecure=true", out conn)); Assert.Equal("type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify;allowinsecure=true", conn.ToString()); } + + [Fact(Timeout = Timeout)] + public async Task CanCloseLndChannel() + { + // TODO: test in all LN implementations, not just LND + //foreach (var test in Tester.GetTestedPairs()) + //{ + var test = Tester.GetTestedLndPair(); + await EnsureConnectedToDestinations(test); + var customer = (ILightningClient)test.Customer; + var senderChannel = (await customer.ListChannels()).First(); + + var senderInfo = await customer.GetInfo(); + var fundingTxIdForChannel = ChannelSetup.GetFundingTxIdForChannel(customer, test.Merchant); + if (String.IsNullOrEmpty(fundingTxIdForChannel)) + throw new InvalidOperationException("Could not find funding Tx ID of channel"); + var closeChannelRequest = new CloseChannelRequest() + { + NodeInfo = senderInfo.NodeInfoList.First(), + ChannelPointOutputIndex = senderChannel.ChannelPoint.N, + ChannelPointFundingTxIdStr = fundingTxIdForChannel + }; + await test.Merchant.CloseChannel(closeChannelRequest, CancellationToken.None); + //} + } } } diff --git a/tests/Tester.cs b/tests/Tester.cs index 8d983523..5ee69295 100644 --- a/tests/Tester.cs +++ b/tests/Tester.cs @@ -87,11 +87,18 @@ public static LndClient CreateLndClientDest() yield return ("Eclair (Client)", CreateEclairClient()); } + private static (string Name, ILightningClient Customer, ILightningClient Merchant) GetTestedCLightningPair() + => ("C-Lightning", CreateCLightningClient(), CreateCLightningClientDest()); + internal static (string Name, LndClient Customer, LndClient Merchant) GetTestedLndPair() + => ("LND", CreateLndClient(), CreateLndClientDest()); + private static (string Name, ILightningClient Customer, ILightningClient Merchant) GetTestedEclairPair() + => ("Eclair", CreateEclairClient(), CreateEclairClientDest()); + public static IEnumerable<(string Name, ILightningClient Customer, ILightningClient Merchant)> GetTestedPairs() { - yield return ("C-Lightning", CreateCLightningClient(), CreateCLightningClientDest()); - yield return ("LND", CreateLndClient(), CreateLndClientDest()); - yield return ("Eclair", CreateEclairClient(), CreateEclairClientDest()); + yield return GetTestedCLightningPair(); + yield return GetTestedLndPair(); + yield return GetTestedEclairPair(); } } } From 68736d13a7a3e31c182a2756a511894e5b14e0ae Mon Sep 17 00:00:00 2001 From: "Andres G. Aragoneses" Date: Wed, 7 Oct 2020 12:32:00 +0200 Subject: [PATCH 6/6] WIP --- src/BTCPayServer.Lightning.LND/LndClient.cs | 3 +- tests/CommonTests.cs | 70 +++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/BTCPayServer.Lightning.LND/LndClient.cs b/src/BTCPayServer.Lightning.LND/LndClient.cs index d1b9da24..62fbcb17 100644 --- a/src/BTCPayServer.Lightning.LND/LndClient.cs +++ b/src/BTCPayServer.Lightning.LND/LndClient.cs @@ -440,7 +440,8 @@ public async Task CloseChannel(CloseChannelRequest closeCh } catch (Exception) { - return new CloseChannelResponse(CloseChannelResult.Failed); + throw; + //return new CloseChannelResponse(CloseChannelResult.Failed); } } diff --git a/tests/CommonTests.cs b/tests/CommonTests.cs index 8a81e234..7fd6ff1b 100644 --- a/tests/CommonTests.cs +++ b/tests/CommonTests.cs @@ -15,6 +15,76 @@ namespace BTCPayServer.Lightning.Tests { public class CommonTests { + uint CountJsonObjects(string jsonString) + { + uint countJsonInner(char? head, string tail, uint braceNestingLevel, bool insideSubString, uint acc) + { + if (!head.HasValue) + { + if (braceNestingLevel != 0) + throw new ArgumentException("jsonString seems to be invalid"); + return acc; + } + + char? newHead = null; + if (!String.IsNullOrEmpty(tail)) + newHead = tail.First(); + var newTail = newHead == null ? null : tail.Substring(1); + + if (!insideSubString && head == '"') { + return countJsonInner(newHead, newTail, braceNestingLevel, true, acc); + } + if (insideSubString) + { + if (head == '"') + return countJsonInner(newHead, newTail, braceNestingLevel, false, acc); + return countJsonInner(newHead, newTail, braceNestingLevel, insideSubString, acc); + } + + if (head == '{') + return countJsonInner(newHead, newTail, braceNestingLevel + 1, insideSubString, acc); + else if (head == '}') + { + if (braceNestingLevel == 0) + throw new ArgumentException("jsonString seems to be invalid"); + var newNestingLevel = braceNestingLevel - 1; + var newAcc = (newNestingLevel == 0) ? acc + 1 : acc; + return countJsonInner(newHead, newTail, newNestingLevel, insideSubString, newAcc); + } + else + return countJsonInner(newHead, newTail, braceNestingLevel, insideSubString, acc); + } + + if (jsonString == string.Empty) + return 0; + if (!jsonString.StartsWith("{")) + throw new ArgumentException("jsonString should start with {"); + if (!jsonString.EndsWith("}")) + throw new ArgumentException("jsonString should end with }"); + + return countJsonInner(jsonString.First(), jsonString.Substring(1), 0, false, 0); + } + + [Fact()] + public void JsonCounter() + { + Assert.Equal(0u, CountJsonObjects(String.Empty)); + + Assert.Throws(() => CountJsonObjects("x")); // not startswith { + Assert.Throws(() => CountJsonObjects("{x")); // not endswith } + + Assert.Equal(1u, CountJsonObjects("{ \"foo\": 1 }")); + Assert.Equal(2u, CountJsonObjects("{ \"foo\": 1 }{ \"bar\": 2 }")); + Assert.Equal(3u, CountJsonObjects("{ \"foo\": 1 }{ \"bar\": 2 }{ \"baz\": 3 }")); + + // ends with bad nesting level + Assert.Throws(() => CountJsonObjects("{{x}")); + Assert.Throws(() => CountJsonObjects("{x}}")); + + // edge case + Assert.Equal(1u, CountJsonObjects("{ \"foo\": \"{\" }")); + } + #if DEBUG public const int Timeout = 20 * 60 * 1000; #else