diff --git a/NBitcoin.Tests/Generators/CryptoGenerator.cs b/NBitcoin.Tests/Generators/CryptoGenerator.cs index c27be9c0a5..26c762a536 100644 --- a/NBitcoin.Tests/Generators/CryptoGenerator.cs +++ b/NBitcoin.Tests/Generators/CryptoGenerator.cs @@ -95,5 +95,24 @@ from raw in Gen.NonEmptyListOf(PrimitiveGenerator.RandomBytes(4)) select NBitcoin.KeyPath.FromBytes(flattenBytes); public static Gen ExtPubKey() => ExtKey().Select(ek => ek.Neuter()); + + public static Gen BitcoinExtPubKey() => + from extKey in ExtPubKey() + from network in ChainParamsGenerator.NetworkGen() + select new BitcoinExtPubKey(extKey, network); + + public static Gen BitcoinExtKey() => + from extKey in ExtKey() + from network in ChainParamsGenerator.NetworkGen() + select new BitcoinExtKey(extKey, network); + + public static Gen RootedKeyPath() => + from parentFingerPrint in HDFingerPrint() + from kp in KeyPath() + select new RootedKeyPath(parentFingerPrint, kp); + + public static Gen HDFingerPrint() => + from x in PrimitiveGenerator.UInt32() + select new HDFingerprint(x); } -} \ No newline at end of file +} diff --git a/NBitcoin.Tests/Generators/OutputDescriptorGenerator.cs b/NBitcoin.Tests/Generators/OutputDescriptorGenerator.cs new file mode 100644 index 0000000000..1a54696e34 --- /dev/null +++ b/NBitcoin.Tests/Generators/OutputDescriptorGenerator.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq; +using FsCheck; +using NBitcoin.Altcoins; +using NBitcoin.Scripting; + +#nullable enable +namespace NBitcoin.Tests.Generators +{ + public class OutputDescriptorGenerator : OutputDescriptorGeneratorBase + { + + public static Arbitrary OutputDescriptorArb() => + Arb.From(OutputDescriptorGen()); + } + + public class RegtestOutputDescriptorGenerator : OutputDescriptorGeneratorBase + { + public static Arbitrary OutputDescriptorArb() => + Arb.From(OutputDescriptorGen(Network.RegTest)); + } + + public class OutputDescriptorGeneratorBase + { + public static Gen OutputDescriptorGen(Network? n = null) => + Gen.OneOf( + AddrOutputDescriptorGen(n), + RawOutputDescriptorGen(), + PKOutputDescriptorGen(n), + PKHOutputDescriptorGen(n), + WPKHOutputDescriptorGen(n), + ComboOutputDescriptorGen(n), + MultisigOutputDescriptorGen(3, n), // top level multisig can not have more than 3 pubkeys. + SHOutputDescriptorGen(n), + WSHOutputDescriptorGen(n) + ); + private static Gen AddrOutputDescriptorGen(Network? n = null) => + from addr in n is null ? AddressGenerator.RandomAddress() : AddressGenerator.RandomAddress(n) + select OutputDescriptor.NewAddr(addr); + + private static Gen RawOutputDescriptorGen() => + from addr in ScriptGenerator.RandomScriptSig() + where addr._Script.Length > 0 + select OutputDescriptor.NewRaw(addr); + private static Gen PKOutputDescriptorGen(Network? n = null) => + from pkProvider in PubKeyProviderGen(n) + select OutputDescriptor.NewPK(pkProvider); + + private static Gen PKHOutputDescriptorGen(Network? n = null) => + from pkProvider in PubKeyProviderGen(n) + select OutputDescriptor.NewPKH(pkProvider); + + private static Gen WPKHOutputDescriptorGen(Network? n = null) => + from pkProvider in PubKeyProviderGen(n) + select OutputDescriptor.NewWPKH(pkProvider); + + private static Gen ComboOutputDescriptorGen(Network? n = null) => + from pkProvider in PubKeyProviderGen(n) + select OutputDescriptor.NewCombo(pkProvider); + + private static Gen MultisigOutputDescriptorGen(int maxN, Network? network = null) => + from n in Gen.Choose(2, maxN) + from m in Gen.Choose(2, n).Select(i => (uint)i) + from pkProviders in Gen.ArrayOf(n, PubKeyProviderGen(network)) + from isSorted in Arb.Generate() + select OutputDescriptor.NewMulti(m, pkProviders, isSorted); + + private static Gen WSHInnerGen(int maxMultisigN, Network? n = null) => + Gen.OneOf( + PKOutputDescriptorGen(n), + PKHOutputDescriptorGen(n), + MultisigOutputDescriptorGen(maxMultisigN, n) + ); + private static Gen InnerOutputDescriptorGen(int maxMultisigN, Network? n = null) => + Gen.OneOf( + WPKHOutputDescriptorGen(n), + WSHInnerGen(maxMultisigN, n) + ); + + // For sh-nested script, max multisig Number is 15. + private static Gen SHOutputDescriptorGen(Network? n = null) => + from inner in Gen.OneOf(InnerOutputDescriptorGen(15, n), WSHOutputDescriptorGen(n)) + select OutputDescriptor.NewSH(inner); + + private static Gen WSHOutputDescriptorGen(Network? n = null) => + from inner in WSHInnerGen(20, n) + select OutputDescriptor.NewWSH(inner); + + #region pubkey providers + + private static Gen PubKeyProviderGen(Network? n = null) => + Gen.OneOf(OriginPubKeyProviderGen(n), ConstPubKeyProviderGen(), HDPubKeyProviderGen(n)); + + private static Gen OriginPubKeyProviderGen(Network? n = null) => + from keyOrigin in CryptoGenerator.RootedKeyPath() + from inner in Gen.OneOf(ConstPubKeyProviderGen(), HDPubKeyProviderGen(n)) + select PubKeyProvider.NewOrigin(keyOrigin, inner); + + private static Gen ConstPubKeyProviderGen() => + from pk in CryptoGenerator.PublicKey() + select PubKeyProvider.NewConst(pk); + + private static Gen HDPubKeyProviderGen(Network? n = null) => + from extPk in n is null ? CryptoGenerator.BitcoinExtPubKey() : CryptoGenerator.ExtPubKey().Select(e => new BitcoinExtPubKey(e, n)) + from kp in CryptoGenerator.KeyPath() + from t in Arb.Generate() + select PubKeyProvider.NewHD(extPk, kp, t); + + # endregion + } +} +#nullable disable diff --git a/NBitcoin.Tests/Helpers/PrimitiveUtils.cs b/NBitcoin.Tests/Helpers/PrimitiveUtils.cs new file mode 100644 index 0000000000..10e78215ec --- /dev/null +++ b/NBitcoin.Tests/Helpers/PrimitiveUtils.cs @@ -0,0 +1,43 @@ + +using System.Collections.Generic; + +namespace NBitcoin.Tests.Helpers +{ + internal static class PrimitiveUtils + { + internal static Coin RandomCoin(Money amount, Script scriptPubKey, bool p2sh) + { + var outpoint = RandOutpoint(); + if(!p2sh) + return new Coin(outpoint, new TxOut(amount, scriptPubKey)); + return new ScriptCoin(outpoint, new TxOut(amount, scriptPubKey.Hash), scriptPubKey); + } + internal static Coin RandomCoin(Money amount, Key receiver) + { + return RandomCoin(amount, receiver.PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.Main)); + } + internal static Coin RandomCoin(Money amount, IDestination receiver) + { + var outpoint = RandOutpoint(); + return new Coin(outpoint, new TxOut(amount, receiver)); + } + + internal static List GetRandomCoinsForAllScriptType(Money amount, Script scriptPubKey) + { + return new List { + RandomCoin(Money.Coins(0.5m), scriptPubKey, true) as ScriptCoin, + new ScriptCoin(RandomCoin(Money.Coins(0.5m), scriptPubKey.WitHash), scriptPubKey), + new ScriptCoin(RandomCoin(Money.Coins(0.5m), scriptPubKey.WitHash.ScriptPubKey.Hash), scriptPubKey) + }; + } + + internal static OutPoint RandOutpoint() + { + return new OutPoint(Rand(), 0); + } + internal static uint256 Rand() + { + return new uint256(RandomUtils.GetBytes(32)); + } + } +} diff --git a/NBitcoin.Tests/OutputDescriptorTests.cs b/NBitcoin.Tests/OutputDescriptorTests.cs new file mode 100644 index 0000000000..0870bc3e09 --- /dev/null +++ b/NBitcoin.Tests/OutputDescriptorTests.cs @@ -0,0 +1,375 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FsCheck; +using FsCheck.Xunit; +using NBitcoin.RPC; +using NBitcoin.Scripting; +using NBitcoin.Scripting.Parser; +using NBitcoin.Tests.Generators; +using static NBitcoin.Tests.Helpers.PrimitiveUtils; +using Xunit; +using Random = System.Random; + +namespace NBitcoin.Tests +{ + public class OutputDescriptorTests + { + public OutputDescriptorTests() + { + Arb.Register(); + DummyKey = new Key(); + } + + private string MaybeInsertSpaces(string s) + { + var shouldReplaceComma = RandomUtils.GetBytes(1)[0] < 128; + if (shouldReplaceComma) + s = s.Replace(",", " , "); + var shouldReplaceLParen = RandomUtils.GetBytes(1)[0] < 128; + if (shouldReplaceLParen) + s = s.Replace("(", " ( "); + var shouldReplaceRParen = RandomUtils.GetBytes(1)[0] < 128; + if (shouldReplaceRParen) + s = s.Replace(")", " ) "); + var shouldAddTopLevelSpaces = RandomUtils.GetBytes(1)[0] < 128; + if (shouldAddTopLevelSpaces) + s = $" {s} "; + return s; + } + + [Property(MaxTest=10)] + [Trait("PropertyTest", "BidirectionalConversion")] + public void ShouldConvertToStringBidirectionally(OutputDescriptor desc) + { + var afterConversion = OutputDescriptor.Parse(MaybeInsertSpaces(desc.ToString())); + Assert.Equal(desc, afterConversion); + Assert.Equal(desc.ToString(), afterConversion.ToString()); + Assert.Equal(desc.GetHashCode(), afterConversion.GetHashCode()); + } + + [Property] + [Trait("PropertyTest", "Verification")] + public void ShouldNotThrowErrorInBasicOperation(OutputDescriptor od) + { + od.IsSolvable(); + od.IsRange(); + for (uint i = 0; i < 4; i++) + { + var repo = new FlatSigningRepository(); + od.TryExpand(i, (keyId) => null, repo, out var scripts); + foreach (var sc in scripts) + { + OutputDescriptor.InferFromScript(sc, repo); + } + } + var repo2 = new FlatSigningRepository(); + OutputDescriptor.Parse(od.ToString(), false, repo2); + od.TryGetPrivateString(repo2, out var res); + } + + + [Fact] + [Trait("UnitTest", "UnitTest")] + public void OutputDescriptorParserTests() + { + // ref: https://github.com/bitcoin/bitcoin/blob/9b085f4863eaefde4bec0638f1cbc8509d6ee59a/doc/descriptors.md + var testVectors = new string[] { + "addr(2N7nD1pG3kK3DYaP34jQKbxB3JnEfMbVea7)", + "pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)", + "pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)", + "wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)", + "sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))", + "combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)", + "sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))", + "multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)", + "sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))", + "wsh(multi(2,03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a))", + "sh(wsh(multi(1,03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8,03499fdf9e895e719cfd64e67f07d38e3226aa7b63678949e6e49b241a60e823e4,02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)))", + "pk(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)", + "pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1'/2)", + "pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)", + "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))", + // same with above except that is has hardend derivation + "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*'))", + + // tests find by property test + "sh(wsh(pk(tpubD6NzVbkrYhZ4XGbCzpyPa9n4DDwStrqKMrpVn4zfmYJ8yWxUbobjAXfpNeeLAYVBU6G9x7aNF8RXrFtLb2QpNzwQ1gJYmqpaE6HbEmKEXaa/472145637'/* )))#u7qwhqhh", + "raw(04bd9d509ca881)#7sr5k80j" + }; + foreach (var i in testVectors) + { + var od = OutputDescriptor.Parse(MaybeInsertSpaces(i)); + Assert.Equal(od, OutputDescriptor.Parse(od.ToString())); + } + } + + + [Fact] + [Trait("Core", "Core")] + public void DescriptorTests() + { + CheckDescriptor( + "combo(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", + "combo(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", + SIGNABLE, + new string[][] + { + new string[]{ + "2103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bdac", + "76a9149a1c78a507689f6f54b847ad1cef1e614ee23f1e88ac", + "00149a1c78a507689f6f54b847ad1cef1e614ee23f1e", + "a91484ab21b1b2fd065d4504ff693d832434b6108d7b87" + } + }); + CheckDescriptor("pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", SIGNABLE, new string[][] { new string[] { "2103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bdac" } }); + CheckDescriptor("pkh([deadbeef/1/2'/3/4']L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "pkh([deadbeef/1/2'/3/4']03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", SIGNABLE, new string[][] { new string[] { "76a9149a1c78a507689f6f54b847ad1cef1e614ee23f1e88ac" } }, ScriptPubKeyType.Legacy, new uint[][] { new uint[] { 1, 0x80000002U, 3, 0x80000004U } }); + CheckDescriptor("wpkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "wpkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", SIGNABLE, new string[][] { new string[] { "00149a1c78a507689f6f54b847ad1cef1e614ee23f1e" } }, ScriptPubKeyType.Segwit); + CheckDescriptor("sh(wpkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "sh(wpkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", SIGNABLE, new string[][] {new string[] { "a91484ab21b1b2fd065d4504ff693d832434b6108d7b87" }}, ScriptPubKeyType.SegwitP2SH); + CheckUnparsable("sh(wpkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY2))", "sh(wpkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5))", "Pubkey '03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5' is invalid"); // Invalid pubkey + CheckUnparsable("pkh(deadbeef/1/2'/3/4']L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "pkh(deadbeef/1/2'/3/4']03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", "Key origin start '[ character expected but not found, got 'd' instead"); // Missing start bracket in key origin + CheckUnparsable("pkh([deadbeef]/1/2'/3/4']L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "pkh([deadbeef]/1/2'/3/4']03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", "Multiple ']' characters found for a single pubkey"); // Multiple end brackets in key origin + + // Basic single-key uncompressed + CheckDescriptor("combo(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "combo(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", SIGNABLE, new string[][]{ new string[]{ "4104a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235ac", "76a914b5bd079c4d57cc7fc28ecf8213a6b791625b818388ac" }}); + CheckDescriptor("pk(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "pk(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", SIGNABLE, new string[][] { new string[] { "4104a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235ac" } }); + CheckDescriptor("pkh(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "pkh(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", SIGNABLE, new string[][] { new string[] { "76a914b5bd079c4d57cc7fc28ecf8213a6b791625b818388ac" } }, ScriptPubKeyType.Legacy); + CheckUnparsable("wpkh(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "wpkh(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)"); // No uncompressed keys in witness + CheckUnparsable("wsh(pk(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss))", "wsh(pk(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235))"); // No uncompressed keys in witness + CheckUnparsable("sh(wpkh(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss))", "sh(wpkh(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235))"); // No uncompressed keys in witness + + // Some unconventional single-key constructions + CheckDescriptor("sh(pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "sh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", SIGNABLE, new string[][] { new string[] { "a9141857af51a5e516552b3086430fd8ce55f7c1a52487" }}, ScriptPubKeyType.Legacy); + CheckDescriptor("sh(pkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "sh(pkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", SIGNABLE, new string[][] { new string[] { "a9141a31ad23bf49c247dd531a623c2ef57da3c400c587" } }, ScriptPubKeyType.Legacy); + CheckDescriptor("wsh(pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "wsh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", SIGNABLE, new string[][] { new string[] { "00202e271faa2325c199d25d22e1ead982e45b64eeb4f31e73dbdf41bd4b5fec23fa" } }, ScriptPubKeyType.Segwit); + CheckDescriptor("wsh(pkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "wsh(pkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", SIGNABLE, new string[][] { new string[] { "0020338e023079b91c58571b20e602d7805fb808c22473cbc391a41b1bd3a192e75b" }}, ScriptPubKeyType.Segwit); + CheckDescriptor("sh(wsh(pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)))", "sh(wsh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)))", SIGNABLE, new string[][] { new string[] { "a91472d0c5a3bfad8c3e7bd5303a72b94240e80b6f1787" } }, ScriptPubKeyType.SegwitP2SH); + CheckDescriptor("sh(wsh(pkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)))", "sh(wsh(pkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)))", SIGNABLE, new string[][] { new string[] { "a914b61b92e2ca21bac1e72a3ab859a742982bea960a87" }}, ScriptPubKeyType.SegwitP2SH); + + // Versions with BIP32 derivations + CheckDescriptor("combo([01234567]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)", "combo([01234567]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)", SIGNABLE, new string[][] { new string[] { "2102d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0ac", "76a91431a507b815593dfc51ffc7245ae7e5aee304246e88ac", "001431a507b815593dfc51ffc7245ae7e5aee304246e", "a9142aafb926eb247cb18240a7f4c07983ad1f37922687" }}); + CheckDescriptor("pk(xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0)", "pk(xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0)", DEFAULT, new string[][] { new string[] { "210379e45b3cf75f9c5f9befd8e9506fb962f6a9d185ac87001ec44a8d3df8d4a9e3ac" }},null, new uint[][] { new uint[] { 0 } }); + CheckDescriptor("pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647'/0)", "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2147483647'/0)", HARDENED, new string[][] { new string[] { "76a914ebdc90806a9c4356c1c88e42216611e1cb4c1c1788ac" } }, ScriptPubKeyType.Legacy, new uint[][] { new uint[] { 0xFFFFFFFFU, 0 }}); + CheckDescriptor("wpkh([ffffffff/13']xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/*)", "wpkh([ffffffff/13']xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/1/2/*)", RANGE, new string[][] { new string[] { "0014326b2249e3a25d5dc60935f044ee835d090ba859" }, new string[] {"0014af0bd98abc2f2cae66e36896a39ffe2d32984fb7"}, new string[] { "00141fa798efd1cbf95cebf912c031b8a4a6e9fb9f27" } }, ScriptPubKeyType.Segwit,new uint[][] { new uint[] { 0x8000000DU, 1, 2, 0 }, new uint[]{ 0x8000000DU, 1, 2, 1 }, new uint[]{ 0x8000000DU, 1, 2, 2 } }); + CheckDescriptor("sh(wpkh(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/*'))", "sh(wpkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*'))", RANGE | HARDENED | DERIVE_HARDENDED, new string[][] { new string[]{ "a9149a4d9901d6af519b2a23d4a2f51650fcba87ce7b87"}, new string[] { "a914bed59fc0024fae941d6e20a3b44a109ae740129287"}, new string[] { "a9148483aa1116eb9c05c482a72bada4b1db24af654387"} }, ScriptPubKeyType.SegwitP2SH,new uint[][] { new uint[]{ 10, 20, 30, 40, 0x80000000U}, new uint[]{ 10, 20, 30, 40, 0x80000001U}, new uint[]{ 10, 20, 30, 40, 0x80000002U} }); + CheckDescriptor("combo(xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334/*)", "combo(xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV/*)", RANGE, new string[][]{ new string[]{ "2102df12b7035bdac8e3bab862a3a83d06ea6b17b6753d52edecba9be46f5d09e076ac","76a914f90e3178ca25f2c808dc76624032d352fdbdfaf288ac","0014f90e3178ca25f2c808dc76624032d352fdbdfaf2","a91408f3ea8c68d4a7585bf9e8bda226723f70e445f087"}, new string[]{ "21032869a233c9adff9a994e4966e5b821fd5bac066da6c3112488dc52383b4a98ecac","76a914a8409d1b6dfb1ed2a3e8aa5e0ef2ff26b15b75b788ac","0014a8409d1b6dfb1ed2a3e8aa5e0ef2ff26b15b75b7","a91473e39884cb71ae4e5ac9739e9225026c99763e6687"} },null, new uint[][]{ new uint[]{ 0 }, new uint[]{ 1 } }); + CheckUnparsable("combo([012345678]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)", "combo([012345678]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)"); // Too long key fingerprint + CheckUnparsable("pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483648)", "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2147483648)"); // BIP 32 path element overflow + CheckUnparsable("pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/1aa)", "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1aa)", "Key path value '1aa' is not a valid uint32"); // Path is not valid uint + + + // Multisig constructions + CheckDescriptor("multi(1,L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "multi(1,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", SIGNABLE, new []{new []{"512103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd4104a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea23552ae"}}); + CheckDescriptor("sortedmulti(1,L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "sortedmulti(1,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", SIGNABLE, new []{new []{"512103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd4104a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea23552ae"}}); + CheckDescriptor("sortedmulti(1,5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss,L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "sortedmulti(1,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", SIGNABLE, new []{new []{"512103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd4104a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea23552ae"}}); + CheckDescriptor("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))", "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))", DEFAULT, new []{ new []{"a91445a9a622a8b0a1269944be477640eedc447bbd8487"}}, ScriptPubKeyType.Legacy, new []{ new uint[]{0x8000006FU,222}, new uint[]{0}}); + CheckDescriptor("sortedmulti(2,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/*,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0/0/*)", "sortedmulti(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0/0/*)", RANGE, new []{ new []{"5221025d5fc65ebb8d44a5274b53bac21ff8307fec2334a32df05553459f8b1f7fe1b62102fbd47cc8034098f0e6a94c6aeee8528abf0a2153a5d8e46d325b7284c046784652ae"}, new []{"52210264fd4d1f5dea8ded94c61e9641309349b62f27fbffe807291f664e286bfbe6472103f4ece6dfccfa37b211eb3d0af4d0c61dba9ef698622dc17eecdf764beeb005a652ae"}, new []{"5221022ccabda84c30bad578b13c89eb3b9544ce149787e5b538175b1d1ba259cbb83321024d902e1a2fc7a8755ab5b694c575fce742c48d9ff192e63df5193e4c7afe1f9c52ae"}}, null, new []{new uint[]{0}, new uint[]{1}, new uint[]{2}, new uint[]{0, 0, 0}, new uint[]{0, 0, 1}, new uint[]{0, 0, 2}}); + CheckDescriptor("wsh(multi(2,xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647'/0,xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/*,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/*'))", "wsh(multi(2,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2147483647'/0,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/1/2/*,xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*'))", HARDENED | RANGE | DERIVE_HARDENDED, new[]{new[]{"0020b92623201f3bb7c3771d45b2ad1d0351ea8fbf8cfe0a0e570264e1075fa1948f"},new[]{"002036a08bbe4923af41cf4316817c93b8d37e2f635dd25cfff06bd50df6ae7ea203"},new[]{"0020a96e7ab4607ca6b261bfe3245ffda9c746b28d3f59e83d34820ec0e2b36c139c"}}, ScriptPubKeyType.Segwit, new []{ new uint[]{0xFFFFFFFFU,0}, new uint[]{1,2,0}, new uint[]{1,2,1}, new uint[]{1,2,2}, new uint[]{10, 20, 30, 40, 0x80000000U}, new uint[]{10, 20, 30, 40, 0x80000001U}, new uint[]{10, 20, 30, 40, 0x80000002U}}); + CheckDescriptor("sh(wsh(multi(16,KzoAz5CanayRKex3fSLQ2BwJpN7U52gZvxMyk78nDMHuqrUxuSJy,KwGNz6YCCQtYvFzMtrC6D3tKTKdBBboMrLTsjr2NYVBwapCkn7Mr,KxogYhiNfwxuswvXV66eFyKcCpm7dZ7TqHVqujHAVUjJxyivxQ9X,L2BUNduTSyZwZjwNHynQTF14mv2uz2NRq5n5sYWTb4FkkmqgEE9f,L1okJGHGn1kFjdXHKxXjwVVtmCMR2JA5QsbKCSpSb7ReQjezKeoD,KxDCNSST75HFPaW5QKpzHtAyaCQC7p9Vo3FYfi2u4dXD1vgMiboK,L5edQjFtnkcf5UWURn6UuuoFrabgDQUHdheKCziwN42aLwS3KizU,KzF8UWFcEC7BYTq8Go1xVimMkDmyNYVmXV5PV7RuDicvAocoPB8i,L3nHUboKG2w4VSJ5jYZ5CBM97oeK6YuKvfZxrefdShECcjEYKMWZ,KyjHo36dWkYhimKmVVmQTq3gERv3pnqA4xFCpvUgbGDJad7eS8WE,KwsfyHKRUTZPQtysN7M3tZ4GXTnuov5XRgjdF2XCG8faAPmFruRF,KzCUbGhN9LJhdeFfL9zQgTJMjqxdBKEekRGZX24hXdgCNCijkkap,KzgpMBwwsDLwkaC5UrmBgCYaBD2WgZ7PBoGYXR8KT7gCA9UTN5a3,KyBXTPy4T7YG4q9tcAM3LkvfRpD1ybHMvcJ2ehaWXaSqeGUxEdkP,KzJDe9iwJRPtKP2F2AoN6zBgzS7uiuAwhWCfGdNeYJ3PC1HNJ8M8,L1xbHrxynrqLKkoYc4qtoQPx6uy5qYXR5ZDYVYBSRmCV5piU3JG9)))","sh(wsh(multi(16,03669b8afcec803a0d323e9a17f3ea8e68e8abe5a278020a929adbec52421adbd0,0260b2003c386519fc9eadf2b5cf124dd8eea4c4e68d5e154050a9346ea98ce600,0362a74e399c39ed5593852a30147f2959b56bb827dfa3e60e464b02ccf87dc5e8,0261345b53de74a4d721ef877c255429961b7e43714171ac06168d7e08c542a8b8,02da72e8b46901a65d4374fe6315538d8f368557dda3a1dcf9ea903f3afe7314c8,0318c82dd0b53fd3a932d16e0ba9e278fcc937c582d5781be626ff16e201f72286,0297ccef1ef99f9d73dec9ad37476ddb232f1238aff877af19e72ba04493361009,02e502cfd5c3f972fe9a3e2a18827820638f96b6f347e54d63deb839011fd5765d,03e687710f0e3ebe81c1037074da939d409c0025f17eb86adb9427d28f0f7ae0e9,02c04d3a5274952acdbc76987f3184b346a483d43be40874624b29e3692c1df5af,02ed06e0f418b5b43a7ec01d1d7d27290fa15f75771cb69b642a51471c29c84acd,036d46073cbb9ffee90473f3da429abc8de7f8751199da44485682a989a4bebb24,02f5d1ff7c9029a80a4e36b9a5497027ef7f3e73384a4a94fbfe7c4e9164eec8bc,02e41deffd1b7cce11cde209a781adcffdabd1b91c0ba0375857a2bfd9302419f3,02d76625f7956a7fc505ab02556c23ee72d832f1bac391bcd2d3abce5710a13d06,0399eb0a5487515802dc14544cf10b3666623762fbed2ec38a3975716e2c29c232)))", SIGNABLE, new []{ new[]{"a9147fc63e13dc25e8a95a3cee3d9a714ac3afd96f1e87"}}, ScriptPubKeyType.SegwitP2SH); + CheckUnparsable("sh(multi(16,KzoAz5CanayRKex3fSLQ2BwJpN7U52gZvxMyk78nDMHuqrUxuSJy,KwGNz6YCCQtYvFzMtrC6D3tKTKdBBboMrLTsjr2NYVBwapCkn7Mr,KxogYhiNfwxuswvXV66eFyKcCpm7dZ7TqHVqujHAVUjJxyivxQ9X,L2BUNduTSyZwZjwNHynQTF14mv2uz2NRq5n5sYWTb4FkkmqgEE9f,L1okJGHGn1kFjdXHKxXjwVVtmCMR2JA5QsbKCSpSb7ReQjezKeoD,KxDCNSST75HFPaW5QKpzHtAyaCQC7p9Vo3FYfi2u4dXD1vgMiboK,L5edQjFtnkcf5UWURn6UuuoFrabgDQUHdheKCziwN42aLwS3KizU,KzF8UWFcEC7BYTq8Go1xVimMkDmyNYVmXV5PV7RuDicvAocoPB8i,L3nHUboKG2w4VSJ5jYZ5CBM97oeK6YuKvfZxrefdShECcjEYKMWZ,KyjHo36dWkYhimKmVVmQTq3gERv3pnqA4xFCpvUgbGDJad7eS8WE,KwsfyHKRUTZPQtysN7M3tZ4GXTnuov5XRgjdF2XCG8faAPmFruRF,KzCUbGhN9LJhdeFfL9zQgTJMjqxdBKEekRGZX24hXdgCNCijkkap,KzgpMBwwsDLwkaC5UrmBgCYaBD2WgZ7PBoGYXR8KT7gCA9UTN5a3,KyBXTPy4T7YG4q9tcAM3LkvfRpD1ybHMvcJ2ehaWXaSqeGUxEdkP,KzJDe9iwJRPtKP2F2AoN6zBgzS7uiuAwhWCfGdNeYJ3PC1HNJ8M8,L1xbHrxynrqLKkoYc4qtoQPx6uy5qYXR5ZDYVYBSRmCV5piU3JG9))","sh(multi(16,03669b8afcec803a0d323e9a17f3ea8e68e8abe5a278020a929adbec52421adbd0,0260b2003c386519fc9eadf2b5cf124dd8eea4c4e68d5e154050a9346ea98ce600,0362a74e399c39ed5593852a30147f2959b56bb827dfa3e60e464b02ccf87dc5e8,0261345b53de74a4d721ef877c255429961b7e43714171ac06168d7e08c542a8b8,02da72e8b46901a65d4374fe6315538d8f368557dda3a1dcf9ea903f3afe7314c8,0318c82dd0b53fd3a932d16e0ba9e278fcc937c582d5781be626ff16e201f72286,0297ccef1ef99f9d73dec9ad37476ddb232f1238aff877af19e72ba04493361009,02e502cfd5c3f972fe9a3e2a18827820638f96b6f347e54d63deb839011fd5765d,03e687710f0e3ebe81c1037074da939d409c0025f17eb86adb9427d28f0f7ae0e9,02c04d3a5274952acdbc76987f3184b346a483d43be40874624b29e3692c1df5af,02ed06e0f418b5b43a7ec01d1d7d27290fa15f75771cb69b642a51471c29c84acd,036d46073cbb9ffee90473f3da429abc8de7f8751199da44485682a989a4bebb24,02f5d1ff7c9029a80a4e36b9a5497027ef7f3e73384a4a94fbfe7c4e9164eec8bc,02e41deffd1b7cce11cde209a781adcffdabd1b91c0ba0375857a2bfd9302419f3,02d76625f7956a7fc505ab02556c23ee72d832f1bac391bcd2d3abce5710a13d06,0399eb0a5487515802dc14544cf10b3666623762fbed2ec38a3975716e2c29c232))", "P2SH script is too large, 547 bytes is larger than 520 bytes"); // P2SH does not fit 16 compressed pubkeys in a redeemscript + CheckUnparsable("wsh(multi(2,[aaaaaaaa][aaaaaaaa]xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647'/0,xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/*,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/*'))", "wsh(multi(2,[aaaaaaaa][aaaaaaaa]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2147483647'/0,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/1/2/*,xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*'))", "Multiple ']' characters found for a single pubkey"); // Double key origin descriptor + CheckUnparsable("wsh(multi(2,[aaaagaaa]xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647'/0,xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/*,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/*'))", "wsh(multi(2,[aaagaaaa]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2147483647'/0,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/1/2/*,xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*'))", "Fingerprint 'aaagaaaa' is not hex"); // Non hex fingerprint + CheckUnparsable("wsh(multi(2,[aaaaaaaa],xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/*,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/*'))", "wsh(multi(2,[aaaaaaaa],xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/1/2/*,xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*'))", "No key provided"); // No public key with origin + CheckUnparsable("wsh(multi(2,[aaaaaaa]xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647'/0,xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/*,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/*'))", "wsh(multi(2,[aaaaaaa]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2147483647'/0,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/1/2/*,xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*'))", "Fingerprint is not 4 bytes (7 characters instead of 8 characters)"); // Too short fingerprint + CheckUnparsable("wsh(multi(2,[aaaaaaaaa]xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647'/0,xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/*,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/*'))", "wsh(multi(2,[aaaaaaaaa]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2147483647'/0,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/1/2/*,xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*'))", "Fingerprint is not 4 bytes (9 characters instead of 8 characters)"); // Too long fingerprint + CheckUnparsable("multi(a,L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "multi(a,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", "Multi threshold 'a' is not valid"); // Invalid threshold + CheckUnparsable("multi(0,L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "multi(0,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", "Multisig threshold cannot be 0, must be at least 1"); // Threshold of 0 + CheckUnparsable("multi(3,L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "multi(3,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", "Multisig threshold cannot be larger than the number of keys; threshold is 3 but only 2 keys specified"); // Threshold larger than number of keys + CheckUnparsable("multi(3,KzoAz5CanayRKex3fSLQ2BwJpN7U52gZvxMyk78nDMHuqrUxuSJy,KwGNz6YCCQtYvFzMtrC6D3tKTKdBBboMrLTsjr2NYVBwapCkn7Mr,KxogYhiNfwxuswvXV66eFyKcCpm7dZ7TqHVqujHAVUjJxyivxQ9X,L2BUNduTSyZwZjwNHynQTF14mv2uz2NRq5n5sYWTb4FkkmqgEE9f)", "multi(3,03669b8afcec803a0d323e9a17f3ea8e68e8abe5a278020a929adbec52421adbd0,0260b2003c386519fc9eadf2b5cf124dd8eea4c4e68d5e154050a9346ea98ce600,0362a74e399c39ed5593852a30147f2959b56bb827dfa3e60e464b02ccf87dc5e8,0261345b53de74a4d721ef877c255429961b7e43714171ac06168d7e08c542a8b8)", "Cannot have 4 pubkeys in bare multisig; only at most 3 pubkeys"); // Threshold larger than number of keys + CheckUnparsable("sh(multi(16,KzoAz5CanayRKex3fSLQ2BwJpN7U52gZvxMyk78nDMHuqrUxuSJy,KwGNz6YCCQtYvFzMtrC6D3tKTKdBBboMrLTsjr2NYVBwapCkn7Mr,KxogYhiNfwxuswvXV66eFyKcCpm7dZ7TqHVqujHAVUjJxyivxQ9X,L2BUNduTSyZwZjwNHynQTF14mv2uz2NRq5n5sYWTb4FkkmqgEE9f,L1okJGHGn1kFjdXHKxXjwVVtmCMR2JA5QsbKCSpSb7ReQjezKeoD,KxDCNSST75HFPaW5QKpzHtAyaCQC7p9Vo3FYfi2u4dXD1vgMiboK,L5edQjFtnkcf5UWURn6UuuoFrabgDQUHdheKCziwN42aLwS3KizU,KzF8UWFcEC7BYTq8Go1xVimMkDmyNYVmXV5PV7RuDicvAocoPB8i,L3nHUboKG2w4VSJ5jYZ5CBM97oeK6YuKvfZxrefdShECcjEYKMWZ,KyjHo36dWkYhimKmVVmQTq3gERv3pnqA4xFCpvUgbGDJad7eS8WE,KwsfyHKRUTZPQtysN7M3tZ4GXTnuov5XRgjdF2XCG8faAPmFruRF,KzCUbGhN9LJhdeFfL9zQgTJMjqxdBKEekRGZX24hXdgCNCijkkap,KzgpMBwwsDLwkaC5UrmBgCYaBD2WgZ7PBoGYXR8KT7gCA9UTN5a3,KyBXTPy4T7YG4q9tcAM3LkvfRpD1ybHMvcJ2ehaWXaSqeGUxEdkP,KzJDe9iwJRPtKP2F2AoN6zBgzS7uiuAwhWCfGdNeYJ3PC1HNJ8M8,L1xbHrxynrqLKkoYc4qtoQPx6uy5qYXR5ZDYVYBSRmCV5piU3JG9,L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))","sh(multi(16,03669b8afcec803a0d323e9a17f3ea8e68e8abe5a278020a929adbec52421adbd0,0260b2003c386519fc9eadf2b5cf124dd8eea4c4e68d5e154050a9346ea98ce600,0362a74e399c39ed5593852a30147f2959b56bb827dfa3e60e464b02ccf87dc5e8,0261345b53de74a4d721ef877c255429961b7e43714171ac06168d7e08c542a8b8,02da72e8b46901a65d4374fe6315538d8f368557dda3a1dcf9ea903f3afe7314c8,0318c82dd0b53fd3a932d16e0ba9e278fcc937c582d5781be626ff16e201f72286,0297ccef1ef99f9d73dec9ad37476ddb232f1238aff877af19e72ba04493361009,02e502cfd5c3f972fe9a3e2a18827820638f96b6f347e54d63deb839011fd5765d,03e687710f0e3ebe81c1037074da939d409c0025f17eb86adb9427d28f0f7ae0e9,02c04d3a5274952acdbc76987f3184b346a483d43be40874624b29e3692c1df5af,02ed06e0f418b5b43a7ec01d1d7d27290fa15f75771cb69b642a51471c29c84acd,036d46073cbb9ffee90473f3da429abc8de7f8751199da44485682a989a4bebb24,02f5d1ff7c9029a80a4e36b9a5497027ef7f3e73384a4a94fbfe7c4e9164eec8bc,02e41deffd1b7cce11cde209a781adcffdabd1b91c0ba0375857a2bfd9302419f3,02d76625f7956a7fc505ab02556c23ee72d832f1bac391bcd2d3abce5710a13d06,0399eb0a5487515802dc14544cf10b3666623762fbed2ec38a3975716e2c29c232,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", "Cannot have 17 keys in multisig; must have between 1 and 16 keys, inclusive"); // Cannot have more than 16 keys in a multisig + + + // Check for invalid nesting of structures + CheckUnparsable("sh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "sh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)"); // P2SH needs a script, not a key + CheckUnparsable("sh(combo(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "sh(combo(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))"); // Old must be top level + CheckUnparsable("wsh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "wsh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)"); // P2WSH needs a script, not a key + CheckUnparsable("wsh(wpkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "wsh(wpkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))"); // Cannot embed witness inside witness + CheckUnparsable("wsh(sh(pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)))", "wsh(sh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)))"); // Cannot embed P2SH inside P2WSH + CheckUnparsable("sh(sh(pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)))", "sh(sh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)))"); // Cannot embed P2SH inside P2SH + CheckUnparsable("wsh(wsh(pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)))", "wsh(wsh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)))"); // Cannot embed P2WSH inside P2WSH + + // Checksums + CheckDescriptor("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfy", "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t", DEFAULT, new string[][]{ new string[]{ "a91445a9a622a8b0a1269944be477640eedc447bbd8487"} }, ScriptPubKeyType.Legacy,new uint[][] { new uint[]{ 0x8000006FU,222 }, new uint[]{ 0 } }); + CheckDescriptor("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))", "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))", DEFAULT, new string[][]{ new string[]{ "a91445a9a622a8b0a1269944be477640eedc447bbd8487"} }, ScriptPubKeyType.Legacy, new uint[][]{ new uint[] { 0x8000006FU,222}, new uint[]{ 0 } }); + CheckUnparsable("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#", "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#"); // Empty checksum + CheckUnparsable("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfyq", "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5tq"); // Too long checksum + CheckUnparsable("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxf", "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5"); // Too short checksum + CheckUnparsable("sh(multi(3,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfy", "sh(multi(3,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t"); // Error in payload + CheckUnparsable("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggssrxfy", "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjq09x4t"); // Error in checksum + CheckUnparsable("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))##ggssrxfy", "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))##tjq09x4t", "Multiple '#' symbols"); // Error in checksum + + // Addr and raw tests + CheckUnparsable("", "addr(asdf)", "Address is not valid"); // Invalid address + CheckUnparsable("", "raw(asdf)", "Raw script is not hex"); // Invalid script + CheckUnparsable("", "raw(Ü)#00000000", "Invalid characters in payload"); // Invalid chars + } + + private const int DEFAULT = 0; + private const int RANGE = 1; // Expected to be ranged descriptor + private const int HARDENED = 2; // derivation needs accesses to private keys + private const int UNSOLVABLE = 4; // This descriptor is not expected to be solvable + private const int SIGNABLE = 8; // We can sign with this descriptor (this is not true when actual BIP32 derivation is used, as that's not integrated in our signing code) + private const int DERIVE_HARDENDED = 16; // The final derivation is hardened, i.e. ends with *' or *h + System.Random Seed = new System.Random(); + + public Key DummyKey { get; } + + private void CheckDescriptor(string priv, string pub, int flags, string[][] scripts, ScriptPubKeyType? spkType = null, uint[][] pathIndex = null) + { + var keysPriv = new FlatSigningRepository(); + var keysPub = new FlatSigningRepository(); + var leftPath = pathIndex; + + var parsePriv = OutputDescriptor.Parse(MaybeInsertSpaces(MaybeUseHInsteadOfApostrophe(priv)), false, keysPriv); + var parsePub = OutputDescriptor.Parse(MaybeInsertSpaces(MaybeUseHInsteadOfApostrophe(pub)), false, keysPub); + + // Check that correct output type is inferred + Assert.Equal(parsePriv.GetScriptPubKeyType(), spkType); + Assert.Equal(parsePub.GetScriptPubKeyType(), spkType); + + // 1. Check private keys are extracted from the private version but not the public one. + Assert.True(keysPriv.Secrets.Count > 0); + Assert.True(keysPub.Secrets.Count == 0); + + // 2. Check both will serialize back to the public version. + string pub1 = parsePriv.ToString(); + string pub2 = parsePub.ToString(); + Assert.True(EqualDescriptorStr(pub, pub1)); + Assert.True(EqualDescriptorStr(pub, pub2)); + + // 3. Check that both can be serialized with private key back to the private version, but not without private key. + Assert.True(parsePub.TryGetPrivateString(keysPriv, out var priv1)); + Assert.True(EqualDescriptorStr(priv, priv1)); + Assert.False(parsePriv.TryGetPrivateString(keysPub, out priv1)); + Assert.True(parsePub.TryGetPrivateString(keysPriv, out priv1)); + Assert.True(EqualDescriptorStr(priv, priv1)); + Assert.False(parsePub.TryGetPrivateString(keysPub, out priv1)); + + // Check that `IsRange()` on both returns the expected result. + Assert.Equal(parsePub.IsRange(), (flags & RANGE) != 0); + Assert.Equal(parsePriv.IsRange(), (flags & RANGE) != 0); + + // * For ranged descriptors, the `scripts` parameter is a list of expected result outputs, for subsequent + // positions to evaluate the descriptors on (so the first element of `scripts` is for evaluating the + // descriptor at 0; the second at 1; and so on). To verify this, we evaluate the descriptors once for + // each element in `scripts`. + // * For non-ranged descriptors, we evaluate the descriptors at positions 0, 1, and 2, but expect the + // same result in each case, namely the first element of `scripts`. Because of that, the size of + // `scripts` must be one in that case + bool isRange = (flags & RANGE) > 0; + if (!isRange) Assert.Single(scripts); + var max = isRange ? scripts.Length : 3; + + for (int i = 0; i < max; ++i) + { + var expectedScript = scripts[isRange ? i : 0]; + for (int t = 0; t < 2; ++t) + { + bool isHardend = (flags & HARDENED) != 0; + var keyProvider = isHardend ? keysPriv : keysPub; + + var scriptProvider = new FlatSigningRepository(); + Assert.True((t != 0 ? parsePriv : parsePub).TryExpand((uint)i, keyProvider.GetPrivateKey, scriptProvider, out var spks)); + Assert.Equal(spks.Count, expectedScript.Length); + + // --- cache --- + + // --- --- + + for (int n = 0; n < spks.Count; ++n) + { + Assert.Equal(expectedScript[n], spks[n].ToHex()); + keysPriv.Merge(scriptProvider); + if ((flags & UNSOLVABLE) == 0) + Assert.True(keysPriv.IsSolvable(spks[n]), $"{spks[n]}\nMust be solvable"); + else + Assert.False(keysPriv.IsSolvable(spks[n]), $"{spks[n]}\nMust be unsolvable"); + + // Check that the information necessary to sign tx has been set to repository. + if ((flags & SIGNABLE) != 0) + { + var b = Network.Main.CreateTransactionBuilder(); + var coin = new Coin(RandOutpoint(), new TxOut(Money.Coins(1.0m), spks[n])); + b.AddCoins(coin); + b.SendFees(Money.Coins(0.0001m)); + b.SendAll(DummyKey); + b.AddKeys(keysPriv.Secrets.Values.Select(s => s.PrivateKey).ToArray()); + b.AddKnownRedeems(keysPriv.Scripts.Values.ToArray()); + var tx = b.BuildTransaction(true); + Assert.Empty(b.Check(tx)); + } + + var inferred = OutputDescriptor.InferFromScript(spks[n], scriptProvider); + Assert.Equal(((flags & UNSOLVABLE) == 0), inferred.IsSolvable()); + Func dummyKeyProvider = (keyId) => null; + var providerInferred = new FlatSigningRepository(); + Assert.True(inferred.TryExpand(0, dummyKeyProvider, providerInferred, out var spksInferred)); + Assert.Single(spksInferred); + Assert.Equal(spksInferred[0], spks[n]); + Assert.Equal(((flags & UNSOLVABLE) == 0), providerInferred.IsSolvable(spksInferred[0])); + Assert.Equal(providerInferred.KeyOrigins, scriptProvider.KeyOrigins); + } + + if (pathIndex != null) + { + var rootedKPs = scriptProvider.KeyOrigins.Values.ToArray(); + foreach (var rootedKP in rootedKPs) + { + Assert.Contains(pathIndex, p => p.SequenceEqual(rootedKP.KeyPath.Indexes)); + leftPath = leftPath.Where(p => !p.SequenceEqual(rootedKP.KeyPath.Indexes)).ToArray(); + } + } + } + } + if (leftPath != null && leftPath.Length != 0) + { + Console.WriteLine("Left path is"); + foreach (var p in pathIndex ?? Enumerable.Empty()) + Console.WriteLine($"{new KeyPath(p)}"); + throw new Exception("leftPath should be null"); + } + } + + private void CheckUnparsable(string prv, string pub, string maybeErrorMsg = null) + { + var keysPrv = new FlatSigningRepository(); + var keysPub = new FlatSigningRepository(); + var isSuccessPriv = OutputDescriptor.TryParse(prv, out var resultPrv, false, keysPrv); + var isSuccessPub = OutputDescriptor.TryParse(prv, out var resultPub, false, keysPub); + Assert.False(isSuccessPriv, prv); + Assert.False(isSuccessPub, pub); + + // same will hold even when we do not give repository. + var isSuccessPrivWithoutRepo = OutputDescriptor.TryParse(prv, out var resultPrv2); + var isSuccessPubWithoutRepo = OutputDescriptor.TryParse(prv, out var resultPub2); + Assert.False(isSuccessPrivWithoutRepo, prv); + Assert.False(isSuccessPubWithoutRepo, pub); + } + + private bool EqualDescriptorStr(string a, string b) + { + // May be need to ignore checksum specifically. + bool aCheck = (a.Length > 9 && a[a.Length - 9] == '#'); + bool bCheck = (b.Length > 9 && b[b.Length - 9] == '#'); + if (aCheck != bCheck) + { + if (aCheck) a = a.Substring(0, a.Length - 9); + if (bCheck) b = b.Substring(0, b.Length - 9); + } + return a == b; + } + + private string MaybeUseHInsteadOfApostrophe(string input) + { + if (Seed.NextDouble() < 0.5) + { + input = input.Replace('\'', 'h'); + // changing apostrophe will breaks checksum, so delete it. + if (input.Length > 9 && input[input.Length - 9] == '#') + input = input.Substring(0, input.Length - 9); + } + return input; + } + + } +} diff --git a/NBitcoin.Tests/RPCClientTests.cs b/NBitcoin.Tests/RPCClientTests.cs index cffc02d46b..7422278af0 100644 --- a/NBitcoin.Tests/RPCClientTests.cs +++ b/NBitcoin.Tests/RPCClientTests.cs @@ -20,6 +20,7 @@ using NBitcoin.Tests.Generators; using static NBitcoin.Tests.Comparer; using System.Net.Http; +using NBitcoin.Scripting; namespace NBitcoin.Tests { @@ -294,7 +295,12 @@ public void CanScanTxoutSet() var funding = rpc.GetRawTransaction(txid); var coin = funding.Outputs.AsCoins().Single(o => o.ScriptPubKey == dest.ScriptPubKey); +#pragma warning disable 618 var result = rpc.StartScanTxoutSet(new ScanTxoutSetObject(ScanTxoutDescriptor.Addr(dest))); +#pragma warning restore 618 + + var resultWithOD = rpc.StartScanTxoutSet(OutputDescriptor.NewAddr(dest)); + Assert.Equal(resultWithOD.TotalAmount, result.TotalAmount); Assert.Equal(101, result.SearchedItems); Assert.True(result.Success); @@ -306,7 +312,9 @@ public void CanScanTxoutSet() rpc.Generate(1); +#pragma warning disable 618 result = rpc.StartScanTxoutSet(new ScanTxoutSetObject(ScanTxoutDescriptor.Addr(dest))); +#pragma warning restore 618 Assert.True(result.SearchedItems > 100); Assert.True(result.Success); @@ -709,6 +717,20 @@ public void CanImportMultiAddresses() rpc.ImportMulti(multiAddresses.ToArray(), false); #endregion + #region ScriptPubKey + internal + label + key = new Key(); + multiAddresses = new List + { + new ImportMultiAddress + { + ScriptPubKey = new ImportMultiAddress.ScriptPubKeyObject(key.ScriptPubKey), + Internal = true, + Label = "Unsuccessful labelling for internal addresses" + } + }; + Assert.Throws(() => rpc.ImportMulti(multiAddresses.ToArray(), false)); + #endregion + #region ScriptPubKey + !internal key = new Key(); multiAddresses = new List @@ -751,6 +773,19 @@ public void CanImportMultiAddresses() rpc.ImportMulti(multiAddresses.ToArray(), false); #endregion + #region Nonstandard scriptPubKey + Public key + !internal + key = new Key(); + var nonStandardSpk = Script.FromHex(key.ScriptPubKey.ToHex() + new Script(OpcodeType.OP_NOP ).ToHex()); + multiAddresses = new List + { + new ImportMultiAddress + { + ScriptPubKey = new ImportMultiAddress.ScriptPubKeyObject(nonStandardSpk), + } + }; + Assert.Throws(() => rpc.ImportMulti(multiAddresses.ToArray(), false)); + #endregion + #region ScriptPubKey + Public key + !internal key = new Key(); multiAddresses = new List @@ -918,6 +953,85 @@ public void CanImportMultiAddresses() #region restart nodes to check for proper serialization/deserialization of watch only address //TODO #endregion + + # region Test importing of a P2SH-P2WPKH address via descriptor + private key + key = new Key(); + var p2shP2wpkhLabel = "Successful P2SH-P2wPKH descriptor import"; + multiAddresses = new List + { + new ImportMultiAddress + { + Desc = OutputDescriptor.Parse($"sh(wpkh({key.PubKey}))"), + Label = p2shP2wpkhLabel, + Keys = new [] { new BitcoinSecret(key, rpc.Network)}, + } + }; + rpc.ImportMulti(multiAddresses.ToArray(), false); + # endregion + + # region Test ranged descriptor fails if range is not specified + + var xpriv = + "tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg"; + var addresses = new List() {"2N7yv4p8G8yEaPddJxY41kPihnWvs39qCMf", "2MsHxyb2JS3pAySeNUsJ7mNnurtpeenDzLA"}; // hdkeypath=m/0'/0'/0' and 1'a + addresses.AddRange(new[] + { + "bcrt1qrd3n235cj2czsfmsuvqqpr3lu6lg0ju7scl8gn", "bcrt1qfqeppuvj0ww98r6qghmdkj70tv8qpchehegrg8" + }); // wpkh subscripts corresponding to the above addresses + + var desc = OutputDescriptor.Parse($"sh(wpkh({xpriv}/0'/0'/*'))"); + multiAddresses = new List + { + new ImportMultiAddress + { + Desc = desc, + } + }; + // Must fail without range + Assert.Throws(() => rpc.ImportMulti(multiAddresses.ToArray(), false)); + multiAddresses = new List + { + new ImportMultiAddress + { + Desc = desc, + Range = 1 + } + }; + rpc.ImportMulti(multiAddresses.ToArray(), false); + # endregion + # region Test importing a descriptor containing a WIF private key + + var wifPriv = "cTe1f5rdT8A8DFgVWTjyPwACsDPJM9ff4QngFxUixCSvvbg1x6sh"; + var address = "2MuhcG52uHPknxDgmGPsV18jSHFBnnRgjPg"; + desc = OutputDescriptor.Parse($"sh(wpkh({wifPriv}))"); + multiAddresses = new List + { + new ImportMultiAddress + { + Desc = desc, + Keys = new [] {new BitcoinSecret(wifPriv) } + } + }; + rpc.ImportMulti(multiAddresses.ToArray(), false); + TestAddress(rpc, address, true, true); + + # endregion + } + } + + /// + /// https://github.com/bitcoin/bitcoin/blob/db26eeba71fb07caae8c4c8a59a80c4ebe0b5797/test/functional/test_framework/wallet_util.py#L111 + /// + private void TestAddress(RPCClient rpc, string address, bool? solvable = null, bool? isMine = null) + { + var addrInfo = rpc.GetAddressInfo(BitcoinAddress.Create(address, rpc.Network)); + if (solvable != null) + { + Assert.Equal(solvable, addrInfo.Solvable); + } + if (isMine != null) + { + Assert.Equal(isMine, addrInfo.IsMine); } } @@ -1249,7 +1363,7 @@ public void MempoolInfoWithHistogram() " }," + " \"total_fees\": 61420473" + " }" + - " }" + + " }" + "}" )); var rpcClient = new RPCClient(Network.Main); diff --git a/NBitcoin.Tests/RPCPropertyTests.cs b/NBitcoin.Tests/RPCPropertyTests.cs new file mode 100644 index 0000000000..23e6a95edb --- /dev/null +++ b/NBitcoin.Tests/RPCPropertyTests.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using FsCheck; +using FsCheck.Xunit; +using NBitcoin.RPC; +using NBitcoin.Scripting; +using Xunit; +using NBitcoin.Tests.Generators; +using Newtonsoft.Json.Linq; + +namespace NBitcoin.Tests +{ + public class RPCPropertyTests + { + public RPCPropertyTests() + { + Arb.Register(); + } + + [Property(MaxTest = 5)] + [Trait("PropertyTest", "RPC")] + public void ShouldAlwaysCreateBitcoinCoreAcceptableOutputDescriptor(OutputDescriptor od) + { + using var builder = NodeBuilderEx.Create(); + var cli = builder.CreateNode().CreateRPCClient(); + builder.StartAll(); + cli.SendCommand("getdescriptorinfo", od.ToString()); + } + } +} diff --git a/NBitcoin.Tests/bip32_tests.cs b/NBitcoin.Tests/bip32_tests.cs index 39c6d006eb..379bb29cb4 100644 --- a/NBitcoin.Tests/bip32_tests.cs +++ b/NBitcoin.Tests/bip32_tests.cs @@ -321,5 +321,24 @@ private void RunTest(TestVector test) pubkey = pubkeyNew; } } + + [Fact] + [Trait("UnitTest", "UnitTest")] + public void CanHandleEmptyKeyPath() + { + var rKeyPath = RootedKeyPath.Parse("01234567"); + Assert.Equal(rKeyPath.KeyPath, KeyPath.Empty); + Assert.Equal("", KeyPath.Empty.ToString()); + Assert.Equal(new byte[0], KeyPath.Empty.ToBytes()); + Assert.Equal("01234567", rKeyPath.ToStringWithEmptyKeyPathAware()); + } + + [Fact] + [Trait("UnitTest", "UnitTest")] + public void KeyPathShouldNotParseBIP32Overflow() + { + Assert.Equal(0x80000000U, uint.Parse("2147483648")); + Assert.Throws(() => KeyPath.Parse("/2147483648")); + } } } diff --git a/NBitcoin.Tests/transaction_tests.cs b/NBitcoin.Tests/transaction_tests.cs index 1f1ad30849..faee0e9a79 100644 --- a/NBitcoin.Tests/transaction_tests.cs +++ b/NBitcoin.Tests/transaction_tests.cs @@ -19,6 +19,7 @@ using Xunit; using Xunit.Abstractions; using Encoders = NBitcoin.DataEncoders.Encoders; +using static NBitcoin.Tests.Helpers.PrimitiveUtils; namespace NBitcoin.Tests { @@ -1734,10 +1735,6 @@ public void CanBuildStealthTransaction() Assert.True(builder.Verify(tx)); //Fully signed ! } - private OutPoint RandOutpoint() - { - return new OutPoint(Rand(), 0); - } [Fact] [Trait("UnitTest", "UnitTest")] @@ -2745,10 +2742,6 @@ public void CanBuildTransaction() Assert.Equal(Money.Coins(1.0m), tx.GetFee(txBuilder.FindSpentCoins(tx))); } - private uint256 Rand() - { - return new uint256(RandomUtils.GetBytes(32)); - } [Fact] [Trait("UnitTest", "UnitTest")] @@ -4403,6 +4396,7 @@ private byte[] ParseHex(string data) } [Fact] + [Trait("UnitTest", "UnitTest")] public void ShouldSendAll() { var builder = Network diff --git a/NBitcoin.Tests/util_tests.cs b/NBitcoin.Tests/util_tests.cs index 785695a6de..45a22cbae5 100644 --- a/NBitcoin.Tests/util_tests.cs +++ b/NBitcoin.Tests/util_tests.cs @@ -139,12 +139,12 @@ public void CanParseKeyPath() "m/0h", "m/0h/1", "m/0/1", - $"m/{uint.MaxValue}/1", $"m/{0x80000000u - 1}h", $"m/{0x80000000u - 1}" }.ToList(); var invalid = new[] { + $"m/{uint.MaxValue}/1", $"m/{((long)uint.MaxValue) + 1}/1", $"k/0/1", $"h/1", diff --git a/NBitcoin/BIP32/BitcoinExtKey.cs b/NBitcoin/BIP32/BitcoinExtKey.cs index e7cb978de9..c15c2bbf00 100644 --- a/NBitcoin/BIP32/BitcoinExtKey.cs +++ b/NBitcoin/BIP32/BitcoinExtKey.cs @@ -45,6 +45,10 @@ public BitcoinExtKey(ExtKey key, Network network) { } + public BitcoinExtKey(BitcoinExtPubKey bitcoinExtPubKey, Key key) + : base(new ExtKey(bitcoinExtPubKey.ExtPubKey, key), bitcoinExtPubKey.Network) + {} + /// /// Gets whether the data is the correct expected length. /// diff --git a/NBitcoin/BIP32/HDFingerPrint.cs b/NBitcoin/BIP32/HDFingerPrint.cs index 54d3deec4a..62eefe889c 100644 --- a/NBitcoin/BIP32/HDFingerPrint.cs +++ b/NBitcoin/BIP32/HDFingerPrint.cs @@ -1,6 +1,7 @@ using NBitcoin.DataEncoders; using System; using System.Collections.Generic; +using System.Linq; using System.Text; namespace NBitcoin @@ -46,6 +47,11 @@ public HDFingerprint(ReadOnlySpan bytes) } #endif + public static HDFingerprint FromKeyId(KeyId id) + { + return new HDFingerprint(id.ToBytes().Take(4).ToArray()); + } + public HDFingerprint(byte[] bytes, int index) { if (bytes == null) diff --git a/NBitcoin/BIP32/KeyPath.cs b/NBitcoin/BIP32/KeyPath.cs index e67ab75636..e358e31533 100644 --- a/NBitcoin/BIP32/KeyPath.cs +++ b/NBitcoin/BIP32/KeyPath.cs @@ -112,13 +112,15 @@ private static bool TryParseCore(string i, out uint index) var nonhardened = hardened ? i.Substring(0, i.Length - 1) : i; if (!uint.TryParse(nonhardened, out index)) return false; + + // when parsing, number equals or greater than 0x80000000 (= 2147483648) should not be allowed. + if (index >= 0x80000000u) + { + index = 0; + return false; + } if (hardened) { - if (index >= 0x80000000u) - { - index = 0; - return false; - } index = index | 0x80000000u; return true; } @@ -128,6 +130,16 @@ private static bool TryParseCore(string i, out uint index) } } + static KeyPath _Empty = new KeyPath(new uint[0]); + + public static KeyPath Empty + { + get + { + return _Empty; + } + } + public KeyPath(params uint[] indexes) { if (indexes.Length > 255) diff --git a/NBitcoin/BIP32/RootedKeyPath.cs b/NBitcoin/BIP32/RootedKeyPath.cs index 07ba9a029a..7c3c3179df 100644 --- a/NBitcoin/BIP32/RootedKeyPath.cs +++ b/NBitcoin/BIP32/RootedKeyPath.cs @@ -23,12 +23,19 @@ public static bool TryParse(string str, out RootedKeyPath result) result = null; var separator = str.IndexOf('/'); if (separator == -1) - return false; - if (!HDFingerprint.TryParse(str.Substring(0, separator), out var fp)) - return false; - if (!NBitcoin.KeyPath.TryParse(str.Substring(separator + 1), out var keyPath)) - return false; - result = new RootedKeyPath(fp, keyPath); + { + if (!HDFingerprint.TryParse(str, out var fp)) + return false; + result = new RootedKeyPath(fp, KeyPath.Empty); + } + else + { + if (!HDFingerprint.TryParse(str.Substring(0, separator), out var fp)) + return false; + if (!NBitcoin.KeyPath.TryParse(str.Substring(separator + 1), out var keyPath)) + return false; + result = new RootedKeyPath(fp, keyPath); + } return true; } public RootedKeyPath(HDFingerprint masterFingerprint, KeyPath keyPath) @@ -86,12 +93,14 @@ public RootedKeyPath GetAccountKeyPath() return new RootedKeyPath(MasterFingerprint, KeyPath.GetAccountKeyPath()); } - public override string ToString() - { - return $"{MasterFingerprint}/{KeyPath}"; - } - + public override string ToString() => $"{MasterFingerprint}/{KeyPath}"; + /// + /// Mostly works same with `ToString()`, but if the `KeyPath` is empty, it just returns master finger print + /// without `/` in the suffix + /// + /// + public string ToStringWithEmptyKeyPathAware() => KeyPath == KeyPath.Empty ? MasterFingerprint.ToString() : ToString(); public override bool Equals(object obj) { RootedKeyPath item = obj as RootedKeyPath; diff --git a/NBitcoin/Crypto/Hashes.cs b/NBitcoin/Crypto/Hashes.cs index 62a4f08396..5ae87db122 100644 --- a/NBitcoin/Crypto/Hashes.cs +++ b/NBitcoin/Crypto/Hashes.cs @@ -103,13 +103,14 @@ public static uint160 Hash160(byte[] data, int offset, int count) #endregion #region RIPEMD160 - private static byte[] RIPEMD160(byte[] data) + public static byte[] RIPEMD160(byte[] data) { return RIPEMD160(data, 0, data.Length); } public static byte[] RIPEMD160(byte[] data, int count) { + if (data == null) throw new ArgumentNullException(nameof(data)); return RIPEMD160(data, 0, count); } diff --git a/NBitcoin/JsonConverters/OutputDescriptorJsonConverter.cs b/NBitcoin/JsonConverters/OutputDescriptorJsonConverter.cs new file mode 100644 index 0000000000..47696db719 --- /dev/null +++ b/NBitcoin/JsonConverters/OutputDescriptorJsonConverter.cs @@ -0,0 +1,67 @@ +#nullable enable +using System; +using System.Reflection; +using Newtonsoft.Json; +using NBitcoin.Scripting; + +namespace NBitcoin.JsonConverters +{ + public class OutputDescriptorJsonConverter : JsonConverter + { + private readonly bool _requireChecksum; + private readonly ISigningRepository? _signingRepository; + + public OutputDescriptorJsonConverter(bool requireChecksum = false, ISigningRepository? signingRepository = null) : base() + { + _requireChecksum = requireChecksum; + _signingRepository = signingRepository; + } + + public override bool CanConvert(Type objectType) + { + return typeof(OutputDescriptor).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); + } + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + reader.AssertJsonType(JsonToken.String); + try + { + if (!OutputDescriptor.TryParse((string) reader.Value, out var od, _requireChecksum, _signingRepository)) + throw new JsonObjectException("Invalid OutputDescriptor", reader); + return od; + } + catch (FormatException ex) + { + throw new JsonObjectException($"Invalid OutputDescriptor {ex}", reader); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + string? str = null; + if (value is OutputDescriptor od) + { + if (_signingRepository is null) + { + str = od.ToString(); + } + else + { + if (od.TryGetPrivateString(_signingRepository, out str ) || str != null) + { + } + else + { + str = od.ToString(); + } + } + } + if (str != null) + writer.WriteValue(str); + } + } +} + +#nullable disable diff --git a/NBitcoin/KeyId.cs b/NBitcoin/KeyId.cs index c69afb1502..c62ba658cd 100644 --- a/NBitcoin/KeyId.cs +++ b/NBitcoin/KeyId.cs @@ -213,6 +213,23 @@ public WitScriptId(Script script) { } + /// + /// When we store internal ScriptId -> Script lookup, having another + /// WitScriptId -> WitScript KVMap will complicate implementation. And require + /// More space because WitScriptId is bigger than ScriptId. But if we use Hash160 as ID, + /// It will cause a problem in case of p2sh-p2wsh because we must hold two scripts + /// (witness program and witness script) with one ScriptId. So instead we use single-RIPEMD160 + /// This is the same way with how bitcoin core handles scripts internally. + /// + public ScriptId _HashForLookUp; + public ScriptId HashForLookUp + { + get{ + return _HashForLookUp ?? (_HashForLookUp = new ScriptId(new uint160(Hashes.RIPEMD160(this.ToBytes())))); + } + } + + public override Script ScriptPubKey { get diff --git a/NBitcoin/RPC/GetAddressInfoResponse.cs b/NBitcoin/RPC/GetAddressInfoResponse.cs index a7201ae637..fcacd3d259 100644 --- a/NBitcoin/RPC/GetAddressInfoResponse.cs +++ b/NBitcoin/RPC/GetAddressInfoResponse.cs @@ -1,6 +1,7 @@ #if !NOJSONNET using System; using System.Collections.Generic; +using NBitcoin.Scripting; using Newtonsoft.Json.Linq; namespace NBitcoin.RPC @@ -9,8 +10,14 @@ public class GetAddressInfoResponse : GetAddressInfoScriptInfoResponse { public bool IsMine { get; private set; } public bool? Solvable { get; private set; } + + [Obsolete("Use Descriptor field instead")] public ScanTxoutDescriptor Desc { get; private set; } +# nullable enable + public OutputDescriptor? Descriptor { get; private set; } +#nullable disable + // present only in p2sh-nested case public GetAddressInfoScriptInfoResponse Embedded { get; private set; } public string Label { get; private set; } @@ -35,7 +42,12 @@ public virtual GetAddressInfoResponse LoadFromJson(JObject raw, Network network) SetSubInfo(this, raw, network); IsMine = raw.Property("ismine").Value.Value(); Solvable = raw.Property("solvable")?.Value.Value(); +#pragma warning disable 618 Desc = raw.Property("desc") == null ? null : new ScanTxoutDescriptor(raw.Property("desc").Value.Value()); +#pragma warning restore 618 + Descriptor = raw.Property("desc") == null + ? null + : OutputDescriptor.Parse(raw.Property("desc").Value.Value()); IsWatchOnly = raw.Property("iswatchonly").Value.Value(); IsScript = raw.Property("isscript").Value.Value(); IsWitness = raw.Property("iswitness").Value.Value(); diff --git a/NBitcoin/RPC/ImportMultiAddress.cs b/NBitcoin/RPC/ImportMultiAddress.cs index 6b1836dc30..4518c3fd0c 100644 --- a/NBitcoin/RPC/ImportMultiAddress.cs +++ b/NBitcoin/RPC/ImportMultiAddress.cs @@ -1,6 +1,8 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; +using NBitcoin.JsonConverters; +using NBitcoin.Scripting; namespace NBitcoin.RPC { @@ -69,6 +71,22 @@ public bool IsAddress [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] public string Label { get; set; } + + [JsonProperty("desc", NullValueHandling = NullValueHandling.Ignore)] + public OutputDescriptor Desc { get; set; } + + [JsonProperty("range", NullValueHandling = NullValueHandling.Ignore)] + public int[] Ranges { get; set; } + + [JsonIgnore] + public int Range + { + set + { + Ranges ??= new[] {0, 0}; + Ranges[1] = value; + } + } } /// diff --git a/NBitcoin/RPC/RPCClient.Wallet.cs b/NBitcoin/RPC/RPCClient.Wallet.cs index 41ffcc0b0e..26ed76f26c 100644 --- a/NBitcoin/RPC/RPCClient.Wallet.cs +++ b/NBitcoin/RPC/RPCClient.Wallet.cs @@ -74,7 +74,7 @@ public List ChangeAddresses } /* - Category Name Implemented + Category Name Implemented ------------------ --------------------------- ----------------------- ------------------ Wallet @@ -123,7 +123,7 @@ wallet walletcreatefundedpsbt */ public partial class RPCClient { - // backupwallet + // backupwallet public void BackupWallet(string path) { @@ -258,7 +258,7 @@ private string ToHex(Transaction tx) // getreceivedbyaddress /// - /// Returns the total amount received by the specified address in transactions with at + /// Returns the total amount received by the specified address in transactions with at /// least one (default) confirmations. It does not count coinbase transactions. /// /// The address whose transactions should be tallied. @@ -270,7 +270,7 @@ public Money GetReceivedByAddress(BitcoinAddress address) } /// - /// Returns the total amount received by the specified address in transactions with at + /// Returns the total amount received by the specified address in transactions with at /// least one (default) confirmations. It does not count coinbase transactions. /// /// The address whose transactions should be tallied. @@ -282,14 +282,14 @@ public async Task GetReceivedByAddressAsync(BitcoinAddress address) } /// - /// Returns the total amount received by the specified address in transactions with the + /// Returns the total amount received by the specified address in transactions with the /// specified number of confirmations. It does not count coinbase transactions. /// /// - /// The minimum number of confirmations an externally-generated transaction must have before - /// it is counted towards the balance. Transactions generated by this node are counted immediately. - /// Typically, externally-generated transactions are payments to this wallet and transactions - /// generated by this node are payments to other wallets. Use 0 to count unconfirmed transactions. + /// The minimum number of confirmations an externally-generated transaction must have before + /// it is counted towards the balance. Transactions generated by this node are counted immediately. + /// Typically, externally-generated transactions are payments to this wallet and transactions + /// generated by this node are payments to other wallets. Use 0 to count unconfirmed transactions. /// Default is 1. /// /// The number of bitcoins received by the address, excluding coinbase transactions. May be 0. @@ -300,14 +300,14 @@ public Money GetReceivedByAddress(BitcoinAddress address, int confirmations) } /// - /// Returns the total amount received by the specified address in transactions with the + /// Returns the total amount received by the specified address in transactions with the /// specified number of confirmations. It does not count coinbase transactions. /// /// - /// The minimum number of confirmations an externally-generated transaction must have before - /// it is counted towards the balance. Transactions generated by this node are counted immediately. - /// Typically, externally-generated transactions are payments to this wallet and transactions - /// generated by this node are payments to other wallets. Use 0 to count unconfirmed transactions. + /// The minimum number of confirmations an externally-generated transaction must have before + /// it is counted towards the balance. Transactions generated by this node are counted immediately. + /// Typically, externally-generated transactions are payments to this wallet and transactions + /// generated by this node are payments to other wallets. Use 0 to count unconfirmed transactions. /// Default is 1. /// /// The number of bitcoins received by the address, excluding coinbase transactions. May be 0. @@ -393,41 +393,58 @@ public async Task ImportAddressAsync(BitcoinAddress address, string label, bool // importmulti + public void ImportMulti(ImportMultiAddress[] addresses, bool rescan) => + ImportMulti(addresses, rescan, null); - public void ImportMulti(ImportMultiAddress[] addresses, bool rescan) + #nullable enable + public void ImportMulti(ImportMultiAddress[] addresses, bool rescan, ISigningRepository? signingRepository) { - ImportMultiAsync(addresses, rescan).GetAwaiter().GetResult(); + ImportMultiAsync(addresses, rescan, signingRepository).GetAwaiter().GetResult(); } + public Task ImportMultiAsync(ImportMultiAddress[] addresses, bool rescan) + => ImportMultiAsync(addresses, rescan, null); + /// + /// + /// + /// + /// + /// If you specify this, This method tries to serialize OutputDescriptor with the private key (If there is any entry in the repository). + /// + /// + public async Task ImportMultiAsync(ImportMultiAddress[] addresses, bool rescan, ISigningRepository? signingRepository) + { + var parameters = new List(); + var array = new JArray(); + parameters.Add(array); + var seria = JsonSerializer.CreateDefault(JsonSerializerSettings); + // -- replace json converter with the one with new `ISigningRepository` + var oldConverter = + seria.Converters + .FirstOrDefault(converter => converter is OutputDescriptorJsonConverter); + if (oldConverter != null) + { + seria.Converters.Remove(oldConverter); + } - JsonSerializerSettings _JsonSerializer; - JsonSerializerSettings JsonSerializerSettings - { - get + signingRepository ??= new FlatSigningRepository(); + foreach (var key in addresses.Where(x => x.Keys != null).SelectMany(x => x.Keys)) { - if (_JsonSerializer == null) + if (key != null) { - var seria = new JsonSerializerSettings(); - Serializer.RegisterFrontConverters(seria, Network); - _JsonSerializer = seria; + signingRepository.SetSecret(key.PubKeyHash, key); } - return _JsonSerializer; } - } - public async Task ImportMultiAsync(ImportMultiAddress[] addresses, bool rescan) - { - var parameters = new List(); + seria.Converters.Add(new OutputDescriptorJsonConverter(false, signingRepository)); - var array = new JArray(); - parameters.Add(array); - var seria = JsonSerializer.CreateDefault(JsonSerializerSettings); + // -- -- foreach (var addr in addresses) { var obj = JObject.FromObject(addr, seria); if (obj["timestamp"] == null || obj["timestamp"].Type == JTokenType.Null) obj["timestamp"] = "now"; else - obj["timestamp"] = new JValue(Utils.DateTimeToUnixTime(addr.Timestamp.Value)); + obj["timestamp"] = new JValue(Utils.DateTimeToUnixTime(addr.Timestamp!.Value)); array.Add(obj); } @@ -448,11 +465,28 @@ public async Task ImportMultiAsync(ImportMultiAddress[] addresses, bool rescan) } } + #nullable disable + + + JsonSerializerSettings _JsonSerializer; + JsonSerializerSettings JsonSerializerSettings + { + get + { + if (_JsonSerializer == null) + { + var seria = new JsonSerializerSettings(); + Serializer.RegisterFrontConverters(seria, Network); + _JsonSerializer = seria; + } + return _JsonSerializer; + } + } // listaccounts /// - /// Lists accounts and their balances, with the default number of confirmations for balances (1), + /// Lists accounts and their balances, with the default number of confirmations for balances (1), /// and not including watch only addresses (default false). /// public IEnumerable ListAccounts() @@ -465,10 +499,10 @@ public IEnumerable ListAccounts() /// Lists accounts and their balances, based on the specified number of confirmations. /// /// - /// The minimum number of confirmations an externally-generated transaction must have before - /// it is counted towards the balance. Transactions generated by this node are counted immediately. - /// Typically, externally-generated transactions are payments to this wallet and transactions - /// generated by this node are payments to other wallets. Use 0 to count unconfirmed transactions. + /// The minimum number of confirmations an externally-generated transaction must have before + /// it is counted towards the balance. Transactions generated by this node are counted immediately. + /// Typically, externally-generated transactions are payments to this wallet and transactions + /// generated by this node are payments to other wallets. Use 0 to count unconfirmed transactions. /// Default is 1. /// /// @@ -481,19 +515,19 @@ public IEnumerable ListAccounts(int confirmations) } /// - /// Lists accounts and their balances, based on the specified number of confirmations, + /// Lists accounts and their balances, based on the specified number of confirmations, /// and including watch only accounts if specified. (Added in Bitcoin Core 0.10.0) /// /// - /// The minimum number of confirmations an externally-generated transaction must have before - /// it is counted towards the balance. Transactions generated by this node are counted immediately. - /// Typically, externally-generated transactions are payments to this wallet and transactions - /// generated by this node are payments to other wallets. Use 0 to count unconfirmed transactions. + /// The minimum number of confirmations an externally-generated transaction must have before + /// it is counted towards the balance. Transactions generated by this node are counted immediately. + /// Typically, externally-generated transactions are payments to this wallet and transactions + /// generated by this node are payments to other wallets. Use 0 to count unconfirmed transactions. /// Default is 1. /// /// - /// If set to true, include watch-only addresses in details and calculations as if they were - /// regular addresses belonging to the wallet. If set to false (the default), treat watch-only + /// If set to true, include watch-only addresses in details and calculations as if they were + /// regular addresses belonging to the wallet. If set to false (the default), treat watch-only /// addresses as if they didn’t belong to this wallet. /// /// @@ -560,11 +594,11 @@ public IEnumerable ListSecrets() // listunspent /// - /// Returns an array of unspent transaction outputs belonging to this wallet. + /// Returns an array of unspent transaction outputs belonging to this wallet. /// /// /// - /// Note: as of Bitcoin Core 0.10.0, outputs affecting watch-only addresses will be returned; + /// Note: as of Bitcoin Core 0.10.0, outputs affecting watch-only addresses will be returned; /// see the spendable field in the results. /// /// @@ -577,7 +611,7 @@ public UnspentCoin[] ListUnspent() /// /// Returns an array of unspent transaction outputs belonging to this wallet, /// specifying the minimum and maximum number of confirmations to include, - /// and the list of addresses to include. + /// and the list of addresses to include. /// public UnspentCoin[] ListUnspent(int minconf, int maxconf, params BitcoinAddress[] addresses) { @@ -587,7 +621,7 @@ public UnspentCoin[] ListUnspent(int minconf, int maxconf, params BitcoinAddress } /// - /// Returns an array of unspent transaction outputs belonging to this wallet. + /// Returns an array of unspent transaction outputs belonging to this wallet. /// public async Task ListUnspentAsync() { @@ -598,7 +632,7 @@ public async Task ListUnspentAsync() /// /// Returns an array of unspent transaction outputs belonging to this wallet, /// specifying the minimum and maximum number of confirmations to include, - /// and the list of addresses to include. + /// and the list of addresses to include. /// public async Task ListUnspentAsync(int minconf, int maxconf, params BitcoinAddress[] addresses) { @@ -609,7 +643,7 @@ public async Task ListUnspentAsync(int minconf, int maxconf, para /// /// Returns an array of unspent transaction outputs belonging to this wallet, - /// with query_options and the list of addresses to include. + /// with query_options and the list of addresses to include. /// /// /// MinimumAmount - Minimum value of each UTXO diff --git a/NBitcoin/RPC/RPCClient.cs b/NBitcoin/RPC/RPCClient.cs index cf57b34283..d6d534d2e6 100644 --- a/NBitcoin/RPC/RPCClient.cs +++ b/NBitcoin/RPC/RPCClient.cs @@ -16,6 +16,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using NBitcoin.Scripting; using static NBitcoin.RPC.BlockchainInfo; namespace NBitcoin.RPC @@ -676,11 +677,74 @@ public async Task UptimeAsync() return TimeSpan.FromSeconds(res.Result.Value()); } + + public ScanTxoutSetResponse StartScanTxoutSet(OutputDescriptor descriptor, uint rangeStart = 0, + uint rangeEnd = 1000) => StartScanTxoutSetAsync(new[] {descriptor}.AsEnumerable(), rangeStart, rangeEnd).GetAwaiter().GetResult(); + + public ScanTxoutSetResponse StartScanTxoutSet(IEnumerable descriptor, + uint rangeStart = 0, uint rangeEnd = 1000) + => StartScanTxoutSetAsync(descriptor, rangeStart, rangeEnd).GetAwaiter().GetResult(); + + public Task StartScanTxoutSetAsync(OutputDescriptor descriptor, uint rangeStart = 0, + uint rangeEnd = 1000) => StartScanTxoutSetAsync(new[] {descriptor}.AsEnumerable(), rangeStart, rangeEnd); + public async Task StartScanTxoutSetAsync(IEnumerable descriptor, uint rangeStart = 0, uint rangeEnd = 1000) + { + if (descriptor == null) + throw new ArgumentNullException(nameof(descriptor)); + + JArray descriptorsJson = new JArray(); + foreach (var descObj in descriptor) + { + JObject descJson = new JObject(); + descJson.Add(new JProperty("desc", descObj.ToString())); + if (descObj.IsRange()) + { + var r = new JArray(); + r.Add(rangeStart); + r.Add(rangeEnd); + descJson.Add(new JProperty("range", r)); + } + descriptorsJson.Add(descJson); + } + + var result = await SendCommandAsync(RPCOperations.scantxoutset, "start", descriptorsJson); + result.ThrowIfError(); + + var jobj = result.Result as JObject; + var amount = Money.Coins(jobj.Property("total_amount").Value.Value()); + var success = jobj.Property("success").Value.Value(); + //searched_items + + var searchedItems = (int)(jobj.Property("txouts") ?? jobj.Property("searched_items")).Value.Value(); + var outputs = new List(); + foreach (var unspent in (jobj.Property("unspents").Value as JArray).OfType()) + { + OutPoint outpoint = OutPoint.Parse($"{unspent.Property("txid").Value.Value()}-{(int)unspent.Property("vout").Value.Value()}"); + var a = Money.Coins(unspent.Property("amount").Value.Value()); + int height = (int)unspent.Property("height").Value.Value(); + var scriptPubKey = Script.FromBytesUnsafe(Encoders.Hex.DecodeData(unspent.Property("scriptPubKey").Value.Value())); + outputs.Add(new ScanTxoutOutput() + { + Coin = new Coin(outpoint, new TxOut(a, scriptPubKey)), + Height = height + }); + } + + return new ScanTxoutSetResponse() + { + Outputs = outputs.ToArray(), + TotalAmount = amount, + Success = success, + SearchedItems = searchedItems + }; + } + /// /// Scans the unspent transaction output set for entries that match certain output descriptors. /// /// /// + [Obsolete("Pass OutputDescriptor[] instead")] public async Task StartScanTxoutSetAsync(params ScanTxoutSetObject[] descriptorObjects) { if (descriptorObjects == null) @@ -733,6 +797,7 @@ public async Task StartScanTxoutSetAsync(params ScanTxoutS /// /// /// + [Obsolete("Pass OutputDescriptor[] instead")] public ScanTxoutSetResponse StartScanTxoutSet(params ScanTxoutSetObject[] descriptorObjects) { return StartScanTxoutSetAsync(descriptorObjects).GetAwaiter().GetResult(); @@ -1642,7 +1707,7 @@ public async Task GetMempoolEntryAsync(uint256 txid, bool throwIfN }; } - private FeeRate AbsurdlyHighFee { get; } = new FeeRate(10_000L); + private FeeRate AbsurdlyHighFee { get; } = new FeeRate(10_000M); public MempoolAcceptResult TestMempoolAccept(Transaction transaction, bool allowHighFees = false) { diff --git a/NBitcoin/RPC/ScanTxoutSetRequest.cs b/NBitcoin/RPC/ScanTxoutSetRequest.cs index 4bf650594d..f765f7e47f 100644 --- a/NBitcoin/RPC/ScanTxoutSetRequest.cs +++ b/NBitcoin/RPC/ScanTxoutSetRequest.cs @@ -5,6 +5,7 @@ namespace NBitcoin.RPC { + [Obsolete("Use PubKeyProvider instead")] public class ScanTxoutPubkey { /// @@ -98,6 +99,8 @@ public override string ToString() return Value; } } + + [Obsolete("Use OutputDescriptor instead")] public class ScanTxoutDescriptor { /// @@ -225,6 +228,8 @@ public override string ToString() return Value; } } + + [Obsolete("Use OutputDescriptor instead")] public class ScanTxoutSetObject { public ScanTxoutSetObject(ScanTxoutDescriptor descriptor, int? range = null) diff --git a/NBitcoin/Scripting/OutputDescriptor.cs b/NBitcoin/Scripting/OutputDescriptor.cs new file mode 100644 index 0000000000..a61256af6e --- /dev/null +++ b/NBitcoin/Scripting/OutputDescriptor.cs @@ -0,0 +1,720 @@ +using System; +using System.Collections.Generic; +using System.Linq; +#nullable enable + +namespace NBitcoin.Scripting +{ + + public abstract class OutputDescriptor : IEquatable + { + # region subtypes + public class Addr : OutputDescriptor + { + public IDestination Address { get; } + public Addr(IDestination address) + { + if (address == null) + throw new ArgumentNullException(nameof(address)); + Address = address; + } + } + + public class Raw : OutputDescriptor + { + public Script Script; + + internal Raw(Script script) + { + if (script is null) + throw new ArgumentNullException(nameof(script)); + if (script.Length == 0) + throw new ArgumentException($"{nameof(script)} must not be empty!"); + Script = script; + } + } + + public class PK : OutputDescriptor + { + public PubKeyProvider PkProvider; + internal PK(PubKeyProvider pkProvider) + { + if (pkProvider == null) + throw new ArgumentNullException(nameof(pkProvider)); + PkProvider = pkProvider; + } + } + + public class PKH : OutputDescriptor + { + public PubKeyProvider PkProvider; + internal PKH(PubKeyProvider pkProvider) + { + if (pkProvider == null) + throw new ArgumentNullException(nameof(pkProvider)); + PkProvider = pkProvider; + } + } + + public class WPKH : OutputDescriptor + { + public PubKeyProvider PkProvider; + internal WPKH(PubKeyProvider pkProvider) + { + if (pkProvider == null) + throw new ArgumentNullException(nameof(pkProvider)); + PkProvider = pkProvider; + } + } + public class Combo : OutputDescriptor + { + public PubKeyProvider PkProvider; + internal Combo(PubKeyProvider pkProvider) + { + if (pkProvider == null) + throw new ArgumentNullException(nameof(pkProvider)); + PkProvider = pkProvider; + } + } + + public class Multi : OutputDescriptor + { + public List PkProviders; + internal Multi(uint threshold, IEnumerable pkProviders, bool isSorted) + { + if (pkProviders == null) + throw new ArgumentNullException(nameof(pkProviders)); + PkProviders = pkProviders.ToList(); + if (PkProviders.Count == 0) + throw new ArgumentException("Multisig Descriptor can not have empty pubkey providers"); + Threshold = threshold; + IsSorted = isSorted; + } + + public uint Threshold { get; } + public bool IsSorted { get; } + } + + public class SH : OutputDescriptor + { + public OutputDescriptor Inner; + internal SH(OutputDescriptor inner) + { + if (inner == null) + throw new ArgumentNullException(nameof(inner)); + if (inner.IsTopLevelOnly()) + throw new ArgumentException($"{inner} can not be inner element for SH"); + Inner = inner; + } + } + + public class WSH : OutputDescriptor + { + public OutputDescriptor Inner; + internal WSH(OutputDescriptor inner) + { + if (inner == null) + throw new ArgumentNullException(nameof(inner)); + if (inner.IsTopLevelOnly() || inner is WSH) + throw new ArgumentException($"{inner} can not be inner element for WSH"); + Inner = inner; + } + } + + private OutputDescriptor() + { + } + + public static OutputDescriptor NewAddr(IDestination dest) => new Addr(dest); + public static OutputDescriptor NewRaw(Script sc) => new Raw(sc); + public static OutputDescriptor NewPK(PubKeyProvider pk) => new PK(pk); + public static OutputDescriptor NewPKH(PubKeyProvider pk) => new PKH(pk); + public static OutputDescriptor NewWPKH(PubKeyProvider pk) => new WPKH(pk); + public static OutputDescriptor NewCombo(PubKeyProvider pk) => new Combo(pk); + public static OutputDescriptor NewMulti(uint m, IEnumerable pks, bool isSorted) => new Multi(m, pks, isSorted); + public static OutputDescriptor NewSH(OutputDescriptor inner) => new SH(inner); + public static OutputDescriptor NewWSH(OutputDescriptor inner) => new WSH(inner); + + public bool IsTopLevelOnly() => this switch + { + Addr _ => true, + Raw _ => true, + Combo _ => true, + SH _ => true, + _ => false + }; + + #endregion + + #region Descriptor specific things + + /// + /// Expand descriptor into actual scriptPubKeys. + /// + /// position index to expand + /// provider to inject private keys in case of hardened derivation + /// repository to which to put resulted information. + /// resulted scriptPubKey + /// + public bool TryExpand( + uint pos, + ISigningRepository privateKeyProvider, + ISigningRepository repo, + out List