From 6784be2ce21caff3e697c8fd52cb8022bffc7e16 Mon Sep 17 00:00:00 2001 From: rockstardev <5191402+rockstardev@users.noreply.github.com> Date: Sun, 23 Mar 2025 17:55:46 -0500 Subject: [PATCH] Adding logic to count signatures in multisig --- BTCPayServer.Data/Data/PendingTransaction.cs | 5 ++ .../FeatureTests/MultisigTests.cs | 13 +++- .../PendingTransactionService.cs | 78 ++++++++++++++----- .../Views/UIWallets/WalletTransactions.cshtml | 18 +++-- 4 files changed, 86 insertions(+), 28 deletions(-) diff --git a/BTCPayServer.Data/Data/PendingTransaction.cs b/BTCPayServer.Data/Data/PendingTransaction.cs index 2241e6145..707ff242d 100644 --- a/BTCPayServer.Data/Data/PendingTransaction.cs +++ b/BTCPayServer.Data/Data/PendingTransaction.cs @@ -54,6 +54,11 @@ public class PendingTransaction: IHasBlob { public string PSBT { get; set; } public List CollectedSignatures { get; set; } = new(); + + public int? SignaturesCollected { get; set; } + // for example: 3/5 + public int? SignaturesNeeded { get; set; } + public int? SignaturesTotal { get; set; } } public class CollectedSignature diff --git a/BTCPayServer.Tests/FeatureTests/MultisigTests.cs b/BTCPayServer.Tests/FeatureTests/MultisigTests.cs index 3cf6a930b..4fadf46f5 100644 --- a/BTCPayServer.Tests/FeatureTests/MultisigTests.cs +++ b/BTCPayServer.Tests/FeatureTests/MultisigTests.cs @@ -123,14 +123,23 @@ public class MultisigTests : UnitTestBase s.Driver.FindElement(By.Id("Outputs_0__Amount")).SendKeys(amount); s.Driver.FindElement(By.Id("CreatePendingTransaction")).Click(); - // now clicking on View to sign transaction + // validating the state of UI + Assert.Equal("0", s.Driver.FindElement(By.Id("Sigs_0__Collected")).Text); + Assert.Equal("2/3", s.Driver.FindElement(By.Id("Sigs_0__Scheme")).Text); + + // now proceeding to click on sign button and sign transactions SignPendingTransactionWithKey(s, address, derivationScheme, resp1); + Assert.Equal("1", s.Driver.FindElement(By.Id("Sigs_0__Collected")).Text); + SignPendingTransactionWithKey(s, address, derivationScheme, resp2); + Assert.Equal("2", s.Driver.FindElement(By.Id("Sigs_0__Collected")).Text); - // Broadcasting transaction and ensuring there is no longer broadcast button + // we should now have enough signatures to broadcast transaction s.Driver.WaitForElement(By.XPath("//a[text()='Broadcast']")).Click(); s.Driver.FindElement(By.Id("BroadcastTransaction")).Click(); Assert.Contains("Transaction broadcasted successfully", s.FindAlertMessage().Text); + + // now that we broadcast transaction, there shouldn't be broadcast button s.Driver.AssertElementNotFound(By.XPath("//a[text()='Broadcast']")); // Abort pending transaction flow diff --git a/BTCPayServer/HostedServices/PendingTransactionService.cs b/BTCPayServer/HostedServices/PendingTransactionService.cs index c62c8e263..8c5f2848c 100644 --- a/BTCPayServer/HostedServices/PendingTransactionService.cs +++ b/BTCPayServer/HostedServices/PendingTransactionService.cs @@ -94,11 +94,28 @@ public class PendingTransactionService( { var network = networkProvider.GetNetwork(cryptoCode); if (network is null) - { throw new NotSupportedException("CryptoCode not supported"); - } var txId = psbt.GetGlobalTransaction().GetHash(); + + int signaturesNeeded = 0; + int signaturesTotal = 0; + + foreach (var input in psbt.Inputs) + { + var script = input.WitnessScript ?? input.RedeemScript; + if (script is null) + continue; + + var multisigParams = PayToMultiSigTemplate.Instance.ExtractScriptPubKeyParameters(script); + if (multisigParams != null) + { + signaturesNeeded = multisigParams.SignatureCount; + signaturesTotal = multisigParams.PubKeys.Length; + break; // assume consistent multisig scheme across all inputs + } + } + await using var ctx = dbContextFactory.CreateContext(); var pendingTransaction = new PendingTransaction { @@ -109,14 +126,24 @@ public class PendingTransactionService( Expiry = expiry, StoreId = storeId, }; - pendingTransaction.SetBlob(new PendingTransactionBlob { PSBT = psbt.ToBase64() }); + + pendingTransaction.SetBlob(new PendingTransactionBlob + { + PSBT = psbt.ToBase64(), + SignaturesCollected = 0, + SignaturesNeeded = signaturesNeeded, + SignaturesTotal = signaturesTotal + }); + ctx.PendingTransactions.Add(pendingTransaction); await ctx.SaveChangesAsync(cancellationToken); + EventAggregator.Publish(new PendingTransactionEvent { Data = pendingTransaction, Type = PendingTransactionEvent.Created }); + return pendingTransaction; } @@ -143,7 +170,7 @@ public class PendingTransactionService( return null; } - var originalPsbtWorkingCopy = PSBT.Parse(blob.PSBT, network.NBitcoinNetwork); + var dbPsbt = PSBT.Parse(blob.PSBT, network.NBitcoinNetwork); // Deduplicate: Check if this exact PSBT (Base64) was already collected var newPsbtBase64 = psbt.ToBase64(); @@ -155,26 +182,29 @@ public class PendingTransactionService( foreach (var collectedSignature in blob.CollectedSignatures) { var collectedPsbt = PSBT.Parse(collectedSignature.ReceivedPSBT, network.NBitcoinNetwork); - originalPsbtWorkingCopy.Combine(collectedPsbt); // combine changes the object + dbPsbt.Combine(collectedPsbt); // combine changes the object } - var originalPsbtWorkingCopyWithNewPsbt = originalPsbtWorkingCopy.Clone(); // Clone before modifying - originalPsbtWorkingCopyWithNewPsbt.Combine(psbt); + var newWorkingCopyPsbt = dbPsbt.Clone(); // Clone before modifying + newWorkingCopyPsbt.Combine(psbt); // Check if new signatures were actually added - bool newSignaturesCollected = false; - for (int i = 0; i < originalPsbtWorkingCopy.Inputs.Count; i++) - { - if (originalPsbtWorkingCopyWithNewPsbt.Inputs[i].PartialSigs.Count > - originalPsbtWorkingCopy.Inputs[i].PartialSigs.Count) - { - newSignaturesCollected = true; - break; - } - } + var oldPubKeys = dbPsbt.Inputs + .SelectMany(input => input.PartialSigs.Keys) + .ToHashSet(); - if (newSignaturesCollected) + var newPubKeys = newWorkingCopyPsbt.Inputs + .SelectMany(input => input.PartialSigs.Keys) + .ToHashSet(); + + newPubKeys.ExceptWith(oldPubKeys); + + var newSignatures = newPubKeys.Count; + if (newSignatures > 0) { + // TODO: For now we're going with estimation of how many signatures were collected until we find better way + // so for example if we have 4 new signatures and only 2 inputs - number of collected signatures will be 2 + blob.SignaturesCollected += newSignatures / newWorkingCopyPsbt.Inputs.Count(); blob.CollectedSignatures.Add(new CollectedSignature { ReceivedPSBT = newPsbtBase64, @@ -183,8 +213,12 @@ public class PendingTransactionService( pendingTransaction.SetBlob(blob); } - if (originalPsbtWorkingCopyWithNewPsbt.TryFinalize(out _)) + if (newWorkingCopyPsbt.TryFinalize(out _)) { + // TODO: Better logic here + if (blob.SignaturesCollected < blob.SignaturesNeeded) + blob.SignaturesCollected = blob.SignaturesNeeded; + pendingTransaction.State = PendingTransactionState.Signed; } @@ -224,6 +258,11 @@ public class PendingTransactionService( if (pt is null) return; pt.State = PendingTransactionState.Cancelled; await ctx.SaveChangesAsync(); + EventAggregator.Publish(new PendingTransactionEvent + { + Data = pt, + Type = PendingTransactionEvent.Cancelled + }); } public async Task Broadcasted(string cryptoCode, string storeId, string transactionId) @@ -247,6 +286,7 @@ public class PendingTransactionService( public const string Created = nameof(Created); public const string SignatureCollected = nameof(SignatureCollected); public const string Broadcast = nameof(Broadcast); + public const string Cancelled = nameof(Cancelled); public PendingTransaction Data { get; set; } = null!; public string Type { get; set; } = null!; diff --git a/BTCPayServer/Views/UIWallets/WalletTransactions.cshtml b/BTCPayServer/Views/UIWallets/WalletTransactions.cshtml index 130b65cdf..4cf32906f 100644 --- a/BTCPayServer/Views/UIWallets/WalletTransactions.cshtml +++ b/BTCPayServer/Views/UIWallets/WalletTransactions.cshtml @@ -176,21 +176,25 @@ Id State - Signature count + Signatures + Scheme Actions - @foreach (var pendingTransaction in Model.PendingTransactions) + @for (var index = 0; index < Model.PendingTransactions.Length; index++) { + var pendingTransaction = Model.PendingTransactions[index]; + var ptblob = @pendingTransaction.GetBlob(); @pendingTransaction.TransactionId @pendingTransaction.State - @pendingTransaction.GetBlob().CollectedSignatures.Count + @ptblob?.SignaturesCollected + @ptblob?.SignaturesNeeded/@ptblob?.SignaturesTotal - @(pendingTransaction.State == PendingTransactionState.Signed ? "Broadcast" : "View") + @(pendingTransaction.State == PendingTransactionState.Signed ? "Broadcast" : "View") - - Abort + Abort }