From 5c380e94198a96e470ec03c9e292412424e9d082 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sun, 20 Apr 2025 00:13:51 +0900 Subject: [PATCH] Add support for wallet policy (BIP388) --- .../BTCPayServer.Client.csproj | 2 +- .../BTCPayServer.Rating.csproj | 2 +- BTCPayServer.Tests/FastTests.cs | 62 +++++-- BTCPayServer/BTCPayServer.csproj | 1 + BTCPayServer/DerivationSchemeParser.cs | 161 +++++++++++++++++- .../PayJoin/PayJoinEndpointController.cs | 4 +- 6 files changed, 213 insertions(+), 19 deletions(-) diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj index d90b37706..7dc11de64 100644 --- a/BTCPayServer.Client/BTCPayServer.Client.csproj +++ b/BTCPayServer.Client/BTCPayServer.Client.csproj @@ -31,7 +31,7 @@ - + diff --git a/BTCPayServer.Rating/BTCPayServer.Rating.csproj b/BTCPayServer.Rating/BTCPayServer.Rating.csproj index 2ef9c74d9..5364f937a 100644 --- a/BTCPayServer.Rating/BTCPayServer.Rating.csproj +++ b/BTCPayServer.Rating/BTCPayServer.Rating.csproj @@ -6,7 +6,7 @@ - + diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index 1bfd68a46..4328f89cf 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -36,6 +36,7 @@ using Microsoft.Extensions.Options; using NBitcoin; using NBitcoin.RPC; using NBitcoin.Scripting.Parser; +using NBitcoin.WalletPolicies; using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json; @@ -893,6 +894,39 @@ namespace BTCPayServer.Tests Assert.Equal(4, tor.Services.Length); } + [Theory] + [InlineData("bcrt1q9zf7xapkmujeee9mfua9a7n6lkehrsv2h0z3nm", "wpkh([aaaaaaaa/84h/1h/0h]tpubDC6aWgsG4Tgiqu9vkK8Hoehc4Zu7oSmQ5G38ZhRJPSr1TabHocNeZ3DqqxnjLD6Zs16kwmEYCsBvGN3xE25BTmZ8ByxEdF2L9b2swdSxm1L/<0;1>/*)")] + [InlineData("mxfapt6u6tZ41UbFxNaEhq8QkKqYm6tvy3", "pkh([aaaaaaaa/44h/1h/0h]tpubDDV486pBqkML6Ywhznz8DS3VS95h3q4A2pUMCc6yy739QpKMg3gA8EXGrjraDBDxrhLsezepjCEfBtak5wngDH4vMh6aXKV8hPN7JsMtdEf/<0;1>/*)")] + [InlineData("2MsMb9CdVZwESkctbQfD5s8v3CszzbSxgUy", "sh(wpkh([aaaaaaaa/49h/1h/0h]tpubDDJa9q5audQLxtPyhrgapyByEHHSWQxKrADKA8dX8xNqAV5zrnnCVyHP9aNxojxi27Beus36V8D4Lqd6dZEDonxVMofgDK92zNLfeKhC54J/<0;1>/*))")] + [InlineData("bcrt1p7cwzrrg5te7r6tpham2qu7vws2x6vlswn9404raasc6aptuv3n2ss3zlrs","tr([aaaaaaaa/86h/1h/0h]tpubDCw9VEKFjxDk4MCLbPeYpsVP5aEP4sCgj1wbexMv38Y67YzczDHgjxBz2rqJDGwiNx8FH8uXyEUwYBruqJBBHW2Lrn4LPAZ3kj1qDkXuV2m/<0;1>/*)")] + [InlineData("2NAd39MvEYKqxfdZzEeW1PX3mYoL66kshWy","sh(sortedmulti(2,[aaaaaaaa/45h]tpubD9DcTBTiabXsjPBnVBKvNadiuhmw7rR56FqePc1kRgwQTiETnHBvgFbWtce3yTgsbRQ2ST1hA5eDoy3V4FwRvHFyEWiQxGaYFxRDDH4eBKb/<0;1>/*,[aaaaaaaa/45h]tpubD8HUcsDEkpLdQEHDDmA8fkLeb6yWXEyTCyzdpLbbEjLQhiiBHSfeYFDDmoEe5Nf9f6YY4LRdUwhkmwSBsSpn1PN7191pcDzP2APhrGXMVg6/<0;1>/*))")] + [InlineData("2MygJNZseGwenG6ASz2nbBAXrMR8gfUbu7h","sh(wsh(sortedmulti(2,[aaaaaaaa/45h]tpubD9DcTBTiabXsjPBnVBKvNadiuhmw7rR56FqePc1kRgwQTiETnHBvgFbWtce3yTgsbRQ2ST1hA5eDoy3V4FwRvHFyEWiQxGaYFxRDDH4eBKb/<0;1>/*,[aaaaaaaa/45h]tpubD8HUcsDEkpLdQEHDDmA8fkLeb6yWXEyTCyzdpLbbEjLQhiiBHSfeYFDDmoEe5Nf9f6YY4LRdUwhkmwSBsSpn1PN7191pcDzP2APhrGXMVg6/<0;1>/*))")] + [InlineData("2MygJNZseGwenG6ASz2nbBAXrMR8gfUbu7h","sh(wsh(sortedmulti(2,[aaaaaaaa/45h]tpubD8HUcsDEkpLdQEHDDmA8fkLeb6yWXEyTCyzdpLbbEjLQhiiBHSfeYFDDmoEe5Nf9f6YY4LRdUwhkmwSBsSpn1PN7191pcDzP2APhrGXMVg6/<0;1>/*,[aaaaaaaa/45h]tpubD9DcTBTiabXsjPBnVBKvNadiuhmw7rR56FqePc1kRgwQTiETnHBvgFbWtce3yTgsbRQ2ST1hA5eDoy3V4FwRvHFyEWiQxGaYFxRDDH4eBKb/<0;1>/*))")] + [InlineData("2MygJNZseGwenG6ASz2nbBAXrMR8gfUbu7h","sh(wsh(multi(2,[aaaaaaaa/45h]tpubD9DcTBTiabXsjPBnVBKvNadiuhmw7rR56FqePc1kRgwQTiETnHBvgFbWtce3yTgsbRQ2ST1hA5eDoy3V4FwRvHFyEWiQxGaYFxRDDH4eBKb/<0;1>/*,[aaaaaaaa/45h]tpubD8HUcsDEkpLdQEHDDmA8fkLeb6yWXEyTCyzdpLbbEjLQhiiBHSfeYFDDmoEe5Nf9f6YY4LRdUwhkmwSBsSpn1PN7191pcDzP2APhrGXMVg6/<0;1>/*))")] + [InlineData("2NEw4G7BMVJhcUXs79vpJcahYzpN8kwgE2N","sh(wsh(multi(2,[aaaaaaaa/45h]tpubD8HUcsDEkpLdQEHDDmA8fkLeb6yWXEyTCyzdpLbbEjLQhiiBHSfeYFDDmoEe5Nf9f6YY4LRdUwhkmwSBsSpn1PN7191pcDzP2APhrGXMVg6/<0;1>/*,[aaaaaaaa/45h]tpubD9DcTBTiabXsjPBnVBKvNadiuhmw7rR56FqePc1kRgwQTiETnHBvgFbWtce3yTgsbRQ2ST1hA5eDoy3V4FwRvHFyEWiQxGaYFxRDDH4eBKb/<0;1>/*))")] + [InlineData("bcrt1qy9tspfhktwm22cp54qv2pxqqwtney2w494xc4mapw3akhjtqx85sfkp6dh","wsh(sortedmulti(2,[aaaaaaaa/45h]tpubD9DcTBTiabXsjPBnVBKvNadiuhmw7rR56FqePc1kRgwQTiETnHBvgFbWtce3yTgsbRQ2ST1hA5eDoy3V4FwRvHFyEWiQxGaYFxRDDH4eBKb/<0;1>/*,[aaaaaaaa/45h]tpubD8HUcsDEkpLdQEHDDmA8fkLeb6yWXEyTCyzdpLbbEjLQhiiBHSfeYFDDmoEe5Nf9f6YY4LRdUwhkmwSBsSpn1PN7191pcDzP2APhrGXMVg6/<0;1>/*))")] + public void CanParseDerivationSchemesBIP388(string expectedAddress, string policy) + { + var networkProvider = CreateNetworkProvider(ChainName.Regtest); + var parser = new DerivationSchemeParser(networkProvider.BTC); + var scheme = parser.ParseOutputDescriptor(policy); + var script = Miniscript.Parse(policy, new MiniscriptParsingSettings(networkProvider.BTC.NBitcoinNetwork) + { + Dialect = MiniscriptDialect.BIP388, + AllowedParameters = ParameterTypeFlags.None + }); + + for (int i = 0; i < 5; i++) + { + var expectedScripts = script.Derive(AddressIntent.Deposit, i).Miniscript.ToScripts(); + var actual = scheme.Item1.GetDerivation(new KeyPath(0, (uint)i)); + Assert.Equal(expectedScripts.ScriptPubKey, actual.ScriptPubKey); + Assert.Equal(expectedScripts.RedeemScript, actual.Redeem); + if (i == 0) + Assert.Equal(expectedAddress, expectedScripts.ScriptPubKey.GetDestinationAddress(networkProvider.BTC.NBitcoinNetwork)!.ToString()); + } + } + [Fact] public void CanParseDerivationSchemes() { @@ -938,9 +972,9 @@ namespace BTCPayServer.Tests Assert.True(((DirectDerivationStrategy)strategyBase).Segwit); // Failure cases - Assert.Throws(() => { parser.Parse("xpubZ661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); }); - Assert.Throws(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general - Assert.Throws(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum + Assert.ThrowsAny(() => { parser.Parse("xpubZ661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); }); + Assert.ThrowsAny(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general + Assert.ThrowsAny(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum } public static WalletFileParsers GetParsers() { @@ -2126,15 +2160,15 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku //we don't support every descriptor, only the ones which represent an HD wallet with stndard derivation paths - Assert.Throws(() => mainnetParser.ParseOutputDescriptor("pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)")); - Assert.Throws(() => mainnetParser.ParseOutputDescriptor("pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)")); - Assert.Throws(() => mainnetParser.ParseOutputDescriptor("wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)")); - Assert.Throws(() => mainnetParser.ParseOutputDescriptor("sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))")); - Assert.Throws(() => mainnetParser.ParseOutputDescriptor("combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)")); - Assert.Throws(() => mainnetParser.ParseOutputDescriptor("sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))")); - Assert.Throws(() => mainnetParser.ParseOutputDescriptor("multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)")); - Assert.Throws(() => mainnetParser.ParseOutputDescriptor("sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))")); - Assert.Throws(() => mainnetParser.ParseOutputDescriptor("sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))")); + Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)")); + Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)")); + Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)")); + Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))")); + Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)")); + Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))")); + Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)")); + Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))")); + Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))")); //let's see what we actually support now @@ -2150,7 +2184,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku "pkh([d34db33f]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)"); Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]", parsedDescriptor.Item1.ToString()); //a master fingerprint must always be present if youre providing rooted path - Assert.Throws(() => mainnetParser.ParseOutputDescriptor("pkh([44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)")); + Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("pkh([44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)")); parsedDescriptor = mainnetParser.ParseOutputDescriptor( @@ -2158,7 +2192,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]", parsedDescriptor.Item1.ToString()); //but a different deriv path from standard (0/*) is not supported - Assert.Throws(() => mainnetParser.ParseOutputDescriptor("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)")); + Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)")); //p2sh-segwit hd wallet parsedDescriptor = mainnetParser.ParseOutputDescriptor( diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 3260ad2cb..2f68e2e46 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -51,6 +51,7 @@ + diff --git a/BTCPayServer/DerivationSchemeParser.cs b/BTCPayServer/DerivationSchemeParser.cs index 69d4270d5..8964e7fed 100644 --- a/BTCPayServer/DerivationSchemeParser.cs +++ b/BTCPayServer/DerivationSchemeParser.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.RegularExpressions; using NBitcoin; using NBitcoin.Scripting; +using NBitcoin.WalletPolicies; using NBXplorer.DerivationStrategy; +using static NBitcoin.WalletPolicies.MiniscriptNode; namespace BTCPayServer { @@ -22,6 +25,159 @@ namespace BTCPayServer } public (DerivationStrategyBase, RootedKeyPath[]) ParseOutputDescriptor(string str) + { + ArgumentNullException.ThrowIfNull(str); + str = str.Trim(); + try + { + return ParseLegacyOutputDescriptor(str); + } + catch + { + return ParseMiniscript(str); + } + } + + private (DerivationStrategyBase, RootedKeyPath[]) ParseMiniscript(string str) + { + bool ExtractMultisigs(ReadOnlySpan nodes, [MaybeNullWhen(false)] out BitcoinExtPubKey[] keys, [MaybeNullWhen(false)] out RootedKeyPath[] keyPaths) + { + keys = new BitcoinExtPubKey[nodes.Length]; + keyPaths = new RootedKeyPath[nodes.Length]; + int i = 0; + foreach (var n in nodes) + { + if (n is MultipathNode { Target: HDKeyNode { Key: BitcoinExtPubKey pk, RootedKeyPath: {} kpath }, DepositIndex: 0, ChangeIndex: 1 }) + { + keys[i] = pk; + keyPaths[i] = kpath; + i++; + } + else + { + return false; + } + } + return true; + } + var factory = BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory; + var script = Miniscript.Parse(str, new MiniscriptParsingSettings(Network) + { + Dialect = MiniscriptDialect.BIP388, + AllowedParameters = ParameterTypeFlags.None + }); + + return script.RootNode switch + { + // ---Single sigs--- + // Taproot + TaprootNode + { + InternalKeyNode: MultipathNode multi, + ScriptTreeRootNode: null + } when ExtractMultisigs([multi], out var pks, out var rpks) + => (factory.CreateDirectDerivationStrategy(pks[0], new() + { + ScriptPubKeyType = ScriptPubKeyType.TaprootBIP86 + }), rpks), + + // P2PKH + FragmentSingleParameter + { + Descriptor: { Name: "pkh" }, + X: MultipathNode multi + } when ExtractMultisigs([multi], out var pks, out var rpks) + => (factory.CreateDirectDerivationStrategy(pks[0], new() + { + ScriptPubKeyType = ScriptPubKeyType.Legacy + }), rpks), + + // P2WPKH + FragmentSingleParameter + { + Descriptor: { Name: "wpkh" }, + X: MultipathNode multi + } when ExtractMultisigs([multi], out var pks, out var rpks) + => (factory.CreateDirectDerivationStrategy(pks[0], new() + { + ScriptPubKeyType = ScriptPubKeyType.Segwit + }), rpks), + + // Wrapped P2WPKH + FragmentSingleParameter + { + Descriptor: { Name: "sh" }, + X: FragmentSingleParameter { + Descriptor: { Name: "wpkh" }, + X: MultipathNode multi + } + } when ExtractMultisigs([multi], out var pks, out var rpks) + => (factory.CreateDirectDerivationStrategy(pks[0], new() + { + ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH + }), rpks), + + // ---Multi sigs--- + // Multsig SH + FragmentSingleParameter + { + Descriptor: { Name: "sh" }, + X: FragmentUnboundedParameters { + Descriptor: { Name: "multi" or "sortedmulti" } desc + } multiNode + } when + multiNode.Parameters.ToArray() is + [ Value.CountValue { Count: var cnt }, .. { } multis] + && ExtractMultisigs(multis, out var pks, out var rpks) + => (factory.CreateMultiSigDerivationStrategy(pks, cnt, new() + { + ScriptPubKeyType = ScriptPubKeyType.Legacy, + KeepOrder = desc.Name == "multi" + }), rpks), + + // P2WSH + FragmentSingleParameter + { + Descriptor: { Name: "wsh" }, + X: FragmentUnboundedParameters { + Descriptor: { Name: "multi" or "sortedmulti" } desc + } multiNode + } when + multiNode.Parameters.ToArray() is + [ Value.CountValue { Count: var cnt }, .. { } multis] + && ExtractMultisigs(multis, out var pks, out var rpks) + => (factory.CreateMultiSigDerivationStrategy(pks, cnt, new() + { + ScriptPubKeyType = ScriptPubKeyType.Segwit, + KeepOrder = desc.Name == "multi" + }), rpks), + + // Wrapped P2WSH + FragmentSingleParameter + { + Descriptor: { Name: "sh" }, + X: FragmentSingleParameter + { + Descriptor: { Name: "wsh" }, + X: FragmentUnboundedParameters { + Descriptor: { Name: "multi" or "sortedmulti" } desc + } multiNode + } + } when + multiNode.Parameters.ToArray() is + [ Value.CountValue { Count: var cnt }, .. { } multis] + && ExtractMultisigs(multis, out var pks, out var rpks) + + => (factory.CreateMultiSigDerivationStrategy(pks, cnt, new() + { + ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH, + KeepOrder = desc.Name == "multi" + }), rpks), + _ => throw new FormatException("Not supporting this script policy (BIP388) yet.") + }; + } + + private (DerivationStrategyBase, RootedKeyPath[]) ParseLegacyOutputDescriptor(string str) { (DerivationStrategyBase, RootedKeyPath[]) ExtractFromPkProvider(PubKeyProvider pubKeyProvider, string suffix = "") @@ -52,11 +208,11 @@ namespace BTCPayServer $"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted ? "" : "-[keeporder]")}"), xpubs.SelectMany(tuple => tuple.Item2).ToArray()); } - + ArgumentNullException.ThrowIfNull(str); str = str.Trim(); + //nbitcoin output descriptor does not support taproot, so let's check if it is a taproot descriptor and fake until it is supported - var outputDescriptor = OutputDescriptor.Parse(str, Network); switch (outputDescriptor) { @@ -97,6 +253,7 @@ namespace BTCPayServer throw new ArgumentOutOfRangeException(nameof(outputDescriptor)); } } + public DerivationStrategyBase Parse(string str) { ArgumentNullException.ThrowIfNull(str); diff --git a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs index 41a910e7e..fcd408a74 100644 --- a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs +++ b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs @@ -197,7 +197,8 @@ namespace BTCPayServer.Payments.PayJoin psbtFormat = false; if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx)) return BadRequest(CreatePayjoinError("original-psbt-rejected", "invalid transaction or psbt")); - ctx.OriginalTransaction = tx; + ctx.OriginalTransaction = tx.Clone(); + tx.RemoveSignatures(); psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork); psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt })).PSBT; for (int i = 0; i < tx.Inputs.Count; i++) @@ -474,6 +475,7 @@ namespace BTCPayServer.Payments.PayJoin } var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork); + newTx.RemoveSignatures(); var newPsbt = PSBT.FromTransaction(newTx, network.NBitcoinNetwork); foreach (var selectedUtxo in selectedUTXOs.Select(o => o.Value)) {