diff --git a/src/Services/LightningService.cs b/src/Services/LightningService.cs
index 17aa15b6..972ecd2f 100644
--- a/src/Services/LightningService.cs
+++ b/src/Services/LightningService.cs
@@ -192,15 +192,17 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest)
try
{
- var signedPsbtCount = channelOperationRequest.ChannelOperationRequestPsbts.Count(
+ var humanSignaturesCount = channelOperationRequest.ChannelOperationRequestPsbts.Count(
x => channelOperationRequest.Wallet != null &&
!x.IsFinalisedPSBT &&
!x.IsInternalWalletPSBT &&
- (channelOperationRequest.Wallet.IsHotWallet || !x.IsTemplatePSBT));
- if (channelOperationRequest.Wallet != null && signedPsbtCount > channelOperationRequest.Wallet.MofN)
+ !x.IsTemplatePSBT);
+
+ //If it is a hot wallet, we dont check the number of (human) signatures
+ if (channelOperationRequest.Wallet != null && !channelOperationRequest.Wallet.IsHotWallet && channelOperationRequest.Wallet != null && humanSignaturesCount != channelOperationRequest.Wallet.MofN -1)
{
- _logger.LogError("The number of signatures exceeds the value set for this wallet");
- throw new InvalidOperationException("The number of signatures exceeds the value set for this wallet");
+ _logger.LogError("The number of human signatures does not match the number of signatures required for this wallet, expected {MofN} but got {HumanSignaturesCount}", channelOperationRequest.Wallet.MofN-1, humanSignaturesCount);
+ throw new InvalidOperationException("The number of human signatures does not match the number of signatures required for this wallet");
}
if (!combinedPSBT.TryGetVirtualSize(out var estimatedVsize))
{
diff --git a/test/FundsManager.Tests/Services/LightningServiceTests.cs b/test/FundsManager.Tests/Services/LightningServiceTests.cs
index 8aff26ee..ac9d813f 100644
--- a/test/FundsManager.Tests/Services/LightningServiceTests.cs
+++ b/test/FundsManager.Tests/Services/LightningServiceTests.cs
@@ -1059,6 +1059,244 @@ public async Task OpenChannel_SuccessSingleSigBip39()
//TODO Remove hack
LightningService.CreateLightningClient = originalCreateLightningClient;
}
+
+ ///
+ /// This tests makes sure that if a multisig wallet is used, the number of signatures is correct.
+ /// This means that we need in in a m-of-n multisig, m-1 signatures so nodeguard is that last one to sign to avoid leaking signatures with SIGHASH_NONE
+ ///
+ [Fact]
+ public async Task OpenChannel_FailedIncorrectNumberOfHumanSigs()
+ {
+ // Arrange
+ Environment.SetEnvironmentVariable("NBXPLORER_URI", "http://10.0.0.2:38762");
+ var dbContextFactory = new Mock>();
+
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(databaseName: "ChannelOpenDb")
+ .Options;
+ var context = new ApplicationDbContext(options);
+ dbContextFactory.Setup(x => x.CreateDbContextAsync(default)).ReturnsAsync(context);
+ var channelOperationRequestRepository = new Mock();
+ var nodeRepository = new Mock();
+
+ var channelOpReqPsbts = new List();
+ var userSignedPSBT =
+ "cHNidP8BAF4BAAAAAfxbrSOgX+b0TEE/+djT9eYQrMqkbB0oS5eACIYo69ilAQAAAAD/////AYSRNXcAAAAAIgAgknIr2R4V8Bi4hnqXM/qI2ZXEy9MNhs8bc7M8k6KHNCAAAAAATwEENYfPAy8RJCyAAAAB/DvuQjoBjOttImoGYyiO0Pte4PqdeQqzcNAw4Ecw5sgDgI4uHNSCvdBxlpQ8WoEz0WmvhgIra7A4F3FkTsB0RNcQH8zk3jAAAIABAACAAQAAgE8BBDWHzwNWrAP0gAAAAfkIrkpmsP+hqxS1WvDOSPKnAiXLkBCQLWkBr5C5Po+BAlGvFeBbuLfqwYlbP19H/+/s2DIaAu8iKY+J0KIDffBgEGDzoLMwAACAAQAAgAEAAIBPAQQ1h88DfblGjQAAAACA3rcaovFO7X83IvRJXhrQefWfwPOD5bJ72dtvXpkhIAOv6z6lwqcNxKoocpZKXi/xFyrzRmob/tA5tiZlSX/FRhDtAhDIMAAAgAEAAIAAAAAAAAEBKwCUNXcAAAAAIgAg0KnQhQLDgpnwn8miRsBXVMFC0ribpYvNSiY/lUGGzM4iAgLYVMVgz+bATgvrRDQbanlASVXtiUwPt9yCgkQfv2kssUcwRAIgQMEU4f0gB9/Sgiw79s3Ug0BO201upuwiKqoUv6/svesCIGmKmt82DHfnLJsbKD7e2y4xEbc/Z1L/kMMkf4zQXuZLAgEDBAIAAAABBWlSIQLYVMVgz+bATgvrRDQbanlASVXtiUwPt9yCgkQfv2kssSEDAmf/CxGXSG9xiPljcG/e5CXFnnukFn0pJ64Q9U2aNL8hA2/OK1mLPmSxkVJC5GJuM8/inCj45Y6pksEvbHlmsVWpU64iBgLYVMVgz+bATgvrRDQbanlASVXtiUwPt9yCgkQfv2kssRgfzOTeMAAAgAEAAIABAACAAAAAAAAAAAAiBgMCZ/8LEZdIb3GI+WNwb97kJcWee6QWfSknrhD1TZo0vxhg86CzMAAAgAEAAIABAACAAAAAAAAAAAAiBgNvzitZiz5ksZFSQuRibjPP4pwo+OWOqZLBL2x5ZrFVqRjtAhDIMAAAgAEAAIAAAAAAAAAAAAAAAAAAAA==";
+ channelOpReqPsbts.Add(new ChannelOperationRequestPSBT()
+ {
+ PSBT = userSignedPSBT,
+ });
+
+ //Lets add a second signed "human" PSBT
+
+ channelOpReqPsbts.Add(new ChannelOperationRequestPSBT()
+ {
+ PSBT = userSignedPSBT,
+ });
+
+ var destinationNode = new Node()
+ {
+ PubKey = "03485d8dcdd149c87553eeb80586eb2bece874d412e9f117304446ce189955d375",
+ ChannelAdminMacaroon = "def",
+ Endpoint = "10.0.0.2"
+ };
+
+ var wallet = CreateWallet.MultiSig(_internalWallet);
+ var operationRequest = new ChannelOperationRequest
+ {
+ RequestType = OperationRequestType.Open,
+ SourceNode = new Node()
+ {
+ PubKey = "03b48034270e522e4033afdbe43383d66d426638927b940d09a8a7a0de4d96e807",
+ ChannelAdminMacaroon = "abc",
+ Endpoint = "10.0.0.1"
+ },
+ DestNode = destinationNode,
+ Wallet = wallet,
+ ChannelOperationRequestPsbts = channelOpReqPsbts,
+ };
+
+ channelOperationRequestRepository
+ .Setup(x => x.GetById(It.IsAny()))
+ .ReturnsAsync(operationRequest);
+
+ var nodes = new List {destinationNode};
+
+ nodeRepository
+ .Setup(x => x.GetAllManagedByNodeGuard())
+ .Returns(Task.FromResult(nodes));
+
+ var lightningClient = Interceptor.For()
+ .Setup(x => x.GetNodeInfoAsync(
+ Arg.Ignore(),
+ Arg.Ignore(),
+ null,
+ Arg.Ignore()
+ ))
+ .Returns(MockHelpers.CreateAsyncUnaryCall(
+ new NodeInfo()
+ {
+ Node = new LightningNode()
+ {
+ Addresses =
+ {
+ new NodeAddress()
+ {
+ Network = "tcp",
+ Addr = "10.0.0.2"
+ }
+ }
+ }
+ }));
+
+ var originalCreateLightningClient = LightningService.CreateLightningClient;
+ LightningService.CreateLightningClient = (_) => lightningClient;
+
+ lightningClient
+ .Setup(x => x.ConnectPeerAsync(
+ Arg.Ignore(),
+ Arg.Ignore(),
+ null,
+ Arg.Ignore()
+ ))
+ .Returns(MockHelpers.CreateAsyncUnaryCall(new ConnectPeerResponse()));
+
+ var noneUpdate = new OpenStatusUpdate();
+ var chanPendingUpdate = new OpenStatusUpdate
+ {
+ ChanPending = new PendingUpdate()
+ };
+ var channelPoint = new ChannelPoint
+ {
+ FundingTxidBytes =
+ ByteString.CopyFromUtf8("e59fa8edcd772213239daef2834d9021d1aecc591d605b426ae32c4bec5fdd7d"),
+ OutputIndex = 1
+ };
+ var chanOpenUpdate = new OpenStatusUpdate
+ {
+ ChanOpen = new ChannelOpenUpdate()
+ {
+ ChannelPoint = channelPoint
+ }
+ };
+
+ var psbtFundUpdate = new OpenStatusUpdate
+ {
+ PsbtFund = new ReadyForPsbtFunding()
+ {
+ Psbt = ByteString.FromBase64(userSignedPSBT)
+ }
+ };
+ lightningClient
+ .Setup(x => x.OpenChannel(
+ Arg.Ignore(),
+ Arg.Ignore(),
+ null,
+ Arg.Ignore()
+ ))
+ .Returns(MockHelpers.CreateAsyncServerStreamingCall(
+ new List()
+ {
+ noneUpdate,
+ chanPendingUpdate,
+ psbtFundUpdate,
+ chanOpenUpdate
+ }));
+
+ channelOperationRequestRepository
+ .Setup(x => x.Update(It.IsAny()))
+ .Returns((true, ""));
+
+ var userSignedPsbtParsed = PSBT.Parse(userSignedPSBT, Network.RegTest);
+ var utxoChanges = new UTXOChanges();
+ var input = userSignedPsbtParsed.Inputs[0];
+ var utxoList = new List()
+ {
+ new UTXO()
+ {
+ Outpoint = input.PrevOut,
+ Index = 0,
+ ScriptPubKey = input.WitnessUtxo.ScriptPubKey,
+ KeyPath = new KeyPath("0/0"),
+ },
+ };
+
+ utxoChanges.Confirmed = new UTXOChange() {UTXOs = utxoList};
+
+ var channelOperationRequestPsbtRepository = new Mock();
+ channelOperationRequestPsbtRepository
+ .Setup(x => x.AddAsync(It.IsAny()))
+ .ReturnsAsync((true, ""));
+
+ lightningClient
+ .Setup(x => x.FundingStateStep(
+ Arg.Ignore(),
+ Arg.Ignore(),
+ null,
+ default))
+ .Returns(new FundingStateStepResp());
+
+ lightningClient
+ .Setup(x => x.FundingStateStepAsync(
+ Arg.Ignore(),
+ Arg.Ignore(),
+ null,
+ default))
+ .Returns(MockHelpers.CreateAsyncUnaryCall(new FundingStateStepResp()));
+
+ // Mock channel repository
+ var channelRepository = new Mock();
+
+ channelRepository
+ .Setup(x => x.GetByChanId(It.IsAny()))
+ .ReturnsAsync(() => null);
+
+ //Mock List channels async
+ var listChannelsResponse = new ListChannelsResponse
+ {
+ Channels =
+ {
+ new Lnrpc.Channel
+ {
+ Active = true,
+ RemotePubkey = "03b48034270e522e4033afdbe43383d66d426638927b940d09a8a7a0de4d96e807",
+ ChannelPoint =
+ $"{LightningHelper.DecodeTxId(channelPoint.FundingTxidBytes)}:{channelPoint.OutputIndex}",
+ ChanId = 124,
+ Capacity = 1000,
+ LocalBalance = 100,
+ RemoteBalance = 900
+ }
+ }
+ };
+
+ lightningClient
+ .Setup(x => x.ListChannelsAsync(
+ Arg.Ignore(),
+ Arg.Ignore(),
+ null,
+ default))
+ .Returns(MockHelpers.CreateAsyncUnaryCall(listChannelsResponse));
+
+ var lightningService = new LightningService(_logger,
+ channelOperationRequestRepository.Object,
+ nodeRepository.Object,
+ dbContextFactory.Object,
+ channelOperationRequestPsbtRepository.Object,
+ channelRepository.Object,
+ null,
+ GetNBXplorerServiceFullyMocked(utxoChanges).Object,
+ null);
+
+ // Act
+ var act = async () => await lightningService.OpenChannel(operationRequest);
+
+ // Assert
+ await act.Should().ThrowAsync();
+
+ //TODO Remove hack
+ LightningService.CreateLightningClient = originalCreateLightningClient;
+ }
[Fact]
public async Task OpenChannel_SuccessSingleSig()