From 4e885de6c785f6d15bf25aa562edb6da92fac72c Mon Sep 17 00:00:00 2001 From: joemphilips Date: Wed, 22 May 2019 12:37:25 +0900 Subject: [PATCH 01/39] Prepare parser for scripting --- NBitcoin/Scripting/Parser/ParseException.cs | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 NBitcoin/Scripting/Parser/ParseException.cs diff --git a/NBitcoin/Scripting/Parser/ParseException.cs b/NBitcoin/Scripting/Parser/ParseException.cs new file mode 100644 index 0000000000..9b2fa79672 --- /dev/null +++ b/NBitcoin/Scripting/Parser/ParseException.cs @@ -0,0 +1,49 @@ +using System; + +namespace NBitcoin.Scripting.Parser +{ + /// + /// Represents an error that occurs during parsing. + /// + public class ParseException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public ParseException() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public ParseException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message + /// and the position where the error occured. + /// + /// The message that describes the error. + /// The position where the error occured. + public ParseException(string message, int position) : base(message) + { + Position = position; + } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, + /// or a null reference (Nothing in Visual Basic) if no inner exception is specified. + public ParseException(string message, Exception innerException) : base(message, innerException) { } + + /// + /// Gets the position of the parsing failure if one is available; otherwise, null. + /// + public int Position + { + get; + } + } +} From 58fa6bbe0a894e11e8f79b0262e1cdaa6a074e88 Mon Sep 17 00:00:00 2001 From: joemphilips Date: Thu, 23 May 2019 14:37:54 +0900 Subject: [PATCH 02/39] Rename ParseException -> ParsingException and inherit from FormatException --- NBitcoin/Scripting/Parser/ParseException.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/NBitcoin/Scripting/Parser/ParseException.cs b/NBitcoin/Scripting/Parser/ParseException.cs index 9b2fa79672..4c50b5e6f1 100644 --- a/NBitcoin/Scripting/Parser/ParseException.cs +++ b/NBitcoin/Scripting/Parser/ParseException.cs @@ -5,18 +5,18 @@ namespace NBitcoin.Scripting.Parser /// /// Represents an error that occurs during parsing. /// - public class ParseException : Exception + public class ParsingException : FormatException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public ParseException() { } + public ParsingException() { } /// - /// Initializes a new instance of the class with a specified error message. + /// Initializes a new instance of the class with a specified error message. /// /// The message that describes the error. - public ParseException(string message) : base(message) { } + public ParsingException(string message) : base(message) { } /// /// Initializes a new instance of the class with a specified error message @@ -24,7 +24,7 @@ public ParseException(string message) : base(message) { } /// /// The message that describes the error. /// The position where the error occured. - public ParseException(string message, int position) : base(message) + public ParsingException(string message, int position) : base(message) { Position = position; } @@ -36,7 +36,7 @@ public ParseException(string message, int position) : base(message) /// The error message that explains the reason for the exception. /// The exception that is the cause of the current exception, /// or a null reference (Nothing in Visual Basic) if no inner exception is specified. - public ParseException(string message, Exception innerException) : base(message, innerException) { } + public ParsingException(string message, Exception innerException) : base(message, innerException) { } /// /// Gets the position of the parsing failure if one is available; otherwise, null. From ff398a5c00c494594fc6fa1ee3da7c498c3924e2 Mon Sep 17 00:00:00 2001 From: joemphilips Date: Fri, 24 May 2019 08:30:50 +0900 Subject: [PATCH 03/39] fixup! Rename ParseException -> ParsingException and inherit from FormatException --- NBitcoin/Scripting/Parser/ParseException.cs | 49 --------------------- 1 file changed, 49 deletions(-) delete mode 100644 NBitcoin/Scripting/Parser/ParseException.cs diff --git a/NBitcoin/Scripting/Parser/ParseException.cs b/NBitcoin/Scripting/Parser/ParseException.cs deleted file mode 100644 index 4c50b5e6f1..0000000000 --- a/NBitcoin/Scripting/Parser/ParseException.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; - -namespace NBitcoin.Scripting.Parser -{ - /// - /// Represents an error that occurs during parsing. - /// - public class ParsingException : FormatException - { - /// - /// Initializes a new instance of the class. - /// - public ParsingException() { } - - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public ParsingException(string message) : base(message) { } - - /// - /// Initializes a new instance of the class with a specified error message - /// and the position where the error occured. - /// - /// The message that describes the error. - /// The position where the error occured. - public ParsingException(string message, int position) : base(message) - { - Position = position; - } - - /// - /// Initializes a new instance of the class with a specified error message - /// and a reference to the inner exception that is the cause of this exception. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, - /// or a null reference (Nothing in Visual Basic) if no inner exception is specified. - public ParsingException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Gets the position of the parsing failure if one is available; otherwise, null. - /// - public int Position - { - get; - } - } -} From fa33110fe39989dc24317cacdae050bcc47cdc6c Mon Sep 17 00:00:00 2001 From: joemphilips Date: Sun, 19 May 2019 12:59:22 +0900 Subject: [PATCH 04/39] Update output descriptor according to bitcoin core --- NBitcoin.Tests/Generators/CryptoGenerator.cs | 18 + .../Generators/OutputDescriptorGenerator.cs | 87 +++ NBitcoin.Tests/MiniscriptTests.cs | 429 +++++++++++++ NBitcoin.Tests/OutputDescriptorTests.cs | 63 ++ NBitcoin/BIP32/HDFingerPrint.cs | 6 + NBitcoin/Scripting/MiniscriptDSLParsers.cs | 142 +++++ NBitcoin/Scripting/OutputDescriptor.cs | 581 ++++++++++++++++++ NBitcoin/Scripting/OutputDescriptorParser.cs | 92 +++ NBitcoin/Scripting/PubKeyProvider.cs | 231 +++++++ 9 files changed, 1649 insertions(+) create mode 100644 NBitcoin.Tests/Generators/OutputDescriptorGenerator.cs create mode 100644 NBitcoin.Tests/MiniscriptTests.cs create mode 100644 NBitcoin.Tests/OutputDescriptorTests.cs create mode 100644 NBitcoin/Scripting/MiniscriptDSLParsers.cs create mode 100644 NBitcoin/Scripting/OutputDescriptor.cs create mode 100644 NBitcoin/Scripting/OutputDescriptorParser.cs create mode 100644 NBitcoin/Scripting/PubKeyProvider.cs diff --git a/NBitcoin.Tests/Generators/CryptoGenerator.cs b/NBitcoin.Tests/Generators/CryptoGenerator.cs index c27be9c0a5..5a8bb5136a 100644 --- a/NBitcoin.Tests/Generators/CryptoGenerator.cs +++ b/NBitcoin.Tests/Generators/CryptoGenerator.cs @@ -95,5 +95,23 @@ 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..64fc15de81 --- /dev/null +++ b/NBitcoin.Tests/Generators/OutputDescriptorGenerator.cs @@ -0,0 +1,87 @@ +using FsCheck; +using NBitcoin.Scripting; + +namespace NBitcoin.Tests.Generators +{ + public class OutputDescriptorGenerator + { + public static Arbitrary OutputDescriptorArb() => + Arb.From(OutputDescriptorGen()); + + public static Gen OutputDescriptorGen() => + Gen.OneOf( + AddrOutputDescriptorGen(), + RawOutputDescriptorGen(), + PKOutputDescriptorGen(), + PKHOutputDescriptorGen(), + WPKHOutputDescriptorGen(), + ComboOutputDescriptorGen(), + MultisigOutputDescriptorGen(), + SHOutputDescriptorGen(), + WSHOutputDescriptorGen() + ); + private static Gen AddrOutputDescriptorGen() => + from addr in AddressGenerator.RandomAddress() + select OutputDescriptor.NewAddr(addr); + + private static Gen RawOutputDescriptorGen() => + from addr in ScriptGenerator.RandomScriptSig() + select OutputDescriptor.NewRaw(addr); + private static Gen PKOutputDescriptorGen() => + from pkProvider in PubKeyProviderGen() + select OutputDescriptor.NewPK(pkProvider); + + private static Gen PKHOutputDescriptorGen() => + from pkProvider in PubKeyProviderGen() + select OutputDescriptor.NewPKH(pkProvider); + + private static Gen WPKHOutputDescriptorGen() => + from pkProvider in PubKeyProviderGen() + select OutputDescriptor.NewWPKH(pkProvider); + + private static Gen ComboOutputDescriptorGen() => + from pkProvider in PubKeyProviderGen() + select OutputDescriptor.NewCombo(pkProvider); + + private static Gen MultisigOutputDescriptorGen() => + from pkProviders in Gen.NonEmptyListOf(PubKeyProviderGen()) + select OutputDescriptor.NewMulti(pkProviders); + + private static Gen InnerOutputDescriptorGen() => + Gen.OneOf( + PKOutputDescriptorGen(), + PKHOutputDescriptorGen(), + WPKHOutputDescriptorGen(), + MultisigOutputDescriptorGen() + ); + private static Gen SHOutputDescriptorGen() => + from inner in Gen.OneOf(InnerOutputDescriptorGen(), WSHOutputDescriptorGen()) + select OutputDescriptor.NewSH(inner); + + private static Gen WSHOutputDescriptorGen() => + from inner in InnerOutputDescriptorGen() + select OutputDescriptor.NewWSH(inner); + + #region pubkey providers + + private static Gen PubKeyProviderGen() => + Gen.OneOf(OriginPubKeyProviderGen(), ConstPubKeyProviderGen(), HDPubKeyProviderGen()); + + private static Gen OriginPubKeyProviderGen() => + from keyOrigin in CryptoGenerator.RootedKeyPath() + from inner in Gen.OneOf(ConstPubKeyProviderGen(), HDPubKeyProviderGen()) + select PubKeyProvider.NewOrigin(keyOrigin, inner); + + private static Gen ConstPubKeyProviderGen() => + from pk in CryptoGenerator.PublicKey() + select PubKeyProvider.NewConst(pk); + + private static Gen HDPubKeyProviderGen() => + from extPk in CryptoGenerator.BitcoinExtPubKey() + from kp in CryptoGenerator.KeyPath() + from t in Arb.Generate() + select PubKeyProvider.NewHD(extPk, kp, t); + + # endregion + } +} diff --git a/NBitcoin.Tests/MiniscriptTests.cs b/NBitcoin.Tests/MiniscriptTests.cs new file mode 100644 index 0000000000..26301e2815 --- /dev/null +++ b/NBitcoin.Tests/MiniscriptTests.cs @@ -0,0 +1,429 @@ +using Xunit; +using NBitcoin.Scripting; +using NBitcoin.Scripting.Parser; +using FsCheck.Xunit; +using FsCheck; +using NBitcoin.Tests.Generators; +using System; +using static NBitcoin.Tests.Helpers.PrimitiveUtils; +using NBitcoin.Crypto; +using System.Collections.Generic; + +namespace NBitcoin.Tests +{ + public class MiniscriptTests + { + public Network Network { get; } + public Key[] Keys { get; } + + public MiniscriptTests() + { + Arb.Register(); + Arb.Register(); + Arb.Register(); + Network = Network.Main; + Keys = new Key[] { new Key(), new Key(), new Key() }; + } + + [Fact] + [Trait("UnitTest", "UnitTest")] + public void DSLParserTests() + { + var pk = Keys[0].PubKey; + var pk2 = Keys[1].PubKey; + var pk3 = Keys[2].PubKey; + DSLParserTestCore("time(100)", AbstractPolicy.NewTime(100)); + DSLParserTestCore($"pk({pk})", AbstractPolicy.NewCheckSig(pk)); + DSLParserTestCore($"multi(2,{pk2},{pk3})", AbstractPolicy.NewMulti(2, new PubKey[] { pk2, pk3 })); + DSLParserTestCore( + $"and(time(10),pk({pk}))", + AbstractPolicy.NewAnd( + AbstractPolicy.NewTime(10), + AbstractPolicy.NewCheckSig(pk) + ) + ); + DSLParserTestCore( + $"and(time(10),and(pk({pk}),multi(2,{pk2},{pk3})))", + AbstractPolicy.NewAnd( + AbstractPolicy.NewTime(10), + AbstractPolicy.NewAnd( + AbstractPolicy.NewCheckSig(pk), + AbstractPolicy.NewMulti(2, new PubKey[] { pk2, pk3 }) + ) + ) + ); + + DSLParserTestCore( + $"thres(2,time(100),multi(2,{pk2},{pk3}))", + AbstractPolicy.NewThreshold( + 2, + new AbstractPolicy[] { + AbstractPolicy.NewTime(100), + AbstractPolicy.NewMulti(2, new [] {pk2, pk3}) + } + ) + ); + + // Bigger than max possible blocknumber of OP_CSV + Assert.Throws(() => MiniscriptDSLParser.DSLParser.Parse($"time(65536)")); + } + + [Fact] + [Trait("UnitTest", "UnitTest")] + public void DSLSubParserTest() + { + var pk = new Key().PubKey; + var pk2 = new Key().PubKey; + var pk3 = new Key().PubKey; + var res = MiniscriptDSLParser.PThresholdExpr.Parse($"thres(2,time(100),multi(2,{pk2},{pk3}))"); + Assert.Equal( + res, + AbstractPolicy.NewThreshold( + 2, + new AbstractPolicy[] { + AbstractPolicy.NewTime(100), + AbstractPolicy.NewMulti(2, new [] {pk2, pk3}) + } + ) + ); + } + + private void DSLParserTestCore(string expr, AbstractPolicy expected) + { + var res = MiniscriptDSLParser.ParseDSL(expr); + Assert.Equal(expected, res); + } + + [Property] + [Trait("PropertyTest", "BidrectionalConversion")] + public void PolicyShouldConvertToDSLBidirectionally(AbstractPolicy policy) + => Assert.Equal(policy, MiniscriptDSLParser.ParseDSL(policy.ToString())); + + [Fact] + [Trait("UnitTest", "UnitTest")] + public void PolicyToAstConversionTest() + { + var p = AbstractPolicy.NewHash(new uint256(0xdeadbeef)); + var p2 = CompiledNode.FromPolicy(p).BestT(0.0, 0.0).Ast.ToPolicy(); + Assert.Equal(p, p2); + } + + [Property] + [Trait("PropertyTest", "Verification")] + public void PolicyShouldCompileToASTAndGetsBack(AbstractPolicy policy) + // Bidirectional conversion is impossible since there are no way to distinguish `or` and `aor` + => CompiledNode.FromPolicy(policy).BestT(0.0, 0.0).Ast.ToPolicy(); + + [Property] + [Trait("PropertyTest", "Verification")] + public void PolicyShouldCompileToScript(AbstractPolicy policy) + => CompiledNode.FromPolicy(policy).BestT(0.0, 0.0).Ast.ToScript(); + + [Fact] + [Trait("UnitTest", "UnitTest")] + public void ScriptDeserializationTest1() + { + var pk1 = new Key().PubKey; + var pk2 = new Key().PubKey; + var pk3 = new Key().PubKey; + var hash1 = new uint256(0xdeadbeef); + var sc = new Script("027be8d8ffe2f50ab5afcebf29ec2c9c75b50334905c9d15046e051c81a4ddbc68 OP_CHECKSIG"); + MiniscriptScriptParser.PPk.Parse(sc); + MiniscriptScriptParser.PAstElemCore.Parse(sc); + DeserializationTestCore(MiniscriptDSLParser.ParseDSL($"pk({pk1})")); + var case1 = $"thres(1,aor(pk({pk1}),hash({hash1})),multi(1,{pk2},{pk3}))"; + DeserializationTestCore(MiniscriptDSLParser.ParseDSL(case1)); + DeserializationTestCore(MiniscriptDSLParser.ParseDSL($"and({case1},pk({pk1}))")); + + var htlcDSL = $"aor(and(hash({hash1}),pk({Keys[0].PubKey})),and(pk({Keys[1].PubKey}),time(10000)))"; + DeserializationTestCore(htlcDSL); + + var dsl = "thres(1, pk(02130c1c9a68369f14e4ce5c58acaa9d592ef8c5dcaf0a9d0fe92321c4bbc64eb3), aor(hash(bcf07a5893c7512fb9f4280690cbffdd6745d6b43e1c578b15f32e62ecca5439), time(0)))"; + DeserializationTestCore(dsl); + } + + [Fact] + [Trait("UnitTest", "UnitTest")] + public void ScriptDeserializationTest2() + { + // and_cat(v.time(0), t.time(0)) + var sc1 = new Script("0 OP_CSV OP_DROP 0 OP_CSV"); + var ast1 = AstElem.NewAndCat(AstElem.NewTimeV(0), AstElem.NewTimeT(0)); + DeserializationTestCore(sc1, ast1); + + var sc2 = new Script("0 OP_CSV"); + var ast2 = AstElem.NewTimeT(0); + DeserializationTestCore(sc2, ast2); + Assert.True(ast2.IsT()); + + // or_if(t.time(0), t.time(0)) + var sc3 = new Script("OP_IF 0 OP_CSV OP_ELSE 0 OP_CSV OP_ENDIF"); + var ast3 = AstElem.NewOrIf(AstElem.NewTimeT(0), AstElem.NewTimeT(0)); + MiniscriptScriptParser.POrIfOfT.Parse(sc3); + DeserializationTestCore(sc3, ast3); + + var sc4 = new Script("OP_DUP OP_IF 0 OP_CSV OP_DROP OP_ENDIF OP_SWAP 0209518deb4a2e7e0db86c611e4bbe4f8a6236478e8af5ac0e10cbc543dab2cfaf OP_CHECKSIG OP_ADD 1 OP_EQUAL"); + var ast4 = AstElem.NewThresh( + 1, + new[] { + AstElem.NewTime(0), + AstElem.NewPkW(new PubKey("0209518deb4a2e7e0db86c611e4bbe4f8a6236478e8af5ac0e10cbc543dab2cfaf")) + } + ); + DeserializationTestCore(sc4, ast4); + + // multi(2, 2) + var sc5 = new Script("2 02e38a30edddfb98c5973427a84f8e04376bd26f9ffaf60924e983f6056e2f020d 02d5b294505603232507635867f07bb498d8021db5b46a8276b6dc2823460b6684 2 OP_CHECKMULTISIG"); + Assert.True(MiniscriptScriptParser.PMulti.Parse(sc5).IsE()); + var ast5 = AstElem.NewMulti(2, new[] { new PubKey("02e38a30edddfb98c5973427a84f8e04376bd26f9ffaf60924e983f6056e2f020d"), new PubKey("02d5b294505603232507635867f07bb498d8021db5b46a8276b6dc2823460b6684") }); + DeserializationTestCore(sc5, ast5); + + // wrap(multi(2, 2)) + var sc6 = new Script("OP_TOALTSTACK 2 02e38a30edddfb98c5973427a84f8e04376bd26f9ffaf60924e983f6056e2f020d 02d5b294505603232507635867f07bb498d8021db5b46a8276b6dc2823460b6684 2 OP_CHECKMULTISIG OP_FROMALTSTACK"); + MiniscriptScriptParser.PWrap.Parse(sc6); + var ast6 = AstElem.NewWrap(ast5); + DeserializationTestCore(sc6, ast6); + + // thresh(1, time(0), wrap(multi(2, 2))) + var sc7 = new Script("OP_DUP OP_IF 0 OP_CSV OP_DROP OP_ENDIF OP_TOALTSTACK 2 02e38a30edddfb98c5973427a84f8e04376bd26f9ffaf60924e983f6056e2f020d 02d5b294505603232507635867f07bb498d8021db5b46a8276b6dc2823460b6684 2 OP_CHECKMULTISIG OP_FROMALTSTACK OP_ADD 1 OP_EQUAL"); + MiniscriptScriptParser.PThresh.Parse(sc7); + var ast7 = AstElem.NewThresh( + 1, + new[] + { + AstElem.NewTime(0), + ast6 + } + ); + DeserializationTestCore(sc7, ast7); + + // and_casc(pk(02468ee57f149cbafe408a4c04cd7e76c03d23f6b1a6d1670a5c416f089dff61d8),time_f(0)) + var sc8 = new Script("02468ee57f149cbafe408a4c04cd7e76c03d23f6b1a6d1670a5c416f089dff61d8 OP_CHECKSIG OP_NOTIF 0 OP_ELSE 0 OP_CSV OP_0NOTEQUAL OP_ENDIF"); + var ast8 = AstElem.NewAndCasc( + AstElem.NewPk(new PubKey("02468ee57f149cbafe408a4c04cd7e76c03d23f6b1a6d1670a5c416f089dff61d8")), + AstElem.NewTimeF(0) + ); + DeserializationTestCore(sc8, ast8); + + // wrap(and_casc(pk(02468ee57f149cbafe408a4c04cd7e76c03d23f6b1a6d1670a5c416f089dff61d8),time_f(0))) + var sc9 = new Script("OP_TOALTSTACK 02468ee57f149cbafe408a4c04cd7e76c03d23f6b1a6d1670a5c416f089dff61d8 OP_CHECKSIG OP_NOTIF 0 OP_ELSE 0 OP_CSV OP_0NOTEQUAL OP_ENDIF OP_FROMALTSTACK "); + var ast9 = AstElem.NewWrap(ast8); + DeserializationTestCore(sc9, ast9); + + // or_cont(E.pk(), V.hash()) + var sc10 = new Script("027b4d201fe93fd448e9bed73c58897fac38329357bd3f94378df39fa8d2e3d247 OP_CHECKSIG OP_NOTIF OP_SIZE 20 OP_EQUALVERIFY OP_SHA256 e014f27cb7ebc9538ec2a02a65437701df1a4ed8b6f125af8dc8528664ee295d OP_EQUALVERIFY OP_ENDIF"); + var ast10 = AstElem.NewOrCont( + AstElem.NewPk(new PubKey("027b4d201fe93fd448e9bed73c58897fac38329357bd3f94378df39fa8d2e3d247")), + AstElem.NewHashV(uint256.Parse("e014f27cb7ebc9538ec2a02a65437701df1a4ed8b6f125af8dc8528664ee295d")) + ); + DeserializationTestCore(sc10, ast10); + + // true(or_cont(E.pk(), V.hash())) + var sc11 = new Script("027b4d201fe93fd448e9bed73c58897fac38329357bd3f94378df39fa8d2e3d247 OP_CHECKSIG OP_NOTIF OP_SIZE 20 OP_EQUALVERIFY OP_SHA256 e014f27cb7ebc9538ec2a02a65437701df1a4ed8b6f125af8dc8528664ee295d OP_EQUALVERIFY OP_ENDIF 1"); + var ast11 = AstElem.NewTrue(ast10); + DeserializationTestCore(sc11, ast11); + + // and_cat(or_cont(pk(), hash_v()), true(or_cont(pk(), hash()))) + // Do not compare equality in this case. + // Why? because the following two will result to exactly the same Script, + // it is impossible to deserialize to the same representation. + // 1. and_cat(a, true(b)) + // 2. true(and_cat(a, b)) + var sc12 = new Script("02619434bc0b8d19236d4894e87878adab38c912947deb1784afabf4097ccb250a OP_CHECKSIG OP_NOTIF OP_SIZE 20 OP_EQUALVERIFY OP_SHA256 3570a42e0c47d105e36da8dcba447fb3911b563468f2d00b3fc9a7e216a07eb9 OP_EQUALVERIFY OP_ENDIF 027b4d201fe93fd448e9bed73c58897fac38329357bd3f94378df39fa8d2e3d247 OP_CHECKSIG OP_NOTIF OP_SIZE 20 OP_EQUALVERIFY OP_SHA256 e014f27cb7ebc9538ec2a02a65437701df1a4ed8b6f125af8dc8528664ee295d OP_EQUALVERIFY OP_ENDIF 1"); + var ast12 = AstElem.NewAndCat( + AstElem.NewOrCont( + AstElem.NewPk(new PubKey("02619434bc0b8d19236d4894e87878adab38c912947deb1784afabf4097ccb250a")), + AstElem.NewHashV(uint256.Parse("3570a42e0c47d105e36da8dcba447fb3911b563468f2d00b3fc9a7e216a07eb9")) + ), + ast11 + ); + MiniscriptScriptParser.ParseScript(sc12); + Assert.Equal(sc12, ast12.ToScript()); + + // thresh_v(1, unlikely(true(hash_v())), time_w) + var sc13 = new Script("OP_IF OP_SIZE 20 OP_EQUALVERIFY OP_SHA256 306442ccaa3b19a381bcf07a5893c7512fb99e280690cbffdd67a2d6b43e1c57 OP_EQUALVERIFY 1 OP_ELSE 0 OP_ENDIF OP_SWAP OP_DUP OP_IF 0 OP_CSV OP_DROP OP_ENDIF OP_ADD 1 OP_EQUALVERIFY"); + var ast13 = AstElem.NewThreshV( + 1, + new AstElem[] { + AstElem.NewUnlikely( + AstElem.NewTrue( + AstElem.NewHashV(uint256.Parse("306442ccaa3b19a381bcf07a5893c7512fb99e280690cbffdd67a2d6b43e1c57") + ) + ) + ), + AstElem.NewTimeW(0) + } + ); + DeserializationTestCore(sc13, ast13); + } + + private void DeserializationTestCore(Script sc, AstElem ast) + { + var sc2 = ast.ToScript(); // serialization test + Assert.Equal(sc, sc2); + var ast2 = MiniscriptScriptParser.PAstElem.Parse(sc); // deserialization test + Assert.Equal(ast, ast2); + } + + private void DeserializationTestCore(string policyStr, bool assert = true) + => DeserializationTestCore(MiniscriptDSLParser.DSLParser.Parse(policyStr), assert); + private void DeserializationTestCore(AbstractPolicy policy, bool assert = true) + { + var ast = CompiledNode.FromPolicy(policy).BestT(0.0, 0.0).Ast; + var sc = ast.ToScript(); + var ast2 = MiniscriptScriptParser.ParseScript(sc); + if (assert) + { + Assert.Equal(ast, ast2); + } + } + + // This is useful for finding failure case. But passing every single case is unnecessary. + // (And probably impossible). so disable it for now. + // e.g. How we distinguish `and_cat(and_cat(a, b), c)` and `and_cat(a, and_cat(b, c))` ? + [Property(Skip="DoesNotHaveToPass")] + [Trait("PropertyTest", "BidirectionalConversion")] + public void ShouldDeserializeScriptOriginatesFromMiniscriptToOrigin(AbstractPolicy policy) + => DeserializationTestCore(policy); + + + [Trait("PropertyTest", "BidirectionalConversion")] + public void ShouldDeserializeScriptOriginatesFromMiniscript(AbstractPolicy policy) + => DeserializationTestCore(policy, false); + + + [Property] + [Trait("PropertyTest", "Verification")] + public void ShouldSatisfyAstWithDummyProviders(AbstractPolicy policy) + { + var ast = CompiledNode.FromPolicy(policy).BestT(0.0, 0.0).Ast; + var dummySig = TransactionSignature.Empty; + var dummyPreImage = new uint256(); + ast.Satisfy(pk => dummySig, _ => dummyPreImage, 65535); + } + + [Fact] + [Trait("UnitTest", "UnitTest")] + public void ShouldThrowCorrectErrorWhenSatisfyCSV() + { + var pk = Keys[0].PubKey; + Func dummySigProvider = + actualPk => actualPk.Equals(pk) ? TransactionSignature.Empty : null; + var ms = Miniscript.Parse($"and(time(1000),pk({pk}))"); + + // case 1: No Age Provided + Assert.False(ms.Ast.TrySatisfy(dummySigProvider, null, null, out var res, out var errors)); + Assert.Single(errors); + Assert.Equal(SatisfyErrorCode.NoAgeProvided, errors[0].Code); + + // case 2: Disabled + var seq2 = new Sequence(1000u | Sequence.SEQUENCE_LOCKTIME_DISABLE_FLAG); + Assert.False(ms.Ast.TrySatisfy(dummySigProvider, null, seq2, out var res2, out var errors2)); + Assert.Single(errors2); + Assert.Equal(SatisfyErrorCode.RelativeLockTimeDisabled, errors2[0].Code); + + // case 3: Relative locktime by time (Instead of blockheight). + var seq3 = new Sequence(Sequence.SEQUENCE_LOCKTIME_TYPE_FLAG | (1500u >> Sequence.SEQUENCE_LOCKTIME_GRANULARITY)); + Assert.False(ms.Ast.TrySatisfy(dummySigProvider, null, seq3, out var res3, out var errors3)); + Assert.Single(errors3); + Assert.Equal(SatisfyErrorCode.UnSupportedRelativeLockTimeType, errors3[0].Code); + + // case 4: Not satisfied. + var seq4 = new Sequence(lockHeight: 999); + Assert.False(ms.Ast.TrySatisfy(dummySigProvider, null, seq4, out var res4, out var errors4)); + Assert.Single(errors4); + Assert.Equal(SatisfyErrorCode.LockTimeNotMet, errors4[0].Code); + + // case 5: Successful case. + var seq5 = new Sequence(lockHeight: 1000); + Assert.True(ms.Ast.TrySatisfy(dummySigProvider, null, seq5, out var res5, out var errors5)); + Assert.Empty(errors5); + } + + [Fact] + [Trait("UnitTest", "UnitTest")] + + public void ShouldPlayWellWithTransactionBuilder_1() + { + // case1: simple timelocked multisig + var dsl = $"and(time(100),multi(2, {Keys[0].PubKey}, {Keys[1].PubKey}))"; + var ms = Miniscript.Parse(dsl); + var builder = Network.CreateTransactionBuilder(); + var coins = GetRandomCoinsForAllScriptType(Money.Coins(0.5m), ms.Script); + builder.AddCoins(coins); + builder.SendFees(Money.Coins(0.001m)); + builder.SendAll(Keys[2]); + builder.AddKeys(Keys[0], Keys[1]); + Assert.False(builder.Verify(builder.BuildTransaction(true))); + builder.OptInRBF = true; + Assert.False(builder.Verify(builder.BuildTransaction(true))); + builder.SetRelativeLockTimeTo(coins, 99); + Assert.False(builder.Verify(builder.BuildTransaction(true))); + builder.SetRelativeLockTimeTo(coins, 100); + var tx = builder.BuildTransaction(true); + Assert.Empty(builder.Check(tx)); + } + + private Tuple> PrepareBuilder(Script sc) + { + var builder = Network.CreateTransactionBuilder(); + var coins = GetRandomCoinsForAllScriptType(Money.Coins(0.5m), sc); + builder.AddCoins(coins) + .SendFees(Money.Coins(0.001m)) + .SendAll(new Key()); // dummy output + return Tuple.Create(builder, coins); + } + + [Fact] + [Trait("UnitTest", "UnitTest")] + + public void ShouldPlayWellWithTransactionBuilder_2() + { + // case2: BIP199 HTLC + var secret1 = new uint256(0xdeadbeef); + var hash1 = new uint256(Hashes.SHA256(secret1.ToBytes()), false); + var dsl = $"aor(and(hash({hash1}),pk({Keys[0].PubKey})),and(pk({Keys[1].PubKey}),time(10000)))"; + var ms = Miniscript.Parse(dsl); + var dummy = Keys[2]; + + // ------ 1: left side of redeem condition. revoking using hash preimage. + var t = PrepareBuilder(ms.Script); + var builder = t.Item1; + builder.AddKeys(Keys[0]); + // we have key for left side redeem condition. but no secret. + Assert.False(builder.Verify(builder.BuildTransaction(true))); + builder.AddPreimages(new uint256(0xdeadbeef111)); // wrong secret. + Assert.False(builder.Verify(builder.BuildTransaction(true))); + builder.AddPreimages(secret1); // now we have correct secret. + Assert.True(builder.Verify(builder.BuildTransaction(true))); + + // --------- 2: right side. revoking after time. + var t2 = PrepareBuilder(ms.Script); + var b2 = t2.Item1; + var coins = t2.Item2; + b2.AddKeys(Keys[1]); + // key itself is not enough + Assert.False(b2.Verify(b2.BuildTransaction(true))); + // Preimage does not help this time. + b2.AddPreimages(secret1); + Assert.False(b2.Verify(b2.BuildTransaction(true))); + // but locktime does. + b2.SetRelativeLockTimeTo(coins, 10000); + Assert.True(b2.Verify(b2.BuildTransaction(true))); + } + + [Property] + [Trait("PropertyTest", "Verification")] + public void ShouldNotThrowErrorWhenTryParsingScript(Script sc) + { + Miniscript.TryParseScript(sc, out var res); + } + + [Property] + [Trait("PropertyTest", "Verification")] + public void ShouldNotThrowErrorWhenTryParsingDSL(NonNull sc) + { + Miniscript.TryParse(sc.Get, out var res); + } + } +} \ No newline at end of file diff --git a/NBitcoin.Tests/OutputDescriptorTests.cs b/NBitcoin.Tests/OutputDescriptorTests.cs new file mode 100644 index 0000000000..5052ae5fd7 --- /dev/null +++ b/NBitcoin.Tests/OutputDescriptorTests.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using FsCheck; +using FsCheck.Xunit; +using NBitcoin.Scripting; +using NBitcoin.Tests.Generators; +using Xunit; + +namespace NBitcoin.Tests +{ + public class OutputDescriptorTests + { + public OutputDescriptorTests() + { + Arb.Register(); + } + + [Property] + [Trait("PropertyTest", "BidirectionalConversion")] + public void DescriptorShouldConvertToStringBidirectionally(OutputDescriptor desc) + { + } + + + [Fact] + [Trait("UnitTest", "UnitTest")] + public void OutputDescriptorParserTests() + { + // https://github.com/bitcoin/bitcoin/blob/9b085f4863eaefde4bec0638f1cbc8509d6ee59a/doc/descriptors.md + var testVectors = new string[] { + "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 hardend derivation + "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*'))" + }; + } + + [Fact] + [Trait("Core", "Core")] + public void DescriptorTests() + { + + } + + private void Check(string prv, string pub, int flags, string[] script, HashSet paths = null) + { + OutputDescriptor.Parse(prv); + OutputDescriptor.Parse(pub); + } + + } +} \ No newline at end of file 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/Scripting/MiniscriptDSLParsers.cs b/NBitcoin/Scripting/MiniscriptDSLParsers.cs new file mode 100644 index 0000000000..42b59de424 --- /dev/null +++ b/NBitcoin/Scripting/MiniscriptDSLParsers.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections; +using System.Linq; +using System.Text.RegularExpressions; +using NBitcoin.Scripting.Parser; +using NBitcoin.DataEncoders; +using System.Collections.Generic; + +namespace NBitcoin.Scripting +{ + internal static class MiniscriptDSLParser + { + internal static readonly Parser SurroundedByBrackets = + from leftB in Parse.Char('(').Token() + from x in Parse.CharExcept(')').Many().Text() + from rightB in Parse.Char(')').Token() + select x; + + private static string[] SafeSplit(string s) + { + var parenthCount = 0; + var items = new List(); + var charSoFar = new List(); + var length = s.Length; + for (int i = 0; i < length; i++) + { + var c = s[i]; + if (c == '(') + { + parenthCount++; + charSoFar.Add(c); + } + else if (c == ')') + { + parenthCount--; + charSoFar.Add(c); + } + else if (parenthCount != 0) + { + charSoFar.Add(c); + } + + if (parenthCount == 0) + { + if (i == length - 1) + { + charSoFar.Add(c); + } + if (c == ',' || i == length - 1) + { + var charsCopy = new List(charSoFar); + charSoFar = new List(); + var item = new String(charsCopy.ToArray()).Trim(); + items.Add(item); + } + else + { + charSoFar.Add(c); + } + } + } + return items.ToArray(); + } + + internal static Parser ExprP(string name) + => + from identifier in Parse.String(name) + from x in SurroundedByBrackets + select x; + + private static Parser ExprPMany(string name) + => + from x in ExprP(name) + select SafeSplit(x); + + private static readonly Parser PPubKeyExpr = + from pk in ExprP("pk").Then(s => Parse.TryConvert(s, c => new PubKey(c))) + select AbstractPolicy.NewCheckSig(pk); + + private static readonly Parser PMultisigExpr = + from contents in ExprPMany("multi") + from m in Parse.TryConvert(contents.First(), UInt32.Parse) + from pks in contents.Skip(1) + .Select(pk => Parse.TryConvert(pk, c => new PubKey(c))) + .Sequence() + select AbstractPolicy.NewMulti(m, pks.ToArray()); + + private static readonly Parser PHashExpr = + from hash in ExprP("hash").Then(s => Parse.TryConvert(s, uint256.Parse)) + select AbstractPolicy.NewHash(hash); + + private static readonly Parser PTimeExpr = + from t in ExprP("time").Then(s => Parse.TryConvert(s, UInt32.Parse)) + where t <= 65535 + select AbstractPolicy.NewTime(t); + + private static Parser> PSubExprs(string name) => + from _n in Parse.String(name) + from _left in Parse.Char('(') + from x in Parse + .Ref(() => DSLParser) + .DelimitedBy(Parse.Char(',').Token()).Token() + from _right in Parse.Char(')') + select x; + private static readonly Parser PAndExpr = + from x in PSubExprs("and") + select AbstractPolicy.NewAnd(x.ElementAt(0), x.ElementAt(1)); + + private static readonly Parser POrExpr = + from x in PSubExprs("or") + select AbstractPolicy.NewOr(x.ElementAt(0), x.ElementAt(1)); + private static readonly Parser PAOrExpr = + from x in PSubExprs("aor") + select AbstractPolicy.NewAsymmetricOr(x.ElementAt(0), x.ElementAt(1)); + + internal static readonly Parser PThresholdExpr = + from _n in Parse.String("thres") + from _left in Parse.Char('(') + from numStr in Parse.Digit.AtLeastOnce().Text() + from _sep in Parse.Char(',') + from num in Parse.TryConvert(numStr, UInt32.Parse) + from x in Parse + .Ref(() => DSLParser) + .DelimitedBy(Parse.Char(',').Token()).Token() + from _right in Parse.Char(')') + where num <= x.Count() + select AbstractPolicy.NewThreshold(num, x.ToArray()); + internal static readonly Parser DSLParser = + (PPubKeyExpr + .Or(PMultisigExpr) + .Or(PTimeExpr) + .Or(PHashExpr) + .Or(PAndExpr) + .Or(POrExpr) + .Or(PAOrExpr) + .Or(PThresholdExpr)).Token(); + + + public static AbstractPolicy ParseDSL(string input) + => DSLParser.Parse(input); + } +} \ No newline at end of file diff --git a/NBitcoin/Scripting/OutputDescriptor.cs b/NBitcoin/Scripting/OutputDescriptor.cs new file mode 100644 index 0000000000..a87e4682d2 --- /dev/null +++ b/NBitcoin/Scripting/OutputDescriptor.cs @@ -0,0 +1,581 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using NBitcoin.Scripting.Parser; + +namespace NBitcoin.Scripting +{ + public interface ISigningProvider + { + IDictionary> Origins { get; } + IDictionary Scripts { get; } + IDictionary PubKeys { get; } + IDictionary Keys { get; } + } + + public class SigningProvider : ISigningProvider + { + public SigningProvider() + { + Origins = new ConcurrentDictionary>(); + Scripts = new ConcurrentDictionary(); + PubKeys = new ConcurrentDictionary(); + Keys = new ConcurrentDictionary(); + } + + public IDictionary> Origins { get; } + + public IDictionary Scripts { get; } + + public IDictionary PubKeys { get; } + + public IDictionary Keys{ get; } + } + + public abstract class OutputDescriptor : IEquatable + { + # region subtypes + public static class Tags + { + public const int AddressDescriptor = 0; + public const int RawDescriptor = 1; + public const int PKDescriptor = 2; + public const int ExtPubKeyOutputDescriptor = 3; + public const int PKHDescriptor = 4; + public const int WPKHDescriptor = 5; + public const int ComboDescriptor = 6; + public const int MultisigDescriptor = 7; + public const int SHDescriptor = 8; + public const int WSHDescriptor = 9; + } + + public class AddressDescriptor : OutputDescriptor + { + public BitcoinAddress Address { get; } + public AddressDescriptor(BitcoinAddress address) : base(Tags.AddressDescriptor) + { + if (address == null) + throw new ArgumentNullException(nameof(address)); + Address = address; + } + } + + public class RawDescriptor : OutputDescriptor + { + public Script Script; + + internal RawDescriptor(Script script) : base(Tags.RawDescriptor) => Script = script ?? throw new ArgumentNullException(nameof(script)); + } + + public class PKDescriptor : OutputDescriptor + { + public PubKeyProvider PkProvider; + internal PKDescriptor(PubKeyProvider pkProvider) : base(Tags.PKDescriptor) + { + if (pkProvider == null) + throw new ArgumentNullException(nameof(pkProvider)); + PkProvider = pkProvider; + } + } + + public class PKHDescriptor : OutputDescriptor + { + public PubKeyProvider PkProvider; + internal PKHDescriptor(PubKeyProvider pkProvider) : base(Tags.PKHDescriptor) + { + if (pkProvider == null) + throw new ArgumentNullException(nameof(pkProvider)); + PkProvider = pkProvider; + } + } + + public class WPKHDescriptor : OutputDescriptor + { + public PubKeyProvider PkProvider; + internal WPKHDescriptor(PubKeyProvider pkProvider) : base(Tags.WPKHDescriptor) + { + if (pkProvider == null) + throw new ArgumentNullException(nameof(pkProvider)); + PkProvider = pkProvider; + } + } + public class ComboDescriptor : OutputDescriptor + { + public PubKeyProvider PkProvider; + internal ComboDescriptor(PubKeyProvider pkProvider) : base(Tags.ComboDescriptor) + { + if (pkProvider == null) + throw new ArgumentNullException(nameof(pkProvider)); + PkProvider = pkProvider; + } + } + + public class MultisigDescriptor : OutputDescriptor + { + public List PkProviders; + internal MultisigDescriptor(IEnumerable pkProviders) : base(Tags.MultisigDescriptor) + { + 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"); + } + } + + public class SHDescriptor : OutputDescriptor + { + public OutputDescriptor Inner; + internal SHDescriptor(OutputDescriptor inner) : base(Tags.SHDescriptor) + { + if (inner == null) + throw new ArgumentNullException(nameof(inner)); + if (inner.IsTopLevelOnly()) + throw new ArgumentException($"{inner} can not be inner element for SHDescriptor"); + Inner = inner; + } + } + + public class WSHDescriptor : OutputDescriptor + { + public OutputDescriptor Inner; + internal WSHDescriptor(OutputDescriptor inner) : base(Tags.WSHDescriptor) + { + if (inner == null) + throw new ArgumentNullException(nameof(inner)); + if (inner.IsTopLevelOnly() || inner.IsWSH()) + throw new ArgumentException($"{inner} can not be inner element for WSHDescriptor"); + Inner = inner; + } + } + + internal int Tag { get; } + private OutputDescriptor(int tag) + { + Tag = tag; + } + + public static OutputDescriptor NewAddr(BitcoinAddress addr) => new AddressDescriptor(addr); + public static OutputDescriptor NewRaw(Script sc) => new RawDescriptor(sc); + public static OutputDescriptor NewPK(PubKeyProvider pk) => new PKDescriptor(pk); + public static OutputDescriptor NewPKH(PubKeyProvider pk) => new PKHDescriptor(pk); + public static OutputDescriptor NewWPKH(PubKeyProvider pk) => new WPKHDescriptor(pk); + public static OutputDescriptor NewCombo(PubKeyProvider pk) => new ComboDescriptor(pk); + public static OutputDescriptor NewMulti(IEnumerable pks) => new MultisigDescriptor(pks); + + public static OutputDescriptor NewSH(OutputDescriptor inner) => new SHDescriptor(inner); + public static OutputDescriptor NewWSH(OutputDescriptor inner) => new WSHDescriptor(inner); + + public bool IsAddr() => Tag == Tags.AddressDescriptor; + public bool IsRaw() => Tag == Tags.RawDescriptor; + public bool IsPK() => Tag == Tags.PKDescriptor; + public bool IsPKH() => Tag == Tags.PKHDescriptor; + public bool IsWPKH() => Tag == Tags.WPKHDescriptor; + public bool IsCombo() => Tag == Tags.ComboDescriptor; + public bool IsMulti() => Tag == Tags.MultisigDescriptor; + public bool IsSH() => Tag == Tags.SHDescriptor; + public bool IsWSH() => Tag == Tags.WSHDescriptor; + + public bool IsTopLevelOnly() => + IsAddr() || IsRaw() || IsCombo() || IsSH(); + + #endregion + + #region Descriptor specific things + public bool TryExpand( + uint pos, + Func secretProvider, + ISigningProvider signingProvider, + out List