From 60c8bc1ed16b60dc09632a7603bd9f7edb562870 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 25 Jun 2024 21:25:00 +0900 Subject: [PATCH] Add support for wallet policies (BIP388) --- NBitcoin.Tests/MiniscriptTests.cs | 563 +++++++++++ NBitcoin.Tests/NBitcoin.Tests.csproj | 7 +- NBitcoin/Crypto/Hashes.cs | 8 +- NBitcoin/NBitcoin.csproj | 6 +- NBitcoin/WalletPolicies/AddressIntent.cs | 10 + NBitcoin/WalletPolicies/DerivationResult.cs | 40 + NBitcoin/WalletPolicies/DeriveParameters.cs | 59 ++ NBitcoin/WalletPolicies/Miniscript.cs | 932 +++++++++++++++++ NBitcoin/WalletPolicies/MiniscriptNode.cs | 953 ++++++++++++++++++ .../MiniscriptReplacementException.cs | 19 + .../MiniscriptRewriterVisitor.cs | 33 + NBitcoin/WalletPolicies/MiniscriptVisitor.cs | 35 + NBitcoin/WalletPolicies/ParsingContext.cs | 194 ++++ .../WalletPolicies/Visitors/DeriveVisitor.cs | 96 ++ .../Visitors/MockScriptVisitor.cs | 84 ++ .../Visitors/ParameterVisitors.cs | 71 ++ .../WalletPolicies/Visitors/ScriptVisitor.cs | 67 ++ .../Visitors/TaprootMerkleRootVisitor.cs | 24 + .../Visitors/TemplateVisitors.cs | 35 + NBitcoin/WalletPolicies/WalletPolicy.cs | 65 ++ 20 files changed, 3295 insertions(+), 6 deletions(-) create mode 100644 NBitcoin.Tests/MiniscriptTests.cs create mode 100644 NBitcoin/WalletPolicies/AddressIntent.cs create mode 100644 NBitcoin/WalletPolicies/DerivationResult.cs create mode 100644 NBitcoin/WalletPolicies/DeriveParameters.cs create mode 100644 NBitcoin/WalletPolicies/Miniscript.cs create mode 100644 NBitcoin/WalletPolicies/MiniscriptNode.cs create mode 100644 NBitcoin/WalletPolicies/MiniscriptReplacementException.cs create mode 100644 NBitcoin/WalletPolicies/MiniscriptRewriterVisitor.cs create mode 100644 NBitcoin/WalletPolicies/MiniscriptVisitor.cs create mode 100644 NBitcoin/WalletPolicies/ParsingContext.cs create mode 100644 NBitcoin/WalletPolicies/Visitors/DeriveVisitor.cs create mode 100644 NBitcoin/WalletPolicies/Visitors/MockScriptVisitor.cs create mode 100644 NBitcoin/WalletPolicies/Visitors/ParameterVisitors.cs create mode 100644 NBitcoin/WalletPolicies/Visitors/ScriptVisitor.cs create mode 100644 NBitcoin/WalletPolicies/Visitors/TaprootMerkleRootVisitor.cs create mode 100644 NBitcoin/WalletPolicies/Visitors/TemplateVisitors.cs create mode 100644 NBitcoin/WalletPolicies/WalletPolicy.cs diff --git a/NBitcoin.Tests/MiniscriptTests.cs b/NBitcoin.Tests/MiniscriptTests.cs new file mode 100644 index 000000000..14dacb3e5 --- /dev/null +++ b/NBitcoin.Tests/MiniscriptTests.cs @@ -0,0 +1,563 @@ +#if !NO_RECORDS +using NBitcoin.DataEncoders; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using NBitcoin.WalletPolicies; +using static NBitcoin.WalletPolicies.MiniscriptNode; +using static NBitcoin.WalletPolicies.Miniscript; +using NBitcoin.Scripting; +using Xunit.Abstractions; +using System.Net; +using NBitcoin.Secp256k1; +using NBitcoin.WalletPolicies.Visitors; + +namespace NBitcoin.Tests +{ + [Trait("UnitTest", "UnitTest")] + public class MiniscriptTests + { + public ITestOutputHelper Log { get; } + + public MiniscriptTests(ITestOutputHelper helper) + { + Log = helper; + } + [Theory] + [InlineData("and_v(v:pk(A),pk(B))")] + [InlineData("and_v(v:pk(A),pk(A))")] + [InlineData("and_v(or_c(pk(B),or_c(pk(C),v:older(1000))),pk(A))")] + [InlineData("and_v(or_c(pk(B),or_c(pk(C),v:older(D))),pk(A))")] + [InlineData("or_d(pk([d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW/**),and_v(v:pkh([33fa0ffc/48'/1'/0'/2']tpubDEqzYAym2MnGqKdqu2ZtGQkDTSrvDWCrcoamspjRJR78nr8w5tAgu371r8LtcyWWWXGemenTMxmoLhQM3ww8gUfobBXUWxLEkfR7kGjD6jC/**),older(65535)))")] + [InlineData("or_c(pk(alice),and_v(pk(bob),older(timelock)))")] + [InlineData("andor(pk(key_remote),or_i(and_v(v:pkh(key_local),hash160(H)),older(1008)),pk(key_revocation))")] + [InlineData("0")] + [InlineData("1")] + [InlineData("pk_k(A)")] + [InlineData("pk_k(@0/**)")] + [InlineData("pk_h(A)")] + [InlineData("pk(A)")] + [InlineData("pkh(A)")] + [InlineData("older(A)")] + [InlineData("after(A)")] + [InlineData("sha256(A)")] + [InlineData("ripemd160(A)")] + [InlineData("hash256(A)")] + [InlineData("hash160(A)")] + [InlineData("andor(A,B,C)")] + [InlineData("and_v(A,B)")] + [InlineData("and_b(A,B)")] + [InlineData("and_n(A,B)")] + [InlineData("or_b(A,B)")] + [InlineData("or_c(A,B)")] + [InlineData("or_d(A,B)")] + [InlineData("or_i(A,B)")] + [InlineData("thresh(2,A,B)")] + [InlineData("multi(1,A)")] + [InlineData("multi_a(3,A,B,C)")] + [InlineData("A")] + [InlineData("a:A")] + [InlineData("s:A")] + [InlineData("c:A")] + [InlineData("t:A")] + [InlineData("d:A")] + [InlineData("v:A")] + [InlineData("j:A")] + [InlineData("n:A")] + [InlineData("l:A")] + [InlineData("u:A")] + [InlineData("dv:older(144)")] + public void CanRoundtripMiniscript(string miniscript) + { + var parsed = Miniscript.Parse(miniscript, new MiniscriptParsingSettings(Network.RegTest, KeyType.Classic)); + var actual = parsed.ToString(); + Assert.Equal(miniscript, actual); + } + + [Theory] + [InlineData(ParameterTypeFlags.All, "pkh(A)", true)] + [InlineData(ParameterTypeFlags.All, "pkh(@0/**)", true)] + [InlineData(ParameterTypeFlags.NamedParameter, "pkh(A)", true)] + [InlineData(ParameterTypeFlags.NamedParameter, "older(A)", true)] + [InlineData(ParameterTypeFlags.NamedParameter, "sha256(A)", true)] + [InlineData(ParameterTypeFlags.NamedParameter, "pkh(@0/**)", false)] + [InlineData(ParameterTypeFlags.KeyPlaceholder, "pkh(A)", false)] + [InlineData(ParameterTypeFlags.KeyPlaceholder, "pkh(@0/**)", true)] + [InlineData(ParameterTypeFlags.KeyPlaceholder, "older(A)", false)] + [InlineData(ParameterTypeFlags.KeyPlaceholder, "sha256(A)", false)] + [InlineData(ParameterTypeFlags.None, "pkh(A)", false)] + [InlineData(ParameterTypeFlags.None, "pkh(@0/**)", false)] + public void CanToggleParameterParsing(ParameterTypeFlags flag, string miniscript, bool expected) + { + var settings = new MiniscriptParsingSettings(Network.Main, KeyType.Classic) { AllowedParameters = flag }; + Assert.Equal(expected, Miniscript.TryParse(miniscript, settings, out _)); + } + + [Fact] + public void CanParseTr() + { + var settings = new MiniscriptParsingSettings(Network.RegTest) { Dialect = MiniscriptDialect.BIP388 }; + var miniscript = "tr(A,{BL,{{EL,FR},DR}})"; + var parsed = Miniscript.Parse(miniscript, settings); + Assert.True(parsed.RootNode is TaprootNode + { + InternalKeyNode: Parameter { Name: "A" }, + ScriptTreeRootNode: TaprootBranchNode + { + Left: Parameter { Name: "BL" }, + Right: TaprootBranchNode + { + Left: TaprootBranchNode + { + Left: Parameter { Name: "EL" }, + Right: Parameter { Name: "FR" } + }, + Right: Parameter { Name: "DR" } + } + } + }); + } + [Fact] + public void CanGenerateTrScript() + { + var settings = new MiniscriptParsingSettings(Network.RegTest) { Dialect = MiniscriptDialect.BIP388 }; + var parsed = Miniscript.Parse("tr(@0/**,{pkh(@1/**),{{pkh(@2/**),pkh(@3/**)},pkh(@4/**)}})", settings); + var keys = GenerateKeys(5); + parsed = parsed.ReplaceKeyPlaceholdersByHDKeys(keys); + parsed = parsed.Derive(AddressIntent.Deposit, 0).Miniscript; + var scripts = parsed.ToScripts(); + var taprootInfo = parsed.GetTaprootInfo(); + Assert.NotNull(taprootInfo); + Assert.NotNull(taprootInfo.MerkleRoot); + + // Example from https://github.com/bitcoin/bips/blob/master/bip-0386.mediawiki + // We can't take literally the example, because the format of output descriptors is more relaxed than BIP388 + // so we need to derive everything separately. + var privKey = new BitcoinSecret("L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1", Network.Main); + var internalKey = new TaprootPubKey(Encoders.Hex.DecodeData("a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd")); + var A = new BitcoinExtKey("xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334", Network.Main).Neuter().Derive(KeyPath.Parse("0")).GetPublicKey().TaprootPubKey; + var B = new BitcoinExtPubKey("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", Network.Main).GetPublicKey().TaprootPubKey; + var C = new PubKey("02df12b7035bdac8e3bab862a3a83d06ea6b17b6753d52edecba9be46f5d09e076").TaprootPubKey; + var D = privKey.PubKey.TaprootPubKey; + parsed = Miniscript.Parse("tr(InternalKey,{pk(A),{{pk(B),pk(C)},pk(D)}})", settings); + parsed = parsed.ReplaceParameters(new() + { + { "A", MiniscriptNode.Create(A) }, + { "B", MiniscriptNode.Create(B) }, + { "C", MiniscriptNode.Create(C) }, + { "D", MiniscriptNode.Create(D) }, + { "InternalKey", MiniscriptNode.Parameter.Create(internalKey) }, + }); + Assert.Equal("512071fff39599a7b78bc02623cbe814efebf1a404f5d8ad34ea80f213bd8943f574", parsed.ToScripts().ScriptPubKey.ToHex()); + + var original = parsed.ToString(); + for (int i = 0; i < original.Length; i++) + { + var ms = original[0..i]; + Assert.False(Miniscript.TryParse(ms, settings, out var err, out _)); + Assert.IsAssignableFrom(err); + ms += "&"; + Assert.False(Miniscript.TryParse(ms, settings, out err, out _)); + Assert.IsAssignableFrom(err); + } + } + + [Fact] + public void CanRoundtripMiniscriptBIP388() + { + var settings = new MiniscriptParsingSettings(Network.RegTest) { Dialect = MiniscriptDialect.BIP388 }; + var miniscript = "wsh(or_i(and_v(v:thresh(1,pkh([f25bdff6/48'/1'/0'/2']tpubDF8cqMgmJ6BMJwMoEhwAfgVDdXs29y6w2qG1i1ciaYVmqQ6cRTjNoqWJZD2kAR6vJrGcpVBVyYEgYm5GE88F3Z2SVbQxqwdbRZeyUeGwTnk/<4;5>/*),a:pkh([6abb52a9/48'/1'/0'/2']tpubDFZTCVU1Sa9nJXCxx97UFvGausHQPFjJyaiDbdr8GNqjCLKwYc8ihegK7yJdcizs9HMbiGA7ke1HiCENVHaERvNANHW7U2Wo2qnRsuqB52r/<4;5>/*),a:pkh([d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW/<4;5>/*)),older(10)),or_d(multi(3,[f25bdff6/48'/1'/0'/2']tpubDF8cqMgmJ6BMJwMoEhwAfgVDdXs29y6w2qG1i1ciaYVmqQ6cRTjNoqWJZD2kAR6vJrGcpVBVyYEgYm5GE88F3Z2SVbQxqwdbRZeyUeGwTnk/<0;1>/*,[6abb52a9/48'/1'/0'/2']tpubDFZTCVU1Sa9nJXCxx97UFvGausHQPFjJyaiDbdr8GNqjCLKwYc8ihegK7yJdcizs9HMbiGA7ke1HiCENVHaERvNANHW7U2Wo2qnRsuqB52r/<0;1>/*,[d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW/<0;1>/*),and_v(v:thresh(2,pkh([f25bdff6/48'/1'/0'/2']tpubDF8cqMgmJ6BMJwMoEhwAfgVDdXs29y6w2qG1i1ciaYVmqQ6cRTjNoqWJZD2kAR6vJrGcpVBVyYEgYm5GE88F3Z2SVbQxqwdbRZeyUeGwTnk/<2;3>/*),a:pkh([6abb52a9/48'/1'/0'/2']tpubDFZTCVU1Sa9nJXCxx97UFvGausHQPFjJyaiDbdr8GNqjCLKwYc8ihegK7yJdcizs9HMbiGA7ke1HiCENVHaERvNANHW7U2Wo2qnRsuqB52r/<2;3>/*),a:pkh([d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW/<2;3>/*)),older(5)))))#757lxvur"; + var noChecksumMiniscript = miniscript.Replace("#757lxvur", ""); + var wrongChecksumMiniscript = miniscript.Replace("#757lxvur", "#756lxvur"); + var cutChecksumMiniscript = miniscript.Replace("#757lxvur", "#756lx"); + var miniscriptWithGarbage = miniscript + "and"; + + var parsed = Miniscript.Parse(noChecksumMiniscript, settings); + var actual = parsed.ToString(); + Assert.Equal(noChecksumMiniscript, actual); + + parsed = Miniscript.Parse(miniscript, settings); + actual = parsed.ToString(); + Assert.Equal(noChecksumMiniscript, actual); + + var ex = Assert.Throws(() => Miniscript.Parse(wrongChecksumMiniscript, settings)); + Assert.IsType(ex.Error); + + ex = Assert.Throws(() => Miniscript.Parse(cutChecksumMiniscript, settings)); + Assert.IsType(ex.Error); + + ex = Assert.Throws(() => Miniscript.Parse(miniscriptWithGarbage, settings)); + Assert.IsType(ex.Error); + + var policy = WalletPolicy.Parse(miniscript, Network.RegTest); + Assert.Equal(miniscript, policy.ToString(true)); + + var address = policy.FullDescriptor.Derive(AddressIntent.Deposit, 1).Miniscript.ToScripts().ScriptPubKey.GetDestinationAddress(Network.TestNet); + Assert.Equal("tb1qkhfzpjc3llj953rdfyzdzy0re88xvlvy4s3z5t0ur49dtlyzh34qhag7m5", address.ToString()); + } + + [Theory] + [InlineData("tr(@0/**)", ScriptPubKeyType.TaprootBIP86)] + [InlineData("wpkh(@0/**)", ScriptPubKeyType.Segwit)] + [InlineData("sh(wpkh(@0/**))", ScriptPubKeyType.SegwitP2SH)] + [InlineData("pkh(@0/**)", ScriptPubKeyType.Legacy)] + public void CanGenerateSegwitAndTaprootFromMiniscript(string str, ScriptPubKeyType scriptPubKeyType) + { + var root = new ExtKey().GetWif(Network.TestNet); + var account = root.Derive(new KeyPath("48'/1'/0'/2'")); + var derivedPubKey = account.Derive(new KeyPath("1/123")).GetPublicKey(); + var keyNode = new HDKeyNode(new KeyPath("48'/1'/0'/2'").ToRootedKeyPath(root), account.Neuter()); + var miniscript = Miniscript.Parse(str, new MiniscriptParsingSettings(root.Network) { Dialect = MiniscriptDialect.BIP388 }); + miniscript = miniscript.ReplaceKeyPlaceholdersByHDKeys([keyNode]); + var miniscriptWithHDKeys = miniscript; + miniscript = miniscript.Derive(AddressIntent.Change, 123).Miniscript; + var scripts = miniscript.ToScripts(); + var expected = derivedPubKey.GetScriptPubKey(scriptPubKeyType); + Assert.Equal(expected, scripts.ScriptPubKey); + + var policy = WalletPolicy.Parse(miniscriptWithHDKeys.ToString(), root.Network); + var k = Assert.Single(policy.KeyInformationVector); + Assert.Equal(k, keyNode); + Assert.Equal(str, policy.DescriptorTemplate.ToString()); + } + + [Fact] + public void CanGenerateSH() + { + var root = new ExtKey().GetWif(Network.TestNet); + var parsingSettings = new MiniscriptParsingSettings(root.Network) { Dialect = MiniscriptDialect.BIP388 }; + var account = root.Derive(new KeyPath("48'/1'/0'/2'")); + var derivedPubKey = account.Derive(new KeyPath("1/123")).GetPublicKey(); + var keyNode = new HDKeyNode(new KeyPath("48'/1'/0'/2'").ToRootedKeyPath(root), account.Neuter()); + var pkh = PayToPubkeyHashTemplate.Instance.GenerateScriptPubKey(derivedPubKey); + var miniscript = Miniscript.Parse("wsh(pkh(@0/**))", parsingSettings); + miniscript = miniscript.ReplaceKeyPlaceholdersByHDKeys([keyNode]); + miniscript = miniscript.Derive(AddressIntent.Change, 123).Miniscript; + var scripts = miniscript.ToScripts(); + Assert.Equal(pkh.WitHash.ScriptPubKey, scripts.ScriptPubKey); + Assert.Equal(pkh, scripts.RedeemScript); + + miniscript = Miniscript.Parse("sh(pkh(@0/**))", parsingSettings); + miniscript = miniscript.ReplaceKeyPlaceholdersByHDKeys([keyNode]); + miniscript = miniscript.Derive(AddressIntent.Change, 123).Miniscript; + scripts = miniscript.ToScripts(); + Assert.Equal(pkh.Hash.ScriptPubKey, scripts.ScriptPubKey); + Assert.Equal(pkh, scripts.RedeemScript); + + miniscript = Miniscript.Parse("sh(wsh(pkh(@0/**)))", parsingSettings); + miniscript = miniscript.ReplaceKeyPlaceholdersByHDKeys([keyNode]); + miniscript = miniscript.Derive(AddressIntent.Change, 123).Miniscript; + scripts = miniscript.ToScripts(); + Assert.Equal(pkh.WitHash.ScriptPubKey.Hash.ScriptPubKey, scripts.ScriptPubKey); + Assert.Equal(pkh, scripts.RedeemScript); + } + + [Theory] + [InlineData("hash256(03a195e87b81956f63837927446ffb42ace1675778264597b9aa0aa0d461b892)", "OP_SIZE 20 OP_EQUALVERIFY OP_HASH256 03a195e87b81956f63837927446ffb42ace1675778264597b9aa0aa0d461b892 OP_EQUAL")] + [InlineData("sha256(03a195e87b81956f63837927446ffb42ace1675778264597b9aa0aa0d461b892)", "OP_SIZE 20 OP_EQUALVERIFY OP_SHA256 03a195e87b81956f63837927446ffb42ace1675778264597b9aa0aa0d461b892 OP_EQUAL")] + [InlineData("ripemd160(03a195e87b81956f63837927446ffb42ace16757)", "OP_SIZE 20 OP_EQUALVERIFY OP_RIPEMD160 03a195e87b81956f63837927446ffb42ace16757 OP_EQUAL")] + [InlineData("hash160(03a195e87b81956f63837927446ffb42ace16757)", "OP_SIZE 20 OP_EQUALVERIFY OP_HASH160 03a195e87b81956f63837927446ffb42ace16757 OP_EQUAL")] + [InlineData("pk(Alice)", " OP_CHECKSIG")] + [InlineData("pk(L4ufc8BCfbWH73SNfJpYBR8nVZmdRqfZu8wCwVXy1nWC3Ac5uMz2)", "029c6d96193c911a05fd9d0583cbb30fe2844f26a312aa58384e0b5d8b9bbfae23 OP_CHECKSIG")] + [InlineData("v:pk(Alice)", " OP_CHECKSIGVERIFY")] + [InlineData("dv:older(144)", "OP_DUP OP_IF 9000 OP_CSV OP_VERIFY OP_ENDIF")] + [InlineData("or_b(pk(key_1),s:pk(key_2))", " OP_CHECKSIG OP_SWAP OP_CHECKSIG OP_BOOLOR")] + [InlineData("or_d(pk(key_likely),pkh(key_unlikely))", " OP_CHECKSIG OP_IFDUP OP_NOTIF OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF")] + [InlineData("and_v(v:pk(key_user),or_d(pk(key_service),older(12960)))", " OP_CHECKSIGVERIFY OP_CHECKSIG OP_IFDUP OP_NOTIF a032 OP_CSV OP_ENDIF")] + [InlineData("thresh(3,pk(key_1),s:pk(key_2),s:pk(key_3),sln:older(12960))", " OP_CHECKSIG OP_SWAP OP_CHECKSIG OP_ADD OP_SWAP OP_CHECKSIG OP_ADD OP_SWAP OP_IF 0 OP_ELSE a032 OP_CSV OP_0NOTEQUAL OP_ENDIF OP_ADD 3 OP_EQUAL")] + [InlineData("andor(pk(key_local),older(1008),pk(key_revocation))", " OP_CHECKSIG OP_NOTIF OP_CHECKSIG OP_ELSE f003 OP_CSV OP_ENDIF")] + [InlineData("t:or_c(pk(key_revocation),and_v(v:pk(key_remote),or_c(pk(key_local),v:hash160(hash_value))))", " OP_CHECKSIG OP_NOTIF OP_CHECKSIGVERIFY OP_CHECKSIG OP_NOTIF OP_SIZE 20 OP_EQUALVERIFY OP_HASH160 OP_EQUALVERIFY OP_ENDIF OP_ENDIF 1")] + [InlineData("andor(pk(key_remote),or_i(and_v(v:pkh(key_local),hash160(hash_value)),older(1008)),pk(key_revocation))", " OP_CHECKSIG OP_NOTIF OP_CHECKSIG OP_ELSE OP_IF OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIGVERIFY OP_SIZE 20 OP_EQUALVERIFY OP_HASH160 OP_EQUAL OP_ELSE f003 OP_CSV OP_ENDIF OP_ENDIF")] + [InlineData("multi(2,A_key,B_key,C_key)", "2 3 OP_CHECKMULTISIG")] + [InlineData("sortedmulti(2,021668c18319ca953898f6346e42d16679ed721506814082d3851a6c26e104d233,03bbcb66e7c5ed0da2d72e6bf3dea4aa5bfc40e33ef9a0f878310a5bfddab97969,027305bfd28e0baa3c18121c9500bed3084dc9074001e9de341122a412af5a0eac)", "2 021668c18319ca953898f6346e42d16679ed721506814082d3851a6c26e104d233 027305bfd28e0baa3c18121c9500bed3084dc9074001e9de341122a412af5a0eac 03bbcb66e7c5ed0da2d72e6bf3dea4aa5bfc40e33ef9a0f878310a5bfddab97969 3 OP_CHECKMULTISIG")] + [InlineData("sortedmulti(1,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", "1 03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd 04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235 2 OP_CHECKMULTISIG")] + [InlineData("multi(1,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", "1 03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd 04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235 2 OP_CHECKMULTISIG")] + [InlineData("multi_a(2,A_key,B_key,C_key)", " OP_CHECKSIG OP_CHECKSIGADD OP_CHECKSIGADD 2 OP_NUMEQUAL")] + [InlineData("older(A)", " OP_CSV")] + [InlineData("l:A", "OP_IF 0 OP_ELSE OP_ENDIF")] + [InlineData("u:A", "OP_IF OP_ELSE 0 OP_ENDIF")] + [InlineData("j:A", "OP_SIZE OP_0NOTEQUAL OP_IF OP_ENDIF")] + [InlineData("d:A", "OP_DUP OP_IF OP_ENDIF")] + public void CanGenerateScript(string miniscript, string expected) + { + var parsed = Miniscript.Parse(miniscript, new MiniscriptParsingSettings(Network.Main, KeyType.Classic)); + Assert.Equal(miniscript, parsed.ToString()); // Sanity check + Assert.Equal(expected, parsed.ToScriptCodeString()); + } + + [Fact] + public void CanParseMusigExpression() + { + var settings = new MiniscriptParsingSettings(Network.Main) { Dialect = MiniscriptDialect.BIP388 }; + var miniscript = "tr(musig(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9,03dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659,023590a94e768f8e1815c2f24b4d80a8e3149316c3518ce7b7ad338368d038ca66))"; + var script = Miniscript.Parse(miniscript, settings); + Assert.Equal("512079e6c3e628c9bfbce91de6b7fb28e2aec7713d377cf260ab599dcbc40e542312", script.ToScripts().ScriptPubKey.ToHex()); + + miniscript = "tr(f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9,pk(musig(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y)/**))"; + + string[] expected = ["512068983d461174afc90c26f3b2821d8a9ced9534586a756763b68371a404635cc8", + "5120368e2d864115181bdc8bb5dc8684be8d0760d5c33315570d71a21afce4afd43e", + "512097a1e6270b33ad85744677418bae5f59ea9136027223bc6e282c47c167b471d5"]; + for (int i = 0; i < expected.Length; i++) + { + script = Miniscript.Parse(miniscript, new MiniscriptParsingSettings(Network.Main) { Dialect = MiniscriptDialect.BIP388 }); + script = script.Derive(AddressIntent.Deposit, i).Miniscript; + Assert.Equal(expected[i], script.ToScripts().ScriptPubKey.ToHex()); + } + + Assert.True(Miniscript.TryParse("tr(musig(A,B,C))", settings, out _)); + // Support nested + Assert.True(Miniscript.TryParse("tr(musig(musig(A,B),C))", settings, out _)); + Assert.True(Miniscript.TryParse("tr(musig(musig(@0/**,@1/**),@2/**))", settings, out _)); + Assert.True(Miniscript.TryParse("tr(musig(A,B)/**)", settings, out _)); + // Support nested multipath + Assert.True(Miniscript.TryParse("tr(musig(@0/**,B)/**)", settings, out _)); + + miniscript = "tr(musig(@0/**,@1/**,musig(@2/**,@3/**))/**)"; + var m = Miniscript.Parse(miniscript, settings); + Assert.Equal(miniscript, m.ToString()); + var keys = GenerateKeys(4); + m = m.ReplaceKeyPlaceholdersByHDKeys(keys); + m = m.Derive(AddressIntent.Deposit, 1).Miniscript; + + var dKeys = keys.Select(k => k.Key.Derive(0).Derive(1).GetPublicKey().ECKey).ToArray(); + var musigNested = ECPubKey.MusigAggregate([dKeys[2], dKeys[3]], true); + var musig = ECPubKey.MusigAggregate([dKeys[0], dKeys[1], musigNested], true); + var expectedPubKey = new ExtPubKey(new PubKey(musig, true), DeriveVisitor.BIP0328CC).Derive(0).Derive(1).GetPublicKey(); + var expectedScript = expectedPubKey.GetScriptPubKey(ScriptPubKeyType.TaprootBIP86); + Assert.Equal(expectedScript, m.ToScripts().ScriptPubKey); + } + + [Fact] + public void CanHandleSpaces() + { + var script = Miniscript.Parse("and_v(v:pk ( A ),pk (B) )", new MiniscriptParsingSettings(Network.Main, KeyType.Classic)); + Assert.Equal("and_v(v:pk(A),pk(B))", script.ToString()); + } + + [Fact] + public void CanReplaceParameters() + { + var parsed = Miniscript.Parse("and_v(or_c(pk(A),or_c(pk(B),v:older(C))),pk(A))", new MiniscriptParsingSettings(Network.Main, KeyType.Classic)); + var a = new Key().PubKey; + var b = new Key().PubKey; + new MiniscriptParsingSettings(Network.Main, KeyType.Classic); + var exception = Assert.Throws(() => parsed.ReplaceParameters(new() + { + { "A", new MiniscriptNode.Value.LockTimeValue(1) } + })); + Assert.Equal("A", exception.ParameterName); + Assert.IsType(exception.Requirement); + + parsed = parsed.ReplaceParameters(new() + { + { "A", new MiniscriptNode.Value.PubKeyValue(a) }, + { "B", new MiniscriptNode.Value.PubKeyValue(b) } + }); + Assert.Equal($"and_v(or_c(pk({a}),or_c(pk({b}),v:older(C))),pk({a}))", parsed.ToString()); + Assert.Single(parsed.Parameters); + parsed = parsed.ReplaceParameters(new() + { + { "C", new MiniscriptNode.Value.LockTimeValue(new LockTime(10)) }, + }); + Assert.Equal($"and_v(or_c(pk({a}),or_c(pk({b}),v:older(10))),pk({a}))", parsed.ToString()); + Assert.Empty(parsed.Parameters); + } + + [Theory] + [InlineData("", typeof(MiniscriptError.IncompleteExpression))] + [InlineData("and_v(v:pk(A),older(A))", typeof(MiniscriptError.MixedParameterType))] + [InlineData("and_v(v:pk(A,B),older(A))", typeof(MiniscriptError.TooManyParameters))] + [InlineData("and_v(older(A))", typeof(MiniscriptError.TooFewParameters))] + [InlineData("and_v(older(A)", typeof(MiniscriptError.IncompleteExpression))] + [InlineData("and_v", typeof(MiniscriptError.IncompleteExpression))] + [InlineData("v:pk(A", typeof(MiniscriptError.IncompleteExpression))] + [InlineData("ando(", typeof(MiniscriptError.UnknownFragmentName))] + [InlineData("ripemd160(03a195e87b81956f63837927446ffb42ace1675)", typeof(MiniscriptError.HashExpected))] + [InlineData("hash160(03a195e87b81956f63837927446ffb42ace167)", typeof(MiniscriptError.HashExpected))] + [InlineData("hash160(03a195e87b81956f63837927446ffb42ace1670000)", typeof(MiniscriptError.HashExpected))] + [InlineData("hash256(03a195e87b81956f63837927446ffb42ace1675778264597b9aa0aa0d461b89)", typeof(MiniscriptError.HashExpected))] + [InlineData("sha256(03a195e87b81956f63837927446ffb42ace1675778264597b9aa0aa0d461b8)", typeof(MiniscriptError.HashExpected))] + [InlineData("multi(2,A)", typeof(MiniscriptError.TooFewParameters))] + [InlineData("thresh(2,A)", typeof(MiniscriptError.TooFewParameters))] + [InlineData("sh(A)", typeof(MiniscriptError.UnknownFragmentName))] + [InlineData("wpkh(A)", typeof(MiniscriptError.UnknownFragmentName))] + [InlineData("tr(A)", typeof(MiniscriptError.UnknownFragmentName))] + [InlineData("pkh(A)B", typeof(MiniscriptError.UnexpectedToken))] + [InlineData("multi(2,545a50b9996bf8573999af86cab671204b7c6453cf953ef00ec5ad613b6c5689)", typeof(MiniscriptError.KeyExpected))] + [InlineData("multi_a(2,02545a50b9996bf8573999af86cab671204b7c6453cf953ef00ec5ad613b6c5689)", typeof(MiniscriptError.KeyExpected))] + public void CheckMiniscriptErrors(string miniscript, Type expectedError) + { + Assert.False(Miniscript.TryParse(miniscript, new MiniscriptParsingSettings(Network.Main, KeyType.Classic), out var error, out _)); + Assert.NotNull(error); + Assert.IsType(expectedError, error); + } + [Theory] + [InlineData("multi(2,545a50b9996bf8573999af86cab671204b7c6453cf953ef00ec5ad613b6c5689)", typeof(MiniscriptError.InvalidTopFragment))] + [InlineData("and_v(v:pk(A,B),older(A))", typeof(MiniscriptError.InvalidTopFragment))] + [InlineData("and_v(older(A))", typeof(MiniscriptError.InvalidTopFragment))] + [InlineData("sh(wsh(sh(pkh(A))))", typeof(MiniscriptError.UnknownFragmentName))] + public void CheckMiniscriptBIP388Errors(string miniscript, Type expectedError) + { + Assert.False(Miniscript.TryParse(miniscript, new MiniscriptParsingSettings(Network.Main) { Dialect = MiniscriptDialect.BIP388 }, out var error, out _)); + Assert.NotNull(error); + Assert.IsType(expectedError, error); + } + + [Theory] + [InlineData("@0/**", "@0", 0, 1)] + [InlineData("@1/**", "@1", 0, 1)] + [InlineData("@1/<0;1>/*", "@1", 0, 1)] + [InlineData("@3/<2;3>/*", "@3", 2, 3)] + public void CanParseMultiPathParameter(string str, string name, int deposit, int change) + { + Assert.True(MultipathNode.TryParse(str, Network.TestNet, out var key) && key.Target is Parameter); + var p = (Parameter)key.Target; + Assert.Equal((name, deposit, change), (p.Name, key.DepositIndex, key.ChangeIndex)); + var shortForm = str.Contains("/**"); + if (shortForm || !key.CanUseShortForm) + Assert.Equal(str, key.ToString()); + } + [Theory] + [InlineData(AddressIntent.Deposit)] + [InlineData(AddressIntent.Change)] + public void CanMassDeriveMiniscripts(AddressIntent intent) + { + HDKeyNode[] keys = GenerateKeys(3); + var miniscript = Miniscript.Parse("multi(2,@0/<0;1>/*,@1/<3;4>/*,@2/**)", Network.Main); + miniscript = miniscript.ReplaceParameters(new() + { + { "@0", keys[0]}, + { "@1", keys[1]}, + { "@2", keys[2]}, + }); + var indexes = Enumerable.Range(15, 5).ToArray(); + var allMiniscripts = miniscript.Derive(new DeriveParameters(intent, indexes)); + Assert.Equal(5, allMiniscripts.Length); + + var typeIdx = intent is AddressIntent.Deposit ? new[] { 0, 3, 0 } : new[] { 1, 4, 1 }; + var a = keys[0].Key.Derive((uint)typeIdx[0]).Derive(15 + 3).GetPublicKey(); + var b = keys[1].Key.Derive((uint)typeIdx[1]).Derive(15 + 3).GetPublicKey(); + var c = keys[2].Key.Derive((uint)typeIdx[2]).Derive(15 + 3).GetPublicKey(); + var derived = allMiniscripts[3].Miniscript; + var expected = $"multi(2,{a},{b},{c})"; + Assert.Equal(expected, derived.ToString()); + + var derivedKey = allMiniscripts[3].DerivedKeys[keys[0]]; + Assert.Equal(new KeyPath($"{typeIdx[0]}/{15 + 3}"), derivedKey.KeyPath); + Assert.Equal(a, ((Value.PubKeyValue)derivedKey.Pubkey).PubKey); + } + + private static HDKeyNode[] GenerateKeys(int count) + { + return Enumerable.Range(0, count).Select(_ => + { + var root = new ExtKey().GetWif(Network.RegTest); + return new HDKeyNode(new KeyPath("48'/1'/0'").ToRootedKeyPath(root.ExtKey), root.Neuter()); + }).ToArray(); + } + + [Fact] + public void CanManipulateKeyExpression() + { + var parsing = new MiniscriptParsingSettings(Network.RegTest) { Dialect = MiniscriptDialect.BIP388 }; + var keyExpr = HDKeyNode.Parse("[d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW", Network.RegTest); + var miniscript = Miniscript.Parse("pkh(@0/<0;1>/*)", parsing); + miniscript = miniscript.ReplaceParameters(new() + { + { "@0", keyExpr}, + }); + Assert.Equal("pkh([d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW/<0;1>/*)", miniscript.ToString()); + Assert.Equal(Network.RegTest, miniscript.Network); + + // Make it readable again + var noKeys = miniscript.ReplaceHDKeysByKeyPlaceholders(out var hdKeys); + Assert.Equal("pkh(@0/<0;1>/*)", noKeys.ToString()); + Assert.Equal("OP_DUP OP_HASH160 /*)> OP_EQUALVERIFY OP_CHECKSIG", noKeys.ToScriptString()); + + // Can We reverse operation? + miniscript = noKeys.ReplaceKeyPlaceholdersByHDKeys(hdKeys); + Assert.Equal("pkh([d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW/<0;1>/*)", miniscript.ToString()); + Assert.Equal(Network.RegTest, miniscript.Network); + + // Can we parse the multi path key information? + miniscript = Miniscript.Parse(miniscript.ToString(), parsing); + + miniscript = miniscript.Derive(AddressIntent.Deposit, 50).Miniscript; + Assert.Equal("pkh(035061f24ab15de479008738557f7120a0c6299ceb1033669303473837b4314342)", miniscript.ToString()); + + Assert.Equal("pkh(035061f24ab15de479008738557f7120a0c6299ceb1033669303473837b4314342)", $"pkh({keyExpr.Key.Derive(new KeyPath("0/50")).GetPublicKey()})"); + + // Let's check how it works with two parameters + var multi = Miniscript.Parse("multi(2,a,b)", miniscript.Network); + var a = CreateMultiPathKeyInformation(); + var b = CreateMultiPathKeyInformation(); + multi = multi.ReplaceParameters(new() + { + { "a", a }, + { "b", b }, + }).ReplaceHDKeysByKeyPlaceholders(out _); + Assert.Equal("multi(2,@0/<1;2>/*,@1/<1;2>/*)", multi.ToString()); + } + + private MultipathNode CreateMultiPathKeyInformation() + { + var k = new ExtKey().GetWif(Network.RegTest); + var root = k; + var accountKeyPath = new KeyPath("44'/1'"); + var account = k.Derive(accountKeyPath).Neuter(); + var hdKey = new HDKeyNode(new RootedKeyPath(root.GetPublicKey().GetHDFingerPrint(), accountKeyPath), account); + return new MultipathNode(1, 2, hdKey, true); + } + + [Fact] + public void CanParseMultiPathHDKey() + { + string i = "[d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW/<0;1>/*"; + var k = MultipathNode.Parse(i, Network.RegTest); + var pk = (HDKeyNode)k.Target; + Assert.Equal(new HDFingerprint(0xf166abd4), pk.RootedKeyPath.MasterFingerprint); + Assert.Equal(new KeyPath("48'/1'/0'/2'"), pk.RootedKeyPath.KeyPath); + Assert.Equal("d4ab66f1/48'/1'/0'/2'", pk.RootedKeyPath.ToString()); + Assert.Equal(new BitcoinExtPubKey("tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW", Network.RegTest), pk.Key); + Assert.Equal(0, k.DepositIndex); + Assert.Equal(1, k.ChangeIndex); + } + [Fact] + public void CanParseKeyInformation() + { + string i = "[d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW"; + var k = HDKeyNode.Parse(i, Network.RegTest); + Assert.Equal(i, k.ToString()); + Assert.Equal(new HDFingerprint(0xf166abd4), k.RootedKeyPath.MasterFingerprint); + Assert.Equal(new KeyPath("48'/1'/0'/2'"), k.RootedKeyPath.KeyPath); + Assert.Equal("d4ab66f1/48'/1'/0'/2'", k.RootedKeyPath.ToString()); + Assert.Equal(new BitcoinExtPubKey("tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW", Network.RegTest), k.Key); + + var xpriv = new ExtKey().GetWif(Network.RegTest); + i = $"[d4ab66f1/48'/1'/0'/2']{xpriv}"; + k = HDKeyNode.Parse(i, Network.RegTest); + Assert.Equal(i, k.ToString()); + Assert.Equal(new HDFingerprint(0xf166abd4), k.RootedKeyPath.MasterFingerprint); + Assert.Equal(new KeyPath("48'/1'/0'/2'"), k.RootedKeyPath.KeyPath); + Assert.Equal("d4ab66f1/48'/1'/0'/2'", k.RootedKeyPath.ToString()); + Assert.Equal(xpriv, k.Key); + + // Garbage at the end + Assert.False(HDKeyNode.TryParse(i + "/", Network.RegTest, out _)); + } + + [Theory] + [InlineData("@1")] + [InlineData("@0/0/**")] + [InlineData("@0/**/*")] + [InlineData("@0/<2147483648,0>/*")] + [InlineData("@0/<,-2>/*")] + public void InvalidMultiPath(string str) + { + Assert.False(MultipathNode.TryParse(str, Network.Main, out _)); + } + } +} +#endif diff --git a/NBitcoin.Tests/NBitcoin.Tests.csproj b/NBitcoin.Tests/NBitcoin.Tests.csproj index 4b029488e..9a1514334 100644 --- a/NBitcoin.Tests/NBitcoin.Tests.csproj +++ b/NBitcoin.Tests/NBitcoin.Tests.csproj @@ -20,10 +20,13 @@ $(DefineConstants);$(AdditionalDefineConstants) - $(DefineConstants);WIN + $(DefineConstants);WIN;NO_RECORDS + + $(DefineConstants);NO_RECORDS + - $(DefineConstants);NETCORE;NOTRACESOURCE;NOCUSTOMSSLVALIDATION;NOHTTPSERVER + $(DefineConstants);NETCORE;NOTRACESOURCE;NOCUSTOMSSLVALIDATION;NOHTTPSERVER;NO_RECORDS $(DefineConstants);NETCORE;HAS_SPAN;NO_BC diff --git a/NBitcoin/Crypto/Hashes.cs b/NBitcoin/Crypto/Hashes.cs index 6c1a7d3e4..1c7c0c3ba 100644 --- a/NBitcoin/Crypto/Hashes.cs +++ b/NBitcoin/Crypto/Hashes.cs @@ -77,7 +77,13 @@ public static uint160 Hash160(byte[] data, int count) public static uint160 Hash160(byte[] data, int offset, int count) { - return new uint160(RIPEMD160(SHA256(data, offset, count))); + return new uint160(Hash160RawBytes(data, offset, count)); + } + public static byte[] Hash160RawBytes(byte[] data) + => Hash160RawBytes(data, 0, data.Length); + public static byte[] Hash160RawBytes(byte[] data, int offset, int count) + { + return RIPEMD160(SHA256(data, offset, count)); } #endregion diff --git a/NBitcoin/NBitcoin.csproj b/NBitcoin/NBitcoin.csproj index e563985a6..cc2847b3d 100644 --- a/NBitcoin/NBitcoin.csproj +++ b/NBitcoin/NBitcoin.csproj @@ -31,7 +31,7 @@ bin\Release\NBitcoin.XML - $(DefineConstants);CLASSICDOTNET;NO_ARRAY_FILL;NULLABLE_SHIMS;NO_SOCKETASYNC + $(DefineConstants);CLASSICDOTNET;NO_ARRAY_FILL;NULLABLE_SHIMS;NO_SOCKETASYNC;NO_RECORDS $(DefineConstants);NOCUSTOMSSLVALIDATION;NO_NATIVERIPEMD160 @@ -41,10 +41,10 @@ true - $(DefineConstants);NO_SOCKETASYNC + $(DefineConstants);NO_SOCKETASYNC;NO_RECORDS - $(DefineConstants);NETSTANDARD;NO_ARRAY_FILL;NULLABLE_SHIMS;NO_NATIVE_RFC2898_HMACSHA512;NO_NATIVERIPEMD160;NO_SOCKETASYNC + $(DefineConstants);NETSTANDARD;NO_ARRAY_FILL;NULLABLE_SHIMS;NO_NATIVE_RFC2898_HMACSHA512;NO_NATIVERIPEMD160;NO_SOCKETASYNC;NO_RECORDS $(DefineConstants);SECP256K1_VERIFY diff --git a/NBitcoin/WalletPolicies/AddressIntent.cs b/NBitcoin/WalletPolicies/AddressIntent.cs new file mode 100644 index 000000000..947530a48 --- /dev/null +++ b/NBitcoin/WalletPolicies/AddressIntent.cs @@ -0,0 +1,10 @@ +#if !NO_RECORDS +namespace NBitcoin.WalletPolicies +{ + public enum AddressIntent + { + Deposit, + Change + } +} +#endif diff --git a/NBitcoin/WalletPolicies/DerivationResult.cs b/NBitcoin/WalletPolicies/DerivationResult.cs new file mode 100644 index 000000000..92df7270e --- /dev/null +++ b/NBitcoin/WalletPolicies/DerivationResult.cs @@ -0,0 +1,40 @@ +#if !NO_RECORDS +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static NBitcoin.WalletPolicies.MiniscriptNode; + +namespace NBitcoin.WalletPolicies; + +public class DerivationResult +{ + internal DerivationResult(Miniscript miniscript, Dictionary derivations) + { + Miniscript = miniscript; + DerivedKeys = derivations; + } + + public Miniscript Miniscript { get; } + /// + /// The derived keys. The values are either or a . + /// + public Dictionary DerivedKeys { get; } +} + +public class Derivation +{ + public Derivation(KeyPath keyPath, Value pubkey) + { + KeyPath = keyPath; + Pubkey = pubkey; + } + public KeyPath KeyPath { get; } + /// + /// The derived key. This is either a or a . + /// + public Value Pubkey { get; } +} +#endif diff --git a/NBitcoin/WalletPolicies/DeriveParameters.cs b/NBitcoin/WalletPolicies/DeriveParameters.cs new file mode 100644 index 000000000..1b542a075 --- /dev/null +++ b/NBitcoin/WalletPolicies/DeriveParameters.cs @@ -0,0 +1,59 @@ +#if !NO_RECORDS +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace NBitcoin.WalletPolicies +{ + public class DeriveParameters + { + /// + /// + /// + /// Whether this is a deposit or change address + /// The address index + public DeriveParameters(AddressIntent intent, int index) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index), "index should be positive"); + Intent = intent; + AddressIndexes = new int[] { index }; + } + /// + /// + /// + /// Whether this is a deposit or change address + /// The addresses to derive + public DeriveParameters(AddressIntent intent, int[]? indexes) + { + Intent = intent; + AddressIndexes = indexes ?? Array.Empty(); + foreach (var idx in AddressIndexes) + if (idx < 0) + throw new ArgumentOutOfRangeException(nameof(indexes), "indexes should be positive"); + } + /// + /// + /// + /// Whether this is a deposit or change address + /// The first address to start generating + /// The number of addresses to generate + /// + public DeriveParameters(AddressIntent intent, int startIndex, int count) + { + if (startIndex < 0) + throw new ArgumentOutOfRangeException(nameof(startIndex), "startIndex should be positive"); + if (startIndex < 0) + throw new ArgumentOutOfRangeException(nameof(count), "count should be positive"); + Intent = intent; + AddressIndexes = Enumerable.Range(startIndex, count).ToArray(); + } + public AddressIntent Intent { get; } + public int[] AddressIndexes { get; set; } + } +} +#endif diff --git a/NBitcoin/WalletPolicies/Miniscript.cs b/NBitcoin/WalletPolicies/Miniscript.cs new file mode 100644 index 000000000..862894245 --- /dev/null +++ b/NBitcoin/WalletPolicies/Miniscript.cs @@ -0,0 +1,932 @@ +#if !NO_RECORDS +#nullable enable +using NBitcoin.DataEncoders; +using NBitcoin.Protocol; +using NBitcoin.Scripting; +using NBitcoin.WalletPolicies.Visitors; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Linq; +using static NBitcoin.WalletPolicies.MiniscriptError; +using static NBitcoin.WalletPolicies.MiniscriptNode; +using static NBitcoin.WalletPolicies.MiniscriptNode.Parameter; +using static NBitcoin.WalletPolicies.MiniscriptNode.Value; + +namespace NBitcoin.WalletPolicies +{ + public enum KeyType + { + Classic, + Taproot + } + + /// + /// How to interprete the script + /// + public enum MiniscriptDialect + { + /// + /// Only allow miniscript policy standard fragments + /// + Strict, + /// + /// Requires BIP388 fragments (tr, pkh, wpkh, sh, wsh) as top level fragments. Also allow the use of checksum. + /// + BIP388 + } + + [Flags] + public enum ParameterTypeFlags + { + None = 0, + /// + /// Allow the use of multipath's key placeholders (eg. @0/**) + /// + KeyPlaceholder = 1, + /// + /// Allow the use of named parameter (eg. pkh(Alice)) + /// + NamedParameter = 2, + /// + /// Allow both key and name placeholders + /// + All = 3 + } + public class MiniscriptParsingSettings + { + /// + /// + /// + /// + /// See for details on how key type ambiguities are resolved. + public MiniscriptParsingSettings(Network network, KeyType? defaultKeyType = null) + { + ArgumentNullException.ThrowIfNull(network); + Network = network; + KeyType = defaultKeyType; + } + + /// + public MiniscriptDialect Dialect { get; set; } + + /// + /// Whether key placeholders or named parameters are allowed. + /// + public ParameterTypeFlags AllowedParameters { get; set; } = ParameterTypeFlags.All; + public Network Network { get; } + /// + /// How to solve ambiguities of key types. For example, with PK(A), is A a taproot key or ecdsa key? + /// Note that if is set to , this parameter is ignored because no ambiguity is possible. + /// The default behavior is to throw an error when parsed if there is an ambiguity. + /// + public KeyType? KeyType { get; set; } + + } + + /// + /// A low level representation of a miniscript script. + /// Use if you just want to parse a descriptor as generated by wallets supporting BIP388. + /// + public class Miniscript + { + public record Scripts(Script ScriptPubKey, Script? RedeemScript); + public record TaprootInfo(TaprootInternalPubKey InternalPubKey, uint256? MerkleRoot); + public MiniscriptNode RootNode { get; } + public IReadOnlyDictionary> Parameters { get; } + + public Network Network { get; } + + internal Miniscript(MiniscriptNode rootNode, Network network, KeyType keyType) + { + if (network is null) + throw new ArgumentNullException(nameof(network)); + if (rootNode is null) + throw new ArgumentNullException(nameof(rootNode)); + Network = network; + RootNode = rootNode; + if (!ParametersVisitor.TryCreateParameters(rootNode, out var error, out var parameters)) + throw new MiniscriptFormatException(error); + Parameters = parameters; + KeyType = keyType; + } + Miniscript( + MiniscriptNode rootNode, + IReadOnlyDictionary> parameters, + Network network, + KeyType keyType + ) + { + RootNode = rootNode; + Parameters = parameters; + Network = network; + KeyType = keyType; + } + public KeyType KeyType { get; } + public override string ToString() => ToString(false); + + public static bool TryParse(string str, Network network, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out Miniscript miniscript) => TryParse(str, new MiniscriptParsingSettings(network), out error, out miniscript); + public static bool TryParse(string str, MiniscriptParsingSettings settings, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out Miniscript miniscript) + { + ArgumentNullException.ThrowIfNull(str); + if (TryParseMiniscript(new ParsingContext(str, settings), out error, out miniscript)) + return true; + miniscript = null; + return false; + } + public static bool TryParse(string str, Network network, [MaybeNullWhen(false)] out Miniscript miniscript) => TryParse(str, new MiniscriptParsingSettings(network), out miniscript); + public static bool TryParse(string str, MiniscriptParsingSettings settings, [MaybeNullWhen(false)] out Miniscript miniscript) + { + return TryParse(str, settings, out _, out miniscript); + } + public static Miniscript Parse(string str, Network network) => Parse(str, new MiniscriptParsingSettings(network)); + public static Miniscript Parse(string str, MiniscriptParsingSettings settings) + { + ArgumentNullException.ThrowIfNull(str); + if (TryParseMiniscript(new ParsingContext(str, settings), out var error, out var miniscript)) + return miniscript; + throw new MiniscriptFormatException(error); + } + + delegate bool Parsing(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node); + + + private static bool TryParseExpressions(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) + { + node = null; + error = null; + if (ctx.CurrentFrame.Parameters.Count == 0) + { + if (!TryParseParameterCount(ctx, out error, out node)) + return false; + if (node is not Value.CountValue count) + return true; + ctx.CurrentFrame.ExpectedParameterCount = count.Count + 1; + return true; + } + else + { + if (ctx.CurrentFrame.ExpectedParameterCount == ctx.CurrentFrame.Parameters.Count + 1) + ctx.CurrentFrame.ExpectedParameterCount = -1; + return TryParseExpression(ctx, out error, out node); + } + } + private static bool TryParsePubKeys(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) + { + node = null; + error = null; + + var expectedKeyType = typeof(T) == typeof(Value.PubKeyValue) ? KeyType.Classic : KeyType.Taproot; + + if (!ctx.TrySetExpectedKeyType(expectedKeyType)) + { + error = new MiniscriptError.UnsupportedFragment(ctx.FragmentIndex, ctx.ExpectedKeyType!.Value); + return false; + } + + if (ctx.CurrentFrame.Parameters.Count == 0) + { + if (!TryParseParameterCount(ctx, out error, out node)) + return false; + if (node is not Value.CountValue count) + return true; + ctx.CurrentFrame.ExpectedParameterCount = count.Count + 1; + return true; + } + else + { + if (ctx.CurrentFrame.ExpectedParameterCount == ctx.CurrentFrame.Parameters.Count + 1) + ctx.CurrentFrame.ExpectedParameterCount = -1; + return TryParseKey(ctx, out error, out node); + } + } + private static bool TryParseParameterCount(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) + { + error = null; + node = null; + var match = Regex.Match(ctx.Remaining, @"^[0-9]+"); + if (uint.TryParse(match.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) && v < 100 && v > 0) + { + ctx.Advance(match.Length); + node = new Value.CountValue((int)v); + return true; + } + error = new MiniscriptError.CountExpected(ctx.Offset); + return false; + } + private static bool TryParse32Bytes(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) => TryParseBytes(ctx, 32, out error, out node); + private static bool TryParse20Bytes(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) => TryParseBytes(ctx, 20, out error, out node); + private static bool TryParseBytes(ParsingContext ctx, int requiredBytes, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) + { + node = null; + error = null; + var match = Regex.Match(ctx.Remaining, @"^[a-f0-9]{17,}"); + if (match.Success && match.Length == requiredBytes * 2) + { + ctx.Advance(match.Length); + node = new Value.HashValue(Encoders.Hex.DecodeData(match.Value)); + return true; + } + + if (TryParseNamedParameter(ctx, new ParameterRequirement.Hash(requiredBytes), out node)) + return true; + + error = new MiniscriptError.HashExpected(ctx.Offset, requiredBytes); + return false; + } + + private static bool TryParseLocktime(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) + { + node = null; + error = null; + var match = Regex.Match(ctx.Remaining, @"^[0-9]+"); + if (uint.TryParse(match.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v)) + { + ctx.Advance(match.Length); + var locktime = new LockTime(v); + node = new Value.LockTimeValue(locktime); + return true; + + } + if (TryParseNamedParameter(ctx, ParameterRequirement.Locktime.Instance, out node)) + return true; + error = new MiniscriptError.LocktimeExpected(ctx.Offset); + return false; + } + + private static bool TryParseNamedParameter(ParsingContext ctx, ParameterRequirement requirement, [MaybeNullWhen(false)] out MiniscriptNode node) + { + static bool IsForbiddenName(string name) => name switch + { + "0" or "1" or "pk_k" or "pk_h" or "pk" or "pkh" or "older" or "after" or "sha256" or "ripemd160" or "hash256" or "hash160" or "andor" or "and_v" or "and_b" or "and_n" or "or_b" or "or_c" or "or_d" or "or_i" or "thresh" or "multi" or "multi_a" or "musig" => true, + "wsh" or "sh" or "tr" => true, + _ => false + }; + node = null; + if (ctx.ParsingSettings.AllowedParameters == ParameterTypeFlags.None) + return false; + var key = Regex.Match(ctx.Remaining, @"^([a-zA-Z0-9_]+)|(@[0-9]{1,4})"); + if (key is { Success: true, Length: <= 16 * 2 } && !IsForbiddenName(key.Value)) + { + if (!key.Value.StartsWith('@') && !ctx.ParsingSettings.AllowedParameters.HasFlag(ParameterTypeFlags.NamedParameter)) + return false; + if (key.Value.StartsWith('@') && !ctx.ParsingSettings.AllowedParameters.HasFlag(ParameterTypeFlags.KeyPlaceholder)) + return false; + ctx.Advance(key.Value.Length); + node = new Parameter(key.Value, requirement); + return true; + } + return false; + } + + internal static bool TryParseKey(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) + { + node = null; + error = null; + ctx.SkipSpaces(); + var key = Regex.Match(ctx.Remaining, @"^[a-f0-9]+"); + if (key is { Success: true, Length: (33 * 2 or (65 * 2) or 32 * 2) }) + { + ctx.Advance(key.Length); + try + { + node = (key.Length, ctx.ExpectedKeyType) switch + { + (33 * 2 or 65 * 2, null or KeyType.Classic) => new Value.PubKeyValue(new PubKey(Encoders.Hex.DecodeData(key.Value))), + (32 * 2, null or KeyType.Taproot) => new Value.TaprootPubKeyValue(new TaprootPubKey(Encoders.Hex.DecodeData(key.Value))), + _ => null + }; + if (node != null) + return true; + } + catch + { + error = new MiniscriptError.KeyExpected(ctx.Offset, ctx.ExpectedKeyType); + return false; + } + } + + if (node is null) + { + TryParseNamedParameter(ctx, new ParameterRequirement.Key(ctx.ExpectedKeyType), out node); + } + + if (node is null && MusigNode.IsMusig(ctx)) + if (!MusigNode.TryParse(ctx, out node, out error)) + return false; + + if (node is Parameter { IsKeyPlaceholder: true } or MusigNode or null) + { + if (node is null) + HDKeyNode.HDKeyNode.TryParse(ctx, out node); + if (node is not null && ctx.Peek('/', out _)) + { + if (node is Parameter p) + node = p with { Requirement = new ParameterRequirement.HDKey() }; + if (!MultipathNode.TryParseMultiPath(ctx, node, out var multiPathNode, out error)) + return false; + node = multiPathNode; + } + } + + if (node is null && !TryParseBase58(ctx, out node)) + { + error = new MiniscriptError.KeyExpected(ctx.Offset, ctx.ExpectedKeyType); + return false; + } + + return true; + } + + private static bool TryParseBase58(ParsingContext ctx, [MaybeNullWhen(false)] out MiniscriptNode node) + { + node = null; + if ((ctx.ExpectedKeyType ?? ctx.DefaultKeyType) is not { } keyType) + return false; + var match = Regex.Match(ctx.Remaining, @"^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{50,}"); + if (match.Success) + { + try + { + var result = ctx.Network.Parse(match.Value); + if (result is IHDKey hdkey) + { + node = new HDKeyValue(hdkey, keyType); + } + if (result is BitcoinSecret secret) + { + node = new Value.PrivateKeyValue(secret); + } + if (node is not null) + { + ctx.Advance(match.Length); + return true; + } + } + catch + { + return false; + } + } + return false; + } + + private static bool TryParseMiniscript(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out Miniscript miniscript) + { + error = null; + miniscript = null; + MiniscriptNode? node = null; + ctx.SkipSpaces(); + + if (ctx.ParsingSettings.Dialect == MiniscriptDialect.BIP388) + { + var match = Regex.Match(ctx.Remaining, "^[a-zA-Z0-9_]+"); + if (!match.Success) + { + error = ctx.IsEnd ? new IncompleteExpression(ctx.Offset) : new UnexpectedToken(ctx.Offset); + return false; + } + var keyType = match.ValueSpan switch + { + "tr" => KeyType.Taproot, + _ => KeyType.Classic + }; + ctx.ExpectedKeyType = keyType; + + var descriptor = match.ValueSpan switch + { + "wpkh" => FragmentDescriptor.wpkh, + "pkh" => FragmentDescriptor.pkh, + "tr" => FragmentDescriptor.tr, + "wsh" => FragmentDescriptor.wsh, + "sh" => FragmentDescriptor.sh, + _ => null + }; + using (var frame = ctx.PushFrame()) + { + frame.FragmentIndex = ctx.Offset; + ctx.Advance(match.Index + match.Length); + + if (!ctx.Peek('(', out error)) + return false; + if (descriptor is null) + { + error = new MiniscriptError.InvalidTopFragment(ctx.Offset); + return false; + } + + if (descriptor == FragmentDescriptor.wpkh || descriptor == FragmentDescriptor.pkh) + { + node = TryParseParameters(ctx, 1, TryParseKey, out error, out var p) ? new FragmentSingleParameter(descriptor, p[0]) : null; + } + else if (ctx.Network.Consensus.SupportTaproot && descriptor == FragmentDescriptor.tr) + { + node = TryParseTaproot(ctx, out error, out var n) ? n : null; + } + else if (ctx.Network.Consensus.SupportSegwit && descriptor == FragmentDescriptor.wsh) + { + node = TryParseParameters(ctx, 1, TryParseExpression, out error, out var p) ? new FragmentSingleParameter(descriptor, p[0]) : null; + } + else if (descriptor == FragmentDescriptor.sh) + { + var prevOffset = ctx.Offset; + ctx.Advance(1); + ctx.SkipSpaces(); + match = Regex.Match(ctx.Remaining, "^((wsh)|(wpkh))"); + if (ctx.Network.Consensus.SupportSegwit && match.Success) + { + using (var frame2 = ctx.PushFrame()) + { + frame2.FragmentIndex = ctx.Offset; + ctx.Advance(match.Index + match.Length); + var wrappedDescriptor = match.ValueSpan switch + { + "wpkh" => FragmentDescriptor.wpkh, + "wsh" => FragmentDescriptor.wsh, + // Can never happen + _ => throw new NotSupportedException() + }; + node = match.ValueSpan switch + { + "wpkh" => TryParseParameters(ctx, 1, TryParseKey, out error, out var p) ? new FragmentSingleParameter(wrappedDescriptor, p[0]) : null, + "wsh" => TryParseParameters(ctx, 1, TryParseExpression, out error, out var p) ? new FragmentSingleParameter(wrappedDescriptor, p[0]) : null, + _ => null + }; + if (node is not null) + { + node = new FragmentSingleParameter(descriptor, node); + ctx.Advance(1); + } + } + } + else + { + ctx.Offset = prevOffset; + node = TryParseParameters(ctx, 1, TryParseExpression, out error, out var p) ? new FragmentSingleParameter(descriptor, p[0]) : null; + } + } + } + } + else + { + TryParseExpression(ctx, out error, out node); + } + + ctx.ExpectedKeyType ??= ctx.DefaultKeyType; + if (node is not null && + ParametersVisitor.TryCreateParameters(node, out error, out var parameters) && + ctx.ExpectedKeyType is not null) + { + ctx.SkipSpaces(); + if (!ctx.IsEnd && ctx.ParsingSettings.Dialect == MiniscriptDialect.BIP388) + { + if (ctx.NextChar == '#') + { + var actualChecksum = ctx.Remaining[1..Math.Min(9, ctx.RemainingChars)]; + var expectedChecksum = OutputDescriptor.GetCheckSum(ctx.Miniscript[0..ctx.Offset]); + if (expectedChecksum != actualChecksum) + { + error = new MiniscriptError.InvalidChecksum(ctx.Offset); + return false; + } + ctx.Advance(9); + ctx.SkipSpaces(); + } + + } + if (!ctx.IsEnd) + { + error = new MiniscriptError.UnexpectedToken(ctx.Offset); + return false; + } + miniscript = new Miniscript(node, parameters, ctx.Network, ctx.ExpectedKeyType.Value); + return true; + } + + if (ctx.ExpectedKeyType is null) + error ??= new AmbiguousKeyType(); + error ??= new IncompleteExpression(ctx.Offset); + miniscript = null; + return false; + } + + private static bool TryParseExpression(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) + { + node = null; + error = null; + + ctx.SkipSpaces(); + + int endOffset = ctx.Offset; + var match = Regex.Match(ctx.Remaining, @"^([a-z0-9_]+?:)?([a-zA-Z0-9_]+)"); + + var wrapperGroup = match.Groups[1]; + var fragmentName = match.Groups[2]; + + if (fragmentName.Success) + { + using var frame = ctx.PushFrame(); + frame.FragmentIndex = ctx.Offset + fragmentName.Index; + ctx.Advance(fragmentName.Index + fragmentName.Length); + node = fragmentName.Value switch + { + "0" => FragmentNoParameter._0, + "1" => FragmentNoParameter._1, + "pk_k" => TryParseParameters(ctx, 1, TryParseKey, out error, out var p) ? FragmentSingleParameter.pk_k(p[0]) : null, + "pk_h" => TryParseParameters(ctx, 1, TryParseKey, out error, out var p) ? FragmentSingleParameter.pk_h(p[0]) : null, + "pk" => TryParseParameters(ctx, 1, TryParseKey, out error, out var p) ? FragmentSingleParameter.pk(p[0]) : null, + "pkh" => TryParseParameters(ctx, 1, TryParseKey, out error, out var p) ? FragmentSingleParameter.pkh(p[0]) : null, + "older" => TryParseParameters(ctx, 1, TryParseLocktime, out error, out var p) ? FragmentSingleParameter.older(p[0]) : null, + "after" => TryParseParameters(ctx, 1, TryParseLocktime, out error, out var p) ? FragmentSingleParameter.after(p[0]) : null, + "sha256" => TryParseParameters(ctx, 1, TryParse32Bytes, out error, out var p) ? FragmentSingleParameter.sha256(p[0]) : null, + "ripemd160" => TryParseParameters(ctx, 1, TryParse20Bytes, out error, out var p) ? FragmentSingleParameter.ripemd160(p[0]) : null, + "hash256" => TryParseParameters(ctx, 1, TryParse32Bytes, out error, out var p) ? FragmentSingleParameter.hash256(p[0]) : null, + "hash160" => TryParseParameters(ctx, 1, TryParse20Bytes, out error, out var p) ? FragmentSingleParameter.hash160(p[0]) : null, + "andor" => TryParseParameters(ctx, 3, TryParseExpression, out error, out var p) ? FragmentThreeParameters.andor(p[0], p[1], p[2]) : null, + "and_v" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.and_v(p[0], p[1]) : null, + "and_b" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.and_b(p[0], p[1]) : null, + "and_n" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.and_n(p[0], p[1]) : null, + "or_b" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.or_b(p[0], p[1]) : null, + "or_c" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.or_c(p[0], p[1]) : null, + "or_d" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.or_d(p[0], p[1]) : null, + "or_i" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.or_i(p[0], p[1]) : null, + "thresh" => TryParseParameters(ctx, 1, TryParseExpressions, out error, out var p) ? FragmentUnboundedParameters.thresh(p) : null, + "sortedmulti" => TryParseParameters(ctx, 1, TryParsePubKeys, out error, out var p) ? FragmentUnboundedParameters.sortedmulti(p) : null, + "multi" => TryParseParameters(ctx, 1, TryParsePubKeys, out error, out var p) ? FragmentUnboundedParameters.multi(p) : null, + "multi_a" => ctx.Network.Consensus.SupportTaproot && TryParseParameters(ctx, 1, TryParsePubKeys, out error, out var p) ? FragmentUnboundedParameters.multi_a(p) : null, + _ => null + }; + if (node is null && error is null) + { + if (ctx.IsEnd || (ctx.NextChar != '(' && fragmentName.Length <= 16)) + { + node = new Parameter(fragmentName.Value, new ParameterRequirement.Fragment()); + } + else + { + error = new UnknownFragmentName(frame.FragmentIndex, fragmentName.Value); + return false; + } + } + } + + if (node is null) + { + error ??= new IncompleteExpression(ctx.Offset); + return false; + } + + if (wrapperGroup.Success) + { + for (var i = wrapperGroup.Value.Length - 2; i >= 0; i--) + { + Wrapper? wrapper = + wrapperGroup.Value[i] switch + { + 'a' => Wrapper.a(node), + 'c' => Wrapper.c(node), + 'd' => Wrapper.d(node), + 'j' => Wrapper.j(node), + 'l' => Wrapper.l(node), + 'n' => Wrapper.n(node), + 's' => Wrapper.s(node), + 't' => Wrapper.t(node), + 'u' => Wrapper.u(node), + 'v' => Wrapper.v(node), + _ => null + }; + if (wrapper is null) + { + error = new MiniscriptError.InvalidWrapper(wrapperGroup.Value[i], ctx.Offset + i); + return false; + } + node = wrapper; + } + } + error = null; + return true; + } + + private static bool TryParseTaproot(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) + { + error = null; + node = null; + if (!ctx.Consume('(', out error)) + return false; + if (!TryParseKey(ctx, out error, out var internalKeyParameter)) + return false; + + if (!ctx.Consume(out var c, out error)) + return false; + + if (c == ')') + { + node = new TaprootNode(internalKeyParameter, null); + return true; + } + else if (c == ',') + { + if (!TryParseTaprootTree(ctx, out error, out var tree)) + return false; + if (!ctx.Consume(')', out error)) + return false; + node = new TaprootNode(internalKeyParameter, tree); + return true; + } + else + { + error = new UnexpectedToken(ctx.Offset); + return false; + } + } + + private static bool TryParseTaprootTree(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) + { + error = null; + node = null; + if (!ctx.Peek(out var c, out error)) + return false; + if (c == '{') + { + ctx.Advance(1); + if (!TryParseTaprootTree(ctx, out error, out var left) || + !ctx.Consume(',', out error) || + !TryParseTaprootTree(ctx, out error, out var right) || + !ctx.Consume('}', out error)) + return false; + node = new TaprootBranchNode(left, right); + return true; + } + else if (TryParseExpression(ctx, out error, out node)) + { + return true; + } + return false; + } + + private static bool TryParseParameters(ParsingContext ctx, int reqParams, Parsing parse, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode[] parameters) + { + var frame = ctx.CurrentFrame; + error = null; + parameters = null; + if (!ctx.Consume('(', out error)) + return false; + + frame.ExpectedParameterCount = reqParams; + while (parse(ctx, out error, out var node)) + { + frame.Parameters.Add(node); + if (frame.Parameters.Count == frame.ExpectedParameterCount) + { + if (!ctx.Consume(')', out error)) + { + if (error is MiniscriptError.UnexpectedToken) + error = new TooManyParameters(ctx.Offset, frame.ExpectedParameterCount); + return false; + } + break; + } + else + { + if (!ctx.Peek(out var c, out error)) + return false; + if (frame.ExpectedParameterCount == -1 && c == ')') + { + ctx.Advance(1); + break; + } + if (c != ',') + { + error = new TooFewParameters(ctx.Offset, frame.ExpectedParameterCount); + return false; + } + ctx.Advance(1); + } + } + if (error is not null) + return false; + parameters = frame.Parameters.ToArray(); + return true; + } + + public Miniscript ReplaceParameters(Dictionary values, bool skipRequirementsCheck = false) + { + if (Parameters.Count is 0) + return this; + var v = new ParameterReplacementVisitor(values); + v.SkipRequirements = skipRequirementsCheck; + return this.Rewrite(v); + } + + /// + /// Generate scriptPubKey and redeemScript from the Miniscript + /// + /// The scriptPubKey and redeemScript + /// Impossible to generate a script while parameters haven't been set + public Scripts ToScripts() + { + if (Parameters.Count > 0) + throw new InvalidOperationException("Impossible to generate a script while parameters haven't been set. Use ToScriptString() if you want a user-readable representation instead."); + var scriptPubKey = RootNode.GetScript(); + Script? redeem = null; + if (GetScriptCodeNode(RootNode) is { } n) + redeem = n.GetScript(); + return new(scriptPubKey, redeem); + } + + /// + /// Generate a user-readable way to view the Bitcoin script generated by the Miniscript. + /// + /// A user-readable Bitcoin script + public string ToScriptString() => new MockScriptVisitor(Network, KeyType).GenerateScript(RootNode); + + /// + /// Generate a user-readable way to view the script actually executed (Can be null of taproot) + /// + /// + public string? ToScriptCodeString() => GetScriptCodeNode(RootNode) is { } n ? new MockScriptVisitor(Network, KeyType).GenerateScript(n) : null; + + private MiniscriptNode? GetScriptCodeNode(MiniscriptNode node) => + node switch + { + Fragment f when f.Descriptor == FragmentDescriptor.wpkh => new FragmentSingleParameter(FragmentDescriptor.pkh, f.Parameters.First()), + Fragment f when f.Descriptor == FragmentDescriptor.wsh => f.Parameters.First(), + Fragment f when f.Descriptor == FragmentDescriptor.sh => GetScriptCodeNode(f.Parameters.First()), + Fragment f when f.Descriptor == FragmentDescriptor.tr => null, + _ => node + }; + + public Miniscript Rewrite(MiniscriptRewriterVisitor rewriterVisitor) + { + var newNode = rewriterVisitor.Visit(RootNode); + return new Miniscript(newNode, Network, KeyType); + } + public void Visit(MiniscriptVisitor visitor) + { + visitor.Visit(RootNode); + } + + /// + /// Returns the taproot internal key and the merkle root if there is any tapscript. + /// + /// Null if the root isn't a taproot node + public TaprootInfo? GetTaprootInfo() + { + if (RootNode is not TaprootNode tn) + return null; + var merkleRoot = TaprootMerkleRootVisitor.GetMerkleRoot(tn); + return new TaprootInfo(tn.GetInternalKey(), merkleRoot); + } + + /// + /// Replace the multi path key expressions in the miniscript by the actual keys. + /// + /// Whether this is a deposit or change address + /// The address index + /// + /// index should be positive + /// The parameter 'keyType' should be set to call this method, as it not possible to guess the keytype to use from the context. + public DerivationResult Derive(AddressIntent intent, int index) + => Derive(new DeriveParameters(intent, index))[0]; + + /// + /// Replace the multi path key expressions in the miniscript by the actual keys. + /// + /// + /// An array of derivation result (one element per index derived) + /// + public DerivationResult[] Derive(DeriveParameters parameters) + { + var visitor = new DeriveVisitor(parameters.Intent, parameters.AddressIndexes, KeyType); + return visitor.Derive(RootNode, Network); + } + + /// + /// Make the miniscript more readable by removing HDKeys (such as [fingerprint]xpub/<0;1>/*) by key placeholders (such as @0/<0;1>/*). It is the reverse operation of + /// + /// The modified + public Miniscript ReplaceHDKeysByKeyPlaceholders(out HDKeyNode[] keys) + { + var visitor = new ExtractTemplateVisitor(); + var newTree = Rewrite(visitor); + keys = visitor.HDKeys.ToArray(); + return newTree; + } + /// + /// Will replace the key placeholders by the actual keys. It is the inverse operation of + /// + /// + /// The modified + public Miniscript ReplaceKeyPlaceholdersByHDKeys(HDKeyNode[] keys) + { + var visitor = new FillTemplateVisitor(keys); + return Rewrite(visitor); + } + + public string ToString(bool checksum) + { + var str = RootNode.ToString(); + if (checksum) + str = OutputDescriptor.AddChecksum(str); + return str; + } + } + public record MiniscriptError + { + public record InvalidChecksum(int Index) : MiniscriptError + { + public override string ToString() => $"Invalid checksum at index {Index}."; + } + public record InvalidTopFragment(int Index) : MiniscriptError + { + public override string ToString() => $"Invalid BIP0388 top fragment at index {Index}. It should be wsh, sh, wpkh, pkh or tr"; + } + public record CountExpected(int Index) : MiniscriptError + { + public override string ToString() => $"Count expected at index {Index}"; + } + public record MixedParameterType(string parameterName) : MiniscriptError + { + public override string ToString() => $"Mixed parameter type '{parameterName}' (The parameter may be reused)"; + } + public record MixedNetworks : MiniscriptError + { + public override string ToString() => "Mixed Networks detected in the expression"; + } + public record MixedKeyTypes : MiniscriptError + { + public override string ToString() => "Mixed KeyTypes detected in the expression"; + } + public record AmbiguousKeyType : MiniscriptError + { + public override string ToString() => $"Ambiguous key type"; + } + public record IncompleteExpression(int Index) : MiniscriptError + { + public override string ToString() => $"Incomplete expression at index {Index}"; + } + + public record ExpectedInteger(int Index) : UnexpectedToken(Index) + { + public override string ToString() => base.ToString() + " (Expected a positive integer)"; + } + public record UnexpectedToken(int Index) : MiniscriptError + { + public override string ToString() => $"Unexpected token at index {Index}"; + } + public record HashExpected(int Index, int RequiredBytes) : MiniscriptError + { + public override string ToString() => $"Hash of length {RequiredBytes} is expected at index {Index}"; + } + public record LocktimeExpected(int Index) : MiniscriptError + { + public override string ToString() => $"Locktime expected at index {Index}"; + } + public record UnsupportedFragment(int Index, KeyType KeyType) : MiniscriptError + { + public override string ToString() => $"Only fragments using KeyType {KeyType} are supported in this context"; + } + public record KeyExpected(int Index, KeyType? Type) : MiniscriptError + { + public override string ToString() => + Type switch + { + KeyType.Taproot => $"Taproot PubKey expected at index {Index} (32 bytes)", + KeyType.Classic => $"PubKey expected at index {Index} (33 bytes)", + _ => $"Key expected at index {Index} (33 or 32 bytes)" + }; + } + public record UnknownFragmentName(int Index, string FragmentName) : MiniscriptError + { + public override string ToString() => $"Unknown fragment name '{FragmentName}' at index {Index}"; + } + public record TooManyParameters(int Index, int Expected) : MiniscriptError + { + public override string ToString() => $"Too many parameters at index {Index}, expected {Expected}"; + } + public record TooFewParameters(int Index, int Expected) : MiniscriptError + { + public override string ToString() => $"Too few parameters at index {Index}, expected {Expected}"; + } + public record InvalidWrapper(char wrapper, int Index) : MiniscriptError + { + public override string ToString() => $"Invalid wrapper '{wrapper}' at index {Index}"; + } + } + public class MiniscriptFormatException : FormatException + { + public MiniscriptFormatException(MiniscriptError error) : base(error.ToString()) + { + Error = error; + } + public MiniscriptError Error { get; } + } +} +#endif diff --git a/NBitcoin/WalletPolicies/MiniscriptNode.cs b/NBitcoin/WalletPolicies/MiniscriptNode.cs new file mode 100644 index 000000000..122d3a58b --- /dev/null +++ b/NBitcoin/WalletPolicies/MiniscriptNode.cs @@ -0,0 +1,953 @@ +#if !NO_RECORDS +#nullable enable +using NBitcoin.Crypto; +using NBitcoin.DataEncoders; +using NBitcoin.Secp256k1; +using NBitcoin.Secp256k1.Musig; +using NBitcoin.WalletPolicies.Visitors; +using System; +using System.Collections.Generic; +using System.ComponentModel.Design; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using static NBitcoin.WalletPolicies.MiniscriptError; +using static NBitcoin.WalletPolicies.MiniscriptNode.ParameterRequirement; +using static NBitcoin.WalletPolicies.MiniscriptNode.Value; + +namespace NBitcoin.WalletPolicies +{ + public class FragmentDescriptor + { + public bool IsOr() => + this == or_b || + this == or_c || + this == or_d || + this == or_i; + public bool IsAnd() => + this == and_v || + this == and_b || + this == and_n; + public bool IsHash() => + this == sha256 || + this == ripemd160 || + this == hash256 || + this == hash160; + private static Op HASH160(List node) => Op.GetPushOp(Hashes.Hash160(node[0].PushData).ToBytes()); + FragmentDescriptor(string name, + Action[], List> addOps) + { + Name = name; + AddOps = addOps; + } + public readonly static FragmentDescriptor _0 = new( + "0", + (v, ops) => ops.Add(OpcodeType.OP_0)); + public readonly static FragmentDescriptor _1 = new( + "1", + (v, ops) => ops.Add(OpcodeType.OP_1)); + + + public readonly static FragmentDescriptor pk_k = new( + "pk_k", + (v, ops) => ops.AddRange(v[0])); + public readonly static FragmentDescriptor pk_h = new( + "pk_h", + (v, ops) => ops.AddRange(new Op[] { OpcodeType.OP_DUP, OpcodeType.OP_HASH160, HASH160(v[0]), OpcodeType.OP_EQUALVERIFY })); + public readonly static FragmentDescriptor older = new( + "older", + (v, ops) => ops.AddRange(new Op[] { v[0][0], OpcodeType.OP_CHECKSEQUENCEVERIFY })); + public readonly static FragmentDescriptor after = new( + "after", + (v, ops) => ops.AddRange(new Op[] { v[0][0], OpcodeType.OP_CHECKLOCKTIMEVERIFY })); + public readonly static FragmentDescriptor sha256 = new( + "sha256", + (v, ops) => ops.AddRange(new Op[] { OpcodeType.OP_SIZE, Op.GetPushOp(0x20), OpcodeType.OP_EQUALVERIFY, OpcodeType.OP_SHA256, v[0][0], OpcodeType.OP_EQUAL })); + public readonly static FragmentDescriptor ripemd160 = new( + "ripemd160", + (v, ops) => ops.AddRange(new Op[] { OpcodeType.OP_SIZE, Op.GetPushOp(0x20), OpcodeType.OP_EQUALVERIFY, OpcodeType.OP_RIPEMD160, v[0][0], OpcodeType.OP_EQUAL })); + public readonly static FragmentDescriptor hash256 = new( + "hash256", + (v, ops) => ops.AddRange(new Op[] { OpcodeType.OP_SIZE, Op.GetPushOp(0x20), OpcodeType.OP_EQUALVERIFY, OpcodeType.OP_HASH256, v[0][0], OpcodeType.OP_EQUAL })); + public readonly static FragmentDescriptor hash160 = new( + "hash160", + (v, ops) => ops.AddRange(new Op[] { OpcodeType.OP_SIZE, Op.GetPushOp(0x20), OpcodeType.OP_EQUALVERIFY, OpcodeType.OP_HASH160, v[0][0], OpcodeType.OP_EQUAL })); + public readonly static FragmentDescriptor and_v = new( + "and_v", + (v, ops) => + { + ops.AddRange(v[0]); + ops.AddRange(v[1]); + }); + + public readonly static FragmentDescriptor and_b = new( + "and_b", + (v, ops) => + { + ops.AddRange(v[0]); + ops.AddRange(v[1]); + ops.Add(OpcodeType.OP_BOOLAND); + }); + public readonly static FragmentDescriptor and_n = new( + "and_n", + (v, ops) => + { + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_NOTIF); + ops.Add(OpcodeType.OP_0); + ops.Add(OpcodeType.OP_ELSE); + ops.AddRange(v[1]); + ops.Add(OpcodeType.OP_ENDIF); + }); + public readonly static FragmentDescriptor or_b = new( + "or_b", + (v, ops) => + { + ops.AddRange(v[0]); + ops.AddRange(v[1]); + ops.Add(OpcodeType.OP_BOOLOR); + }); + public readonly static FragmentDescriptor or_d = new( + "or_d", + (v, ops) => + { + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_IFDUP); + ops.Add(OpcodeType.OP_NOTIF); + ops.AddRange(v[1]); + ops.Add(OpcodeType.OP_ENDIF); + }); + public readonly static FragmentDescriptor or_c = new( + "or_c", + (v, ops) => + { + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_NOTIF); + ops.AddRange(v[1]); + ops.Add(OpcodeType.OP_ENDIF); + }); + public readonly static FragmentDescriptor or_i = new( + "or_i", + (v, ops) => + { + ops.Add(OpcodeType.OP_IF); + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_ELSE); + ops.AddRange(v[1]); + ops.Add(OpcodeType.OP_ENDIF); + }); + public readonly static FragmentDescriptor andor = new( + "andor", + (v, ops) => + { + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_NOTIF); + ops.AddRange(v[2]); + ops.Add(OpcodeType.OP_ELSE); + ops.AddRange(v[1]); + ops.Add(OpcodeType.OP_ENDIF); + }); + public readonly static FragmentDescriptor thresh = new( + "thresh", + (v, ops) => + { + int i = 0; + i++; // Skip count + ops.AddRange(v[i++]); + while (i < v.Length) + { + ops.AddRange(v[i++]); + ops.Add(OpcodeType.OP_ADD); + } + ops.Add(v[0][0]); + ops.Add(OpcodeType.OP_EQUAL); + }); + public readonly static FragmentDescriptor multi = new( + "multi", + (v, ops) => + { + int i = 0; + while (i < v.Length) + { + ops.Add(v[i++][0]); + } + ops.Add(Op.GetPushOp(v.Length - 1)); + ops.Add(OpcodeType.OP_CHECKMULTISIG); + }); + public readonly static FragmentDescriptor sortedmulti = new( + "sortedmulti", + (v, ops) => + { + var pks = new byte[v.Length - 1][]; + for (int i = 1; i < v.Length; i++) + { + pks[i-1] = v[i][0].PushData; + } + Array.Sort(pks, BytesComparer.Instance); + ops.Add(v[0][0]); + for (int i = 1; i < v.Length; i++) + { + ops.Add(Op.GetPushOp(pks[i-1])); + } + ops.Add(Op.GetPushOp(v.Length - 1)); + ops.Add(OpcodeType.OP_CHECKMULTISIG); + }); + public readonly static FragmentDescriptor multi_a = new( + "multi_a", + (v, ops) => + { + int i = 0; + i++; // Skip count + ops.Add(v[i++][0]); + ops.Add(OpcodeType.OP_CHECKSIG); + while (i < v.Length) + { + ops.Add(v[i++][0]); + ops.Add(OpcodeType.OP_CHECKSIGADD); + } + ops.Add(v[0][0]); + ops.Add(OpcodeType.OP_NUMEQUAL); + }); + + public readonly static FragmentDescriptor a = new( + "a", + (v, ops) => + { + ops.Add(OpcodeType.OP_TOALTSTACK); + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_FROMALTSTACK); + }); + public readonly static FragmentDescriptor s = new( + "s", + (v, ops) => + { + ops.Add(OpcodeType.OP_SWAP); + ops.AddRange(v[0]); + }); + public readonly static FragmentDescriptor c = new( + "c", + (v, ops) => + { + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_CHECKSIG); + }); + public readonly static FragmentDescriptor t = new( + "t", + (v, ops) => + { + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_1); + }); + public readonly static FragmentDescriptor d = new( + "d", + (v, ops) => + { + ops.Add(OpcodeType.OP_DUP); + ops.Add(OpcodeType.OP_IF); + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_ENDIF); + }); + public readonly static FragmentDescriptor v = new( + "v", + (v, ops) => + { + ops.AddRange(v[0]); + var lastOp = ops[^1]; + var verify = lastOp.Code switch + { + OpcodeType.OP_NUMEQUAL => OpcodeType.OP_NUMEQUALVERIFY, + OpcodeType.OP_EQUAL => OpcodeType.OP_EQUALVERIFY, + OpcodeType.OP_CHECKSIG => OpcodeType.OP_CHECKSIGVERIFY, + OpcodeType.OP_CHECKMULTISIG => OpcodeType.OP_CHECKMULTISIGVERIFY, + _ => OpcodeType.OP_VERIFY + }; + if (verify == OpcodeType.OP_VERIFY) + ops.Add(OpcodeType.OP_VERIFY); + else + ops[^1] = verify; + }); + public readonly static FragmentDescriptor j = new( + "j", + (v, ops) => + { + ops.Add(OpcodeType.OP_SIZE); + ops.Add(OpcodeType.OP_0NOTEQUAL); + ops.Add(OpcodeType.OP_IF); + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_ENDIF); + }); + public readonly static FragmentDescriptor n = new( + "n", + (v, ops) => + { + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_0NOTEQUAL); + }); + public readonly static FragmentDescriptor l = new( + "l", + (v, ops) => + { + ops.Add(OpcodeType.OP_IF); + ops.Add(OpcodeType.OP_0); + ops.Add(OpcodeType.OP_ELSE); + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_ENDIF); + }); + public readonly static FragmentDescriptor u = new( + "u", + (v, ops) => + { + ops.Add(OpcodeType.OP_IF); + ops.AddRange(v[0]); + ops.Add(OpcodeType.OP_ELSE); + ops.Add(OpcodeType.OP_0); + ops.Add(OpcodeType.OP_ENDIF); + }); + + public readonly static FragmentDescriptor pkh = new( + "pkh", + (v, ops) => ops.AddRange(new Op[] { OpcodeType.OP_DUP, OpcodeType.OP_HASH160, HASH160(v[0]), OpcodeType.OP_EQUALVERIFY, OpcodeType.OP_CHECKSIG })); + public readonly static FragmentDescriptor wpkh = new( + "wpkh", + (v, ops) => ops.AddRange(new Op[] { OpcodeType.OP_0, HASH160(v[0]) })); + public readonly static FragmentDescriptor sh = new( + "sh", + (v, ops) => + { + var hash = Hashes.Hash160RawBytes(new Script(v[0]).ToBytes()); + ops.Add(OpcodeType.OP_HASH160); + ops.Add(Op.GetPushOp(hash)); + ops.Add(OpcodeType.OP_EQUAL); + }); + public readonly static FragmentDescriptor wsh = new( + "wsh", + (v, ops) => + { + var hash = Hashes.SHA256(new Script(v[0]).ToBytes()); + ops.Add(OpcodeType.OP_0); + ops.Add(Op.GetPushOp(hash)); + }); + + public readonly static FragmentDescriptor tr = new( + "tr", + (v, ops) => + { + var internalKey = new TaprootInternalPubKey(v[0][0].PushData); + uint256? merkleTree = v.Length > 1 ? new uint256(v[1][0].PushData) : null; + ops.AddRange(new Op[] { OpcodeType.OP_1, Op.GetPushOp(TaprootFullPubKey.Create(internalKey, merkleTree).OutputKey.ToBytes()) }); + }); + + public readonly static FragmentDescriptor pk = new( + "pk", + (v, ops) => ops.AddRange(new[] { v[0][0], (Op)OpcodeType.OP_CHECKSIG })); + + public readonly static FragmentDescriptor musig = new( + "musig", + (v, ops) => + { + var pubkeys = v.Select(i => ECPubKey.Create(i[0].PushData)).ToArray(); + var agg = ECPubKey.MusigAggregate(pubkeys, true).ToXOnlyPubKey(); + ops.Add(Op.GetPushOp(agg.ToBytes())); + }); + /// + /// Nested musig. It put a 33 bytes public key rather than x-only on the stack + /// + public readonly static FragmentDescriptor musig33 = new( + "musig", + (v, ops) => + { + var pubkeys = v.Select(i => ECPubKey.Create(i[0].PushData)).ToArray(); + var agg = ECPubKey.MusigAggregate(pubkeys, true); + ops.Add(Op.GetPushOp(agg.ToBytes())); + }); + + public string Name { get; } + public Action[], List> AddOps { get; } + public override string ToString() => Name; + } + public record MiniscriptNode + { + public Script GetScript() + { + var visitor = new ScriptVisitor(); + visitor.Visit(this); + return visitor.ops switch + { + { Count: 0 } => Script.Empty, + { Count: 1 } => new Script(visitor.ops.Pop()), + _ => throw new InvalidOperationException("Failure to generate script") + }; + } + public record TaprootBranchNode(MiniscriptNode Left, MiniscriptNode Right) : MiniscriptNode + { + protected override void ToString(StringBuilder builder) + { + builder.Append('{'); + builder.Append(Left.ToString()); + builder.Append(','); + builder.Append(Right.ToString()); + builder.Append('}'); + } + } + /// + /// A BIP388 top level tr + /// + /// + /// Can either be a or a + public record TaprootNode(MiniscriptNode InternalKeyNode, MiniscriptNode? ScriptTreeRootNode) : Fragment(FragmentDescriptor.tr) + { + public override IEnumerable Parameters + { + get + { + yield return InternalKeyNode; + if (ScriptTreeRootNode is { }) + yield return ScriptTreeRootNode; + } + } + + public TaprootInternalPubKey GetInternalKey() + => new TaprootInternalPubKey(InternalKeyNode.GetScript().ToOps().First().PushData); + } + public static Value.PubKeyValue Create(PubKey pubkey) => new Value.PubKeyValue(pubkey); + public static Value.PrivateKeyValue Create(BitcoinSecret key) => new Value.PrivateKeyValue(key); + public static Value.TaprootPubKeyValue Create(TaprootPubKey pubkey) => new Value.TaprootPubKeyValue(pubkey); + + /// + /// A musig node in the form of "musig(A,B,C)". See BIP0390. + /// + public record MusigNode : FragmentUnboundedParameters + { + internal static bool IsMusig(ParsingContext ctx) + { + using var memento = ctx.StartMemento(false); + if (ctx.ExpectedKeyType is not (null or KeyType.Taproot) && !ctx.NetstedMusig) + return false; + return ctx.Peek("musig", out _); + } + internal static bool TryParse(ParsingContext ctx, [MaybeNullWhen(false)] out MiniscriptNode node, [MaybeNullWhen(true)] out MiniscriptError error) + { + node = null; + error = null; + if (ctx.ExpectedKeyType is not (null or KeyType.Taproot) && !ctx.NetstedMusig) + { + error = new MixedKeyTypes(); + return false; + } + + // musig(KEY, KEY, ..., KEY) + using var frame = ctx.PushFrame(); + frame.FragmentIndex = ctx.Offset; + var initialKeyType = ctx.ExpectedKeyType; + var wasNested = ctx.NetstedMusig; + try + { + ctx.NetstedMusig = true; + using (var memento = ctx.StartMemento(false)) + { + if (!ctx.Consume("musig", out error)) + return false; + ctx.ExpectedKeyType = KeyType.Classic; + if (!ctx.Consume('(', out error)) + return false; + next: + if (!Miniscript.TryParseKey(ctx, out error, out var p)) + return false; + frame.Parameters.Add(p); + if (!ctx.Consume(out var c, out error)) + return false; + if (c == ',') + goto next; + if (c != ')') + { + error = new UnexpectedToken(ctx.Offset); + return false; + } + memento.Commit(); + } + } + finally + { + ctx.ExpectedKeyType = initialKeyType ?? KeyType.Taproot; + ctx.NetstedMusig = wasNested; + } + node = new MusigNode(wasNested, frame.Parameters.ToArray()); + return true; + } + + public MusigNode(bool isNested, MiniscriptNode[] parameters) : base(isNested ? FragmentDescriptor.musig33 : FragmentDescriptor.musig, parameters) + { + IsNested = isNested; + } + public bool IsNested { get; init; } + public PubKey GetAggregatePubKey() + { + var pks = this.Parameters + .Select(p => p.GetScript()) + .Select(s => ECPubKey.Create(s.ToOps().First().PushData)) + .ToArray(); + return new PubKey(ECPubKey.MusigAggregate(pks, true), true); + } + } + + /// + /// A multipath expression (BIP0388). Target can either be: + /// + /// A key expression (BIP0380) + /// A key placeholders (BIP0388) + /// A musig exression (BIP0390) + /// + /// It only supports X/**, or X/<depositIndex;changeIndex>/* where X is a key expression or a parameter. + /// + /// + /// + /// + /// If <0;1>/* path should be be noted /** instead + public record MultipathNode(int DepositIndex, int ChangeIndex, MiniscriptNode Target, bool ShortForm) : MiniscriptNode + { + internal static MultipathNode Parse(string str, Network network) + { + if (TryParse(str, network, out var node)) + return node; + throw new FormatException("Invalid MultipathNode"); + } + internal static bool TryParse(string str, Network network, [MaybeNullWhen(false)] out MultipathNode node) + { + ArgumentNullException.ThrowIfNull(str); + ArgumentNullException.ThrowIfNull(network); + node = null; + var ctx = new ParsingContext(str, new(network)); + if (Miniscript.TryParseKey(ctx, out _, out var res) && + res is MultipathNode n && + ctx.IsEnd) + { + node = n; + return true; + } + return false; + } + + internal static bool TryParseMultiPath(ParsingContext ctx, MiniscriptNode target, [MaybeNullWhen(false)] out MiniscriptNode node, [MaybeNullWhen(true)] out MiniscriptError error) + { + using var tx = ctx.StartMemento(false); + error = null; + node = null; + int depositIndex, changeIndex; + var pathMatch = Regex.Match(ctx.Remaining, "^((/<(\\d+);(\\d+)>/\\*)|(/\\*\\*))"); + if (!pathMatch.Success) + { + error = new UnexpectedToken(ctx.Offset); + return false; + } + bool shortForm = pathMatch.Value == "/**"; + if (shortForm) + { + depositIndex = 0; + changeIndex = 1; + } + else + { + if (!int.TryParse(pathMatch.Groups[3].Value, out depositIndex) || depositIndex < 0) + { + error = new ExpectedInteger(ctx.Offset); + return false; + } + if (!int.TryParse(pathMatch.Groups[4].Value, out changeIndex) || changeIndex < 0) + { + error = new ExpectedInteger(ctx.Offset); + return false; + } + } + ctx.Advance(pathMatch.Length); + node = new MultipathNode(depositIndex, changeIndex, target, shortForm); + tx.Commit(); + return true; + } + + public bool CanDerive(AddressIntent intent) + { + if (Target is not (HDKeyNode or MusigNode)) + return false; + var idx = GetTypeIndex(intent); + return (idx >> 31) == 0; // Make sure it isn't hardened derivation + } + public int GetTypeIndex(AddressIntent intent) => intent switch { AddressIntent.Deposit => DepositIndex, _ => ChangeIndex }; + protected override void ToString(StringBuilder builder) + { + if (CanUseShortForm && ShortForm) + { + builder.Append($"{Target}/**"); + } + else + { + builder.Append($"{Target}/<{DepositIndex};{ChangeIndex}>/*"); + } + } + + public bool CanUseShortForm => (DepositIndex, ChangeIndex) == (0, 1); + } + + /// + /// + /// + /// The (ie. d4ab66f1/48'/1'/0'/2') + /// The or + public record HDKeyNode(RootedKeyPath RootedKeyPath, IHDKey Key) : MiniscriptNode + { + public static HDKeyNode Parse(string str, Network network) + { + if (TryParse(str, network, out var n)) + return n; + throw new FormatException("Invalid HDKeyNode"); + } + public static bool TryParse(string str, Network network, [MaybeNullWhen(false)] out HDKeyNode hdKeyNode) + { + ArgumentNullException.ThrowIfNull(str); + ArgumentNullException.ThrowIfNull(network); + hdKeyNode = null; + ParsingContext ctx = new ParsingContext(str, new(network)); + if (!TryParse(ctx, out var n) || !ctx.IsEnd) + return false; + hdKeyNode = (HDKeyNode)n; + return true; + } + + internal static bool TryParse(ParsingContext ctx, [MaybeNullWhen(false)] out MiniscriptNode node) + { + node = null; + var match = Regex.Match(ctx.Remaining, @"^\[([a-f0-9'h/]+)\]([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{100,})"); + if (!match.Success) + return false; + if (!RootedKeyPath.TryParse(match.Groups[1].Value, out RootedKeyPath rootedKeyPath)) + return false; + IHDKey? key; + try + { + key = ctx.Network.Parse(match.Groups[2].Value) as IHDKey; + } + catch + { + return false; + } + if (key is null) + return false; + node = new HDKeyNode(rootedKeyPath, key); + ctx.Advance(match.Length); + return true; + } + + protected override void ToString(StringBuilder builder) + { + builder.Append($"[{RootedKeyPath}]{Key}"); + } + } + public abstract record Fragment(FragmentDescriptor Descriptor) : MiniscriptNode + { + public abstract IEnumerable Parameters { get; } + + protected override void ToString(StringBuilder builder) + { + builder.Append(Descriptor.Name); + builder.Append("("); + bool first = true; + foreach (var parameter in Parameters) + { + if (first) + first = false; + else + builder.Append(","); + parameter.ToString(builder); + } + builder.Append(")"); + } + + public static FragmentThreeParameters Create(FragmentDescriptor descriptor, MiniscriptNode x, MiniscriptNode y, MiniscriptNode z) => new FragmentThreeParameters(descriptor, x, y, z); + public static FragmentSingleParameter Create(FragmentDescriptor descriptor, MiniscriptNode x) => new FragmentSingleParameter(descriptor, x); + public static FragmentNoParameter Zero() => new FragmentNoParameter(FragmentDescriptor._0); + public static FragmentNoParameter One() => new FragmentNoParameter(FragmentDescriptor._1); + } + protected virtual void ToString(StringBuilder builder) { } + public sealed override string ToString() + { + var sb = new StringBuilder(); + ToString(sb); + return sb.ToString(); + } + + public record FragmentNoParameter(FragmentDescriptor Descriptor) : Fragment(Descriptor) + { + public readonly static FragmentNoParameter _0 = new FragmentNoParameter(FragmentDescriptor._0); + public readonly static FragmentNoParameter _1 = new FragmentNoParameter(FragmentDescriptor._1); + public override IEnumerable Parameters => Array.Empty(); + protected override void ToString(StringBuilder builder) + { + builder.Append(Descriptor.Name); + } + } + + public record FragmentUnboundedParameters : Fragment + { + public static FragmentUnboundedParameters thresh(MiniscriptNode[] parameters) => new FragmentUnboundedParameters(FragmentDescriptor.thresh, parameters); + public static FragmentUnboundedParameters multi(MiniscriptNode[] parameters) => new FragmentUnboundedParameters(FragmentDescriptor.multi, parameters); + public static FragmentUnboundedParameters sortedmulti(MiniscriptNode[] parameters) => new FragmentUnboundedParameters(FragmentDescriptor.sortedmulti, parameters); + public static FragmentUnboundedParameters multi_a(MiniscriptNode[] parameters) => new FragmentUnboundedParameters(FragmentDescriptor.multi_a, parameters); + + + private readonly MiniscriptNode[] parameters; + + public FragmentUnboundedParameters(FragmentDescriptor descriptor, MiniscriptNode[] parameters) : base(descriptor) + { + this.parameters = parameters; + } + public override IEnumerable Parameters => parameters; + } + public record FragmentThreeParameters(FragmentDescriptor Descriptor, MiniscriptNode X, MiniscriptNode Y, MiniscriptNode Z) : Fragment(Descriptor) + { + public static FragmentThreeParameters andor(MiniscriptNode X, MiniscriptNode Y, MiniscriptNode Z) => new FragmentThreeParameters(FragmentDescriptor.andor, X, Y, Z); + public override IEnumerable Parameters + { + get + { + yield return X; + yield return Y; + yield return Z; + } + } + } + public record FragmentTwoParameters(FragmentDescriptor Descriptor, MiniscriptNode X, MiniscriptNode Y) : Fragment(Descriptor) + { + public static FragmentTwoParameters and_v(MiniscriptNode X, MiniscriptNode Y) => new FragmentTwoParameters(FragmentDescriptor.and_v, X, Y); + public static FragmentTwoParameters and_b(MiniscriptNode X, MiniscriptNode Y) => new FragmentTwoParameters(FragmentDescriptor.and_b, X, Y); + public static FragmentTwoParameters and_n(MiniscriptNode X, MiniscriptNode Y) => new FragmentTwoParameters(FragmentDescriptor.and_n, X, Y); + public static FragmentTwoParameters or_b(MiniscriptNode X, MiniscriptNode Y) => new FragmentTwoParameters(FragmentDescriptor.or_b, X, Y); + public static FragmentTwoParameters or_c(MiniscriptNode X, MiniscriptNode Y) => new FragmentTwoParameters(FragmentDescriptor.or_c, X, Y); + public static FragmentTwoParameters or_d(MiniscriptNode X, MiniscriptNode Y) => new FragmentTwoParameters(FragmentDescriptor.or_d, X, Y); + public static FragmentTwoParameters or_i(MiniscriptNode X, MiniscriptNode Y) => new FragmentTwoParameters(FragmentDescriptor.or_i, X, Y); + public override IEnumerable Parameters + { + get + { + yield return X; + yield return Y; + } + } + } + + public record FragmentSingleParameter(FragmentDescriptor Descriptor, MiniscriptNode X) : Fragment(Descriptor) + { + public static FragmentSingleParameter pk(MiniscriptNode X) => new FragmentSingleParameter(FragmentDescriptor.pk, X); + public static FragmentSingleParameter pkh(MiniscriptNode X) => new FragmentSingleParameter(FragmentDescriptor.pkh, X); + public static FragmentSingleParameter wpkh(MiniscriptNode X) => new FragmentSingleParameter(FragmentDescriptor.wpkh, X); + public static FragmentSingleParameter tr(MiniscriptNode X) => new FragmentSingleParameter(FragmentDescriptor.tr, X); + public static FragmentSingleParameter pk_k(MiniscriptNode X) => new FragmentSingleParameter(FragmentDescriptor.pk_k, X); + public static FragmentSingleParameter pk_h(MiniscriptNode X) => new FragmentSingleParameter(FragmentDescriptor.pk_h, X); + public static FragmentSingleParameter older(MiniscriptNode X) => new FragmentSingleParameter(FragmentDescriptor.older, X); + public static FragmentSingleParameter after(MiniscriptNode X) => new FragmentSingleParameter(FragmentDescriptor.after, X); + public static FragmentSingleParameter sha256(MiniscriptNode X) => new FragmentSingleParameter(FragmentDescriptor.sha256, X); + public static FragmentSingleParameter ripemd160(MiniscriptNode X) => new FragmentSingleParameter(FragmentDescriptor.ripemd160, X); + public static FragmentSingleParameter hash256(MiniscriptNode X) => new FragmentSingleParameter(FragmentDescriptor.hash256, X); + public static FragmentSingleParameter hash160(MiniscriptNode X) => new FragmentSingleParameter(FragmentDescriptor.hash160, X); + public override IEnumerable Parameters + { + get + { + yield return X; + } + } + } + + + public abstract record ParameterRequirement + { + public abstract bool Check(MiniscriptNode node); + public record Fragment : ParameterRequirement + { + public override bool Check(MiniscriptNode node) + { + return node is MiniscriptNode.Fragment; + } + public sealed override string ToString() + { + return "A Fragment is expected"; + } + } + public record HDKey : ParameterRequirement + { + public override bool Check(MiniscriptNode node) + { + return node is MiniscriptNode.HDKeyNode; + } + public sealed override string ToString() + { + return "A HDKeyNode is expected"; + } + } + public record Key(KeyType? RequiredType) : ParameterRequirement + { + public override bool Check(MiniscriptNode node) + { + return (node, RequiredType) switch + { + (PubKeyValue, KeyType.Classic) => true, + (TaprootPubKeyValue, KeyType.Taproot) => true, + (TaprootPubKeyValue or PubKeyValue, null) => true, + (HDKeyValue, _) => true, + (MultipathNode, _) => true, + _ => false + }; + } + + public sealed override string ToString() + { + return RequiredType switch + { + KeyType.Classic => "A PubKeyValue (33 bytes) or a MultiPathKeyInformationNode is expected", + KeyType.Taproot => "A TaprootPubKeyValue (32 bytes) or a MultiPathKeyInformationNode is expected", + _ => "A PubKeyValue (33 bytes), TaprootPubKeyValue (32 bytes) or a MultiPathKeyInformationNode is expected" + }; + } + } + public record Hash(int RequiredBytes) : ParameterRequirement + { + public override bool Check(MiniscriptNode node) + { + return node is Value.HashValue h && h.Hash.Length == RequiredBytes; + } + public sealed override string ToString() + { + return $"A BytesValue of {RequiredBytes} bytes is expected"; + } + } + public record Locktime : ParameterRequirement + { + public readonly static Locktime Instance = new(); + public override bool Check(MiniscriptNode node) + { + return node is Value.LockTimeValue; + } + public sealed override string ToString() + { + return "A LockTimeValue is expected"; + } + } + } + public record Parameter(string Name, ParameterRequirement Requirement) : MiniscriptNode + { + /// + /// True if in the form of @1, @2, @3, etc. + /// + public bool IsKeyPlaceholder => Name.StartsWith("@", StringComparison.Ordinal); + protected override void ToString(StringBuilder builder) + { + builder.Append(Name); + } + } + + public abstract record Value : MiniscriptNode + { + public abstract Op CreatePushOp(); + public record CountValue(int Count) : Value + { + public override Op CreatePushOp() => Op.GetPushOp(Count); + protected override void ToString(StringBuilder builder) + { + builder.Append(Count.ToString(CultureInfo.InvariantCulture)); + } + } + public record LockTimeValue(NBitcoin.LockTime LockTime) : Value + { + public override Op CreatePushOp() => Op.GetPushOp(LockTime.Value); + protected override void ToString(StringBuilder builder) + { + builder.Append(LockTime.Value.ToString(CultureInfo.InvariantCulture)); + } + } + public record HashValue(byte[] Hash) : Value + { + public HashValue(uint160 hash) : this(hash.ToBytes()) { } + public HashValue(uint256 hash) : this(hash.ToBytes()) { } + public override Op CreatePushOp() => Op.GetPushOp(Hash); + protected override void ToString(StringBuilder builder) + { + builder.Append(Encoders.Hex.EncodeData(Hash)); + } + } + public record PubKeyValue(PubKey PubKey) : Value + { + public override Op CreatePushOp() => Op.GetPushOp(PubKey.ToBytes()); + protected override void ToString(StringBuilder builder) + { + builder.Append(PubKey.ToHex()); + } + } + public record PrivateKeyValue(BitcoinSecret Key) : PubKeyValue(Key.PubKey) + { + protected override void ToString(StringBuilder builder) + { + builder.Append(Key.ToString()); + } + } + public record HDKeyValue(IHDKey Key, KeyType KeyType) : Value + { + public override Op CreatePushOp() => KeyType switch + { + KeyType.Classic => Op.GetPushOp(Key.GetPublicKey().ToBytes()), + _ => Op.GetPushOp(Key.GetPublicKey().TaprootPubKey.ToBytes()) + }; + protected override void ToString(StringBuilder builder) + { + builder.Append(Key.ToString()); + } + } + public record TaprootPubKeyValue(TaprootPubKey PubKey) : Value + { + public TaprootInternalPubKey AsInternalPubKey() => new TaprootInternalPubKey(PubKey.ToBytes()); + public override Op CreatePushOp() => Op.GetPushOp(PubKey.ToBytes()); + protected override void ToString(StringBuilder builder) + { + builder.Append(PubKey.ToString()); + } + } + } + public record Wrapper(FragmentDescriptor Descriptor, MiniscriptNode X) : FragmentSingleParameter(Descriptor, X) + { + public static Wrapper a(MiniscriptNode X) => new(FragmentDescriptor.a, X); + public static Wrapper s(MiniscriptNode X) => new(FragmentDescriptor.s, X); + public static Wrapper c(MiniscriptNode X) => new(FragmentDescriptor.c, X); + public static Wrapper t(MiniscriptNode X) => new(FragmentDescriptor.t, X); + public static Wrapper d(MiniscriptNode X) => new(FragmentDescriptor.d, X); + public static Wrapper v(MiniscriptNode X) => new(FragmentDescriptor.v, X); + public static Wrapper j(MiniscriptNode X) => new(FragmentDescriptor.j, X); + public static Wrapper n(MiniscriptNode X) => new(FragmentDescriptor.n, X); + public static Wrapper l(MiniscriptNode X) => new(FragmentDescriptor.l, X); + public static Wrapper u(MiniscriptNode X) => new(FragmentDescriptor.u, X); + + public IEnumerable FlattenWrappers() + { + yield return this; + Wrapper wrapper = this; + while (wrapper.X is Wrapper nextWrapper) + { + yield return nextWrapper; + wrapper = nextWrapper; + } + } + + protected override void ToString(StringBuilder builder) + { + var wrappers = FlattenWrappers().ToArray(); + var wrappersStr = String.Concat(wrappers.Select(w => w.Descriptor.Name)); + builder.Append(wrappersStr); + builder.Append(":"); + wrappers[^1].X.ToString(builder); + } + } + } +} +#endif diff --git a/NBitcoin/WalletPolicies/MiniscriptReplacementException.cs b/NBitcoin/WalletPolicies/MiniscriptReplacementException.cs new file mode 100644 index 000000000..c85c6438a --- /dev/null +++ b/NBitcoin/WalletPolicies/MiniscriptReplacementException.cs @@ -0,0 +1,19 @@ +#if !NO_RECORDS +#nullable enable + +using System; + +namespace NBitcoin.WalletPolicies; + +public class MiniscriptReplacementException : Exception +{ + public MiniscriptReplacementException(string parameterName, MiniscriptNode.ParameterRequirement requirement) + : base($"The parameter {parameterName} doesn't fit the requirement ({requirement})") + { + ParameterName = parameterName; + Requirement = requirement; + } + public string ParameterName { get; } + public MiniscriptNode.ParameterRequirement Requirement { get; } +} +#endif diff --git a/NBitcoin/WalletPolicies/MiniscriptRewriterVisitor.cs b/NBitcoin/WalletPolicies/MiniscriptRewriterVisitor.cs new file mode 100644 index 000000000..cfe793e53 --- /dev/null +++ b/NBitcoin/WalletPolicies/MiniscriptRewriterVisitor.cs @@ -0,0 +1,33 @@ +#if !NO_RECORDS +#nullable enable +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static NBitcoin.WalletPolicies.MiniscriptNode; + +namespace NBitcoin.WalletPolicies +{ + public class MiniscriptRewriterVisitor + { + public virtual MiniscriptNode Visit(MiniscriptNode node) + { + return node switch + { + TaprootBranchNode t => t with { Left = Visit(t.Left), Right = Visit(t.Right) }, + TaprootNode t => t with { InternalKeyNode = Visit(t.InternalKeyNode), ScriptTreeRootNode = t.ScriptTreeRootNode is null ? null : Visit(t.ScriptTreeRootNode) }, + MusigNode f => new MusigNode(f.IsNested, f.Parameters.Select(Visit).ToArray()), + Wrapper w => w with { X = Visit(w.X) }, + FragmentSingleParameter f => f with { X = Visit(f.X) }, + FragmentTwoParameters f => f with { X = Visit(f.X), Y = Visit(f.Y) }, + FragmentThreeParameters f => f with { X = Visit(f.X), Y = Visit(f.Y), Z = Visit(f.Z) }, + FragmentUnboundedParameters f => new FragmentUnboundedParameters(f.Descriptor, f.Parameters.Select(Visit).ToArray()), + MultipathNode n => n with { Target = Visit(n.Target) }, + _ => node + }; + } + } +} +#endif diff --git a/NBitcoin/WalletPolicies/MiniscriptVisitor.cs b/NBitcoin/WalletPolicies/MiniscriptVisitor.cs new file mode 100644 index 000000000..438d92912 --- /dev/null +++ b/NBitcoin/WalletPolicies/MiniscriptVisitor.cs @@ -0,0 +1,35 @@ +#if !NO_RECORDS +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static NBitcoin.WalletPolicies.MiniscriptNode; + +namespace NBitcoin.WalletPolicies +{ + public class MiniscriptVisitor + { + public virtual void Visit(MiniscriptNode node) + { + if (node is Fragment p) + { + foreach (var param in p.Parameters) + { + Visit(param); + } + } + else if (node is TaprootBranchNode tb) + { + Visit(tb.Left); + Visit(tb.Right); + } + else if (node is MultipathNode n) + { + Visit(n.Target); + } + } + } +} +#endif diff --git a/NBitcoin/WalletPolicies/ParsingContext.cs b/NBitcoin/WalletPolicies/ParsingContext.cs new file mode 100644 index 000000000..7346ff5b7 --- /dev/null +++ b/NBitcoin/WalletPolicies/ParsingContext.cs @@ -0,0 +1,194 @@ +#if !NO_RECORDS +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using static NBitcoin.WalletPolicies.MiniscriptError; + +namespace NBitcoin.WalletPolicies +{ + class ParsingContext + { + internal class Frame : IDisposable + { + internal Frame(ParsingContext ctx) + { + this.ctx = ctx; + } + public int ExpectedParameterCount; + public List Parameters = new List(); + private ParsingContext ctx; + + public int FragmentIndex { get; internal set; } + public void Dispose() + { + ctx._frames.Pop(); + } + } + public ParsingContext(string miniscript, MiniscriptParsingSettings settings) + { + ArgumentNullException.ThrowIfNull(settings); + ArgumentNullException.ThrowIfNull(miniscript); + Miniscript = miniscript; + this.ParsingSettings = settings; + } + public string Miniscript { get; } + + internal MiniscriptParsingSettings ParsingSettings; + + public Network Network => ParsingSettings.Network; + public int Offset { get; internal set; } + public bool Advance(int charCount) + { + SkipSpaces(); + if (Offset + charCount > Miniscript.Length) + return false; + Offset += charCount; + SkipSpaces(); + return true; + } + public void SkipSpaces() + { + while (Offset < Miniscript.Length && char.IsWhiteSpace(Miniscript[Offset])) + { + Offset++; + } + } + public char NextChar => Miniscript[Offset]; + public bool IsEnd => Offset >= Miniscript.Length; + public int RemainingChars => Miniscript.Length - Offset; + public string Remaining => Miniscript[Offset..]; + Stack _frames = new(); + public Frame CurrentFrame => _frames.Peek(); + public Frame PushFrame() + { + Frame f = new Frame(this); + _frames.Push(f); + return f; + } + public KeyType? ExpectedKeyType { get; internal set; } + + public bool TrySetExpectedKeyType(KeyType keyType) + { + if (ExpectedKeyType is null) + { + ExpectedKeyType = keyType; + return true; + } + return ExpectedKeyType == keyType; + } + public int FragmentIndex { get; internal set; } + public KeyType? DefaultKeyType => ParsingSettings.KeyType; + + public bool NetstedMusig { get; internal set; } + + public override string ToString() => Remaining; + + public bool Peek(string str, [MaybeNullWhen(true)] out MiniscriptError error) + { + error = null; + SkipSpaces(); + if (!Remaining.StartsWith(str, StringComparison.Ordinal)) + { + error = new UnexpectedToken(Offset); + return false; + } + return true; + } + public bool Peek(char c, [MaybeNullWhen(true)] out MiniscriptError error) + { + if (!Peek(out var c2, out error)) + return false; + if (c2 != c) + { + error = new UnexpectedToken(Offset); + return false; + } + return true; + } + public bool Peek(out char c, [MaybeNullWhen(true)] out MiniscriptError error) + { + c = default; + SkipSpaces(); + if (IsEnd) + { + error = new IncompleteExpression(Offset - 1); + return false; + } + c = this.NextChar; + error = null; + return true; + } + public bool Consume(string str, [MaybeNullWhen(true)] out MiniscriptError error) + { + error = null; + SkipSpaces(); + if (!Remaining.StartsWith(str, StringComparison.Ordinal)) + { + error = new UnexpectedToken(Offset); + return false; + } + Advance(str.Length); + return true; + } + public bool Consume(out char c, [MaybeNullWhen(true)] out MiniscriptError error) + { + var r = Peek(out c, out error); + if (r) + Advance(1); + return r; + } + public bool Consume(char c, [MaybeNullWhen(true)] out MiniscriptError error) + { + SkipSpaces(); + if (!Peek(out var c2, out error)) + return false; + if (c2 != c) + { + error = new UnexpectedToken(Offset); + return false; + } + Advance(1); + error = null; + return true; + } + + public Memento StartMemento(bool commit) => new Memento(this, commit); + + public class Memento : IDisposable + { + private ParsingContext context; + private readonly KeyType? expectedKeyType; + private readonly bool nestedMusig; + private bool commit; + private readonly int offset; + + public Memento(ParsingContext context, bool commit) + { + this.context = context; + this.expectedKeyType = this.context.ExpectedKeyType; + this.nestedMusig = this.context.NetstedMusig; + this.offset = context.Offset; + this.commit = commit; + } + public void Commit() => commit = true; + public void Rollback() => commit = false; + + + public void Dispose() + { + if (!commit) + { + this.context.Offset = offset; + this.context.ExpectedKeyType = expectedKeyType; + this.context.NetstedMusig = nestedMusig; + } + } + } + } +} +#endif diff --git a/NBitcoin/WalletPolicies/Visitors/DeriveVisitor.cs b/NBitcoin/WalletPolicies/Visitors/DeriveVisitor.cs new file mode 100644 index 000000000..9741f009d --- /dev/null +++ b/NBitcoin/WalletPolicies/Visitors/DeriveVisitor.cs @@ -0,0 +1,96 @@ +#if !NO_RECORDS +#nullable enable +using NBitcoin.DataEncoders; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static NBitcoin.WalletPolicies.MiniscriptNode; + +namespace NBitcoin.WalletPolicies.Visitors; + +internal class DeriveVisitor(AddressIntent Intent, int[] Indexes, KeyType KeyType) : MiniscriptRewriterVisitor +{ + Dictionary _Replacements = new(); + int idx = -1; + + public DerivationResult[] Derive(MiniscriptNode node, Network network) + { + DerivationResult[] result = new DerivationResult[Indexes.Length]; + Parallel.For(0, Indexes.Length, i => + { + var visitor = new DeriveVisitor(Intent, Indexes, KeyType) + { + idx = Indexes[i], + _DerivedCache = _DerivedCache, + }; + var miniscript = new Miniscript(visitor.Visit(node), network, KeyType); + result[i] = new DerivationResult(miniscript, visitor._Derivations); + }); + return result; + } + + internal static readonly byte[] BIP0328CC = Encoders.Hex.DecodeData("868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965"); + public override MiniscriptNode Visit(MiniscriptNode node) + { + if (node is MultipathNode { Target: MusigNode }) + { + var wasNestedMusig = _nestedMusig; + _nestedMusig = true; + try + { + node = base.Visit(node); + } + finally + { + _nestedMusig = wasNestedMusig; + } + } + else + { + node = base.Visit(node); + } + if (node is MiniscriptNode.MultipathNode mki && mki.CanDerive(Intent)) + { + if (mki.Target is HDKeyNode { Key: var pk } xpub) + { + var value = GetPublicKey(mki, pk); + _Derivations.TryAdd(xpub, value); + node = value.Pubkey; + } + else if (mki.Target is MusigNode musig) + { + var aggregatePk = musig.GetAggregatePubKey(); + var aggregatePkExt = new ExtPubKey(aggregatePk, BIP0328CC); + node = GetPublicKey(mki, aggregatePkExt).Pubkey; + } + } + return node; + } + + private Derivation GetPublicKey(MiniscriptNode.MultipathNode mki, IHDKey k) + { + var type = mki.GetTypeIndex(Intent); + k = DeriveIntent(k, type); + k = k.Derive((uint)idx); + var keyType = _nestedMusig ? KeyType.Classic : KeyType; + return new Derivation(new KeyPath([(uint)type, (uint)idx]), keyType switch + { + KeyType.Taproot => MiniscriptNode.Create(k.GetPublicKey().TaprootPubKey), + _ => MiniscriptNode.Create(k.GetPublicKey()) + }); + } + Dictionary _Derivations = new(); + ConcurrentDictionary<(IHDKey, int), Lazy> _DerivedCache = new(); + + public bool _nestedMusig = false; + + private IHDKey DeriveIntent(IHDKey k, int typeIndex) + { + // When we derive 0/1/*, "0/1" is common to multiple derivations, so we cache it + return _DerivedCache.GetOrAdd((k, typeIndex), new Lazy(() => k.Derive((uint)typeIndex))).Value; + } +} +#endif diff --git a/NBitcoin/WalletPolicies/Visitors/MockScriptVisitor.cs b/NBitcoin/WalletPolicies/Visitors/MockScriptVisitor.cs new file mode 100644 index 000000000..4c170393c --- /dev/null +++ b/NBitcoin/WalletPolicies/Visitors/MockScriptVisitor.cs @@ -0,0 +1,84 @@ +#if !NO_RECORDS +#nullable enable + +using NBitcoin.DataEncoders; +using System; +using System.Collections.Generic; +using System.Text; +using static NBitcoin.WalletPolicies.MiniscriptNode; + +namespace NBitcoin.WalletPolicies.Visitors; + +internal class MockScriptVisitor(Network Network, KeyType KeyType) : MiniscriptRewriterVisitor +{ + List ops = new(); + List<(string Old, string New)> replacements = new(); + internal string GenerateScript(MiniscriptNode rootNode) + { + // We create a fragment with dummy values to generate the script + var frag = (Fragment)Visit(rootNode); + var script = frag.GetScript(); + var sb = new StringBuilder(script.ToString()); + // Then we replace the dummy values by the user-friendly parameter names + foreach (var replacement in replacements) + { + sb.Replace(replacement.Old, replacement.New); + } + return sb.ToString(); + } + + public override MiniscriptNode Visit(MiniscriptNode node) + { + return node switch + { + MultipathNode { Target: Parameter p } => CreateDummy(p, node.ToString()), + Parameter p => CreateDummy(p), + _ => base.Visit(node) + }; + } + + private MiniscriptNode CreateDummy(Parameter p, string? paramName = null) + { + MiniscriptNode modifiedParameter; + if (p.Requirement is MiniscriptNode.ParameterRequirement.Key or MiniscriptNode.ParameterRequirement.HDKey) + { + paramName ??= p.Name; + var pk = new Key().PubKey; + if (KeyType is KeyType.Taproot) + { + var pkk = pk.TaprootPubKey; + replacements.Add((pkk.ToString(), $"<{paramName}>")); + modifiedParameter = new Value.TaprootPubKeyValue(pkk); + } + else + { + replacements.Add((pk.ToString(), $"<{paramName}>")); + replacements.Add((pk.Hash.ToString(), $"")); + modifiedParameter = new Value.PubKeyValue(pk); + } + } + else if (p.Requirement is MiniscriptNode.ParameterRequirement.Hash h) + { + var bytes = RandomUtils.GetBytes(h.RequiredBytes); + replacements.Add((Encoders.Hex.EncodeData(bytes), $"<{p.Name}>")); + modifiedParameter = new Value.HashValue(bytes); + } + else if (p.Requirement is MiniscriptNode.ParameterRequirement.Locktime l) + { + var rand = new LockTime(RandomUtils.GetUInt32()); + replacements.Add((Encoders.Hex.EncodeData(Op.GetPushOp(rand.Value).PushData), $"<{p.Name}>")); + modifiedParameter = new Value.LockTimeValue(rand); + } + else if (p.Requirement is MiniscriptNode.ParameterRequirement.Fragment) + { + var pkStr = new Key().PubKey.ToString(); + var pk = $"pk_k({pkStr})"; + replacements.Add((pkStr, $"<{p.Name}>")); + modifiedParameter = Miniscript.Parse(pk, new MiniscriptParsingSettings(Network, KeyType)).RootNode; + } + else + throw new InvalidOperationException($"Unable to generate the script's string with a requirement of type {p.Requirement.GetType().Name}. (This shouldn't happen)"); + return modifiedParameter; + } +} +#endif diff --git a/NBitcoin/WalletPolicies/Visitors/ParameterVisitors.cs b/NBitcoin/WalletPolicies/Visitors/ParameterVisitors.cs new file mode 100644 index 000000000..56582218c --- /dev/null +++ b/NBitcoin/WalletPolicies/Visitors/ParameterVisitors.cs @@ -0,0 +1,71 @@ +#if !NO_RECORDS +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using static NBitcoin.WalletPolicies.MiniscriptNode; + +namespace NBitcoin.WalletPolicies.Visitors; +internal class ParametersVisitor : MiniscriptVisitor +{ + internal static bool TryCreateParameters(MiniscriptNode node, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out IReadOnlyDictionary> parameters) + { + error = null; + parameters = null; + var visitor = new ParametersVisitor(); + visitor.Visit(node); + foreach (var kv in visitor.Parameters) + { + if (kv.Value.ToHashSet().Count != 1) + { + error = new MiniscriptError.MixedParameterType(kv.Key); + return false; + } + } + parameters = visitor.Parameters.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value); + return true; + } + public Dictionary> Parameters { get; } = new(); + public override void Visit(MiniscriptNode node) + { + if (node is Parameter p) + { + if (!Parameters.TryGetValue(p.Name, out var list)) + { + list = new List(); + Parameters.Add(p.Name, list); + } + list.Add(p); + } + base.Visit(node); + } +} + +internal class ParameterReplacementVisitor : MiniscriptRewriterVisitor +{ + private readonly Dictionary _Parameters; + + public ParameterReplacementVisitor(Dictionary parameters) + { + _Parameters = parameters; + } + + public bool SkipRequirements { get; set; } + + public override MiniscriptNode Visit(MiniscriptNode node) + { + if (node is MiniscriptNode.Parameter p) + { + if (_Parameters.TryGetValue(p.Name, out var replacement)) + { + if (!SkipRequirements && !p.Requirement.Check(replacement)) + throw new MiniscriptReplacementException(p.Name, p.Requirement); + return replacement; + } + } + return base.Visit(node); + } +} +#endif diff --git a/NBitcoin/WalletPolicies/Visitors/ScriptVisitor.cs b/NBitcoin/WalletPolicies/Visitors/ScriptVisitor.cs new file mode 100644 index 000000000..d46438b17 --- /dev/null +++ b/NBitcoin/WalletPolicies/Visitors/ScriptVisitor.cs @@ -0,0 +1,67 @@ +#if !NO_RECORDS +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using static NBitcoin.WalletPolicies.MiniscriptNode; + +namespace NBitcoin.WalletPolicies.Visitors; + +internal class ScriptVisitor : MiniscriptVisitor +{ + internal Stack> ops = new(); + public override void Visit(MiniscriptNode node) + { + if (node is TaprootNode trn) + { + this.Visit(trn.InternalKeyNode); + var merkleRoot = TaprootMerkleRootVisitor.GetMerkleRoot(trn); + if (merkleRoot is not null) + { + ops.Push([Op.GetPushOp(merkleRoot.ToBytes())]); + } + var parameters = GetParameters(trn.ScriptTreeRootNode is null ? 1 : 2); + var script = new List(); + trn.Descriptor.AddOps(parameters, script); + ops.Push(script); + } + else + { + var stackSizeBefore = ops.Count; + base.Visit(node); + var actualParameterCount = ops.Count - stackSizeBefore; + if (node is MiniscriptNode.Value v) + { + ops.Push([v.CreatePushOp()]); + } + else if (node is Fragment f) + { + var parameterCount = f.Parameters.Count(); + AssertParameterCount(parameterCount, actualParameterCount, f.Descriptor.Name); + List[] parameters = GetParameters(parameterCount); + var script = new List(); + f.Descriptor.AddOps(parameters, script); + ops.Push(script); + } + } + } + + private List[] GetParameters(int parameterCount) + { + var parameters = new List[parameterCount]; + for (int i = 0; i < parameterCount; i++) + { + parameters[i] = ops.Pop(); + } + Array.Reverse(parameters); + return parameters; + } + + private void AssertParameterCount(int expectedParameterCount, int actualParameterCount, string name) + { + if (expectedParameterCount != actualParameterCount) + throw new InvalidOperationException($"Expected {expectedParameterCount} parameters, got {actualParameterCount}. ({name})"); + } +} +#endif diff --git a/NBitcoin/WalletPolicies/Visitors/TaprootMerkleRootVisitor.cs b/NBitcoin/WalletPolicies/Visitors/TaprootMerkleRootVisitor.cs new file mode 100644 index 000000000..8e718c238 --- /dev/null +++ b/NBitcoin/WalletPolicies/Visitors/TaprootMerkleRootVisitor.cs @@ -0,0 +1,24 @@ +#if !NO_RECORDS +#nullable enable +using static NBitcoin.WalletPolicies.MiniscriptNode; + +namespace NBitcoin.WalletPolicies.Visitors; + +internal class TaprootMerkleRootVisitor +{ + TaprootBuilder taprootBuilder = new TaprootBuilder(); + public static uint256? GetMerkleRoot(TaprootNode node) + { + if (node.ScriptTreeRootNode is null) + return null; + return new TaprootMerkleRootVisitor().Visit(node.ScriptTreeRootNode).Hash; + } + + public TaprootNodeInfo Visit(MiniscriptNode node) + => node switch + { + TaprootBranchNode tbn => Visit(tbn.Left) + Visit(tbn.Right), + _ => TaprootNodeInfo.NewLeaf(new TapScript(node.GetScript(), TapLeafVersion.C0)) + }; +} +#endif diff --git a/NBitcoin/WalletPolicies/Visitors/TemplateVisitors.cs b/NBitcoin/WalletPolicies/Visitors/TemplateVisitors.cs new file mode 100644 index 000000000..0dfbe20e2 --- /dev/null +++ b/NBitcoin/WalletPolicies/Visitors/TemplateVisitors.cs @@ -0,0 +1,35 @@ +#if !NO_RECORDS +#nullable enable +using System.Collections.Generic; +using static NBitcoin.WalletPolicies.MiniscriptNode; + +namespace NBitcoin.WalletPolicies.Visitors; + +internal class ExtractTemplateVisitor : MiniscriptRewriterVisitor +{ + int index = 0; + public List HDKeys { get; } = new(); + public override MiniscriptNode Visit(MiniscriptNode node) + { + if (node is MultipathNode { Target: HDKeyNode { } hdKey } mpi) + { + HDKeys.Add(hdKey); + var target = new Parameter("@" + index++, new ParameterRequirement.HDKey()); + return mpi with { Target = target }; + } + return base.Visit(node); + } +} +internal class FillTemplateVisitor(HDKeyNode[] Keys) : MiniscriptRewriterVisitor +{ + public override MiniscriptNode Visit(MiniscriptNode node) + { + if (node is MultipathNode { Target: Parameter { } p } mpi) + { + var index = int.Parse(p.Name[1..].ToString()); + return mpi with { Target = Keys[index] }; + } + return base.Visit(node); + } +} +#endif diff --git a/NBitcoin/WalletPolicies/WalletPolicy.cs b/NBitcoin/WalletPolicies/WalletPolicy.cs new file mode 100644 index 000000000..13b211a17 --- /dev/null +++ b/NBitcoin/WalletPolicies/WalletPolicy.cs @@ -0,0 +1,65 @@ +#if !NO_RECORDS +#nullable enable +using NBitcoin.Scripting; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static NBitcoin.WalletPolicies.MiniscriptNode; + +namespace NBitcoin.WalletPolicies +{ + /// + /// Use this class to parse a wallet policy as from documented by BIP0388 + /// + public class WalletPolicy + { + /// + /// The full descriptor with all the multi path hdkeys + /// + public Miniscript FullDescriptor { get; } + /// + /// The descriptor with the keys replaced by placeholder, easier to read. + /// + public Miniscript DescriptorTemplate { get; } + /// + /// The HD Keys used in the policy + /// + public HDKeyNode[] KeyInformationVector { get; } + + public WalletPolicy(Miniscript miniscript) + { + ArgumentNullException.ThrowIfNull(miniscript); + if (miniscript.Parameters.Count != 0) + throw new ArgumentException("Policy should not have parameters", paramName: nameof(miniscript)); + this.DescriptorTemplate = miniscript.ReplaceHDKeysByKeyPlaceholders(out var keys); + if (!IsBIP388(miniscript)) + throw new ArgumentException("A policy should either be wsh, sh, pkh, tr or wpkh as top level node", paramName: nameof(miniscript)); + this.KeyInformationVector = keys; + this.FullDescriptor = miniscript; + } + + public static WalletPolicy Parse(string str, Network network) + { + var miniscript = Miniscript.Parse(str, new MiniscriptParsingSettings(network) { Dialect = MiniscriptDialect.BIP388, AllowedParameters = ParameterTypeFlags.None }); + return new WalletPolicy(miniscript); + } + + private static bool IsBIP388(Miniscript miniscript) + => miniscript.RootNode switch + { + Fragment f when f.Descriptor == FragmentDescriptor.wsh => true, + Fragment f when f.Descriptor == FragmentDescriptor.sh => true, + Fragment f when f.Descriptor == FragmentDescriptor.pkh => true, + Fragment f when f.Descriptor == FragmentDescriptor.tr => true, + Fragment f when f.Descriptor == FragmentDescriptor.wpkh => true, + _ => false + }; + + public override string ToString() => ToString(false); + public string ToString(bool checksum) => this.FullDescriptor.ToString(checksum); + } +} +#endif