diff --git a/.travis.yml b/.travis.yml
index 29ed4248c4..c13ab36732 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -28,3 +28,7 @@ before_install:
script:
- dotnet build ./NBitcoin.Tests/NBitcoin.Tests.csproj /p:TargetFrameworkOverride=$TargetFrameworkOverride -c Release -f netcoreapp2.1 $EXTRA_CONSTANT
- dotnet test --no-build -c Release -f netcoreapp2.1 ./NBitcoin.Tests/NBitcoin.Tests.csproj --filter "RestClient=RestClient|RPCClient=RPCClient|Protocol=Protocol|Core=Core|UnitTest=UnitTest" -p:ParallelizeTestCollections=false
+ - dotnet build ./NBitcoin.Miniscript.Tests/FSharp/NBitcoin.Miniscript.Tests.FSharp.fsproj /p:TargetFrameworkOverride=$TargetFrameworkOverride -c Release -f netcoreapp2.1 $EXTRA_CONSTANT
+ - dotnet run --no-build -c Release --project ./NBitcoin.Miniscript.Tests/FSharp/NBitcoin.Miniscript.Tests.FSharp.fsproj -f netcoreapp2.1
+ - dotnet build ./NBitcoin.Miniscript.Tests/CSharp/NBitcoin.Miniscript.Tests.CSharp.csproj /p:TargetFrameworkOverride=$TargetFrameworkOverride -c Release -f netcoreapp2.1 $EXTRA_CONSTANT
+ - dotnet test --no-build -c Release -f netcoreapp2.1 ./NBitcoin.Miniscript.Tests/CSharp/NBitcoin.Miniscript.Tests.CSharp.csproj -p:ParallelizeTestCollections=false
diff --git a/NBitcoin.Altcoins/NBitcoin.Altcoins.csproj b/NBitcoin.Altcoins/NBitcoin.Altcoins.csproj
index 7c63ce347a..21621b9ed7 100644
--- a/NBitcoin.Altcoins/NBitcoin.Altcoins.csproj
+++ b/NBitcoin.Altcoins/NBitcoin.Altcoins.csproj
@@ -12,7 +12,7 @@
1.0.1.42
- net461;net452;netstandard1.3;netcoreapp2.1;netstandard2.0
+ net452;net461;netstandard1.3;netcoreapp2.1;netstandard2.0
netstandard2.0
$(TargetFrameworkOverride)
1591;1573;1572;1584;1570;3021
@@ -34,4 +34,4 @@
true
bin\Release\NBitcoin.Altcoins.XML
-
\ No newline at end of file
+
diff --git a/NBitcoin.Miniscript.Tests/CSharp/AssertEx.cs b/NBitcoin.Miniscript.Tests/CSharp/AssertEx.cs
new file mode 100644
index 0000000000..33048e9ac1
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/CSharp/AssertEx.cs
@@ -0,0 +1,50 @@
+using NBitcoin.Crypto;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace NBitcoin.Miniscript.Tests.CSharp
+{
+ class AssertEx
+ {
+ [DebuggerHidden]
+ internal static void Error(string msg)
+ {
+ Assert.False(true, msg);
+ }
+ [DebuggerHidden]
+ internal static void Equal(T actual, T expected)
+ {
+ Assert.Equal(expected, actual);
+ }
+ [DebuggerHidden]
+ internal static void CollectionEquals(T[] actual, T[] expected)
+ {
+ if(actual.Length != expected.Length)
+ Assert.False(true, "Actual.Length(" + actual.Length + ") != Expected.Length(" + expected.Length + ")");
+
+ for(int i = 0; i < actual.Length; i++)
+ {
+ if(!Object.Equals(actual[i], expected[i]))
+ Assert.False(true, "Actual[" + i + "](" + actual[i] + ") != Expected[" + i + "](" + expected[i] + ")");
+ }
+ }
+
+ [DebuggerHidden]
+ internal static void StackEquals(ContextStack stack1, ContextStack stack2)
+ {
+ var hash1 = stack1.Select(o => Hashes.Hash256(o)).ToArray();
+ var hash2 = stack2.Select(o => Hashes.Hash256(o)).ToArray();
+ AssertEx.CollectionEquals(hash1, hash2);
+ }
+
+ internal static void CollectionEquals(System.Collections.BitArray bitArray, int p)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/NBitcoin.Miniscript.Tests/CSharp/MiniscriptPSBTTests.cs b/NBitcoin.Miniscript.Tests/CSharp/MiniscriptPSBTTests.cs
new file mode 100644
index 0000000000..65957c15f2
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/CSharp/MiniscriptPSBTTests.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using Xunit;
+using NBitcoin.Crypto;
+using NBitcoin.BIP174;
+using NBitcoin.Miniscript;
+using static NBitcoin.Miniscript.AbstractPolicy;
+using System.Linq;
+
+namespace NBitcoin.Miniscript.Tests.CSharp
+{
+ public class MiniscriptPSBTTests
+ {
+ private Key[] privKeys { get; }
+ public Network Network { get; }
+
+ public MiniscriptPSBTTests()
+ {
+ privKeys = new[] { new Key(), new Key(), new Key(), new Key() };
+ Network = Network.Main;
+ }
+
+ private TransactionSignature GetDummySig()
+ {
+ var hash = new uint256();
+ var ecdsa = privKeys[0].Sign(hash);
+ return new TransactionSignature(ecdsa, SigHash.All);
+ }
+
+ [Fact]
+ public void ShouldSatisfyMiniscript()
+ {
+ var policyStr = $"aor(and(pk({privKeys[0].PubKey}), time({10000})), multi(2, {privKeys[0].PubKey}, {privKeys[1].PubKey})";
+ var ms = Miniscript.FromStringUnsafe(policyStr);
+ Assert.NotNull(ms);
+
+ // We can write AbstractPolicy directly instead of using string representation.
+ var pubKeys = privKeys.Select(p => p.PubKey).Take(2).ToArray();
+ var policy = new AsymmetricOr(
+ new And(
+ new AbstractPolicy.Key(privKeys[0].PubKey),
+ new Time(new LockTime(10000))
+ ),
+ new Multi(2, pubKeys)
+ );
+ // And it is EqualityComparable by default. 🎉
+ var msFromPolicy = Miniscript.FromPolicyUnsafe(policy);
+ Assert.Equal(ms, msFromPolicy);
+
+ Func dummySignatureProvider =
+ pk => pk == privKeys[0].PubKey ? GetDummySig() : null;
+ Assert.Throws(() => ms.SatisfyUnsafe(dummySignatureProvider));
+
+ Assert.Throws(() => ms.SatisfyUnsafe(dummySignatureProvider, null, 9999u));
+ var r3 = ms.Satisfy(dummySignatureProvider, null, 10000u);
+ Assert.True(r3.IsOk);
+
+ Func dummySignatureProvider2 =
+ pk => (pk == privKeys[0].PubKey || pk == privKeys[1].PubKey) ? GetDummySig() : null;
+ var r5 = ms.Satisfy(dummySignatureProvider2);
+ Assert.True(r5.IsOk);
+ }
+
+ [Fact]
+ public void ShouldSatisfyPSBTWithComplexScript()
+ {
+ // case 1: bip199 HTLC
+ var alice = privKeys[0];
+ var bob = privKeys[1];
+ var bobSecret = new uint256(0xdeadbeef);
+ var bobHash = new uint256(Hashes.SHA256(bobSecret.ToBytes()), false);
+ var policyStr = $"aor(and(hash({bobHash}), pk({bob.PubKey})), and(pk({alice.PubKey}), time({10000})))";
+ var ms = Miniscript.FromStringUnsafe(policyStr);
+ var script = ms.ToScript();
+ var funds = Utils.CreateDummyFunds(Network, privKeys, script);
+ var tx = Utils.CreateTxToSpendFunds(funds, privKeys, script, false, false);
+ var psbt = PSBT.FromTransaction(tx)
+ .AddTransactions(funds)
+ .AddScript(script);
+
+ // Can not finalize without signatures.
+ Assert.Throws(() => psbt.FinalizeUnsafe(h => h == bobHash ? bobSecret : null, age: 10001u));
+ // It has signature but it is not matured.
+ psbt.SignAll(alice);
+ Assert.Throws(() => psbt.FinalizeUnsafe(h => h == bobHash ? bobSecret : null, age: 9999u));
+
+ // it has both signature and a secret.
+ psbt.SignAll(bob);
+ psbt.FinalizeUnsafe(h => h == bobHash ? bobSecret : null);
+ Assert.True(psbt.CanExtractTX());
+
+ var txExtracted = psbt.ExtractTX();
+ var builder = Network.CreateTransactionBuilder();
+ builder.AddCoins(Utils.DummyFundsToCoins(funds, script, privKeys[0])).AddKeys(privKeys);
+ if (!builder.Verify(txExtracted, (Money)null, out var errors))
+ throw new InvalidOperationException(errors.Aggregate(string.Empty, (a, b) => a + ";\n" + b));
+ }
+ }
+}
diff --git a/NBitcoin.Miniscript.Tests/CSharp/NBitcoin.Miniscript.Tests.CSharp.csproj b/NBitcoin.Miniscript.Tests/CSharp/NBitcoin.Miniscript.Tests.CSharp.csproj
new file mode 100644
index 0000000000..db2b8d8a4d
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/CSharp/NBitcoin.Miniscript.Tests.CSharp.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net461;netstandard2.0;netcoreapp2.1;
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/NBitcoin.Tests/PSBTTests.cs b/NBitcoin.Miniscript.Tests/CSharp/PSBTTests.cs
similarity index 98%
rename from NBitcoin.Tests/PSBTTests.cs
rename to NBitcoin.Miniscript.Tests/CSharp/PSBTTests.cs
index 47edb2cdaa..85cec5e69e 100644
--- a/NBitcoin.Tests/PSBTTests.cs
+++ b/NBitcoin.Miniscript.Tests/CSharp/PSBTTests.cs
@@ -1,5 +1,7 @@
+using static NBitcoin.Utils;
using NBitcoin.BIP174;
using Xunit;
+using NBitcoin.Tests;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;
@@ -7,11 +9,11 @@
using NBitcoin.DataEncoders;
using System.Collections.Generic;
using System.Linq;
-using static NBitcoin.Tests.Comparer;
using Xunit.Abstractions;
-namespace NBitcoin.Tests
+namespace NBitcoin.Miniscript.Tests.CSharp
{
+
public class PSBTTests
{
private readonly ITestOutputHelper Output;
@@ -146,7 +148,7 @@ public void CanUpdate()
Assert.Single(signedPSBTWithCoins.Inputs[4].PartialSigs);
Assert.Single(signedPSBTWithCoins.Inputs[5].PartialSigs);
var ex = Assert.Throws(() =>
- signedPSBTWithCoins.Finalize()
+ signedPSBTWithCoins.FinalizeUnsafe()
);
var finalizationErrors = ex.InnerExceptions;
// Only p2wpkh and p2sh-p2wpkh will succeed.
@@ -191,7 +193,7 @@ public void CanUpdate()
Assert.False(whollySignedPSBT.CanExtractTX());
- var finalizedPSBT = whollySignedPSBT.Finalize();
+ var finalizedPSBT = whollySignedPSBT.FinalizeUnsafe();
Assert.True(finalizedPSBT.CanExtractTX());
var finalTX = finalizedPSBT.ExtractTX();
@@ -228,7 +230,7 @@ public void ShouldCaptureExceptionInFinalization()
var tx = CreateTxToSpendFunds(funds, keys, redeem, false, false);
var psbt = PSBT.FromTransaction(tx);
- var ex = Assert.Throws(() => psbt.Finalize());
+ var ex = Assert.Throws(() => psbt.FinalizeUnsafe());
var errors = ex.InnerExceptions;
Assert.Equal(6, errors.Count);
}
@@ -377,7 +379,7 @@ public void ShouldPassTheLongestTestInBIP174()
expected = PSBT.Parse((string)testcase["psbtcombined"]);
Assert.Equal(expected, combined);
- var finalized = psbt.Finalize();
+ var finalized = psbt.FinalizeUnsafe();
expected = PSBT.Parse((string)testcase["psbtfinalized"]);
Assert.Equal(expected, finalized);
diff --git a/NBitcoin.Miniscript.Tests/CSharp/RPCClientTests.cs b/NBitcoin.Miniscript.Tests/CSharp/RPCClientTests.cs
new file mode 100644
index 0000000000..6c437ad14c
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/CSharp/RPCClientTests.cs
@@ -0,0 +1,313 @@
+using Xunit;
+using NBitcoin;
+using NBitcoin.Tests;
+using NBitcoin.RPC;
+using NBitcoin.BIP174;
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using Xunit.Abstractions;
+
+namespace NBitcoin.Miniscript.Tests.CSharp
+{
+ public class RPCClientTests
+ {
+ internal PSBTComparer PSBTComparerInstance { get; }
+ public ITestOutputHelper Output { get; }
+
+ public RPCClientTests(ITestOutputHelper output)
+ {
+ PSBTComparerInstance = new PSBTComparer();
+
+ Output = output;
+ }
+ [Fact]
+ public void ShouldCreatePSBTAcceptableByRPCAsExpected()
+ {
+ using (var builder = NodeBuilderEx.Create())
+ {
+ var node = builder.CreateNode();
+ node.Start();
+ var client = node.CreateRPCClient();
+
+ var keys = new Key[] { new Key(), new Key(), new Key() };
+ var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(ki => ki.PubKey).ToArray());
+ var funds = PSBTTests.CreateDummyFunds(Network.TestNet, keys, redeem);
+
+ // case1: PSBT from already fully signed tx
+ var tx = PSBTTests.CreateTxToSpendFunds(funds, keys, redeem, true, true);
+ // PSBT without previous outputs but with finalized_script_witness will throw an error.
+ var psbt = PSBT.FromTransaction(tx.Clone(), true);
+ Assert.Throws(() => psbt.ToBase64());
+
+ // after adding coins, will not throw an error.
+ psbt.AddCoins(funds.SelectMany(f => f.Outputs.AsCoins()).ToArray());
+ CheckPSBTIsAcceptableByRealRPC(psbt.ToBase64(), client);
+
+ // but if we use rpc to convert tx to psbt, it will discard input scriptSig and ScriptWitness.
+ // So it will be acceptable by any other rpc.
+ psbt = PSBT.FromTransaction(tx.Clone());
+ CheckPSBTIsAcceptableByRealRPC(psbt.ToBase64(), client);
+
+ // case2: PSBT from tx with script (but without signatures)
+ tx = PSBTTests.CreateTxToSpendFunds(funds, keys, redeem, true, false);
+ psbt = PSBT.FromTransaction(tx, true);
+ // it has witness_script but has no prevout so it will throw an error.
+ Assert.Throws(() => psbt.ToBase64());
+ // after adding coins, will not throw error.
+ psbt.AddCoins(funds.SelectMany(f => f.Outputs.AsCoins()).ToArray());
+ CheckPSBTIsAcceptableByRealRPC(psbt.ToBase64(), client);
+
+ // case3: PSBT from tx without script nor signatures.
+ tx = PSBTTests.CreateTxToSpendFunds(funds, keys, redeem, false, false);
+ psbt = PSBT.FromTransaction(tx, true);
+ // This time, it will not throw an error at the first place.
+ // Since sanity check for witness input will not complain about witness-script-without-witnessUtxo
+ CheckPSBTIsAcceptableByRealRPC(psbt.ToBase64(), client);
+
+ var dummyKey = new Key();
+ var dummyScript = new Script("OP_DUP " + "OP_HASH160 " + Op.GetPushOp(dummyKey.PubKey.Hash.ToBytes()) + " OP_EQUALVERIFY");
+
+ // even after adding coins and scripts ...
+ var psbtWithCoins = psbt.Clone().AddCoins(funds.SelectMany(f => f.Outputs.AsCoins()).ToArray());
+ CheckPSBTIsAcceptableByRealRPC(psbtWithCoins.ToBase64(), client);
+ psbtWithCoins.AddScript(redeem);
+ CheckPSBTIsAcceptableByRealRPC(psbtWithCoins.ToBase64(), client);
+ var tmp = psbtWithCoins.Clone().AddScript(dummyScript); // should not change with dummyScript
+ Assert.Equal(psbtWithCoins, tmp, PSBTComparerInstance);
+ // or txs and scripts.
+ var psbtWithTXs = psbt.Clone().AddTransactions(funds);
+ CheckPSBTIsAcceptableByRealRPC(psbtWithTXs.ToBase64(), client);
+ psbtWithTXs.AddScript(redeem);
+ CheckPSBTIsAcceptableByRealRPC(psbtWithTXs.ToBase64(), client);
+ tmp = psbtWithTXs.Clone().AddScript(dummyScript);
+ Assert.Equal(psbtWithTXs, tmp, PSBTComparerInstance);
+
+ // Let's don't forget about hd KeyPath
+ psbtWithTXs.AddKeyPath(keys[0].PubKey, Tuple.Create((uint)1234, KeyPath.Parse("m/1'/2/3")));
+ psbtWithTXs.AddPathTo(3, keys[1].PubKey, 4321, KeyPath.Parse("m/3'/2/1"));
+ psbtWithTXs.AddPathTo(0, keys[1].PubKey, 4321, KeyPath.Parse("m/3'/2/1"), false);
+ CheckPSBTIsAcceptableByRealRPC(psbtWithTXs.ToBase64(), client);
+
+ // What about after adding some signatures?
+ psbtWithTXs.SignAll(keys);
+ CheckPSBTIsAcceptableByRealRPC(psbtWithTXs.ToBase64(), client);
+ tmp = psbtWithTXs.Clone().SignAll(dummyKey); // Try signing with unrelated key should not change anything
+ Assert.Equal(psbtWithTXs, tmp, PSBTComparerInstance);
+ // And finalization?
+ psbtWithTXs.FinalizeUnsafe();
+ CheckPSBTIsAcceptableByRealRPC(psbtWithTXs.ToBase64(), client);
+ }
+ return;
+ }
+
+ ///
+ /// Just Check if the psbt is acceptable by bitcoin core rpc.
+ ///
+ ///
+ ///
+ private void CheckPSBTIsAcceptableByRealRPC(string base64, RPCClient client)
+ => client.SendCommand(RPCOperations.decodepsbt, base64);
+
+ [Fact]
+ public void ShouldWalletProcessPSBTAndExtractMempoolAcceptableTX()
+ {
+ using (var builder = NodeBuilderEx.Create())
+ {
+ var node = builder.CreateNode();
+ node.Start();
+
+ var client = node.CreateRPCClient();
+
+ // ensure the wallet has whole kinds of coins ...
+ var addr = client.GetNewAddress();
+ client.GenerateToAddress(101, addr);
+ addr = client.GetNewAddress(new GetNewAddressRequest() { AddressType = AddressType.Bech32 });
+ client.SendToAddress(addr, Money.Coins(15));
+ addr = client.GetNewAddress(new GetNewAddressRequest() { AddressType = AddressType.P2SHSegwit });
+ client.SendToAddress(addr, Money.Coins(15));
+ var tmpaddr = new Key();
+ client.GenerateToAddress(1, tmpaddr.PubKey.GetAddress(node.Network));
+
+ // case 1: irrelevant psbt.
+ var keys = new Key[] { new Key(), new Key(), new Key() };
+ var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(ki => ki.PubKey).ToArray());
+ var funds = PSBTTests.CreateDummyFunds(Network.TestNet, keys, redeem);
+ var tx = PSBTTests.CreateTxToSpendFunds(funds, keys, redeem, true, true);
+ var psbt = PSBT.FromTransaction(tx, true)
+ .AddTransactions(funds)
+ .AddScript(redeem);
+ var case1Result = client.WalletProcessPSBT(psbt);
+ // nothing must change for the psbt unrelated to the wallet.
+ Assert.Equal(psbt, case1Result.PSBT, PSBTComparerInstance);
+
+ // case 2: psbt relevant to the wallet. (but already finalized)
+ var kOut = new Key();
+ tx = builder.Network.CreateTransaction();
+ tx.Outputs.Add(new TxOut(Money.Coins(45), kOut)); // This has to be big enough since the wallet must use whole kinds of address.
+ var fundTxResult = client.FundRawTransaction(tx);
+ Assert.Equal(3, fundTxResult.Transaction.Inputs.Count);
+ var psbtFinalized = PSBT.FromTransaction(fundTxResult.Transaction, true);
+ var result = client.WalletProcessPSBT(psbtFinalized, false);
+ Assert.False(result.PSBT.CanExtractTX());
+ result = client.WalletProcessPSBT(psbtFinalized, true);
+ Assert.True(result.PSBT.CanExtractTX());
+
+ // case 3a: psbt relevant to the wallet (and not finalized)
+ var spendableCoins = client.ListUnspent().Where(c => c.IsSpendable).Select(c => c.AsCoin());
+ tx = builder.Network.CreateTransaction();
+ foreach (var coin in spendableCoins)
+ tx.Inputs.Add(coin.Outpoint);
+ tx.Outputs.Add(new TxOut(Money.Coins(45), kOut));
+ var psbtUnFinalized = PSBT.FromTransaction(tx, true);
+
+ var type = SigHash.All;
+ // unsigned
+ result = client.WalletProcessPSBT(psbtUnFinalized, false, type, bip32derivs: true);
+ Assert.False(result.Complete);
+ Assert.False(result.PSBT.CanExtractTX());
+ var ex2 = Assert.Throws(
+ () => result.PSBT.FinalizeUnsafe()
+ );
+ var errors2 = ex2.InnerExceptions;
+ Assert.NotEmpty(errors2);
+ foreach (var psbtin in result.PSBT.Inputs)
+ {
+ Assert.Equal(SigHash.Undefined, psbtin.SighashType);
+ Assert.NotEmpty(psbtin.HDKeyPaths);
+ }
+
+ // signed
+ result = client.WalletProcessPSBT(psbtUnFinalized, true, type);
+ // does not throw
+ result.PSBT.FinalizeUnsafe();
+
+ var txResult = result.PSBT.ExtractTX();
+ var acceptResult = client.TestMempoolAccept(txResult, true);
+ Assert.True(acceptResult.IsAllowed, acceptResult.RejectReason);
+ }
+ }
+
+ // refs: https://github.com/bitcoin/bitcoin/blob/df73c23f5fac031cc9b2ec06a74275db5ea322e3/doc/psbt.md#workflows
+ // with 2 difference.
+ // 1. one user (David) do not use bitcoin core (only NBitcoin)
+ // 2. 4-of-4 instead of 2-of-3
+ // 3. In version 0.17, `importmulti` can not handle witness script so only p2sh are considered here. TODO: fix
+ [Fact]
+ public void ShouldPerformMultisigProcessingWithCore()
+ {
+ using (var builder = NodeBuilderEx.Create())
+ {
+ if (!builder.NodeImplementation.Version.Contains("0.17"))
+ throw new Exception("Test must be updated!");
+ var nodeAlice = builder.CreateNode();
+ var nodeBob = builder.CreateNode();
+ var nodeCarol = builder.CreateNode();
+ var nodeFunder = builder.CreateNode();
+ var david = new Key();
+ builder.StartAll();
+
+ // prepare multisig script and watch with node.
+ var nodes = new CoreNode[] { nodeAlice, nodeBob, nodeCarol };
+ var clients = nodes.Select(n => n.CreateRPCClient()).ToArray();
+ var addresses = clients.Select(c => c.GetNewAddress());
+ var addrInfos = addresses.Select((a, i) => clients[i].GetAddressInfo(a));
+ var pubkeys = new List { david.PubKey };
+ pubkeys.AddRange(addrInfos.Select(i => i.PubKey).ToArray());
+ var script = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(4, pubkeys.ToArray());
+ var aMultiP2SH = script.Hash.ScriptPubKey;
+ // var aMultiP2WSH = script.WitHash.ScriptPubKey;
+ // var aMultiP2SH_P2WSH = script.WitHash.ScriptPubKey.Hash.ScriptPubKey;
+ var multiAddresses = new BitcoinAddress[] { aMultiP2SH.GetDestinationAddress(builder.Network) };
+ var importMultiObject = new ImportMultiAddress[] {
+ new ImportMultiAddress()
+ {
+ ScriptPubKey = new ImportMultiAddress.ScriptPubKeyObject(multiAddresses[0]),
+ RedeemScript = script.ToHex(),
+ Internal = true,
+ },
+ /*
+ new ImportMultiAddress()
+ {
+ ScriptPubKey = new ImportMultiAddress.ScriptPubKeyObject(aMultiP2WSH),
+ RedeemScript = script.ToHex(),
+ Internal = true,
+ },
+ new ImportMultiAddress()
+ {
+ ScriptPubKey = new ImportMultiAddress.ScriptPubKeyObject(aMultiP2SH_P2WSH),
+ RedeemScript = script.WitHash.ScriptPubKey.ToHex(),
+ Internal = true,
+ },
+ new ImportMultiAddress()
+ {
+ ScriptPubKey = new ImportMultiAddress.ScriptPubKeyObject(aMultiP2SH_P2WSH),
+ RedeemScript = script.ToHex(),
+ Internal = true,
+ }
+ */
+ };
+
+ for (var i = 0; i < clients.Length; i++)
+ {
+ var c = clients[i];
+ Output.WriteLine($"Importing for {i}");
+ c.ImportMulti(importMultiObject, false);
+ }
+
+ // pay from funder
+ nodeFunder.Generate(103);
+ var funderClient = nodeFunder.CreateRPCClient();
+ funderClient.SendToAddress(aMultiP2SH, Money.Coins(40));
+ // funderClient.SendToAddress(aMultiP2WSH, Money.Coins(40));
+ // funderClient.SendToAddress(aMultiP2SH_P2WSH, Money.Coins(40));
+ nodeFunder.Generate(1);
+ foreach (var n in nodes)
+ {
+ nodeFunder.Sync(n, true);
+ }
+
+ // pay from multisig address
+ // first carol creates psbt
+ var carol = clients[2];
+ // check if we have enough balance
+ var info = carol.GetBlockchainInfoAsync().Result;
+ Assert.Equal((ulong)104, info.Blocks);
+ var balance = carol.GetBalance(0, true);
+ // Assert.Equal(Money.Coins(120), balance);
+ Assert.Equal(Money.Coins(40), balance);
+
+ var aSend = new Key().PubKey.GetAddress(nodeAlice.Network);
+ var outputs = new Dictionary();
+ outputs.Add(aSend, Money.Coins(10));
+ var fundOptions = new FundRawTransactionOptions() { SubtractFeeFromOutputs = new int[] { 0 }, IncludeWatching = true };
+ PSBT psbt = carol.WalletCreateFundedPSBT(null, outputs, 0, fundOptions).PSBT;
+ psbt = carol.WalletProcessPSBT(psbt).PSBT;
+
+ // second, Bob checks and process psbt.
+ var bob = clients[1];
+ Assert.Contains(multiAddresses, a =>
+ psbt.Inputs.Any(psbtin => psbtin.WitnessUtxo?.ScriptPubKey == a.ScriptPubKey) ||
+ psbt.Inputs.Any(psbtin => (bool)psbtin.NonWitnessUtxo?.Outputs.Any(o => a.ScriptPubKey == o.ScriptPubKey))
+ );
+ var psbt1 = bob.WalletProcessPSBT(psbt.Clone()).PSBT;
+
+ // at the same time, David may do the ;
+ psbt.SignAll(david);
+ var alice = clients[0];
+ var psbt2 = alice.WalletProcessPSBT(psbt).PSBT;
+
+ // not enough signatures
+ Assert.Throws(() => psbt.FinalizeIndexUnsafe(0));
+
+ // So let's combine.
+ var psbtCombined = psbt1.Combine(psbt2);
+
+ // Finally, anyone can finalize and broadcast the psbt.
+ var tx = psbtCombined.FinalizeUnsafe().ExtractTX();
+ var result = alice.TestMempoolAccept(tx);
+ Assert.True(result.IsAllowed, result.RejectReason);
+ }
+ }
+ }
+ }
\ No newline at end of file
diff --git a/NBitcoin.Miniscript.Tests/CSharp/Utils.cs b/NBitcoin.Miniscript.Tests/CSharp/Utils.cs
new file mode 100644
index 0000000000..11ad2656d2
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/CSharp/Utils.cs
@@ -0,0 +1,101 @@
+using System;
+using NBitcoin;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace NBitcoin.Miniscript.Tests.CSharp
+{
+ ///
+ /// Copied and tweaked from `NBitcoin.Tests.PSBTTests` .
+ /// It could possibly reference the method directly, but we prefered to keep the libraries separated.
+ ///
+ public class Utils
+ {
+ public Utils()
+ {
+ }
+ static internal ICoin[] DummyFundsToCoins(IEnumerable txs, Script redeem, Key key)
+ {
+ var barecoins = txs.SelectMany(tx => tx.Outputs.AsCoins()).ToArray();
+ var coins = new ICoin[barecoins.Length];
+ coins[0] = barecoins[0];
+ coins[1] = barecoins[1];
+ coins[2] = redeem != null ? new ScriptCoin(barecoins[2], redeem) : barecoins[2]; // p2sh
+ coins[3] = redeem != null ? new ScriptCoin(barecoins[3], redeem) : barecoins[3]; // p2wsh
+ coins[4] = key != null ? new ScriptCoin(barecoins[4], key.PubKey.WitHash.ScriptPubKey) : barecoins[4]; // p2sh-p2wpkh
+ coins[5] = redeem != null ? new ScriptCoin(barecoins[5], redeem) : barecoins[5]; // p2sh-p2wsh
+ return coins;
+ }
+
+ static internal Transaction CreateTxToSpendFunds(
+ Transaction[] funds,
+ Key[] keys,
+ Script redeem,
+ bool withScript,
+ bool sign
+ )
+ {
+ var tx = Network.Main.CreateTransaction();
+ tx.Inputs.Add(new OutPoint(funds[0].GetHash(), 0)); // p2pkh
+ tx.Inputs.Add(new OutPoint(funds[0].GetHash(), 1)); // p2wpkh
+ tx.Inputs.Add(new OutPoint(funds[1].GetHash(), 0)); // p2sh
+ tx.Inputs.Add(new OutPoint(funds[2].GetHash(), 0)); // p2wsh
+ tx.Inputs.Add(new OutPoint(funds[3].GetHash(), 0)); // p2sh-p2wpkh
+ tx.Inputs.Add(new OutPoint(funds[4].GetHash(), 0)); // p2sh-p2wsh
+
+ var dummyOut = new TxOut(Money.Coins(0.599m), keys[0]);
+ tx.Outputs.Add(dummyOut);
+
+ if (withScript)
+ {
+ // OP_0 + three empty signatures
+ var emptySigPush = new Script(OpcodeType.OP_0, OpcodeType.OP_0, OpcodeType.OP_0, OpcodeType.OP_0);
+ tx.Inputs[0].ScriptSig = PayToPubkeyHashTemplate.Instance.GenerateScriptSig(null, keys[0].PubKey);
+ tx.Inputs[1].WitScript = PayToWitPubKeyHashTemplate.Instance.GenerateWitScript(null, keys[0].PubKey);
+ tx.Inputs[2].ScriptSig = emptySigPush + Op.GetPushOp(redeem.ToBytes());
+ tx.Inputs[3].WitScript = PayToWitScriptHashTemplate.Instance.GenerateWitScript(emptySigPush, redeem);
+ tx.Inputs[4].ScriptSig = new Script(Op.GetPushOp(keys[0].PubKey.WitHash.ScriptPubKey.ToBytes()));
+ tx.Inputs[4].WitScript = PayToWitPubKeyHashTemplate.Instance.GenerateWitScript(null, keys[0].PubKey);
+ tx.Inputs[5].ScriptSig = new Script(Op.GetPushOp(redeem.WitHash.ScriptPubKey.ToBytes()));
+ tx.Inputs[5].WitScript = PayToWitScriptHashTemplate.Instance.GenerateWitScript(emptySigPush, redeem);
+ }
+
+ if (sign)
+ {
+ tx.Sign(keys, DummyFundsToCoins(funds, redeem, keys[0]));
+ }
+ return tx;
+ }
+
+ static public Transaction[] CreateDummyFunds(Network network, Key[] keyForOutput, Script redeem)
+ {
+ // 1. p2pkh and p2wpkh
+ var tx1 = network.CreateTransaction();
+ tx1.Inputs.Add(TxIn.CreateCoinbase(200));
+ tx1.Outputs.Add(new TxOut(Money.Coins(0.1m), keyForOutput[0].PubKey.Hash));
+ tx1.Outputs.Add(new TxOut(Money.Coins(0.1m), keyForOutput[0].PubKey.WitHash));
+
+ // 2. p2sh-multisig
+ var tx2 = network.CreateTransaction();
+ tx2.Inputs.Add(TxIn.CreateCoinbase(200));
+ tx2.Outputs.Add(new TxOut(Money.Coins(0.1m), redeem.Hash));
+
+ // 3. p2wsh
+ var tx3 = network.CreateTransaction();
+ tx3.Inputs.Add(TxIn.CreateCoinbase(200));
+ tx3.Outputs.Add(new TxOut(Money.Coins(0.1m), redeem.WitHash));
+
+ // 4. p2sh-p2wpkh
+ var tx4 = network.CreateTransaction();
+ tx4.Inputs.Add(TxIn.CreateCoinbase(200));
+ tx4.Outputs.Add(new TxOut(Money.Coins(0.1m), keyForOutput[0].PubKey.WitHash.ScriptPubKey.Hash));
+
+ // 5. p2sh-p2wsh
+ var tx5 = network.CreateTransaction();
+ tx5.Inputs.Add(TxIn.CreateCoinbase(200));
+ tx5.Outputs.Add(new TxOut(Money.Coins(0.1m), redeem.WitHash.ScriptPubKey.Hash.ScriptPubKey));
+ return new Transaction[] { tx1, tx2, tx3, tx4, tx5 };
+
+ }
+ }
+}
diff --git a/NBitcoin.Tests/data/psbt.json b/NBitcoin.Miniscript.Tests/CSharp/data/psbt.json
similarity index 100%
rename from NBitcoin.Tests/data/psbt.json
rename to NBitcoin.Miniscript.Tests/CSharp/data/psbt.json
diff --git a/NBitcoin.Miniscript.Tests/FSharp/AssemblyInfo.fs b/NBitcoin.Miniscript.Tests/FSharp/AssemblyInfo.fs
new file mode 100644
index 0000000000..a80a2cc957
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/AssemblyInfo.fs
@@ -0,0 +1,41 @@
+// Auto-Generated by FAKE; do not edit
+namespace System
+
+open System.Reflection
+
+[]
+[]
+[]
+[]
+[]
+[]
+[]
+[]
+do ()
+
+module internal AssemblyVersionInformation =
+ []
+ let AssemblyTitle = "NBitcoin.Miniscript.Tests.FSharp"
+
+ []
+ let AssemblyProduct = "NBitcoin.Miniscript.Tests.FSharp"
+
+ []
+ let AssemblyVersion = "0.1.0"
+
+ []
+ let AssemblyMetadata_ReleaseDate = "2017-03-17T00:00:00.0000000"
+
+ []
+ let AssemblyFileVersion = "0.1.0"
+
+ []
+ let AssemblyInformationalVersion = "0.1.0"
+
+ []
+ let AssemblyMetadata_ReleaseChannel = "release"
+
+ []
+ let AssemblyMetadata_GitHash = "bb8964b54bee133e9af64d316dc2cfee16df7f72"
diff --git a/NBitcoin.Miniscript.Tests/FSharp/Generators/Lib.fs b/NBitcoin.Miniscript.Tests/FSharp/Generators/Lib.fs
new file mode 100644
index 0000000000..26f283d665
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/Generators/Lib.fs
@@ -0,0 +1,48 @@
+namespace NBitcoin.Miniscript.Tests.Generators
+
+open FsCheck
+open NBitcoin.Miniscript
+open NBitcoin.Miniscript.Tests.Generators.Policy
+
+type Generators =
+ static member Policy() : Arbitrary = // policy |> Arb.fromGen
+ { new Arbitrary() with
+ override this.Generator = policy
+ // This shrinker does its job. But it is far from ideal.
+ // 1. nested shrinking does not work well
+ // 2. Must use Seq instead of List
+ override this.Shrinker(p: AbstractPolicy) =
+ let rec shrinkPolicy p =
+ match p with
+ | Key k -> []
+ | Multi(m, pks) -> [Multi(1u, pks.[0..0]); AbstractPolicy.Key pks.[0]]
+ | AbstractPolicy.Hash h -> []
+ | AbstractPolicy.Time t -> []
+ | AbstractPolicy.Threshold (k, ps) ->
+ let shrinkThres (k, (ps: AbstractPolicy[])) =
+ let k2 = if k = 1u then k else k - 1u
+ let ps2 = Arb.shrink(ps)
+ ps2 |> Seq.toList |> List.map(fun p -> AbstractPolicy.Threshold(k2, p))
+ let subexpr = ps |> Array.toList
+ if ps.Length = 1 then subexpr else shrinkThres(k, ps)
+ | AbstractPolicy.And(p1, p2) ->
+ let shrinkedAnd = shrinkNested AbstractPolicy.And p1 p2
+ List.concat[shrinkedAnd; [p1; p2;]]
+ | AbstractPolicy.Or(p1, p2) ->
+ let shrinkedOr = shrinkNested AbstractPolicy.Or p1 p2
+ List.concat[shrinkedOr; [p1; p2;]]
+ | AbstractPolicy.AsymmetricOr(p1, p2) ->
+ let shrinkedAOr = shrinkNested AbstractPolicy.AsymmetricOr p1 p2
+ List.concat[shrinkedAOr; [p1; p2;]]
+
+ /// Helper for shrinking nested types
+ and shrinkNested expectedType p1 p2 =
+ let shrinkedSub1 = shrinkPolicy p1
+ let shrinkedSub2 = shrinkPolicy p2
+ shrinkedSub1
+ |> List.collect(fun p1e -> shrinkedSub2
+ |> List.map(fun p2e -> p1e, p2e))
+ |> List.map expectedType
+
+ shrinkPolicy p |> List.toSeq
+ }
diff --git a/NBitcoin.Miniscript.Tests/FSharp/Generators/NBitcoin.fs b/NBitcoin.Miniscript.Tests/FSharp/Generators/NBitcoin.fs
new file mode 100644
index 0000000000..2ce4de08f2
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/Generators/NBitcoin.fs
@@ -0,0 +1,11 @@
+namespace NBitcoin.Miniscript.Tests.Generators
+
+module internal NBitcoin =
+ open FsCheck
+ open NBitcoin.Miniscript.Tests.Generators.Primitives
+
+ let pubKeyGen =
+ let k = NBitcoin.Key() // prioritize speed for randomness
+ Gen.constant (k) |> Gen.map (fun k -> k.PubKey)
+
+ let uint256Gen = bytesOfNGen 32 |> Gen.map NBitcoin.uint256
diff --git a/NBitcoin.Miniscript.Tests/FSharp/Generators/Policy.fs b/NBitcoin.Miniscript.Tests/FSharp/Generators/Policy.fs
new file mode 100644
index 0000000000..d94ee0d9f7
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/Generators/Policy.fs
@@ -0,0 +1,58 @@
+namespace NBitcoin.Miniscript.Tests.Generators
+
+module internal Policy =
+ open FsCheck
+ open NBitcoin.Miniscript
+ open NBitcoin.Miniscript.Tests.Generators.NBitcoin
+
+ let multiContentsGen = gen { let! n = Gen.choose (1, 20) |> Gen.map uint32
+ let! subN = Gen.choose ((int n), 20)
+ let! subs = Gen.arrayOfLength subN pubKeyGen
+ return (n, subs) }
+
+ let nonRecursivePolicyGen : Gen =
+ Gen.frequency [ (2, Gen.map Key pubKeyGen)
+
+ (1,
+ Gen.map (fun (num, pks) -> Multi(num, pks))
+ multiContentsGen)
+ (2, Gen.map Hash uint256Gen)
+ (2, Arb.generate |> Gen.map NBitcoin.LockTime |> Gen.map(Time)) ]
+
+ let policy =
+ let rec policy' s =
+ match s with
+ | 0 -> nonRecursivePolicyGen
+ | n when n > 0 ->
+ let subPolicyGen = policy' (n / 2)
+ Gen.frequency [ (2, nonRecursivePolicyGen)
+ (3, recursivePolicyGen subPolicyGen) ]
+ | _ -> invalidArg "s" "Only positive arguments are allowed!"
+
+ and recursivePolicyGen (subPolicyGen : Gen) =
+ Gen.oneof
+ [ Gen.map (fun (t, ps) -> Threshold(t, ps))
+ (thresholdContentsGen subPolicyGen)
+
+ Gen.map2 (fun subP1 subP2 -> And(subP1, subP2)) subPolicyGen
+ subPolicyGen
+
+ Gen.map2 (fun subP1 subP2 -> Or(subP1, subP2)) subPolicyGen
+ subPolicyGen
+
+ Gen.map2 (fun subP1 subP2 -> AsymmetricOr(subP1, subP2))
+ subPolicyGen subPolicyGen ]
+
+ and thresholdContentsGen (subGen : Gen<_>) = gen { let! n = Gen.choose
+ (1, 6)
+ |> Gen.map
+ uint32
+ let! subN = Gen.choose
+ ((int
+ n),
+ 6)
+ let! subs = Gen.arrayOfLength
+ subN
+ subGen
+ return (n, subs) }
+ Gen.sized policy'
diff --git a/NBitcoin.Miniscript.Tests/FSharp/Generators/Primitives.fs b/NBitcoin.Miniscript.Tests/FSharp/Generators/Primitives.fs
new file mode 100644
index 0000000000..81282073c8
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/Generators/Primitives.fs
@@ -0,0 +1,9 @@
+namespace NBitcoin.Miniscript.Tests.Generators
+
+module internal Primitives =
+ open FsCheck
+
+ let byteGen = Gen.choose (0, 127) |> Gen.map byte
+ let bytesGen = Gen.listOf byteGen
+ let nonEmptyBytesGen = Gen.nonEmptyListOf byteGen
+ let bytesOfNGen n = Gen.arrayOfLength n byteGen
diff --git a/NBitcoin.Miniscript.Tests/FSharp/Main.fs b/NBitcoin.Miniscript.Tests/FSharp/Main.fs
new file mode 100644
index 0000000000..9506f7c20b
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/Main.fs
@@ -0,0 +1,6 @@
+module ExpectoTemplate
+
+open Expecto
+
+[]
+let main argv = Tests.runTestsInAssembly defaultConfig argv
diff --git a/NBitcoin.Miniscript.Tests/FSharp/MiniScriptCompilerTests.fs b/NBitcoin.Miniscript.Tests/FSharp/MiniScriptCompilerTests.fs
new file mode 100644
index 0000000000..216162c6a2
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/MiniScriptCompilerTests.fs
@@ -0,0 +1,38 @@
+module MiniScriptCompilerTests
+
+open Expecto
+open Expecto.Logging
+open Expecto.Logging.Message
+open NBitcoin.Miniscript.Tests.Generators
+open NBitcoin.Miniscript
+open NBitcoin.Miniscript.Compiler
+open NBitcoin.Miniscript.MiniscriptParser
+
+let logger = Log.create "MiniscriptCompiler"
+
+let config =
+ { FsCheckConfig.defaultConfig with arbitrary = [ typeof ]
+ maxTest = 300
+ endSize = 16
+ receivedArgs =
+ fun _ name no args ->
+ logger.debugWithBP
+ (eventX
+ "For {test} {no}, generated {args}"
+ >> setField "test" name
+ >> setField "no" no
+ >> setField "args" args) }
+
+[]
+let tests =
+ testList "miniscript compiler" [ testPropertyWithConfig config
+ "should compile arbitrary input" <| fun (p : AbstractPolicy) ->
+ let node = CompiledNode.fromPolicy (p)
+ let t = node.Compile()
+ Expect.isOk (t.CastT())
+
+ testPropertyWithConfig config
+ "Should compile arbitrary input to actual bitcoin script" <| fun (p: AbstractPolicy) ->
+ let m = CompiledNode.fromPolicy(p).Compile()
+ Expect.isNotNull (m.ToScript()) "script was empty"
+ ]
diff --git a/NBitcoin.Miniscript.Tests/FSharp/MiniScriptDecompilerTests.fs b/NBitcoin.Miniscript.Tests/FSharp/MiniScriptDecompilerTests.fs
new file mode 100644
index 0000000000..a2812cdf11
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/MiniScriptDecompilerTests.fs
@@ -0,0 +1,380 @@
+module MiniScriptDecompilerTests
+
+open Expecto
+open Expecto.Logging
+open NBitcoin
+open NBitcoin.Miniscript.Utils
+open NBitcoin.Miniscript.MiniscriptParser
+open NBitcoin.Miniscript.Tests.Generators
+open NBitcoin.Miniscript
+open NBitcoin.Miniscript.AST
+open NBitcoin.Miniscript.Miniscript
+open NBitcoin.Miniscript.Utils.Parser
+open NBitcoin.Miniscript.Compiler
+open NBitcoin.Miniscript.Decompiler
+
+let logger = Log.create "MiniscriptDeCompiler"
+let keys =
+ [ "028c28a97bf8298bc0d23d8c749452a32e694b65e30a9472a3954ab30fe5324caa";
+ "03ab1ac1872a38a2f196bed5a6047f0da2c8130fe8de49fc4d5dfb201f7611d8e2";
+ "039729247032c0dfcf45b4841fcd72f6e9a2422631fc3466cf863e87154754dd40";
+ "032564fe9b5beef82d3703a607253f31ef8ea1b365772df434226aee642651b3fa";
+ "0289637f97580a796e050791ad5a2f27af1803645d95df021a3c2d82eb8c2ca7ff" ]
+
+let keysList =
+ keys
+ |> List.map (PubKey)
+ |> List.toArray
+
+let longKeysList = keysList.[0] |> Array.replicate 20
+// --------- AST <-> Script ---------
+let checkParseResult res expected =
+ match res with
+ | Ok (ast) -> Expect.equal ast expected "failed to deserialize properly"
+ | Result.Error e ->
+ let name, msg, pos = e
+ failwithf "name: %s\nmsg: %s\npos: %d" name msg pos
+
+[]
+let tests =
+ testList "Decompiler" [ testCase "case1" <| fun _ ->
+ let pk = PubKey(keys.[0])
+ let pk2 = PubKey(keys.[1])
+ let boolAndWE = ETree(E.ParallelAnd(E.CheckSig(pk), W.Time(!> 1u)))
+ let sc = boolAndWE.ToScript()
+ let res = Miniscript.Decompiler.parseScript sc
+ checkParseResult res boolAndWE
+
+ testCase "case2" <| fun _ ->
+
+ let pk = PubKey(keys.[0])
+ let pk2 = PubKey(keys.[1])
+ let delayedOrV = VTree(V.DelayedOr(Q.Pubkey(pk), Q.Pubkey(pk2)))
+ let sc = delayedOrV.ToScript()
+ let res = Miniscript.Decompiler.parseScript sc
+ checkParseResult res delayedOrV
+
+ testCase "Should decompile Multisig from template" <| fun _ ->
+ let sc = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(2, keysList)
+ let ms = Miniscript.Decompiler.parseScriptUnsafe sc
+ ()
+
+ testCase "Should pass the testcase in rust-miniscript" <| fun _ ->
+
+ let roundtrip (miniscriptResult : Result)
+ (s : Script) =
+ match miniscriptResult with
+ | Ok tree ->
+ let ser = tree.ToScript()
+ Expect.equal ser s
+ "Serialized Miniscript does not match expected script"
+ let deser =
+ Miniscript.fromScriptUnsafe s
+ Expect.equal deser tree
+ "deserialized script does not match expected MiniScript"
+ | Result.Error e -> failwith e
+
+ let r1 =
+ Miniscript.fromAST
+ (AST.TTree
+ (T.CastE
+ (E.CheckSig
+ (PubKey
+ (keys.[0])))))
+ let s1 =
+ Script
+ (sprintf "%s %s"
+ (keys.[0].ToString())
+ "OP_CHECKSIG")
+ roundtrip r1 s1
+ let r2 =
+ Miniscript.fromAST
+ (AST.TTree
+ (T.CastE
+ (E.CheckMultiSig
+ (3u, keysList))))
+ let s2 =
+ Script
+ (sprintf
+ "OP_3 %s %s %s %s %s OP_5 OP_CHECKMULTISIG"
+ keys.[0] keys.[1]
+ keys.[2] keys.[3]
+ keys.[4])
+ roundtrip r2 s2
+
+ let r3Partial =
+ Miniscript.fromAST(
+ TTree(
+ T.And(
+ V.CheckMultiSig(
+ 2u,
+ keysList.[2..3]),
+ T.Time(!> 10000u)
+ )
+ )
+ )
+
+ let policy32 =
+ sprintf
+ "2 %s %s 2 OP_CHECKMULTISIGVERIFY"
+ keys.[2] keys.[3]
+
+ let s3Partial = Script(sprintf "%s 1027 OP_CSV" policy32)
+ roundtrip r3Partial s3Partial
+
+ // Liquid policy
+ let r3 =
+ Miniscript.fromAST
+ (AST.TTree
+ (T.CascadeOr
+ (E.CheckMultiSig
+ (2u,
+ keysList.[0..1]),
+ T.And
+ (V.CheckMultiSig
+ (2u,
+ keysList.[2..3]),
+ T.Time
+ (!> 10000u)))))
+ let policy31 =
+ sprintf
+ "2 %s %s 2 OP_CHECKMULTISIG"
+ keys.[0] keys.[1]
+ let tmp = sprintf "%s OP_IFDUP OP_NOTIF %s 1027 OP_CSV OP_ENDIF"
+ policy31 policy32
+ let s3 =
+ Script(tmp)
+ roundtrip r3 s3
+
+ let r4 =
+ Miniscript.fromAST
+ (TTree(T.Time(!> 921u)))
+ let s4 = Script("9903 OP_CSV")
+ roundtrip r4 s4
+
+ let r5 = Miniscript.fromAST (TTree(
+ T.SwitchOrV(
+ V.CheckSig(keysList.[0]),
+ V.And(
+ V.CheckSig(keysList.[1]),
+ V.CheckSig(keysList.[2])
+ )
+ )
+ )
+ )
+
+ let scriptStr = sprintf "OP_IF %s OP_CHECKSIGVERIFY OP_ELSE %s OP_CHECKSIGVERIFY %s OP_CHECKSIGVERIFY OP_ENDIF 1"
+ keys.[0] keys.[1] keys.[2]
+ let s5 = Script(scriptStr)
+ roundtrip r5 s5
+
+ ]
+
+// --------- converting all the way down to ----
+// --------- Policy <-> AST <-> Script ---------
+let config =
+ { FsCheckConfig.defaultConfig with arbitrary = [ typeof ]
+ maxTest = 30
+ endSize = 32 }
+
+let roundTripFromMiniScript (m: Miniscript) =
+ let sc = m.ToScript()
+ let m2 = Miniscript.fromScriptUnsafe sc
+ Expect.equal m2 m "failed"
+
+let roundtrip p =
+ let m = CompiledNode.fromPolicy(p).Compile()
+ roundTripFromMiniScript (Miniscript.fromASTUnsafe(m))
+
+let hash = uint256.Parse("59141e52303a755307114c2a5e6823010b3f1d586216742f396d4b06106e222c")
+
+[]
+let tests2 =
+ testList "Should convert Policy <-> AST <-> Script" [
+ /// This test did good job for finding some bugs.
+ /// But however, some cases are unfixable so leave it as pending test.
+ /// specifically, the case is when there is a nested `and`.
+ /// `and(and(1, 2), 3)` is semantically equal to `and(1, and(2, 3))`
+ /// But the assertion will fail, so leave it untested.
+ // TODO: (Ideally, we should have stomComparison` for AST and Policy)
+ ptestPropertyWithConfig config "Every possible MiniScript" <| roundtrip
+ testCase "Case found by property tests: 1" <| fun _ ->
+ let input = AbstractPolicy.Or(
+ Key(keysList.[0]),
+ AbstractPolicy.And(
+ AbstractPolicy.Time(!> 2u),
+ AbstractPolicy.Time(!> 1u)
+ )
+ )
+ let m = CompiledNode.fromPolicy(input).Compile()
+ let sc = m.ToScript()
+ let customParser = TokenParser.pT
+ let ops = sc.ToOps() |> Seq.toArray
+ let customState = {ops=ops; position=ops.Length - 1}
+ let m2 = run customParser customState
+ Expect.isOk m2 "failed"
+
+ testCase "Case found by property tests: 2" <| fun _ ->
+ let input = Miniscript.fromASTUnsafe(TTree(T.HashEqual(hash)))
+ roundTripFromMiniScript input
+
+ testCase "Case found by property tests: 3" <| fun _ ->
+ let input = Miniscript.fromASTUnsafe(TTree(T.And(V.Time(LockTime(1u)), T.Time(LockTime(1u)))))
+ roundTripFromMiniScript input
+
+ testCase "Case found by property tests: 4" <| fun _ ->
+ let input = Miniscript.fromASTUnsafe(TTree(
+ T.CastE(
+ E.Threshold(
+ 1u,
+ E.CheckSig(keysList.[0]),
+ [| W.CheckSig(keysList.[0]) |]
+ )
+ )
+ )
+ )
+ roundTripFromMiniScript input
+ testCase "Case found by property tests: 5" <| fun _ ->
+ let input = Miniscript.fromASTUnsafe(TTree(
+ T.CastE(
+ E.Likely(
+ F.Threshold(
+ 1u,
+ E.CheckSig(keysList.[0]),
+ [| W.CheckSig(keysList.[0]) |]
+ )
+ )
+ )
+ )
+ )
+ roundTripFromMiniScript input
+ testCase "Case found by property tests: 6" <| fun _ ->
+ let input = Miniscript.fromASTUnsafe(TTree(T.And(
+ V.CheckMultiSig(1u, longKeysList),
+ T.Time(!> 1u)
+ )))
+ roundTripFromMiniScript input
+ testCase "Case found by property tests: 7" <| fun _ ->
+ let input = Miniscript.fromASTUnsafe(TTree(
+ T.CastE(
+ E.SwitchOrRight(E.Time(!> 1u), F.Time(!> 1u))
+ )
+ )
+ )
+ roundTripFromMiniScript input
+ testCase "Case found by property tests: 8" <| fun _ ->
+ let input = Miniscript.fromASTUnsafe(TTree(
+ T.And(
+ V.Time(!> 2u),
+ T.CastE(
+ E.Threshold(
+ 1u,
+ E.Time(!> 4u),
+ [|W.Time(!> 5u)|]
+ ))
+ )
+ ))
+
+ roundTripFromMiniScript input
+ testCase "Case found by property tests: 9" <| fun _ ->
+ let input = Miniscript.fromASTUnsafe(TTree(
+ T.CastE(
+ E.Likely(F.And(V.Time(!> 2u), F.Time(!> 2u)))
+ )
+ ))
+
+ roundTripFromMiniScript input
+ ptestCase "Can NOT handle nested And" <| fun _ ->
+ let input = Miniscript.fromASTUnsafe(TTree(
+ T.And(
+ V.Time(!> 3u),
+ T.And(
+ V.Time(!> 4u),
+ T.Time(!> 4u))
+ )
+ )
+ )
+
+ roundTripFromMiniScript input
+ ]
+
+let private roundtripParserAndAST (parser: Parser<_, _>) (ast: AST) =
+ let sc = ast.ToScript()
+ let ops = sc.ToOps() |> Seq.toArray
+ let initialState = {ops=ops;position=ops.Length - 1}
+ match run parser initialState with
+ | Ok r -> Expect.equal ast (fst r) "AST is not equal"
+ | Result.Error e -> failwithf "%A" e
+
+[]
+let deserializationTestWithParser =
+ testList "deserialization test with parser" [
+ testCase "Case found by property tests: 5_2" <| fun _ ->
+ let input =
+ ETree(
+ E.Likely(
+ F.Time(!> 1u)
+ )
+ )
+ let parser = TokenParser.pE
+ roundtripParserAndAST parser input
+ testCase "Case found by property tests: 5_3" <| fun _ ->
+ let input = FTree(
+ F.Threshold(
+ 1u,
+ E.CheckSig(keysList.[0]),
+ [| W.CheckSig(keysList.[0]) |]
+ )
+ )
+ let parser = TokenParser.pF
+ roundtripParserAndAST parser input
+ testCase "Case found by property tests: 6_1" <| fun _ ->
+ let input =
+ VTree(V.CheckMultiSig(1u, longKeysList))
+ let parser = TokenParser.pV
+ roundtripParserAndAST parser input
+ testCase "Case found by property tests: 7_1" <| fun _ ->
+ let input =
+ WTree(W.CastE(E.CheckSig(keysList.[0])))
+ let parser = TokenParser.pW
+ roundtripParserAndAST parser input
+
+ testCase "Case found by property tests: 7_2" <| fun _ ->
+ let input =
+ WTree(W.CastE(E.SwitchOrRight(E.Time(!> 1u), F.Time(!> 1u))))
+
+ let parser = TokenParser.pW
+ roundtripParserAndAST parser input
+ testCase "Case found by property tests: 8_1" <| fun _ ->
+ let input =
+ FTree(F.SwitchOr(F.Time(!> 1u), F.Time(!> 1u)))
+
+ let parser = TokenParser.pF
+ roundtripParserAndAST parser input
+ testCase "Case found by property tests: 8_2" <| fun _ ->
+ let input =
+ VTree(V.SwitchOr(V.Time(!> 1u), V.Time(!> 1u)))
+
+ let parser = TokenParser.pV
+ roundtripParserAndAST parser input
+
+ let input =
+ TTree(T.SwitchOr(T.Time(!> 1u), T.Time(!> 1u)))
+ let parser = TokenParser.pT
+ roundtripParserAndAST parser input
+
+ let input =
+ VTree(V.SwitchOrT(T.Time(!> 1u), T.Time(!> 1u)))
+ let parser = TokenParser.pV
+ roundtripParserAndAST parser input
+ testCase "Case found by property tests: 8_3" <| fun _ ->
+ let input =
+ ETree(E.SwitchOrRight(E.Time(!> 1u), F.Time(!> 1u)))
+ let parser = TokenParser.pE
+ roundtripParserAndAST parser input
+ testCase "Case found by property tests: 9_2" <| fun _ ->
+ let input =
+ FTree(F.And(V.Time(!> 2u), F.Time(!> 2u)))
+ let parser = TokenParser.pF
+ roundtripParserAndAST parser input
+ ]
\ No newline at end of file
diff --git a/NBitcoin.Miniscript.Tests/FSharp/MiniScriptParserTests.fs b/NBitcoin.Miniscript.Tests/FSharp/MiniScriptParserTests.fs
new file mode 100644
index 0000000000..eca0c456f8
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/MiniScriptParserTests.fs
@@ -0,0 +1,101 @@
+module MiniScriptParserTests
+
+open NBitcoin.Miniscript
+open NBitcoin.Miniscript.MiniscriptParser
+open Expecto
+open Expecto.Logging
+open Expecto.Logging.Message
+open NBitcoin.Miniscript.Tests.Generators
+open NBitcoin.Miniscript.Utils
+
+let logger = Log.create "MiniscriptParser"
+let pk1Str =
+ "0225629523a16986d3bf575e1163f7dff024d734ad1e656b55ba94e41ede2fdfb6"
+let pk2Str =
+ "03b0ad2ab4133717f26f3a50dfb3a0df0664c88045a1b80005aac88284003a98d3"
+let pk3Str =
+ "02717a8c5d9fc77bc12cfe1171f51c5e5178048a5b8f66ca11088dced56d8bf469"
+
+let check =
+ function
+ | AbstractPolicy p -> ()
+ | _ -> failwith "Failed to parse policy"
+
+let config =
+ { FsCheckConfig.defaultConfig with arbitrary = [ typeof ]
+ maxTest = 30
+ endSize = 32
+ receivedArgs =
+ fun _ name no args ->
+ logger.debugWithBP
+ (eventX
+ "For {test} {no}, generated {args}"
+ >> setField "test" name
+ >> setField "no" no
+ >> setField "args" args) }
+
+[]
+let tests =
+ testList "miniscript parser tests"
+ [ testCase "Should print"
+ <| fun _ ->
+ let pk1, pk2, pk3 =
+ NBitcoin.PubKey(pk1Str), NBitcoin.PubKey(pk2Str),
+ NBitcoin.PubKey(pk3Str)
+ let testdata1 : AbstractPolicy =
+ And
+ (Key(pk1),
+ Or
+ (Multi(1u, [| pk2; pk3 |]),
+ AsymmetricOr(Key(pk1), Time(!> 1000u))))
+ let actual = testdata1.ToString()
+ let expected =
+ sprintf
+ "and(pk(%s),or(multi(1,%s,%s),aor(pk(%s),time(1000))))"
+ pk1Str pk2Str pk3Str pk1Str
+ Expect.equal actual expected
+ "Policy.print did not work as expected"
+ testCase "ParseTest1" <| fun _ ->
+ let d1 = sprintf "pk(%s)" pk1Str
+ check d1
+ let d2 = sprintf "multi(1,%s,%s)" pk1Str pk2Str
+ check d2
+ let d3 = sprintf "hash(%s)" (NBitcoin.uint256().ToString())
+ check d3
+ let d4 = "time(1000)"
+ check d4
+ let d5 = sprintf "thres(2,%s,%s,%s)" d1 d2 d3
+ check d5
+ let d6 = sprintf "and(%s,%s)" d2 d3
+ check d6
+ let d7 = sprintf "or(%s, %s)" d4 d5
+ check d7
+ let d8 = sprintf "aor(%s,%s)" d6 d7
+ check d8
+ testCase "parsing input with noise" <| fun _ ->
+ let dataWithWhiteSpace =
+ sprintf
+ "thres ( 2 , and (pk ( %s ) , aor( multi (2, %s , %s ) , time ( 1000)) ), pk(%s))"
+ pk1Str pk1Str pk1Str pk2Str
+ check dataWithWhiteSpace
+ let dataWithNewLine =
+ sprintf
+ "thres ( \r\n2 , and \n(pk ( \n%s ) , aor( multi \n(2, %s ,%s )\n, time ( 1000)) ), pk(%s))"
+ pk1Str pk1Str pk1Str pk2Str
+ check dataWithNewLine
+ testCase "bidirectional conversion" <| fun _ ->
+ let data =
+ sprintf
+ "thres(2,and(pk(%s),aor(multi(2,%s,%s),time(1000))),pk(%s))"
+ pk1Str pk1Str pk1Str pk2Str
+
+ let data2 =
+ match data with
+ | AbstractPolicy p -> p.ToString()
+ | _ -> failwith "Failed to parse policy"
+ Expect.equal data data2 "Could not parse symmetrically"
+
+ testPropertyWithConfig config "Should convert <-> " <| fun (p : AbstractPolicy) ->
+ match p.ToString() with
+ | AbstractPolicy p2 -> Expect.equal p p2
+ | _ -> failwith "Failed to convert bidirectionally" ]
diff --git a/NBitcoin.Miniscript.Tests/FSharp/NBitcoin.Miniscript.Tests.FSharp.fsproj b/NBitcoin.Miniscript.Tests/FSharp/NBitcoin.Miniscript.Tests.FSharp.fsproj
new file mode 100644
index 0000000000..8f4dab9ad7
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/NBitcoin.Miniscript.Tests.FSharp.fsproj
@@ -0,0 +1,30 @@
+
+
+ Exe
+ netstandard2.0;netcoreapp2.1;net461
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NBitcoin.Miniscript.Tests/FSharp/SatisfyTests.fs b/NBitcoin.Miniscript.Tests/FSharp/SatisfyTests.fs
new file mode 100644
index 0000000000..a65fded966
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/SatisfyTests.fs
@@ -0,0 +1,50 @@
+module SatisfyTests
+
+open Expecto
+open NBitcoin
+open NBitcoin.Miniscript
+open NBitcoin.Miniscript
+open System.Linq
+
+[]
+let tests =
+ testList "Miniscript.Satisfy" [
+ testCase "Should Satisfy simple script" <| fun _ ->
+ let key = NBitcoin.Key()
+ let scriptStr = sprintf "and(pk(%s), time(%d))" (key.PubKey.ToString()) 10000u
+ let ms = Miniscript.fromStringUnsafe scriptStr
+ let t = ms.ToAST().CastTUnsafe()
+
+ let dummyKeyFn pk = None
+ let r1 = Satisfy.satisfyT (dummyKeyFn |> Some, None, None) t
+ Expect.isError r1 "should not satisfy with dummy function"
+
+ let dummySig = TransactionSignature.Empty
+
+ let keyFn (pk: PubKey) = if pk.Equals(key.PubKey) then Some(dummySig) else None
+ let r2 = Satisfy.satisfyT (Some keyFn, None, None) t
+ Expect.isError r2 "should not satisfy the time"
+
+ let dummyAge = LockTime 10001
+ let r3 = Satisfy.satisfyT (Some keyFn, None, Some dummyAge) t
+
+ Expect.isOk r3 "could not satisfy"
+
+ testCase "Should Satisfy simple script from facade" <| fun _ ->
+ let key = NBitcoin.Key()
+ let scriptStr = sprintf "and(pk(%s), time(%d))" (key.PubKey.ToString()) 10000u
+ let ms = Miniscript.fromStringUnsafe scriptStr
+ let dummyKeyFn pk = None
+ let r1 = ms.Satisfy(?keyFn=Some(dummyKeyFn))
+ let dummySig = TransactionSignature.Empty
+
+ let keyFn (pk: PubKey) = if pk.Equals(key.PubKey) then Some(dummySig) else None
+ let r2 = ms.Satisfy(?keyFn=Some keyFn)
+ Expect.isError r2 "should not satisfy the time"
+
+ let dummyAge = LockTime 10001u
+ let r3 = ms.Satisfy(?keyFn=Some keyFn, ?age=Some dummyAge)
+
+ Expect.isOk r3 "could not satisfy"
+
+ ]
diff --git a/NBitcoin.Miniscript.Tests/FSharp/fsc.props b/NBitcoin.Miniscript.Tests/FSharp/fsc.props
new file mode 100644
index 0000000000..3fb4e62948
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/fsc.props
@@ -0,0 +1,21 @@
+
+
+
+
+ true
+ true
+ true
+
+
+ C:\Program Files (x86)\Microsoft SDKs\F#\4.1\Framework\v4.0
+ fsc.exe
+
+
+ /Library/Frameworks/Mono.framework/Versions/Current/Commands
+ fsharpc
+
+
+ /usr/bin
+ fsharpc
+
+
diff --git a/NBitcoin.Miniscript.Tests/FSharp/netfx.props b/NBitcoin.Miniscript.Tests/FSharp/netfx.props
new file mode 100644
index 0000000000..12a67e1e0c
--- /dev/null
+++ b/NBitcoin.Miniscript.Tests/FSharp/netfx.props
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ true
+
+
+ /Library/Frameworks/Mono.framework/Versions/Current/lib/mono
+ /usr/lib/mono
+ /usr/local/lib/mono
+
+
+ $(BaseFrameworkPathOverrideForMono)/4.5-api
+ $(BaseFrameworkPathOverrideForMono)/4.5.1-api
+ $(BaseFrameworkPathOverrideForMono)/4.5.2-api
+ $(BaseFrameworkPathOverrideForMono)/4.6-api
+ $(BaseFrameworkPathOverrideForMono)/4.6.1-api
+ $(BaseFrameworkPathOverrideForMono)/4.6.2-api
+ $(BaseFrameworkPathOverrideForMono)/4.7-api
+ $(BaseFrameworkPathOverrideForMono)/4.7.1-api
+ true
+
+
+ $(FrameworkPathOverride)/Facades;$(AssemblySearchPaths)
+
+
diff --git a/NBitcoin.Miniscript/AssemblyInfo.fs b/NBitcoin.Miniscript/AssemblyInfo.fs
new file mode 100644
index 0000000000..c3ab35bf4b
--- /dev/null
+++ b/NBitcoin.Miniscript/AssemblyInfo.fs
@@ -0,0 +1,36 @@
+// Auto-Generated by FAKE; do not edit
+namespace System
+
+open System.Reflection
+open System.Runtime.CompilerServices
+
+[]
+[]
+[]
+[]
+[]
+[]
+[]
+[]
+[]
+
+do ()
+
+module internal AssemblyVersionInformation =
+ []
+ let AssemblyTitle = "NBitcoin.Miniscript"
+
+ []
+ let AssemblyProduct = "NBitcoin.Miniscript"
+
+ []
+ let AssemblyVersion = "0.1.0"
+
+ []
+ let AssemblyFileVersion = "0.1.0"
+
+ []
+ let AssemblyInformationalVersion = "0.1.0"
+
+ []
+ let AssemblyMetadata_ReleaseChannel = "release"
diff --git a/NBitcoin.Miniscript/Miniscript.fs b/NBitcoin.Miniscript/Miniscript.fs
new file mode 100644
index 0000000000..424152a6c6
--- /dev/null
+++ b/NBitcoin.Miniscript/Miniscript.fs
@@ -0,0 +1,124 @@
+namespace NBitcoin.Miniscript
+
+open System
+open System.Collections.Generic
+open System.Runtime.InteropServices
+
+open NBitcoin.Miniscript.AST
+open NBitcoin.Miniscript.Decompiler
+open NBitcoin.Miniscript.Compiler
+open NBitcoin.Miniscript.Satisfy
+open NBitcoin.Miniscript.MiniscriptParser
+open NBitcoin
+
+/// Exception types to enable consumer to use try-catch style handling instead of `Result`
+/// Why we define it here instead of putting it into `Utis` ?
+/// Because a code for core logics should never throw `Exception` and instead use Result,
+/// And we must basically restrict public-facing interfaces to this file.
+type MiniscriptException(msg: string, ex: exn) =
+ inherit Exception(msg, ex)
+ new (msg) = MiniscriptException(msg, null)
+
+type MiniscriptSatisfyException(reason: FailureCase, ex: exn) =
+ inherit MiniscriptException(sprintf "Failed to satisfy, got: %A" reason, ex)
+ new (reason) = MiniscriptSatisfyException(reason, null)
+
+/// wrapper for top-level AST
+module public Miniscript =
+ type Miniscript = private Miniscript of T
+
+ let internal fromAST (t : AST) : Result =
+ match t.CastT() with
+ | Ok t -> Ok(Miniscript(t))
+ | o -> Error (sprintf "AST was not top-level (T) representation\n%A" o)
+
+ let internal fromASTUnsafe(t: AST) =
+ match fromAST t with
+ | Ok t -> t
+ | Error e -> failwith e
+
+ []
+ let public fromPolicy(p: AbstractPolicy) =
+ (CompiledNode.FromPolicy p).Compile() |> fromAST
+
+ []
+ let public fromPolicyUnsafe(p: AbstractPolicy) =
+ match fromPolicy p with
+ | Ok p -> p
+ | Error e -> failwith e
+
+ []
+ let public fromString (s: string) =
+ match s with
+ | AbstractPolicy p -> fromPolicy p
+ | _ -> Error("failed to parse String policy")
+
+ []
+ let public fromStringUnsafe (s: string) =
+ match fromString s with
+ | Ok m -> m
+ | Error e -> failwith e
+
+
+ let internal toAST (m : Miniscript) =
+ match m with
+ | Miniscript a -> TTree(a)
+
+ []
+ let public fromScript (s : NBitcoin.Script) =
+ parseScript s |> Result.mapError(fun e -> e.ToString()) >>= fromAST
+
+ []
+ let public fromScriptUnsafe (s : NBitcoin.Script) =
+ match fromScript s with
+ | Ok res -> res
+ | Error e -> failwith e
+
+ let private toScript (m : Miniscript) : Script =
+ let ast = toAST m
+ ast.ToScript()
+
+ []
+ let public satisfy (Miniscript t) (providers: ProviderSet) =
+ satisfyT (providers) t
+
+ let private dictToFn (d: IDictionary<_ ,_>) k =
+ match d.TryGetValue k with
+ | (true, v) -> Some v
+ | (false, _) -> None
+
+ let private toFSharpFunc<'TIn, 'TOut> (f: Func<'TIn, 'TOut>) =
+ fun input ->
+ let v = f.Invoke(input)
+ if isNull (box v) then None else Some v
+ type Miniscript with
+ member this.ToScript() = toScript this
+ member internal this.ToAST() = toAST this
+ /// Facade for F#
+ member this.Satisfy(?keyFn: SignatureProvider,
+ ?hashFn: PreImageProvider,
+ ?age: LockTime) =
+ satisfy this (keyFn, hashFn, age)
+ member this.SatisfyUnsafe(?keyFn: SignatureProvider,
+ ?hashFn: PreImageProvider,
+ ?age: LockTime) =
+ match satisfy this (keyFn, hashFn, age) with
+ | Ok item -> item
+ | Error e -> raise (MiniscriptSatisfyException(e))
+
+ /// Facade for C#
+ member this.SatisfyUnsafe([)>] keyFn: Func,
+ [)>] hashFn: Func,
+ [] age: uint32) =
+ let maybeFsharpKeyFn = if isNull keyFn then None else Some(toFSharpFunc(keyFn))
+ let maybeFsharpHashFn = if isNull hashFn then None else Some(toFSharpFunc(hashFn))
+ let maybeAge = if age = 0u then None else Some(LockTime(age))
+ this.SatisfyUnsafe(?keyFn=maybeFsharpKeyFn, ?hashFn=maybeFsharpHashFn, ?age=maybeAge)
+
+ member this.Satisfy([)>] keyFn: Func,
+ [)>] hashFn: Func,
+ [] age: uint32) =
+ let maybeFsharpKeyFn = if isNull keyFn then None else Some(toFSharpFunc(keyFn))
+ let maybeFsharpHashFn = if isNull hashFn then None else Some(toFSharpFunc(hashFn))
+ let maybeAge = if age = 0u then None else Some(LockTime(age))
+ this.Satisfy(?keyFn=maybeFsharpKeyFn, ?hashFn=maybeFsharpHashFn, ?age=maybeAge)
diff --git a/NBitcoin.Miniscript/MiniscriptAST.fs b/NBitcoin.Miniscript/MiniscriptAST.fs
new file mode 100644
index 0000000000..d9deaff2a6
--- /dev/null
+++ b/NBitcoin.Miniscript/MiniscriptAST.fs
@@ -0,0 +1,563 @@
+namespace NBitcoin.Miniscript
+
+open NBitcoin
+open NBitcoin.Miniscript.Utils
+open System.Text
+
+module internal AST =
+ // TODO: Use unativeint instead of uint?
+
+ /// "E"xpression. takes more than one inputs from the stack, if it satisfies the condition,
+ /// It will leave 1 onto the stack, otherwise leave 0
+ /// E and W are the only type which is able to dissatisfy without failing the whole script.
+ type E =
+ | CheckSig of PubKey
+ | CheckMultiSig of uint32 * PubKey []
+ | Time of LockTime
+ | Threshold of (uint32 * E * W [])
+ | ParallelAnd of (E * W)
+ | CascadeAnd of (E * F)
+ | ParallelOr of (E * W)
+ | CascadeOr of (E * E)
+ | SwitchOrLeft of (E * F)
+ | SwitchOrRight of (E * F)
+ | Likely of F
+ | Unlikely of F
+
+ /// "W"rapped. say top level element is `X`, then consume items from the next element.
+ /// and leave one of [1,X] [X,1] if it satisfied the condition. otherwise
+ /// leave [0,X] or [X,0] onto the stack.
+ and W =
+ | CheckSig of PubKey
+ | HashEqual of uint256
+ | Time of LockTime
+ | CastE of E
+
+ /// "Q"ueue. Similar to F, but leaves public key buffer on the stack instead of 1
+ and Q =
+ | Pubkey of PubKey
+ | And of (V * Q)
+ | Or of (Q * Q)
+
+
+ /// "F"orced. Similar to T, but always leaves 1 on the stack.
+ and F =
+ | CheckSig of PubKey
+ | CheckMultiSig of uint32 * PubKey []
+ | Time of LockTime
+ | HashEqual of uint256
+ | Threshold of (uint32 * E * W [])
+ | And of (V * F)
+ | CascadeOr of (E * V)
+ | SwitchOr of (F * F)
+ | SwitchOrV of (V * V)
+ | DelayedOr of (Q * Q)
+
+ /// "V"erify. Similar to the T, but does not leave anything on the stack
+ and V =
+ | CheckSig of PubKey
+ | CheckMultiSig of uint32 * PubKey []
+ | Time of LockTime
+ | HashEqual of uint256
+ | Threshold of (uint32 * E * W [])
+ | And of (V * V)
+ | CascadeOr of (E * V)
+ | SwitchOr of (V * V)
+ | SwitchOrT of (T * T)
+ | DelayedOr of (Q * Q)
+
+ /// "T"opLevel representation. Must be satisfied, and leave zero (or non-zero) value onto the stack
+ and T =
+ | Time of LockTime
+ | HashEqual of uint256
+ | And of (V * T)
+ | ParallelOr of (E * W)
+ | CascadeOr of (E * T)
+ | CascadeOrV of (E * V)
+ | SwitchOr of (T * T)
+ | SwitchOrV of (V * V)
+ | DelayedOr of (Q * Q)
+ | CastE of E
+
+ type AST =
+ | ETree of E
+ | QTree of Q
+ | WTree of W
+ | FTree of F
+ | VTree of V
+ | TTree of T
+
+ type ASTType =
+ | EExpr
+ | QExpr
+ | WExpr
+ | FExpr
+ | VExpr
+ | TExpr
+
+ let private encodeUint (n: uint32) =
+ Op.GetPushOp(int64 n).ToString()
+
+ let private encodeInt (n: int32) =
+ Op.GetPushOp(int64 n).ToString()
+
+ type E with
+
+ member this.Print() =
+ match this with
+ | CheckSig pk -> sprintf "E.pk(%s)" (pk.ToHex())
+ | CheckMultiSig(m, pks) ->
+ sprintf "E.multi(%d,%s)" m
+ (pks
+ |> Array.fold (fun acc k -> sprintf "%s,%s" acc (k.ToString()))
+ "")
+ | Time t -> sprintf "E.time(%s)" (t.ToString())
+ | Threshold(num, e, ws) ->
+ sprintf "E.thres(%d,%s,%s)" num (e.Print())
+ (ws
+ |> Array.fold (fun acc w -> sprintf "%s,%s" acc (w.Print())) "")
+ | ParallelAnd(e, w) -> sprintf "E.and_p(%s,%s)" (e.Print()) (w.Print())
+ | CascadeAnd(e, f) -> sprintf "E.and_c(%s,%s)" (e.Print()) (f.Print())
+ | ParallelOr(e, w) -> sprintf "E.or_p(%s,%s)" (e.Print()) (w.Print())
+ | CascadeOr(e, e2) -> sprintf "E.or_c(%s,%s)" (e.Print()) (e2.Print())
+ | SwitchOrLeft(e, f) -> sprintf "E.or_s(%s,%s)" (e.Print()) (f.Print())
+ | SwitchOrRight(e, f) -> sprintf "E.or_r(%s,%s)" (e.Print()) (f.Print())
+ | Likely f -> sprintf "E.lift_l(%s)" (f.Print())
+ | Unlikely f -> sprintf "E.lift_u(%s)" (f.Print())
+
+ member this.Serialize(sb : StringBuilder) : StringBuilder =
+ match this with
+ | CheckSig pk -> sb.AppendFormat(" {0} OP_CHECKSIG", pk)
+ | CheckMultiSig(m, pks) ->
+ sb.AppendFormat(" {0}", (encodeUint m)) |> ignore
+ for pk in pks do
+ do sb.AppendFormat(" {0}", (pk.ToHex())) |> ignore
+ sb.AppendFormat(" {0} OP_CHECKMULTISIG", encodeInt(pks.Length)) |> ignore
+ sb
+ | Time t ->
+ sb.AppendFormat(" OP_DUP OP_IF {0} OP_CSV OP_DROP OP_ENDIF", encodeUint(!> t))
+ | Threshold(k, e, ws) ->
+ e.Serialize(sb) |> ignore
+ for w in ws do
+ w.Serialize(sb) |> ignore
+ sb.Append(" OP_ADD") |> ignore
+ sb.AppendFormat(" {0} OP_EQUAL", (encodeUint k))
+ | ParallelAnd(l, r) ->
+ l.Serialize(sb) |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_BOOLAND")
+ | CascadeAnd(l, r) ->
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_NOTIF 0 OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF")
+ | ParallelOr(l, r) ->
+ l.Serialize(sb) |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_BOOLOR")
+ | CascadeOr(l, r) ->
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_IFDUP OP_NOTIF") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF")
+ | SwitchOrLeft(l, r) ->
+ sb.Append(" OP_IF") |> ignore
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF")
+ | SwitchOrRight(l, r) ->
+ sb.Append(" OP_NOTIF") |> ignore
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF")
+ | Likely(f) ->
+ sb.Append(" OP_NOTIF") |> ignore
+ f.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE 0 OP_ENDIF")
+ | Unlikely(f) ->
+ sb.Append(" OP_IF") |> ignore
+ f.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE 0 OP_ENDIF")
+
+ member this.ToE() = this
+ member this.ToT() =
+ match this with
+ | ParallelOr(l, r) -> T.ParallelOr(l, r)
+ | x -> T.CastE(x)
+
+ and Q with
+
+ member this.Print() =
+ match this with
+ | Pubkey p -> sprintf "Q.pk(%s)" (p.ToString())
+ | And(v, q) -> sprintf "Q.and(%s,%s)" (v.Print()) (q.Print())
+ | Or(q1, q2) -> sprintf "Q.or(%s,%s)" (q1.Print()) (q2.Print())
+
+ member this.Serialize(sb : StringBuilder) : StringBuilder =
+ match this with
+ | Pubkey pk -> sb.AppendFormat(" {0}", (pk.ToHex()))
+ | And(l, r) ->
+ l.Serialize(sb) |> ignore
+ r.Serialize(sb)
+ | Or(l, r) ->
+ sb.Append(" OP_IF") |> ignore
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF")
+
+ and W with
+
+ member this.Print() =
+ match this with
+ | CheckSig pk -> sprintf "W.pk(%s)" (pk.ToString())
+ | HashEqual u -> sprintf "W.hash(%s)" (u.ToString())
+ | Time t -> sprintf "W.time(%s)" (t.ToString())
+ | CastE e -> e.Print()
+
+ member this.Serialize(sb : StringBuilder) : StringBuilder =
+ match this with
+ | CheckSig pk ->
+ sb.Append(" OP_SWAP") |> ignore
+ sb.AppendFormat(" {0}", (pk.ToHex())) |> ignore
+ sb.Append(" OP_CHECKSIG")
+ | HashEqual h ->
+ sb.Append
+ (sprintf " OP_SWAP OP_SIZE OP_0NOTEQUAL OP_IF OP_SIZE %s OP_EQUALVERIFY OP_SHA256"
+ (encodeInt 32))
+ |> ignore
+ sb.AppendFormat(" {0}", h.ToString()) |> ignore
+ sb.Append(" OP_EQUALVERIFY 1 OP_ENDIF")
+ | Time t ->
+ sb.AppendFormat
+ (" OP_SWAP OP_DUP OP_IF {0} OP_CSV OP_DROP OP_ENDIF", (encodeUint (!> t)))
+ | CastE e ->
+ sb.Append(" OP_TOALTSTACK") |> ignore
+ e.Serialize(sb) |> ignore
+ sb.Append(" OP_FROMALTSTACK")
+
+ and F with
+
+ member this.Print() =
+ match this with
+ | CheckSig pk -> sprintf "F.pk(%s)" (pk.ToString())
+ | CheckMultiSig(m, pks) ->
+ sprintf "F.multi(%d,%s)" m
+ (pks
+ |> Array.fold (fun acc k -> sprintf "%s,%s" acc (k.ToString()))
+ "")
+ | Time t -> sprintf "F.time(%s)" (t.ToString())
+ | HashEqual h -> sprintf "F.hash(%s)" (h.ToString())
+ | Threshold(num, e, ws) ->
+ sprintf "F.thres(%d,%s,%s)" num (e.Print())
+ (ws
+ |> Array.fold (fun acc w -> sprintf "%s,%s" acc (w.Print())) "")
+ | And(l, r) -> sprintf "F.and(%s,%s)" (l.Print()) (r.Print())
+ | CascadeOr(l, r) -> sprintf "F.or_v(%s,%s)" (l.Print()) (r.Print())
+ | SwitchOr(l, r) -> sprintf "F.or_s(%s,%s)" (l.Print()) (r.Print())
+ | SwitchOrV(l, r) -> sprintf "F.or_a(%s,%s)" (l.Print()) (r.Print())
+ | DelayedOr(l, r) -> sprintf "F.or_d(%s,%s)" (l.Print()) (r.Print())
+
+ member this.ToE() = this
+
+ member this.ToT() =
+ match this with
+ | CascadeOr(l, r) -> T.CascadeOrV(l, r)
+ | SwitchOrV(l, r) -> T.SwitchOrV(l, r)
+ | x -> failwith (sprintf "%s is not a T" (x.Print()))
+
+ member this.Serialize(sb : StringBuilder) : StringBuilder =
+ match this with
+ | CheckSig pk ->
+ sb.AppendFormat(" {0} OP_CHECKSIGVERIFY 1", (pk.ToHex()))
+ | CheckMultiSig(m, pks) ->
+ sb.AppendFormat(" {0}", (encodeUint m)) |> ignore
+ for pk in pks do
+ sb.AppendFormat(" {0}", (pk.ToHex())) |> ignore
+ sb.AppendFormat(" {0} OP_CHECKMULTISIGVERIFY 1", (encodeInt pks.Length))
+ | Time t -> sb.AppendFormat(" {0} OP_CSV OP_0NOTEQUAL", (encodeUint (!> t)))
+ | HashEqual h ->
+ sb.AppendFormat
+ (" OP_SIZE 20 OP_EQUALVERIFY OP_SHA256 {0} OP_EQUALVERIFY 1", h)
+ | Threshold(k, e, ws) ->
+ e.Serialize(sb) |> ignore
+ for w in ws do
+ w.Serialize(sb) |> ignore
+ sb.Append(" OP_ADD") |> ignore
+ sb.AppendFormat(" {0} OP_EQUALVERIFY 1", (encodeUint k))
+ | And(l, r) ->
+ l.Serialize(sb) |> ignore
+ r.Serialize(sb)
+ | SwitchOr(l, r) ->
+ sb.AppendFormat(" OP_IF") |> ignore
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF")
+ | SwitchOrV(l, r) ->
+ sb.AppendFormat(" OP_IF") |> ignore
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF 1")
+ | CascadeOr(l, r) ->
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_NOTIF") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF 1")
+ | DelayedOr(l, r) ->
+ sb.Append(" OP_IF") |> ignore
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF OP_CHECKSIGVERIFY 1")
+
+ and V with
+
+ member this.Print() =
+ match this with
+ | CheckSig pk -> sprintf "V.pk(%s)" (pk.ToString())
+ | CheckMultiSig(m, pks) ->
+ sprintf "V.multi(%d,%s)" m
+ (pks
+ |> Array.fold (fun acc k -> sprintf "%s,%s" acc (k.ToString()))
+ "")
+ | Time t -> sprintf "V.time(%s)" (t.ToString())
+ | HashEqual h -> sprintf "V.hash(%s)" (h.ToString())
+ | Threshold(num, e, ws) ->
+ sprintf "V.thres(%d,%s,%s)" num (e.Print())
+ (ws
+ |> Array.fold (fun acc w -> sprintf "%s,%s" acc (w.Print())) "")
+ | And(l, r) -> sprintf "V.and(%s,%s)" (l.Print()) (r.Print())
+ | CascadeOr(l, r) -> sprintf "V.or_v(%s,%s)" (l.Print()) (r.Print())
+ | SwitchOr(l, r) -> sprintf "V.or_s(%s,%s)" (l.Print()) (r.Print())
+ | SwitchOrT(l, r) -> sprintf "V.or_a(%s,%s)" (l.Print()) (r.Print())
+ | DelayedOr(l, r) -> sprintf "V.or_d(%s,%s)" (l.Print()) (r.Print())
+
+ member this.Serialize(sb : StringBuilder) : StringBuilder =
+ match this with
+ | CheckSig pk ->
+ sb.AppendFormat(" {0} OP_CHECKSIGVERIFY ", (pk.ToHex()))
+ | CheckMultiSig(m, pks) ->
+ sb.AppendFormat(" {0}", (encodeUint m)) |> ignore
+ for pk in pks do
+ sb.AppendFormat(" {0}", (pk.ToHex())) |> ignore
+ sb.AppendFormat(" {0} OP_CHECKMULTISIGVERIFY", (encodeInt pks.Length))
+ | Time t -> sb.AppendFormat(" {0} OP_CSV OP_DROP", (encodeUint (!> t)))
+ | HashEqual h ->
+ sb.AppendFormat
+ (" OP_SIZE 20 OP_EQUALVERIFY OP_SHA256 {0} OP_EQUALVERIFY", h)
+ | Threshold(k, e, ws) ->
+ e.Serialize(sb) |> ignore
+ for w in ws do
+ w.Serialize(sb) |> ignore
+ sb.Append(" OP_ADD") |> ignore
+ sb.AppendFormat(" {0} OP_EQUALVERIFY", (encodeUint k))
+ | And(l, r) ->
+ l.Serialize(sb) |> ignore
+ r.Serialize(sb)
+ | SwitchOr(l, r) ->
+ sb.AppendFormat(" OP_IF") |> ignore
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF")
+ | SwitchOrT(l, r) ->
+ sb.AppendFormat(" OP_IF") |> ignore
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF OP_VERIFY")
+ | CascadeOr(l, r) ->
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_NOTIF") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF")
+ | DelayedOr(l, r) ->
+ sb.Append(" OP_IF") |> ignore
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF OP_CHECKSIGVERIFY")
+
+ and T with
+
+ member this.Print() =
+ match this with
+ | Time t -> sprintf "T.time(%s)" (t.ToString())
+ | HashEqual h -> sprintf "T.hash(%s)" (h.ToString())
+ | And(l, r) -> sprintf "T.and_p(%s,%s)" (l.Print()) (r.Print())
+ | ParallelOr(l, r) -> sprintf "T.or_vp(%s,%s)" (l.Print()) (r.Print())
+ | CascadeOr(l, r) -> sprintf "T.or_c(%s,%s)" (l.Print()) (r.Print())
+ | CascadeOrV(l, r) -> sprintf "T.or_v(%s,%s)" (l.Print()) (r.Print())
+ | SwitchOr(l, r) -> sprintf "T.or_s(%s,%s)" (l.Print()) (r.Print())
+ | SwitchOrV(l, r) -> sprintf "T.or_a(%s,%s)" (l.Print()) (r.Print())
+ | DelayedOr(l, r) -> sprintf "T.or_d(%s,%s)" (l.Print()) (r.Print())
+ | CastE e -> sprintf "T.%s" (e.Print())
+
+ member this.Serialize(sb : StringBuilder) : StringBuilder =
+ match this with
+ | Time t -> sb.AppendFormat(" {0} OP_CSV", (encodeUint (!> t)))
+ | HashEqual h ->
+ sb.AppendFormat
+ (" OP_SIZE 20 OP_EQUALVERIFY OP_SHA256 {0} OP_EQUAL", h)
+ | And(l, r) ->
+ l.Serialize(sb) |> ignore
+ r.Serialize(sb)
+ | ParallelOr(l, r) ->
+ l.Serialize(sb) |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_BOOLOR")
+ | CascadeOr(l, r) ->
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_IFDUP OP_NOTIF") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF")
+ | CascadeOrV(l, r) ->
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_NOTIF") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF 1")
+ | SwitchOr(l, r) ->
+ sb.AppendFormat(" OP_IF") |> ignore
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF")
+ | SwitchOrV(l, r) ->
+ sb.AppendFormat(" OP_IF") |> ignore
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF 1")
+ | DelayedOr(l, r) ->
+ sb.Append(" OP_IF") |> ignore
+ l.Serialize(sb) |> ignore
+ sb.Append(" OP_ELSE") |> ignore
+ r.Serialize(sb) |> ignore
+ sb.Append(" OP_ENDIF OP_CHECKSIG")
+ | CastE e -> e.Serialize(sb)
+
+ type AST with
+
+ member this.Print() =
+ match this with
+ | ETree e -> e.Print()
+ | QTree q -> q.Print()
+ | WTree w -> w.Print()
+ | FTree f -> f.Print()
+ | VTree v -> v.Print()
+ | TTree t -> t.Print()
+
+ member this.ToScript() =
+ let sb = StringBuilder()
+ match this with
+ | ETree e ->
+ let s = e.Serialize(sb)
+ NBitcoin.Script(s.ToString())
+ | QTree q ->
+ let s = q.Serialize(sb)
+ NBitcoin.Script(s.ToString())
+ | WTree w ->
+ let s = w.Serialize(sb)
+ NBitcoin.Script(s.ToString())
+ | FTree f ->
+ let s = f.Serialize(sb)
+ NBitcoin.Script(s.ToString())
+ | VTree v ->
+ let s = v.Serialize(sb)
+ NBitcoin.Script(s.ToString())
+ | TTree t ->
+ let s = t.Serialize(sb)
+ let str = s.ToString()
+ NBitcoin.Script(str)
+ member this.GetASTType() =
+ match this with
+ | ETree _ -> EExpr
+ | QTree _ -> QExpr
+ | WTree _ -> WExpr
+ | FTree _ -> FExpr
+ | VTree _ -> VExpr
+ | TTree _ -> TExpr
+
+ member this.IsT() =
+ match this with
+ | ETree _
+ | TTree _ -> true
+ | FTree f ->
+ match f with
+ | F.CascadeOr _
+ | F.SwitchOrV _ -> true
+ | _ -> false
+ | _ -> false
+
+ member this.CastT() : Result =
+ match this with
+ | TTree t -> Ok t
+ | FTree f ->
+ match f with
+ | F.CascadeOr(l, r) -> Ok(T.CascadeOrV(l, r))
+ | F.SwitchOrV(l, r) -> Ok(T.SwitchOrV(l, r))
+ | _ -> Error(sprintf "failed to cast %s" (this.Print()))
+ | ETree e ->
+ match e with
+ | E.ParallelOr(l, r) -> Ok(T.ParallelOr(l, r))
+ | otherE -> Ok(T.CastE(otherE))
+ | _ -> Error(sprintf "failed to cast %s" (this.Print()))
+
+ member this.CastE() : Result =
+ match this with
+ | ETree e -> Ok e
+ | _ -> Error(sprintf "failed to cast %s" (this.Print()))
+
+ member this.CastQ() : Result =
+ match this with
+ | QTree q -> Ok q
+ | _ -> Error(sprintf "failed to cast %s" (this.Print()))
+
+ member this.CastW() : Result =
+ match this with
+ | WTree w -> Ok w
+ | _ -> Error(sprintf "failed to cast %s" (this.Print()))
+
+ member this.CastF() : Result =
+ match this with
+ | FTree f -> Ok f
+ | _ -> Error(sprintf "failed to cast %s" (this.Print()))
+
+ member this.CastV() : Result =
+ match this with
+ | VTree v -> Ok v
+ | _ -> Error(sprintf "failed to cast %s" (this.Print()))
+
+ member this.CastTUnsafe() : T =
+ match this.CastT() with
+ | Ok t -> t
+ | Error s -> failwith s
+
+ member this.CastEUnsafe() : E =
+ match this.CastE() with
+ | Ok e -> e
+ | Error s -> failwith s
+
+ member this.CastQUnsafe() : Q =
+ match this.CastQ() with
+ | Ok q -> q
+ | Error s -> failwith s
+
+ member this.CastWUnsafe() : W =
+ match this.CastW() with
+ | Ok w -> w
+ | Error s -> failwith s
+
+ member this.CastFUnsafe() : F =
+ match this.CastF() with
+ | Ok f -> f
+ | Error s -> failwith s
+
+ member this.CastVUnsafe() : V =
+ match this.CastV() with
+ | Ok v -> v
+ | Error s -> failwith s
diff --git a/NBitcoin.Miniscript/MiniscriptCompiler.fs b/NBitcoin.Miniscript/MiniscriptCompiler.fs
new file mode 100644
index 0000000000..cdcdebe494
--- /dev/null
+++ b/NBitcoin.Miniscript/MiniscriptCompiler.fs
@@ -0,0 +1,1082 @@
+namespace NBitcoin.Miniscript
+
+open NBitcoin
+open NBitcoin.Miniscript.Utils
+open NBitcoin.Miniscript.MiniscriptParser
+open Miniscript.AST
+
+module internal Compiler =
+ type CompiledNode =
+ | Pk of NBitcoin.PubKey
+ | Multi of uint32 * PubKey []
+ | Hash of uint256
+ | Time of LockTime
+ | Threshold of uint32 * CompiledNode []
+ | And of left : CompiledNode * right : CompiledNode
+ | Or of left : CompiledNode * right : CompiledNode * leftProb : float * rightProb : float
+
+ type Cost =
+ { ast : AST
+ pkCost : uint32
+ satCost : float
+ dissatCost : float }
+
+ /// Intermediary value before computing parent Cost
+ type CostTriple =
+ { parent : AST
+ left : Cost
+ right : Cost
+ /// In case of F ast, we can tell the compiler that
+ /// it can be combined as an E expression in two ways.
+ /// This is equivalent to `->` in this macro
+ /// ref: https://github.com/apoelstra/rust-miniscript/blob/ac36d4bacd6440458a57b4bd2013ea1c27058709/src/policy/compiler.rs#L333
+ condCombine : bool }
+
+ module Cost =
+ /// Casts F -> E
+ let likely (fcost : Cost) : Cost =
+ { ast = ETree(E.Likely(fcost.ast.CastFUnsafe()))
+ pkCost = fcost.pkCost + 4u
+ satCost = fcost.satCost * 1.0
+ dissatCost = 2.0 }
+
+ let unlikely (fcost : Cost) : Cost =
+ { ast = ETree(E.Unlikely(fcost.ast.CastFUnsafe()))
+ pkCost = fcost.pkCost + 4u
+ satCost = fcost.satCost * 2.0
+ dissatCost = 1.0 }
+
+ let fromPairToTCost (left : Cost) (right : Cost) (newAst : T)
+ (lweight : float) (rweight : float) =
+ match newAst with
+ | T.Time _ | T.HashEqual _ | T.CastE _ -> failwith "unreachable"
+ | T.And _ ->
+ { ast = TTree newAst
+ pkCost = left.pkCost + right.pkCost
+ satCost = left.satCost + right.satCost
+ dissatCost = 0.0 }
+ | T.ParallelOr _ ->
+ { ast = TTree newAst
+ pkCost = left.pkCost + right.pkCost + 1u
+ satCost =
+ (left.satCost + right.dissatCost) * lweight
+ + (right.satCost + left.dissatCost) * rweight
+ dissatCost = 0.0 }
+ | T.CascadeOr _ | T.CascadeOrV _ ->
+ { ast = TTree newAst
+ pkCost = left.pkCost + right.pkCost + 3u
+ satCost =
+ left.satCost * lweight
+ + (right.satCost + left.dissatCost) * rweight
+ dissatCost = 0.0 }
+ | T.SwitchOr _ ->
+ { ast = TTree newAst
+ pkCost = left.pkCost + right.pkCost + 3u
+ satCost =
+ (left.satCost + 2.0) * lweight
+ + (right.satCost + 1.0) * rweight
+ dissatCost = 0.0 }
+ | T.SwitchOrV _ ->
+ { ast = TTree newAst
+ pkCost = left.pkCost + right.pkCost + 4u
+ satCost =
+ (left.satCost + 2.0) * lweight
+ + (right.satCost + 1.0) * rweight
+ dissatCost = 0.0 }
+ | T.DelayedOr _ ->
+ { ast = TTree newAst
+ pkCost = left.pkCost + right.pkCost + 4u
+ satCost =
+ 72.0 + (left.satCost + 2.0) * lweight
+ + (right.satCost + 1.0) * rweight
+ dissatCost = 0.0 }
+
+ let fromPairToVCost (left : Cost) (right : Cost) (newAst : V)
+ (lweight : float) (rweight : float) =
+ match newAst with
+ | V.CheckSig _ | V.CheckMultiSig _ | V.Time _ | V.HashEqual _ | V.Threshold _ ->
+ failwith "unreachable"
+ | V.And _ ->
+ { ast = VTree newAst
+ pkCost = left.pkCost + right.pkCost
+ satCost = left.satCost + right.satCost
+ dissatCost = 0.0 }
+ | V.CascadeOr _ ->
+ { ast = VTree newAst
+ pkCost = left.pkCost + right.pkCost + 2u
+ satCost =
+ (left.satCost * lweight)
+ + (right.satCost + left.dissatCost) * rweight
+ dissatCost = 0.0 }
+ | V.SwitchOr _ ->
+ { ast = VTree newAst
+ pkCost = left.pkCost + right.pkCost + 3u
+ satCost =
+ (left.satCost + 2.0) * lweight
+ + (right.satCost + 1.0) * rweight
+ dissatCost = 0.0 }
+ | V.SwitchOrT _ ->
+ { ast = VTree newAst
+ pkCost = left.pkCost + right.pkCost + 4u
+ satCost =
+ (left.satCost + 2.0) * lweight
+ + (right.satCost + 1.0) * rweight
+ dissatCost = 0.0 }
+ | V.DelayedOr _ ->
+ { ast = VTree newAst
+ pkCost = left.pkCost + right.pkCost + 4u
+ satCost =
+ (72.0 + left.satCost + 2.0) * lweight
+ + (72.0 + right.satCost + 1.0) * rweight
+ dissatCost = 0.0 }
+
+ let fromPairToFCost (left : Cost) (right : Cost) (newAst : F)
+ (lweight : float) (rweight : float) =
+ match newAst with
+ | F.CheckSig _ | F.CheckMultiSig _ | F.Time _ | F.HashEqual _ | F.Threshold _ ->
+ failwith "unreachable"
+ | F.And _ ->
+ { ast = FTree newAst
+ pkCost = left.pkCost + right.pkCost
+ satCost = left.satCost + right.satCost
+ dissatCost = 0.0 }
+ | F.CascadeOr _ ->
+ { ast = FTree newAst
+ pkCost = left.pkCost + right.pkCost + 3u
+ satCost =
+ left.satCost * lweight
+ + (right.satCost + left.dissatCost) * rweight
+ dissatCost = 0.0 }
+ | F.SwitchOr _ ->
+ { ast = FTree newAst
+ pkCost = left.pkCost + right.pkCost + 3u
+ satCost =
+ (left.satCost + 2.0) * lweight
+ + (right.satCost + 1.0) * rweight
+ dissatCost = 0.0 }
+ | F.SwitchOrV _ ->
+ { ast = FTree newAst
+ pkCost = left.pkCost + right.pkCost + 4u
+ satCost =
+ (left.satCost + 2.0) * lweight
+ + (right.satCost + 1.0) * rweight
+ dissatCost = 0.0 }
+ | F.DelayedOr _ ->
+ { ast = FTree newAst
+ pkCost = left.pkCost + right.pkCost + 5u
+ satCost =
+ 72.0 + (left.satCost + 2.0) * lweight
+ + (right.satCost + 1.0) * rweight
+ dissatCost = 0.0 }
+
+ let fromPairToECost (left : Cost) (right : Cost) (newAst : E)
+ (lweight : float) (rweight : float) =
+ let pkCost = left.pkCost + right.pkCost
+ match newAst with
+ | E.CheckSig _ | E.CheckMultiSig _ | E.Time _ | E.Threshold _ | E.Likely _ | E.Unlikely _ | E.ParallelAnd _ ->
+ { ast = ETree newAst
+ pkCost = pkCost + 1u
+ satCost = left.satCost + right.satCost
+ dissatCost = left.dissatCost + right.dissatCost }
+ | E.CascadeAnd _ ->
+ { ast = ETree newAst
+ pkCost = pkCost + 4u
+ satCost = left.satCost + right.satCost
+ dissatCost = left.dissatCost }
+ | E.ParallelOr _ ->
+ { ast = ETree newAst
+ pkCost = pkCost + 1u
+ satCost =
+ left.satCost * lweight
+ + (right.satCost + left.dissatCost) * rweight
+ dissatCost = left.dissatCost + right.dissatCost }
+ | E.CascadeOr _ ->
+ { ast = ETree newAst
+ pkCost = left.pkCost + right.pkCost + 3u
+ satCost =
+ left.satCost * lweight
+ + (right.satCost + left.dissatCost) * rweight
+ dissatCost = left.dissatCost + right.dissatCost }
+ | E.SwitchOrLeft _ ->
+ { ast = ETree newAst
+ pkCost = pkCost + 3u
+ satCost =
+ (left.satCost + 2.0) * lweight
+ + (right.satCost + 1.0) * rweight
+ dissatCost = left.dissatCost + 2.0 }
+ | E.SwitchOrRight _ ->
+ { ast = ETree newAst
+ pkCost = pkCost + 3u
+ satCost =
+ (left.satCost + 1.0) * lweight
+ + (right.satCost + 2.0) * rweight
+ dissatCost = left.dissatCost + 1.0 }
+
+ // TODO: Consider about treating swap case (eft <=> right) here.
+ let fromTriple (triple : CostTriple) (lweight : float) (rweight : float) : Cost [] =
+ match triple.parent with
+ | TTree t ->
+ fromPairToTCost triple.left triple.right t lweight rweight
+ |> Array.singleton
+ | ETree e ->
+ fromPairToECost triple.left triple.right e lweight rweight
+ |> Array.singleton
+ | FTree f ->
+ match triple.condCombine with
+ | (false) ->
+ fromPairToFCost triple.left triple.right f lweight rweight
+ |> Array.singleton
+ | (true) ->
+ let costBeforeCast =
+ fromPairToFCost triple.left triple.right f lweight rweight
+ [| likely (costBeforeCast)
+ unlikely (costBeforeCast) |]
+ | VTree v ->
+ fromPairToVCost triple.left triple.right v lweight rweight
+ |> Array.singleton
+ | _ -> failwith "unreachable"
+ let minCost (a : Cost, b : Cost, p_sat : float, p_dissat : float) =
+ let weightOne =
+ (float a.pkCost) + p_sat * a.satCost + p_dissat * a.dissatCost
+ let weightTwo =
+ (float b.pkCost) + p_sat * b.satCost + p_dissat * a.dissatCost
+ if weightOne < weightTwo then a
+ else if weightOne > weightTwo then b
+ else if a.satCost < b.satCost then a
+ else b
+
+ let foldCosts (p_sat : float) (p_dissat : float) (cs : Cost []) =
+ cs
+ |> Array.toList
+ |> List.reduce (fun a b -> minCost (a, b, p_sat, p_dissat))
+
+ // equivalent to rules! macro in rust-miniscript
+ let getMinimumCost (triples : CostTriple []) (pSat) (pDissat)
+ (lweight : float) (rweight : float) : Cost =
+ triples
+ |> Array.collect (fun p -> fromTriple p 0.0 0.0)
+ |> foldCosts pSat pDissat
+
+ module CompiledNode =
+ /// bytes length when a number is encoded as bitcoin CScriptNum
+ let private scriptNumCost n =
+ if n <= 16u then 1u
+ else if n < 0x100u then 2u
+ else if n < 0x10000u then 3u
+ else if n < 0x1000000u then 4u
+ else 5u
+
+ let private minCost (one : Cost, two : Cost, p_sat : float, p_dissat) =
+ let weightOne =
+ (float one.pkCost) + p_sat * one.satCost + p_dissat * one.dissatCost
+ let weightTwo =
+ (float two.pkCost) + p_sat * two.satCost + p_dissat * two.dissatCost
+ if weightOne < weightTwo then one
+ else if weightTwo < weightOne then one
+ else if one.satCost < two.satCost then one
+ else two
+
+ let private getPkCost m (pks : PubKey []) =
+ match (m > 16u, pks.Length > 16) with
+ | (true, true) -> 4
+ | (true, false) -> 3
+ | (false, true) -> 3
+ | (false, false) -> 2
+
+ let rec fromPolicy (p : AbstractPolicy) : CompiledNode =
+ match p with
+ | Key k -> Pk k
+ | AbstractPolicy.Multi(m, pks) -> Multi(m, pks)
+ | AbstractPolicy.Hash h -> Hash h
+ | AbstractPolicy.Time t -> Time t
+ | AbstractPolicy.Threshold(n, subexprs) ->
+ let ps = subexprs |> Array.map fromPolicy
+ Threshold(n, ps)
+ | AbstractPolicy.And(e1, e2) -> And(fromPolicy e1, fromPolicy e2)
+ | AbstractPolicy.Or(e1, e2) -> Or(fromPolicy e1, fromPolicy e2, 0.5, 0.5)
+ | AbstractPolicy.AsymmetricOr(e1, e2) ->
+ Or(fromPolicy e1, fromPolicy e2, 127.0 / 128.0, 1.0 / 128.0)
+
+ // TODO: cache
+ let rec bestT (node : CompiledNode, p_sat : float, p_dissat : float) : Cost =
+ match node with
+ | Pk _ | Multi _ | Threshold _ ->
+ let e = bestE (node, p_sat, p_dissat)
+ { ast = TTree(T.CastE(e.ast.CastEUnsafe()))
+ pkCost = e.pkCost
+ satCost = e.satCost
+ dissatCost = 0.0 }
+ | Time t ->
+ let num_cost = scriptNumCost (!> t)
+ { ast = TTree(T.Time t)
+ pkCost = 1u + uint32 num_cost
+ satCost = 0.0
+ dissatCost = 0.0 }
+ | Hash h ->
+ { ast = TTree(T.HashEqual h)
+ pkCost = 39u
+ satCost = 33.0
+ dissatCost = 0.0 }
+ | And(l, r) ->
+ let vl = bestV (l, p_sat, 0.0)
+ let vr = bestV (r, p_sat, 0.0)
+ let tl = bestT (l, p_sat, 0.0)
+ let tr = bestT (r, p_sat, 0.0)
+
+ let possibleCases =
+ [| { parent =
+ TTree
+ (T.And(vl.ast.CastVUnsafe(), tr.ast.CastTUnsafe()))
+ left = vl
+ right = tr
+ condCombine = false }
+ { parent =
+ TTree
+ (T.And(vr.ast.CastVUnsafe(), tl.ast.CastTUnsafe()))
+ left = vl
+ right = tr
+ condCombine = false } |]
+ Cost.getMinimumCost possibleCases p_sat p_dissat 0.0 0.0
+ | Or(l, r, lweight, rweight) ->
+ let le = bestE (l, (p_sat * lweight), (p_sat * rweight))
+ let re = bestE (r, (p_sat * rweight), (p_sat * lweight))
+ let lw = bestW (l, (p_sat * lweight), (p_sat * rweight))
+ let rw = bestW (r, (p_sat * rweight), (p_sat * lweight))
+ let lt = bestT (l, (p_sat * lweight), 0.0)
+ let rt = bestT (r, (p_sat * lweight), 0.0)
+ let lv = bestV (l, (p_sat * lweight), 0.0)
+ let rv = bestV (r, (p_sat * lweight), 0.0)
+ let maybelq = bestQ (l, (p_sat * lweight), 0.0)
+ let mayberq = bestQ (r, (p_sat * lweight), 0.0)
+
+ let possibleCases =
+ [| { parent =
+ TTree
+ (T.ParallelOr
+ (le.ast.CastEUnsafe(), rw.ast.CastWUnsafe()))
+ left = le
+ right = rw
+ condCombine = false }
+ { parent =
+ TTree
+ (T.ParallelOr
+ (re.ast.CastEUnsafe(), lw.ast.CastWUnsafe()))
+ left = re
+ right = lw
+ condCombine = false }
+ { parent =
+ TTree
+ (T.CascadeOr
+ (le.ast.CastEUnsafe(), rt.ast.CastTUnsafe()))
+ left = le
+ right = rt
+ condCombine = false }
+ { parent =
+ TTree
+ (T.CascadeOr
+ (re.ast.CastEUnsafe(), lt.ast.CastTUnsafe()))
+ left = re
+ right = lt
+ condCombine = false }
+ { parent =
+ TTree
+ (T.CascadeOrV
+ (le.ast.CastEUnsafe(), rv.ast.CastVUnsafe()))
+ left = le
+ right = rv
+ condCombine = false }
+ { parent =
+ TTree
+ (T.CascadeOrV
+ (re.ast.CastEUnsafe(), lv.ast.CastVUnsafe()))
+ left = re
+ right = lv
+ condCombine = false }
+ { parent =
+ TTree
+ (T.SwitchOr
+ (lt.ast.CastTUnsafe(), rt.ast.CastTUnsafe()))
+ left = lt
+ right = rt
+ condCombine = false }
+ { parent =
+ TTree
+ (T.SwitchOr
+ (rt.ast.CastTUnsafe(), lt.ast.CastTUnsafe()))
+ left = rt
+ right = lt
+ condCombine = false }
+ { parent =
+ TTree
+ (T.SwitchOrV
+ (lv.ast.CastVUnsafe(), rv.ast.CastVUnsafe()))
+ left = lv
+ right = rv
+ condCombine = false }
+ { parent =
+ TTree
+ (T.SwitchOrV
+ (rv.ast.CastVUnsafe(), lv.ast.CastVUnsafe()))
+ left = rv
+ right = lv
+ condCombine = false } |]
+
+ let casesWithQ =
+ match maybelq, mayberq with
+ | Some lq, Some rq ->
+ Array.append possibleCases [| { parent =
+ TTree
+ (T.DelayedOr
+ (lq.ast.CastQUnsafe
+ (),
+ rq.ast.CastQUnsafe
+ ()))
+ left = lq
+ right = rq
+ condCombine = false } |]
+ | _ -> possibleCases
+
+ Cost.getMinimumCost casesWithQ p_sat 0.0 lweight rweight
+
+ and bestE (node : CompiledNode, p_sat : float, p_dissat : float) : Cost =
+ match node with
+ | Pk k ->
+ { ast = ETree(E.CheckSig k)
+ pkCost = 35u
+ satCost = 72.0
+ dissatCost = 1.0 }
+ | Multi(m, pks) ->
+ let num_cost = getPkCost m pks
+
+ let options =
+ [ { ast = ETree(E.CheckMultiSig(m, pks))
+ pkCost = uint32 (num_cost + 34 * pks.Length + 1)
+ satCost = 2.0
+ dissatCost = 1.0 } ]
+ if not (p_dissat > 0.0) then options.[0]
+ else
+ let bestf = bestF (node, p_sat, 0.0)
+
+ let options2 =
+ [ Cost.likely (bestf)
+ Cost.unlikely (bestf) ]
+ List.concat [ options; options2 ]
+ |> List.toArray
+ |> Cost.foldCosts p_sat p_dissat
+ | Time n ->
+ let num_cost = scriptNumCost (!> n)
+ { ast = ETree(E.Time n)
+ pkCost = 5u + num_cost
+ satCost = 2.0
+ dissatCost = 1.0 }
+ | Hash h ->
+ let fcost = bestF (node, p_sat, p_dissat)
+ minCost (Cost.likely fcost, Cost.unlikely fcost, p_sat, p_dissat)
+ | And(l, r) ->
+ let le = bestE (l, p_sat, p_dissat)
+ let re = bestE (r, p_sat, p_dissat)
+ let lw = bestW (l, p_sat, p_dissat)
+ let rw = bestW (r, p_sat, p_dissat)
+ let lf = bestF (l, p_sat, 0.0)
+ let rf = bestF (r, p_sat, 0.0)
+ let lv = bestV (l, p_sat, 0.0)
+ let rv = bestV (r, p_sat, 0.0)
+
+ let possibleCases =
+ [| { parent =
+ ETree
+ (E.ParallelAnd
+ (le.ast.CastEUnsafe(), rw.ast.CastWUnsafe()))
+ left = le
+ right = rw
+ condCombine = false }
+ { parent =
+ ETree
+ (E.ParallelAnd
+ (re.ast.CastEUnsafe(), lw.ast.CastWUnsafe()))
+ left = re
+ right = lw
+ condCombine = false }
+ { parent =
+ ETree
+ (E.CascadeAnd
+ (le.ast.CastEUnsafe(), rf.ast.CastFUnsafe()))
+ left = le
+ right = rf
+ condCombine = false }
+ { parent =
+ ETree
+ (E.CascadeAnd
+ (re.ast.CastEUnsafe(), lf.ast.CastFUnsafe()))
+ left = re
+ right = lf
+ condCombine = false }
+ { parent =
+ FTree
+ (F.And(lv.ast.CastVUnsafe(), rf.ast.CastFUnsafe()))
+ left = lv
+ right = rf
+ condCombine = true }
+ { parent =
+ FTree
+ (F.And(rv.ast.CastVUnsafe(), lf.ast.CastFUnsafe()))
+ left = rv
+ right = lf
+ condCombine = true } |]
+ Cost.getMinimumCost possibleCases p_sat p_dissat 0.5 0.5
+ | Or(l, r, lweight, rweight) ->
+ let le_par =
+ bestE (l, (p_sat * lweight), (p_dissat + p_sat * rweight))
+ let re_par =
+ bestE (r, (p_sat * lweight), (p_dissat + p_sat * rweight))
+ let lw_par =
+ bestW (l, (p_sat * lweight), (p_dissat + p_sat * rweight))
+ let rw_par =
+ bestW (r, (p_sat * lweight), (p_dissat + p_sat * rweight))
+ let le_cas = bestE (l, (p_sat * lweight), (p_dissat))
+ let re_cas = bestE (r, (p_sat * lweight), (p_dissat))
+ let le_cond_par = bestE (l, (p_sat * lweight), (p_sat * rweight))
+ let re_cond_par = bestE (r, (p_sat * lweight), (p_sat * lweight))
+ let lv = bestV (l, (p_sat * lweight), 0.0)
+ let rv = bestV (r, (p_sat * rweight), 0.0)
+ let lf = bestF (l, (p_sat * lweight), 0.0)
+ let rf = bestF (r, (p_sat * rweight), 0.0)
+ let maybelq = bestQ (l, (p_sat * lweight), 0.0)
+ let mayberq = bestQ (r, (p_sat * rweight), 0.0)
+
+ let possibleCases =
+ [| { parent =
+ ETree
+ (E.ParallelOr
+ (le_par.ast.CastEUnsafe(),
+ rw_par.ast.CastWUnsafe()))
+ left = le_par
+ right = rw_par
+ condCombine = false }
+ { parent =
+ ETree
+ (E.ParallelOr
+ (re_par.ast.CastEUnsafe(),
+ lw_par.ast.CastWUnsafe()))
+ left = re_par
+ right = lw_par
+ condCombine = false }
+ { parent =
+ ETree
+ (E.CascadeOr
+ (le_par.ast.CastEUnsafe(),
+ re_cas.ast.CastEUnsafe()))
+ left = le_par
+ right = re_cas
+ condCombine = false }
+ { parent =
+ ETree
+ (E.CascadeOr
+ (re_par.ast.CastEUnsafe(),
+ le_cas.ast.CastEUnsafe()))
+ left = re_par
+ right = le_cas
+ condCombine = false }
+ { parent =
+ ETree
+ (E.SwitchOrLeft
+ (le_cas.ast.CastEUnsafe(),
+ rf.ast.CastFUnsafe()))
+ left = le_cas
+ right = rf
+ condCombine = false }
+ { parent =
+ ETree
+ (E.SwitchOrLeft
+ (re_cas.ast.CastEUnsafe(),
+ lf.ast.CastFUnsafe()))
+ left = re_cas
+ right = lf
+ condCombine = false }
+ { parent =
+ ETree
+ (E.SwitchOrRight
+ (le_cas.ast.CastEUnsafe(),
+ rf.ast.CastFUnsafe()))
+ left = le_cas
+ right = rf
+ condCombine = false }
+ { parent =
+ ETree
+ (E.SwitchOrRight
+ (re_cas.ast.CastEUnsafe(),
+ lf.ast.CastFUnsafe()))
+ left = re_cas
+ right = lf
+ condCombine = false }
+ { parent =
+ FTree
+ (F.CascadeOr
+ (le_cond_par.ast.CastEUnsafe(),
+ rv.ast.CastVUnsafe()))
+ left = le_cas
+ right = rv
+ condCombine = true }
+ { parent =
+ FTree
+ (F.CascadeOr
+ (re_cond_par.ast.CastEUnsafe(),
+ lv.ast.CastVUnsafe()))
+ left = re_cas
+ right = lv
+ condCombine = true }
+ { parent =
+ FTree
+ (F.SwitchOr
+ (lf.ast.CastFUnsafe(), rf.ast.CastFUnsafe()))
+ left = lf
+ right = rf
+ condCombine = true }
+ { parent =
+ FTree
+ (F.SwitchOr
+ (rf.ast.CastFUnsafe(), lf.ast.CastFUnsafe()))
+ left = rf
+ right = lf
+ condCombine = true }
+ { parent =
+ FTree
+ (F.SwitchOrV
+ (lv.ast.CastVUnsafe(), rv.ast.CastVUnsafe()))
+ left = lv
+ right = rv
+ condCombine = true }
+ { parent =
+ FTree
+ (F.SwitchOrV
+ (rv.ast.CastVUnsafe(), lv.ast.CastVUnsafe()))
+ left = rv
+ right = lv
+ condCombine = true } |]
+
+ let casesWithQ =
+ match maybelq, mayberq with
+ | Some lq, Some rq ->
+ Array.append possibleCases [| { parent =
+ FTree
+ (F.DelayedOr
+ (lq.ast.CastQUnsafe
+ (),
+ rq.ast.CastQUnsafe
+ ()))
+ left = lq
+ right = rq
+ condCombine = true }
+ { parent =
+ FTree
+ (F.DelayedOr
+ (rq.ast.CastQUnsafe
+ (),
+ lq.ast.CastQUnsafe
+ ()))
+ left = rq
+ right = lq
+ condCombine = true } |]
+ | _ -> possibleCases
+
+ Cost.getMinimumCost casesWithQ p_sat p_dissat lweight rweight
+ | Threshold(n, subs) ->
+ let num_cost = scriptNumCost n
+ let avgCost = float n / float subs.Length
+ let e =
+ bestE
+ (subs.[0], (p_sat * avgCost),
+ (p_dissat + p_sat * (1.0 - avgCost)))
+ let ws =
+ subs
+ |> Array.map
+ (fun s ->
+ bestW
+ (s, (p_sat * avgCost),
+ (p_dissat + p_sat * (1.0 - avgCost))))
+ let pk_cost =
+ ws
+ |> Array.fold (fun acc w -> acc + w.pkCost)
+ (1u + num_cost + e.pkCost)
+ let sat_cost =
+ ws |> Array.fold (fun acc w -> acc + w.satCost) e.satCost
+ let dissat_cost =
+ ws |> Array.fold (fun acc w -> acc + w.dissatCost) e.dissatCost
+ let wsast = ws |> Array.map (fun w -> w.ast.CastWUnsafe())
+
+ let cond =
+ { ast = ETree(E.Threshold(n, e.ast.CastEUnsafe(), wsast))
+ pkCost = pk_cost
+ satCost = sat_cost
+ dissatCost = dissat_cost }
+
+ let f = bestF (node, p_sat, 0.0)
+ let cond1 = Cost.likely (f)
+ let cond2 = Cost.unlikely (f)
+ let nonCond = Cost.minCost (cond1, cond2, p_sat, p_dissat)
+ Cost.minCost (cond, nonCond, p_sat, p_dissat)
+
+ and bestQ (node : CompiledNode, p_sat : float, p_dissat : float) : Cost option =
+ match node with
+ | Pk pk ->
+ { ast = QTree(Q.Pubkey(pk))
+ pkCost = 34u
+ satCost = 0.0
+ dissatCost = 0.0 }
+ |> Some
+ | And(l, r) ->
+ let maybelq = bestQ (l, p_sat, p_dissat)
+ let mayberq = bestQ (r, p_sat, p_dissat)
+
+ let cost v q =
+ { ast = QTree(Q.And(v.ast.CastVUnsafe(), q.ast.CastQUnsafe()))
+ pkCost = v.pkCost + q.pkCost
+ satCost = v.satCost + q.satCost
+ dissatCost = 0.0 }
+
+ let op =
+ match maybelq, mayberq with
+ | None, Some rq ->
+ let lv = bestV (l, p_sat, p_dissat)
+ [| cost lv rq |]
+ | Some lq, None ->
+ let rv = bestV (r, p_sat, p_dissat)
+ [| cost rv lq |]
+ | Some lq, Some rq ->
+ let lv = bestV (l, p_sat, p_dissat)
+ let rv = bestV (r, p_sat, p_dissat)
+ [| cost lv rq
+ cost rv lq |]
+ | None, None -> [||]
+
+ if op.Length = 0 then None
+ else
+ op
+ |> Cost.foldCosts p_sat p_dissat
+ |> Some
+ | Or(l, r, lweight, rweight) ->
+ let maybelq = bestQ (l, (p_sat * lweight), 0.0)
+ let mayberq = bestQ (r, (p_sat + rweight), 0.0)
+ match maybelq, mayberq with
+ | Some lq, Some rq ->
+ [| { ast =
+ QTree(Q.Or(lq.ast.CastQUnsafe(), rq.ast.CastQUnsafe()))
+ pkCost = lq.pkCost + rq.pkCost + 3u
+ satCost =
+ lweight * (2.0 + lq.satCost)
+ + rweight * (1.0 + rq.satCost)
+ dissatCost = 0.0 }
+ { ast =
+ QTree(Q.Or(rq.ast.CastQUnsafe(), lq.ast.CastQUnsafe()))
+ pkCost = rq.pkCost + lq.pkCost + 3u
+ satCost =
+ lweight * (1.0 + lq.satCost)
+ + rweight * (2.0 + rq.satCost)
+ dissatCost = 0.0 } |]
+ |> Cost.foldCosts p_sat p_dissat
+ |> Some
+ | _ -> None
+ | _ -> None
+
+ and bestW (node : CompiledNode, p_sat : float, p_dissat : float) : Cost =
+ match node with
+ | Pk k ->
+ { ast = WTree(W.CheckSig(k))
+ pkCost = 36u
+ satCost = 72.0
+ dissatCost = 1.0 }
+ | Time t ->
+ let num_cost = scriptNumCost (!> t)
+ { ast = WTree(W.Time(t))
+ pkCost = 6u + num_cost
+ satCost = 2.0
+ dissatCost = 1.0 }
+ | Hash h ->
+ { ast = WTree(W.HashEqual(h))
+ pkCost = 45u
+ satCost = 33.0
+ dissatCost = 1.0 }
+ | _ ->
+ let c = bestE (node, p_sat, p_dissat)
+ { c with ast = WTree(W.CastE(c.ast.CastEUnsafe()))
+ pkCost = c.pkCost + 2u }
+
+ and bestF (node : CompiledNode, p_sat : float, p_dissat : float) : Cost =
+ match node with
+ | Pk k ->
+ { ast = FTree(F.CheckSig(k))
+ pkCost = 36u
+ satCost = 72.0
+ dissatCost = 1.0 }
+ | Multi(m, pks) ->
+ let num_cost = getPkCost m pks
+ { ast = FTree(F.CheckMultiSig(m, pks))
+ pkCost = uint32 (num_cost + 34 * pks.Length) + 2u
+ satCost = 1.0 + 72.0 * float m
+ dissatCost = 0.0 }
+ | Time t ->
+ let num_cost = scriptNumCost (!> t)
+ { ast = FTree(F.Time(t))
+ pkCost = 2u + num_cost
+ satCost = 0.0
+ dissatCost = 0.0 }
+ | Hash h ->
+ { ast = FTree(F.HashEqual(h))
+ pkCost = 40u
+ satCost = 33.0
+ dissatCost = 0.0 }
+ | And(l, r) ->
+ let vl = bestV (l, p_sat, 0.0)
+ let vr = bestV (r, p_sat, 0.0)
+ let fl = bestF (l, p_sat, 0.0)
+ let fr = bestF (r, p_sat, 0.0)
+
+ let possibleCases =
+ [| { parent =
+ FTree
+ (F.And(vl.ast.CastVUnsafe(), fr.ast.CastFUnsafe()))
+ left = vl
+ right = fr
+ condCombine = false }
+ { parent =
+ FTree
+ (F.And(vr.ast.CastVUnsafe(), fl.ast.CastFUnsafe()))
+ left = vr
+ right = fl
+ condCombine = false } |]
+ Cost.getMinimumCost possibleCases p_sat 0.0 0.5 0.5
+ | Or(l, r, lweight, rweight) ->
+ let le_par = bestE (l, (p_sat * lweight), (p_sat + rweight))
+ let re_par = bestE (r, (p_sat * rweight), (p_sat * lweight))
+ let lf = bestF (l, (p_sat * lweight), 0.0)
+ let rf = bestF (r, (p_sat * rweight), 0.0)
+ let lv = bestV (l, (p_sat * lweight), 0.0)
+ let rv = bestV (r, (p_sat * rweight), 0.0)
+ let maybelq = bestQ (l, (p_sat * lweight), 0.0)
+ let mayberq = bestQ (r, (p_sat * rweight), 0.0)
+
+ let possibleCases =
+ [| { parent =
+ FTree
+ (F.CascadeOr
+ (le_par.ast.CastEUnsafe(),
+ rv.ast.CastVUnsafe()))
+ left = le_par
+ right = rv
+ condCombine = false }
+ { parent =
+ FTree
+ (F.CascadeOr
+ (re_par.ast.CastEUnsafe(),
+ lv.ast.CastVUnsafe()))
+ left = re_par
+ right = lv
+ condCombine = false }
+ { parent =
+ FTree
+ (F.SwitchOr
+ (lf.ast.CastFUnsafe(), rf.ast.CastFUnsafe()))
+ left = lf
+ right = rf
+ condCombine = false }
+ { parent =
+ FTree
+ (F.SwitchOr
+ (rf.ast.CastFUnsafe(), lf.ast.CastFUnsafe()))
+ left = rf
+ right = lf
+ condCombine = false }
+ { parent =
+ FTree
+ (F.SwitchOrV
+ (lv.ast.CastVUnsafe(), rv.ast.CastVUnsafe()))
+ left = lv
+ right = rv
+ condCombine = false }
+ { parent =
+ FTree
+ (F.SwitchOrV
+ (rv.ast.CastVUnsafe(), lv.ast.CastVUnsafe()))
+ left = rv
+ right = lv
+ condCombine = false } |]
+
+ let casesWithQ =
+ match maybelq, mayberq with
+ | Some lq, Some rq ->
+ Array.append possibleCases [| { parent =
+ FTree
+ (F.DelayedOr
+ (lq.ast.CastQUnsafe
+ (),
+ rq.ast.CastQUnsafe
+ ()))
+ left = lq
+ right = rq
+ condCombine = false } |]
+ | _ -> possibleCases
+
+ Cost.getMinimumCost casesWithQ p_sat 0.0 lweight rweight
+ | Threshold(n, subs) ->
+ let num_cost = scriptNumCost n
+ let avg_cost = float n / float subs.Length
+ let e =
+ bestE
+ (subs.[0], (p_sat * avg_cost),
+ (p_dissat + p_sat * (1.0 - avg_cost)))
+ let ws =
+ subs
+ |> Array.map
+ (fun s ->
+ bestW
+ (s, (p_sat * avg_cost),
+ (p_dissat + p_sat * (1.0 - avg_cost))))
+ let pk_cost =
+ ws
+ |> Array.fold (fun acc w -> acc + w.pkCost + 1u)
+ (2u + num_cost + e.pkCost)
+ let sat_cost =
+ ws |> Array.fold (fun acc w -> acc + w.satCost) e.satCost
+ let dissat_cost =
+ ws |> Array.fold (fun acc w -> acc + w.dissatCost) e.dissatCost
+ let wsast = ws |> Array.map (fun w -> w.ast.CastWUnsafe())
+ { ast = FTree(F.Threshold(n, e.ast.CastEUnsafe(), wsast))
+ pkCost = pk_cost
+ satCost = sat_cost * avg_cost + dissat_cost * (1.0 - avg_cost)
+ dissatCost = 0.0 }
+
+ and bestV (node : CompiledNode, p_sat : float, p_dissat : float) : Cost =
+ match node with
+ | Pk k ->
+ { ast = VTree(V.CheckSig(k))
+ pkCost = 35u
+ satCost = 0.0
+ dissatCost = 0.0 }
+ | Multi(m, pks) ->
+ let num_cost = getPkCost m pks
+ { ast = VTree(V.CheckMultiSig(m, pks))
+ pkCost = uint32 (num_cost + 34 * pks.Length + 1)
+ satCost = 1.0 + 72.0 * float m
+ dissatCost = 0.0 }
+ | Time t ->
+ let num_cost = scriptNumCost (!> t)
+ { ast = VTree(V.Time(t))
+ pkCost = 2u + num_cost
+ satCost = 0.0
+ dissatCost = 0.0 }
+ | Hash h ->
+ { ast = VTree(V.HashEqual(h))
+ pkCost = 39u
+ satCost = 33.0
+ dissatCost = 0.0 }
+ | And(l, r) ->
+ let lv = bestV (l, p_sat, 0.0)
+ let rv = bestV (r, p_sat, 0.0)
+ { ast = VTree(V.And(lv.ast.CastVUnsafe(), rv.ast.CastVUnsafe()))
+ pkCost = lv.pkCost + rv.pkCost
+ satCost = lv.satCost + rv.satCost
+ dissatCost = 0.0 }
+ | Or(l, r, lweight, rweight) ->
+ let le_par = bestE (l, (p_sat * lweight), (p_sat * rweight))
+ let re_par = bestE (r, (p_sat * rweight), (p_sat * lweight))
+ let lt = bestT (l, (p_sat * lweight), 0.0)
+ let rt = bestT (r, (p_sat * rweight), 0.0)
+ let lv = bestV (l, (p_sat * lweight), 0.0)
+ let rv = bestV (r, (p_sat * rweight), 0.0)
+ let maybelq = bestQ (l, (p_sat * lweight), 0.0)
+ let mayberq = bestQ (r, (p_sat * rweight), 0.0)
+
+ let possibleCases =
+ [| { parent =
+ VTree
+ (V.CascadeOr
+ (le_par.ast.CastEUnsafe(),
+ rv.ast.CastVUnsafe()))
+ left = le_par
+ right = rv
+ condCombine = false }
+ { parent =
+ VTree
+ (V.CascadeOr
+ (re_par.ast.CastEUnsafe(),
+ lv.ast.CastVUnsafe()))
+ left = re_par
+ right = lv
+ condCombine = false }
+ { parent =
+ VTree
+ (V.SwitchOr
+ (lv.ast.CastVUnsafe(), rv.ast.CastVUnsafe()))
+ left = lv
+ right = rv
+ condCombine = false }
+ { parent =
+ VTree
+ (V.SwitchOr
+ (rv.ast.CastVUnsafe(), lv.ast.CastVUnsafe()))
+ left = rv
+ right = lv
+ condCombine = false }
+ { parent =
+ VTree
+ (V.SwitchOrT
+ (lt.ast.CastTUnsafe(), rt.ast.CastTUnsafe()))
+ left = lt
+ right = rt
+ condCombine = false }
+ { parent =
+ VTree
+ (V.SwitchOrT
+ (rt.ast.CastTUnsafe(), lt.ast.CastTUnsafe()))
+ left = rt
+ right = lt
+ condCombine = false } |]
+
+ let casesWithQ =
+ match maybelq, mayberq with
+ | Some lq, Some rq ->
+ Array.append possibleCases [| { parent =
+ VTree
+ (V.DelayedOr
+ (lq.ast.CastQUnsafe
+ (),
+ rq.ast.CastQUnsafe
+ ()))
+ left = lq
+ right = rq
+ condCombine = false } |]
+ | _ -> possibleCases
+
+ Cost.getMinimumCost casesWithQ p_sat 0.0 lweight rweight
+ | Threshold(n, subs) ->
+ let num_cost = scriptNumCost n
+ let avg_cost = float n / float subs.Length
+ let e =
+ bestE
+ (subs.[0], (p_sat * avg_cost), (p_sat * (1.0 - avg_cost)))
+ let ws =
+ subs
+ |> Array.map
+ (fun s ->
+ bestW
+ (s, (p_sat * avg_cost), (p_sat * (1.0 - avg_cost))))
+ let pk_cost =
+ ws
+ |> Array.fold (fun acc w -> acc + w.pkCost + 1u)
+ (1u + num_cost + e.pkCost)
+ let sat_cost =
+ ws |> Array.fold (fun acc w -> acc + w.satCost) e.satCost
+ let dissat_cost =
+ ws |> Array.fold (fun acc w -> acc + w.dissatCost) e.dissatCost
+ let wsast = ws |> Array.map (fun w -> w.ast.CastWUnsafe())
+ { ast = VTree(V.Threshold(n, e.ast.CastEUnsafe(), wsast))
+ pkCost = pk_cost
+ satCost = sat_cost * avg_cost + dissat_cost * (1.0 - avg_cost)
+ dissatCost = 0.0 }
+
+ type CompiledNode with
+ static member FromPolicy (p : AbstractPolicy) = CompiledNode.fromPolicy p
+ member this.Compile() =
+ let node = CompiledNode.bestT (this, 1.0, 0.0)
+ node.ast
+
\ No newline at end of file
diff --git a/NBitcoin.Miniscript/MiniscriptDecompiler.fs b/NBitcoin.Miniscript/MiniscriptDecompiler.fs
new file mode 100644
index 0000000000..de3ff94a49
--- /dev/null
+++ b/NBitcoin.Miniscript/MiniscriptDecompiler.fs
@@ -0,0 +1,655 @@
+module NBitcoin.Miniscript.Decompiler
+
+open NBitcoin
+open System
+open NBitcoin.Miniscript.Utils.Parser
+open Miniscript.AST
+
+/// Subset of Bitcoin Script which is used in Miniscript
+type Token =
+ private
+ | BoolAnd
+ | BoolOr
+ | Add
+ | Equal
+ | EqualVerify
+ | CheckSig
+ | CheckSigVerify
+ | CheckMultiSig
+ | CheckMultiSigVerify
+ | CheckSequenceVerify
+ | FromAltStack
+ | ToAltStack
+ | Drop
+ | Dup
+ | If
+ | IfDup
+ | NotIf
+ | Else
+ | EndIf
+ | ZeroNotEqual
+ | Size
+ | Swap
+ | Tuck
+ | Verify
+ | Hash160
+ | Sha256
+ | Number of uint32
+ | Hash160Hash of uint160
+ | Sha256Hash of uint256
+ | Pk of NBitcoin.PubKey
+ | Any
+
+type TokenCategory =
+ private
+ | BoolAnd
+ | BoolOr
+ | Add
+ | Equal
+ | EqualVerify
+ | CheckSig
+ | CheckSigVerify
+ | CheckMultiSig
+ | CheckMultiSigVerify
+ | CheckSequenceVerify
+ | FromAltStack
+ | ToAltStack
+ | Drop
+ | Dup
+ | If
+ | IfDup
+ | NotIf
+ | Else
+ | EndIf
+ | ZeroNotEqual
+ | Size
+ | Swap
+ | Tuck
+ | Verify
+ | Hash160
+ | Sha256
+ | Number
+ | Hash160Hash
+ | Sha256Hash
+ | Pk
+ | Any
+
+type ParseException(msg, ex : exn) =
+ inherit Exception(msg, ex)
+ new(msg) = ParseException(msg, null)
+
+type Token with
+ member this.GetItem() =
+ match this with
+ | Number n -> box n |> Some
+ | Hash160Hash h -> box h |> Some
+ | Sha256Hash h -> box h |> Some
+ | Pk pk -> box pk |> Some
+ | _ -> None
+ member this.GetItemUnsafe() =
+ match this with
+ | Number n -> n :> obj
+ | Hash160Hash h -> h :> obj
+ | Sha256Hash h -> h :> obj
+ | Pk pk -> pk :> obj
+ | i -> failwith (sprintf "failed to get item from %A" i)
+
+ // usual reflection is not working for extracing name of each case. So we need this.
+ member this.GetCategory() =
+ match this with
+ | BoolAnd -> TokenCategory.BoolAnd
+ | BoolOr -> TokenCategory.BoolOr
+ | Add -> TokenCategory.Add
+ | Equal -> TokenCategory.Equal
+ | EqualVerify -> TokenCategory.EqualVerify
+ | CheckSig -> TokenCategory.CheckSig
+ | CheckSigVerify -> TokenCategory.CheckSigVerify
+ | CheckMultiSig -> TokenCategory.CheckMultiSig
+ | CheckMultiSigVerify -> TokenCategory.CheckMultiSigVerify
+ | CheckSequenceVerify -> TokenCategory.CheckSequenceVerify
+ | FromAltStack -> TokenCategory.FromAltStack
+ | ToAltStack -> TokenCategory.ToAltStack
+ | Drop -> TokenCategory.Drop
+ | Dup -> TokenCategory.Dup
+ | If -> TokenCategory.If
+ | IfDup -> TokenCategory.IfDup
+ | NotIf -> TokenCategory.NotIf
+ | Else -> TokenCategory.Else
+ | EndIf -> TokenCategory.EndIf
+ | ZeroNotEqual -> TokenCategory.ZeroNotEqual
+ | Size -> TokenCategory.Size
+ | Swap -> TokenCategory.Swap
+ | Tuck -> TokenCategory.Tuck
+ | Verify -> TokenCategory.Verify
+ | Hash160 -> TokenCategory.Hash160
+ | Sha256 -> TokenCategory.Sha256
+ | Number _ -> TokenCategory.Number
+ | Hash160Hash _ -> TokenCategory.Hash160Hash
+ | Sha256Hash _ -> TokenCategory.Sha256Hash
+ | Pk _ -> TokenCategory.Pk
+ | Any -> TokenCategory.Any
+
+let private tryGetItemFromOp (op: Op) =
+ let size = op.PushData.Length
+ match size with
+ | 20 -> Ok(Token.Hash160Hash(uint160 (op.PushData, false)))
+ | 32 ->
+ let i = uint256 (op.PushData, false)
+ Ok(Token.Sha256Hash(i))
+ | 33 ->
+ try
+ Ok(Token.Pk(NBitcoin.PubKey(op.PushData)))
+ with :? FormatException as ex ->
+ Error(ParseException("Invalid Public Key", ex))
+ | _ ->
+ match op.GetInt().HasValue with
+ | true ->
+ let v = op.GetInt().Value
+ /// no need to check v >= 0 since it is checked in NBitcoin side
+ Ok(Token.Number(uint32 v))
+ | false ->
+ Error(ParseException(sprintf "Invalid push with Opcode %O" op))
+
+let private castOpToToken (op : Op) : Result =
+ match (op.Code) with
+ | OpcodeType.OP_BOOLAND -> Ok(Token.BoolAnd)
+ | OpcodeType.OP_BOOLOR -> Ok(Token.BoolOr)
+ | OpcodeType.OP_EQUAL -> Ok(Token.Equal)
+ | OpcodeType.OP_EQUALVERIFY -> Ok(Token.EqualVerify)
+ | OpcodeType.OP_CHECKSIG -> Ok(Token.CheckSig)
+ | OpcodeType.OP_CHECKSIGVERIFY -> Ok(Token.CheckSigVerify)
+ | OpcodeType.OP_CHECKMULTISIG -> Ok(Token.CheckMultiSig)
+ | OpcodeType.OP_CHECKMULTISIGVERIFY -> Ok(Token.CheckMultiSigVerify)
+ | OpcodeType.OP_CHECKSEQUENCEVERIFY -> Ok(Token.CheckSequenceVerify)
+ | OpcodeType.OP_FROMALTSTACK -> Ok(Token.FromAltStack)
+ | OpcodeType.OP_TOALTSTACK -> Ok(Token.ToAltStack)
+ | OpcodeType.OP_DROP -> Ok(Token.Drop)
+ | OpcodeType.OP_DUP -> Ok(Token.Dup)
+ | OpcodeType.OP_IF -> Ok(Token.If)
+ | OpcodeType.OP_IFDUP -> Ok(Token.IfDup)
+ | OpcodeType.OP_NOTIF -> Ok(Token.NotIf)
+ | OpcodeType.OP_ELSE -> Ok(Token.Else)
+ | OpcodeType.OP_ENDIF -> Ok(Token.EndIf)
+ | OpcodeType.OP_0NOTEQUAL -> Ok(Token.ZeroNotEqual)
+ | OpcodeType.OP_SIZE -> Ok(Token.Size)
+ | OpcodeType.OP_SWAP -> Ok(Token.Swap)
+ | OpcodeType.OP_TUCK -> Ok(Token.Tuck)
+ | OpcodeType.OP_VERIFY -> Ok(Token.Verify)
+ | OpcodeType.OP_HASH160 -> Ok(Token.Hash160)
+ | OpcodeType.OP_SHA256 -> Ok(Token.Sha256)
+ | OpcodeType.OP_ADD -> Ok(Token.Add)
+ | OpcodeType.OP_0 -> Ok(Token.Number 0u)
+ | OpcodeType.OP_1 -> Ok(Token.Number 1u)
+ | OpcodeType.OP_2 -> Ok(Token.Number 2u)
+ | OpcodeType.OP_3 -> Ok(Token.Number 3u)
+ | OpcodeType.OP_4 -> Ok(Token.Number 4u)
+ | OpcodeType.OP_5 -> Ok(Token.Number 5u)
+ | OpcodeType.OP_6 -> Ok(Token.Number 6u)
+ | OpcodeType.OP_7 -> Ok(Token.Number 7u)
+ | OpcodeType.OP_8 -> Ok(Token.Number 8u)
+ | OpcodeType.OP_9 -> Ok(Token.Number 9u)
+ | OpcodeType.OP_10 -> Ok(Token.Number 10u)
+ | OpcodeType.OP_11 -> Ok(Token.Number 11u)
+ | OpcodeType.OP_12 -> Ok(Token.Number 12u)
+ | OpcodeType.OP_13 -> Ok(Token.Number 13u)
+ | OpcodeType.OP_14 -> Ok(Token.Number 14u)
+ | OpcodeType.OP_15 -> Ok(Token.Number 15u)
+ | OpcodeType.OP_16 -> Ok(Token.Number 16u)
+ | otherOp when (byte 0x01) <= (byte otherOp) && (byte otherOp) < (byte 0x4B) ->
+ tryGetItemFromOp op
+ | otherOp when (byte 0x4B) <= (byte otherOp) ->
+ Error(ParseException(sprintf "Miniscript does not support pushdata bigger than 33. Got %s" (otherOp.ToString())))
+ | unknown ->
+ Error(ParseException(sprintf "Unknown Opcode to Miniscript %s" (unknown.ToString())))
+
+type State = {
+ ops: Op[]
+ position: int
+}
+
+type private TokenParser = Parser
+
+let nextToken state =
+ if state.ops.Length - 1 < state.position then
+ state, None
+ else
+ let newState = { state with position = state.position + 1 }
+ let tk = state.ops.[state.position]
+ newState, Some(tk)
+
+
+module internal TokenParser =
+ let pToken (cat: TokenCategory) =
+ let name = sprintf "pToken %A" cat
+ let innerFn state =
+ if state.position < 0 then
+ Error(name, "no more input", 0)
+ else
+ let pos = state.position
+ let ops = state.ops.[pos]
+ let r = castOpToToken ops
+ match r with
+ | Error pex ->
+ let msg = sprintf "opcode %s is not supported by Miniscript %s" ops.Name pex.Message
+ Error(name, msg, pos)
+ | Ok actualToken ->
+ let actualCat = actualToken.GetCategory()
+ if cat = Any || cat = actualCat then
+ let newState = { state with position=state.position - 1 }
+ Ok (actualToken.GetItem(), newState)
+ else
+ let msg = sprintf "token is not the one expected \nactual: %A\nexpected: %A" actualCat cat
+ Error(name, msg, pos)
+ {parseFn=innerFn; name=name}
+ let mutable pENoPostProcess, pENoPostProcessImpl = createParserForwardedToRef()
+
+ let mutable pW, pWImpl = createParserForwardedToRef()
+ let mutable pE, pEImpl = createParserForwardedToRef()
+ let mutable pV, pVImpl = createParserForwardedToRef()
+ let mutable pQ, pQImpl = createParserForwardedToRef()
+ let mutable pT, pTImpl = createParserForwardedToRef()
+ let mutable pF, pFImpl = createParserForwardedToRef()
+
+ // ---- common helpers ----
+ let private pTime1 = (pToken EndIf)
+ >>. (pToken (Drop))
+ >>. (pToken (CheckSequenceVerify))
+ >>. (pToken Number)
+ .>> (pToken If) .>> (pToken Dup)
+
+ // TODO: restrict to only specific number
+ let private pNumberN n =
+ let numberValidateParser (maybeNumberObj: obj option) =
+ let name = sprintf "number validator %d" n
+ let innerFn state =
+ let actual = maybeNumberObj.Value :?> uint32
+ if actual = n then
+ Ok(n, state)
+ else
+ let msg = sprintf "failed in number validation\nexpected: %d\nactual: %d" n actual
+ Error(name, msg, state.position)
+
+ {parseFn=innerFn;name=name}
+ (pToken Number) >>= numberValidateParser
+
+ let private multisigBind (expectedType: ASTType) (nAndPks: obj option * obj option list, maybeMObj: obj option) =
+ let n = (fst nAndPks).Value :?> uint32
+ let pks = (snd nAndPks)
+ |> List.rev
+ |> List.toArray
+ |> Array.map(fun pkobj -> pkobj.Value :?> PubKey)
+ let m = maybeMObj.Value :?> uint32
+ let name = sprintf "Parser for Multisig of type %A" expectedType
+ let innerFn (state: State) =
+ if pks.Length = (int n) then
+ match expectedType with
+ | EExpr -> Ok(ETree(E.CheckMultiSig(m, pks)), state)
+ | VExpr -> Ok(VTree(V.CheckMultiSig(m, pks)), state)
+ | _ -> failwith "unreachable!"
+ else
+ let msg = (sprintf "Invalid Multisig Script\nn was %d but actual pubkey length was %d" n pks.Length)
+ Error(name, msg, state.position)
+
+ {parseFn=innerFn; name=name}
+
+
+ // ---- W ---------
+ let pWCheckSig = (pToken CheckSig)
+ >>. (pToken Pk) .>> (pToken Swap)
+ |>> fun maybePKObj -> WTree(W.CheckSig (maybePKObj.Value :?> NBitcoin.PubKey))
+ > "Parser W.Checksig"
+
+ let pWTime = (pTime1
+ .>> (pToken Swap)
+ |>> fun o -> WTree(W.Time(LockTime(o.Value :?> uint32))))
+ > "Parser W.Time"
+
+ let pWCastE = (pToken FromAltStack)
+ >>. (pE) .>> (pToken ToAltStack)
+ |>> fun expr ->
+ WTree(W.CastE(expr.CastEUnsafe()))
+
+ let pWHashEqual = (pToken EndIf >>. pF .>> pToken If .>> pToken ZeroNotEqual .>> pToken Size .>> pToken Swap)
+ >>=(
+ fun ast ->
+ let name = "pWHashEqualValidator"
+ let innerFn state =
+ match ast.CastF() with
+ | Ok fexpr ->
+ match fexpr with
+ | F.HashEqual hash ->
+ Ok(WTree(W.HashEqual(hash)), state)
+ | e ->
+ let msg = sprintf "unexpected expr\nexpected: F.HashEqual\nactual: %A" e
+ Error(name, msg, state.position)
+ | Error e -> failwith "unreachable"
+ {parseFn=innerFn; name=name}
+ )
+
+ // ---- E ---------
+ let pEParallelAnd = ((pToken BoolAnd)
+ >>. pW .>>. pE
+ |>> fun (astW, astE) ->
+ ETree(E.ParallelAnd(astE.CastEUnsafe(), astW.CastWUnsafe())))
+ > "Parser E.ParallelAnd"
+
+ let pEParallelOr = ((pToken BoolOr)
+ >>. pW .>>. pE
+ |>> fun (astW, astE) ->
+ ETree(E.ParallelOr(astE.CastEUnsafe(), astW.CastWUnsafe())))
+ > "Parser E.ParallelAnd"
+
+ let pEThreshold = (((pToken Equal) >>. (pToken Number))
+ .>>. (many1 (pToken Add >>. pW))
+ .>>. (pENoPostProcess)
+ |>> fun (kws, east) ->
+ let k = (fst kws).Value :?> uint32
+ let e = east.CastEUnsafe()
+ let ws = (snd kws)
+ |> List.toArray
+ |> Array.rev
+ |> Array.map(fun ast -> ast.CastWUnsafe())
+ ETree(E.Threshold(k, e, ws))
+ ) > "Parser E.Threshold"
+
+ let pECheckSig = (pToken CheckSig)
+ >>. (pToken Pk)
+ |>> fun maybePKObj -> ETree(E.CheckSig (maybePKObj.Value :?> NBitcoin.PubKey))
+ > "Parser E.Checksig"
+
+ let pECheckMultisig = (pToken CheckMultiSig) >>. (pToken Number)
+ .>>. (many1 (pToken Pk))
+ .>>. (pToken Number)
+ >>= multisigBind EExpr
+
+ let pETime = pWTime
+ <|> (pTime1 |>> fun maybeNumberObj -> ETree(E.Time(LockTime(maybeNumberObj.Value :?> uint32))))
+
+
+ let private pLikelyPrefix = (pToken EndIf) >>. pNumberN(0u) >>. pToken Else >>. pF
+
+ let pEUnlikely = pLikelyPrefix
+ .>> pToken If
+ |>> fun (fexpr) -> ETree(E.Unlikely(fexpr.CastFUnsafe()))
+
+ let pELikely = pLikelyPrefix
+ .>> pToken NotIf
+ |>> fun (fexpr) -> ETree(E.Likely(fexpr.CastFUnsafe()))
+
+ let pECascadeAnd = (pToken EndIf) >>. pF .>> pToken Else
+ .>>. ((pNumberN 0u) >>. (pToken NotIf) >>. pE)
+ |>> fun (rightF, leftE) ->
+ ETree(E.CascadeAnd(leftE.CastEUnsafe(), rightF.CastFUnsafe()))
+
+ let pESwitchOrLeft = ((pToken EndIf) >>. pF .>> pToken Else)
+ .>>. ((pE) .>> pToken If)
+ |>> fun (rightF, leftE) ->
+ ETree(E.SwitchOrLeft(leftE.CastEUnsafe(), rightF.CastFUnsafe()))
+
+ let pESwitchOrRight = (pToken EndIf >>. pF .>> pToken Else)
+ .>>. (pE .>> pToken NotIf)
+ |>> fun (rightF, leftE) ->
+ ETree(E.SwitchOrRight(leftE.CastEUnsafe(), rightF.CastFUnsafe()))
+
+ // ---- V -------
+ let pVDelayedOr = (((pToken CheckSigVerify)
+ >>. (pToken EndIf) >>. pQ) .>>. (pToken Else >>. pQ .>> pToken If)
+ |>> fun (q1, q2) ->
+ VTree(V.DelayedOr(q2.CastQUnsafe(), q1.CastQUnsafe()))
+ ) > "P.VDelayedOr"
+
+ let pVHashEqual = ((pToken EqualVerify) >>. ((pToken Sha256Hash)
+ .>> (pToken Sha256) .>> (pToken EqualVerify) .>> (pNumberN 32u) .>> (pToken Size))
+ |>> (fun maybeHashObj ->
+ let hash = maybeHashObj.Value :?> uint256
+ VTree(V.HashEqual(hash))
+ )
+ ) > "Parser pVHashEqual"
+
+ let pVThreshold = ((pToken EqualVerify) >>. (pToken Number))
+ .>>. (many1 (pToken Add >>. pW))
+ .>>. (pE)
+ |>> fun (kws, east) ->
+ let k = (fst kws).Value :?> uint32
+ let e = east.CastEUnsafe()
+ let ws = (snd kws)
+ |> List.toArray
+ |> Array.rev
+ |> Array.map(fun ast -> ast.CastWUnsafe())
+ VTree(V.Threshold(k, e, ws))
+
+ let pVCheckSig = ((pToken CheckSigVerify)
+ >>. (pToken Pk)
+ |>> fun maybePkObj -> VTree(V.CheckSig(maybePkObj.Value :?> PubKey))
+ ) > "Parser pVCheckSig"
+
+ let pVCheckMultisig = (pToken CheckMultiSigVerify)
+ >>. (pToken Number)
+ .>>. (many1 (pToken Pk))
+ .>>. (pToken Number)
+ >>= multisigBind VExpr
+
+ let pVTime = pToken Drop >>. pToken CheckSequenceVerify >>. pToken Number
+ |>> fun maybeNumberObj ->
+ let n = maybeNumberObj.Value :?> uint32
+ VTree(V.Time(LockTime(n)))
+
+ let pVSwitchOr = (pToken EndIf >>. pV .>> pToken Else)
+ .>>. (pV .>> pToken If)
+ |>> fun (rightV, leftV) ->
+ VTree(V.SwitchOr(leftV.CastVUnsafe(), rightV.CastVUnsafe()))
+
+ let pVCascadeOr = (pToken EndIf >>. pV .>> pToken NotIf)
+ .>>. pE
+ |>> fun (rightV, leftE) ->
+ VTree(V.CascadeOr(leftE.CastEUnsafe(), rightV.CastVUnsafe()))
+
+ let pVSwitchOrT = (pToken Verify >>. pToken EndIf >>. pT .>> pToken Else)
+ .>>. (pT .>> pToken If)
+ |>> fun (rightT, leftT) ->
+ VTree(V.SwitchOrT(leftT.CastTUnsafe(), rightT.CastTUnsafe()))
+
+ // ---- Q -------
+ let pQPubKey = ((pToken Pk)
+ |>> fun pk -> QTree(Q.Pubkey(pk.Value :?> NBitcoin.PubKey))
+ ) > "P.QPubKey"
+
+
+ let pQOr = ((pToken EndIf) >>. pQ)
+ .>>. ((pToken Else) >>. pQ .>> pToken(If))
+ |>> fun (l, r) -> QTree(Q.Or(r.CastQUnsafe(), l.CastQUnsafe()))
+ // ---- T -------
+
+ let pTHashEqual = ((pToken Equal
+ >>. pToken Sha256Hash
+ .>> pToken Sha256
+ .>> pToken EqualVerify
+ .>> pNumberN 32u
+ .>> pToken Size)
+ |>> fun maybeHash -> TTree(T.HashEqual(maybeHash.Value :?> uint256)))
+ > "Parser T.HashEqual"
+
+ let pTDelayedOr = ((pToken CheckSig) >>. (pToken EndIf)
+ >>. pQ .>>. (pToken Else >>. pQ .>> pToken If)
+ |>> fun (q1, q2) -> TTree(T.DelayedOr(q2.CastQUnsafe(), q2.CastQUnsafe()))
+ ) > "Parser T.DelayedOr"
+
+ let pTTime = ((pToken CheckSequenceVerify) >>. (pToken Number)
+ |>> fun (maybeNumberObj) ->
+ let n = maybeNumberObj.Value :?> uint32
+ TTree(T.Time(LockTime(n)))
+ ) > "Parser T.Time"
+
+ let pTSwitchOr = ((pToken EndIf >>. pT .>> pToken Else)
+ .>>. (pT .>> pToken If)
+ |>> fun (rightT, leftT) ->
+ TTree(T.SwitchOr(leftT.CastTUnsafe(), rightT.CastTUnsafe()))
+ ) > "Parser T.SwitchOr"
+
+ let pTCascadeOr = (pToken EndIf >>. pT .>> pToken NotIf .>> pToken IfDup)
+ .>>. pE
+ |>> fun (rightT, leftE) ->
+ TTree(T.CascadeOr(leftE.CastEUnsafe(), rightT.CastTUnsafe()))
+ // ---- F -------
+ let pFTime = (pToken ZeroNotEqual)
+ >>. (pToken CheckSequenceVerify)
+ >>. (pToken Number)
+ |>> fun (maybeNumberObj) ->
+ let n = maybeNumberObj.Value :?> uint32
+ FTree(F.Time(LockTime(n)))
+
+ let pFSwitchOr = ((pToken EndIf) >>. pF .>> pToken Else)
+ .>>. (pF .>> pToken If)
+ |>> fun (rightF, leftF) ->
+ FTree(F.SwitchOr(leftF.CastFUnsafe(), rightF.CastFUnsafe()))
+
+ let pFFromV = (pNumberN 1u >>. pV)
+ >>=(
+ fun ast ->
+ let name = "pFFromV"
+ let innerFn state =
+ match ast.CastVUnsafe() with
+ | V.CheckSig pk ->
+ Ok(FTree(F.CheckSig(pk)), state)
+ | V.CheckMultiSig (m, pks) ->
+ Ok(FTree(F.CheckMultiSig(m, pks)), state)
+ | V.HashEqual hash ->
+ Ok(FTree(F.HashEqual(hash)), state)
+ | V.Threshold(k, e, ws)->
+ Ok(FTree(F.Threshold(k, e, ws)), state)
+ | V.CascadeOr(l, r)->
+ Ok(FTree(F.CascadeOr(l, r)), state)
+ | V.SwitchOr(l, r)->
+ Ok(FTree(F.SwitchOrV(l, r)), state)
+ | V.DelayedOr(l, r)->
+ Ok(FTree(F.DelayedOr(l, r)), state)
+ | e ->
+ let msg = sprintf "unexpected expr\nactual: %A" e
+ Error(name, msg, state.position)
+ {parseFn=innerFn; name=name}
+ )
+
+ // ---- Composition ----
+ let mutable SubExpressionParser, SubExpressionParserImpl = createParserForwardedToRef()
+ let private shouldPostProcess(info: AST * State) =
+ let ast = fst info
+ let state = snd info
+ if state.position = -1 then
+ Ok(false)
+ else
+ /// If last opcode is a certain one, no need for post processing.
+ let checkLastOp state =
+ let lastOp = state.ops.[state.position]
+ let lastToken = castOpToToken lastOp
+ match lastToken with
+ | Error e -> Error ("PostProcess",
+ sprintf "Unexpected Exception in post process\nerror: %A" e,
+ 0)
+ | Ok(Token.If)
+ | Ok(Token.NotIf)
+ | Ok(Token.Else) -> Ok(false)
+ | Ok(Token.ToAltStack) -> Ok(false)
+ | _ -> Ok(true)
+
+ match ast.GetASTType() with
+ | TExpr
+ | VExpr
+ | EExpr
+ | QExpr
+ | FExpr ->
+ checkLastOp state
+ | _ -> Ok(false)
+
+ let postProcess (ast: AST) =
+ let name = "postProcess"
+ let innerFn state =
+ match shouldPostProcess(ast, state) with
+ | Error e ->
+ Error e
+ | Ok(false) ->
+ Ok((ast), state)
+ | Ok(true) ->
+ let rightAST = ast
+
+ match run SubExpressionParser state with
+ | Error e ->
+ Error e
+ | Ok result ->
+ let leftAST, state = result
+ let leftV = leftAST.CastVUnsafe()
+ match (rightAST.GetASTType()) with
+ | TExpr -> Ok(TTree(T.And(leftV, rightAST.CastTUnsafe())), state)
+ | EExpr ->
+ Ok(TTree(T.And(leftV, rightAST.CastTUnsafe())), state)
+ | QExpr -> Ok(QTree(Q.And(leftV, rightAST.CastQUnsafe())), state)
+ | FExpr ->
+ match rightAST.CastT() with
+ | Ok t -> Ok(TTree(t), state)
+ | Error _ -> Ok(FTree(F.And(leftV, rightAST.CastFUnsafe())), state)
+ | VExpr -> Ok(VTree(V.And(leftV, rightAST.CastVUnsafe())), state)
+ | _ -> failwith "unreachable"
+
+ {parseFn=innerFn; name = name}
+
+ /// validate AST is a specific type
+ let pTryCastToType (expected: ASTType) (ast: AST) =
+ let name = "pIsTypeOf"
+ let innerFn state =
+ if ast.GetASTType() = expected then
+ Ok(ast, state)
+ else if expected = TExpr && ast.IsT() then
+ Ok(TTree(ast.CastTUnsafe()), state)
+ else
+ let msg = sprintf "AST is not the expected type\nexpected: %A\nactual: %A" expected ast
+ Error(name, msg, state.position)
+ {parseFn=innerFn; name=name}
+
+ do pENoPostProcessImpl := choice [
+ pECheckSig
+ pEParallelAnd
+ pEParallelOr
+ pEThreshold
+ pECheckMultisig
+ pETime
+ pESwitchOrLeft
+ pESwitchOrRight
+ pELikely
+ pEUnlikely
+ pECascadeAnd
+ ]
+
+ do SubExpressionParserImpl := (choice [
+ pWCheckSig; pWTime; pWCastE; pWHashEqual
+ pENoPostProcess
+ pVDelayedOr
+ pVHashEqual
+ pVThreshold
+ pVCheckSig
+ pVCheckMultisig
+ pVTime
+ pVSwitchOr
+ pVCascadeOr
+ pVSwitchOrT
+ pQPubKey; pQOr
+ pTHashEqual; pTDelayedOr; pTTime; pTSwitchOr; pTCascadeOr
+ pFTime
+ pFSwitchOr
+ pFFromV
+ ] >>= postProcess) > "SubexpressionParser"
+
+ do pWImpl := SubExpressionParser >>= pTryCastToType WExpr
+ do pEImpl := SubExpressionParser >>= pTryCastToType EExpr
+ do pVImpl := SubExpressionParser >>= pTryCastToType VExpr
+ do pQImpl := SubExpressionParser >>= pTryCastToType QExpr
+ do pTImpl := SubExpressionParser >>= pTryCastToType TExpr
+ do pFImpl := SubExpressionParser >>= pTryCastToType FExpr
+
+let internal parseScript (sc: Script) =
+ let ops = (sc.ToOps() |> Seq.toArray)
+ let initialState = {ops=ops; position=ops.Length - 1}
+ run TokenParser.SubExpressionParser initialState |> Result.map(fst)
+
+let internal parseScriptUnsafe sc =
+ match parseScript sc with
+ | Ok r -> r
+ | Error e -> failwith (printParserError e)
diff --git a/NBitcoin.Miniscript/MiniscriptParser.fs b/NBitcoin.Miniscript/MiniscriptParser.fs
new file mode 100644
index 0000000000..70d5b67edb
--- /dev/null
+++ b/NBitcoin.Miniscript/MiniscriptParser.fs
@@ -0,0 +1,143 @@
+namespace NBitcoin.Miniscript
+
+open NBitcoin
+open System.Text.RegularExpressions
+open System
+
+/// High level representation of Miniscript
+type AbstractPolicy =
+ | Key of PubKey
+ | Multi of uint32 * PubKey []
+ | Hash of uint256
+ | Time of NBitcoin.LockTime
+ | Threshold of uint32 * AbstractPolicy []
+ | And of AbstractPolicy * AbstractPolicy
+ | Or of AbstractPolicy * AbstractPolicy
+ | AsymmetricOr of AbstractPolicy * AbstractPolicy
+ override this.ToString() =
+ match this with
+ | Key k1 -> sprintf "pk(%s)" (string (k1.ToHex()))
+ | Multi(m, klist) ->
+ klist
+ |> Seq.map (fun k -> string (k.ToHex()))
+ |> Seq.reduce (fun a b -> sprintf "%s,%s" a b)
+ |> sprintf "multi(%d,%s)" m
+ | Hash h -> sprintf "hash(%s)" (string (h.ToString()))
+ | Time t -> sprintf "time(%d)" (t.Value)
+ | Threshold(m, plist) ->
+ plist
+ |> Array.map (fun p -> p.ToString())
+ |> Array.reduce (fun a b -> sprintf "%s,%s" a b)
+ |> sprintf "thres(%d,%s)" m
+ | And(p1, p2) -> sprintf "and(%s,%s)" (p1.ToString()) (p2.ToString())
+ | Or(p1, p2) -> sprintf "or(%s,%s)" (p1.ToString()) (p2.ToString())
+ | AsymmetricOr(p1, p2) ->
+ sprintf "aor(%s,%s)" (p1.ToString()) (p2.ToString())
+
+module MiniscriptParser =
+
+ // parser
+ let quoted = Regex(@"\((.*)\)")
+
+ let rec (|SurroundedByBrackets|_|) (s : string) =
+ let s2 = s.Trim()
+ if s2.StartsWith("(") && s2.EndsWith(")") then
+ Some(s2.TrimStart('(').TrimEnd(')'))
+ else None
+
+ let (|Expression|_|) (prefix : string) (s : string) =
+ let s = s.Trim()
+ if s.StartsWith(prefix) then Some(s.Substring(prefix.Length))
+ else None
+
+ let (|PubKeyPattern|_|) (s : string) =
+ try
+ Some(PubKey(s))
+ with :? FormatException as ex -> None
+
+ let (|PubKeysPattern|_|) (s : string) =
+ let s = s.Trim().Split(',')
+ match UInt32.TryParse(s.[0]) with
+ | (false, _) -> None
+ | (true, i) ->
+ try
+ let pks =
+ s.[1..s.Length - 1] |> Array.map (fun hex -> PubKey(hex.Trim()))
+ Some(i, pks)
+ with :? FormatException -> None
+
+ let (|Hash|_|) (s : string) =
+ try
+ Some(uint256 (s.Trim()))
+ with :? FormatException -> None
+
+ let (|Time|_|) (s : string) =
+ try
+ Some(uint32 (s.Trim()))
+ with :? FormatException -> None
+
+ // Split with "," but only when not surroounded by parenthesis
+ let rec safeSplit (s : string) (acc : string list) (index : int) (openNum : int)
+ (currentChunk : char []) =
+ if s.Length = index then
+ let lastChunk = String.Concat(Array.append currentChunk [| ')' |])
+ let lastAcc = List.append acc [ lastChunk ]
+ lastAcc |> List.toArray
+ else
+ let c = s.[index]
+ if c = '(' then
+ let newChunk = Array.append currentChunk [| c |]
+ safeSplit s acc (index + 1) (openNum + 1) newChunk
+ elif c = ')' then
+ let newChunk = Array.append currentChunk [| c |]
+ safeSplit s acc (index + 1) (openNum - 1) newChunk
+ elif openNum = 0 && (c = ',') then
+ let newElement = String.Concat(currentChunk)
+ let newAcc = List.append acc [ newElement ]
+ safeSplit s newAcc (index + 1) (openNum) [||]
+ else
+ let newChunk = Array.append currentChunk [| c |]
+ safeSplit s acc (index + 1) (openNum) newChunk
+
+ let rec (|AbstractPolicy|_|) s =
+ let s = Regex.Replace(s, @"[|\s|\n|\r\n]+", "")
+ match s with
+ | Expression "pk" (SurroundedByBrackets(PubKeyPattern pk)) -> Some(Key pk)
+ | Expression "multi" (SurroundedByBrackets(PubKeysPattern pks)) ->
+ Multi((fst pks), (snd pks)) |> Some
+ | Expression "hash" (SurroundedByBrackets(Hash hash)) -> Some(Hash hash)
+ | Expression "time" (SurroundedByBrackets(Time t)) -> Some(Time(LockTime(t)))
+ // recursive matches
+ | Expression "thres" (SurroundedByBrackets(Threshold thres)) ->
+ Some(Threshold(thres))
+ | Expression "and" (SurroundedByBrackets(And(expr1, expr2))) ->
+ And(expr1, expr2) |> Some
+ | Expression "or" (SurroundedByBrackets(Or(expr1, expr2))) ->
+ Or(expr1, expr2) |> Some
+ | Expression "aor" (SurroundedByBrackets(AsymmetricOr(expr1, expr2))) ->
+ AsymmetricOr(expr1, expr2) |> Some
+ | _ -> None
+
+ and (|Threshold|_|) (s : string) =
+ let s = safeSplit s [] 0 0 [||]
+ let thresholdStr = s.[0]
+ match UInt32.TryParse(thresholdStr) with
+ | (true, threshold) ->
+ let subPolicy = s.[1..s.Length - 1] |> Array.choose ((|AbstractPolicy|_|))
+ if subPolicy.Length <> s.Length - 1 then None
+ else Some(threshold, subPolicy)
+ | (false, _) -> None
+
+ and (|And|_|) (s : string) = twoSubExpressions s
+
+ and (|Or|_|) (s : string) = twoSubExpressions s
+
+ and (|AsymmetricOr|_|) (s : string) = twoSubExpressions s
+
+ and twoSubExpressions (s : string) =
+ let s = safeSplit s [] 0 0 [||]
+ if s.Length <> 2 then None
+ else
+ let subPolicies = s |> Array.choose ((|AbstractPolicy|_|))
+ if subPolicies.Length <> s.Length then None
+ else Some(subPolicies.[0], subPolicies.[1])
diff --git a/NBitcoin.Miniscript/NBitcoin.Miniscript.fsproj b/NBitcoin.Miniscript/NBitcoin.Miniscript.fsproj
new file mode 100644
index 0000000000..e97ae157db
--- /dev/null
+++ b/NBitcoin.Miniscript/NBitcoin.Miniscript.fsproj
@@ -0,0 +1,30 @@
+
+
+ net461;netcoreapp2.1;netstandard2.0
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NBitcoin.Miniscript/PSBTExtension.fs b/NBitcoin.Miniscript/PSBTExtension.fs
new file mode 100644
index 0000000000..7127ca71cf
--- /dev/null
+++ b/NBitcoin.Miniscript/PSBTExtension.fs
@@ -0,0 +1,195 @@
+namespace NBitcoin
+open System
+open System.Linq
+open System.Collections.Generic
+open System.Runtime.CompilerServices
+open System.Runtime.InteropServices
+open NBitcoin.Miniscript
+open NBitcoin.Miniscript.Utils
+open NBitcoin.BIP174
+
+type PSBTFinalizationException(msg: string, ex: exn) =
+ inherit System.Exception(msg, ex)
+ new (msg) = PSBTFinalizationException(msg, null)
+
+[]
+type PSBTExtension =
+ static member private keyFn (psbtin: PSBTInput) (sc: Script) (pk: PubKey): TransactionSignature =
+ let sigHash = if psbtin.SighashType = SigHash.Undefined then SigHash.All else psbtin.SighashType
+ match psbtin.PartialSigs.TryGetValue(pk.Hash) with
+ | (true, sigPair) -> TransactionSignature(snd sigPair, sigHash)
+ | (false, _) -> null
+
+ static member private getSig (partialSigs: IDictionary<_, _>) =
+ try
+ partialSigs.First() |> Some
+ with
+ | :? InvalidOperationException as e ->
+ None
+
+ static member private tryCheckWitness
+ (hashFn: Func)
+ (age: uint32)
+ (psbt: PSBT)
+ (sigHash)
+ (dummyTX: Transaction)
+ (index: int)
+ (prevOut: TxOut)
+ (ctx: ScriptEvaluationContext)
+ (isP2SH: bool)
+ (spk: Script): Result =
+ let psbtin = psbt.Inputs.[index]
+ if PayToWitPubKeyHashTemplate.Instance.CheckScriptPubKey(spk) then
+ match PSBTExtension.getSig psbtin.PartialSigs with
+ | None -> Error(PSBTFinalizationException("No signature for p2pkh"))
+ | Some sigPair ->
+ let txSig = TransactionSignature(snd sigPair.Value, sigHash)
+ dummyTX.Inputs.[index].WitScript <- PayToWitPubKeyHashTemplate.Instance.GenerateWitScript(txSig, fst sigPair.Value)
+ let ss = if isP2SH then Script(Op.GetPushOp(spk.ToBytes())) else Script.Empty
+ if not (ctx.VerifyScript(ss, dummyTX, index, prevOut)) then
+ let errorMsg = sprintf "Script verification failed for p2wpkh %s" (ctx.Error.ToString())
+ Error(PSBTFinalizationException(errorMsg))
+ else
+ psbt.Inputs.[index].FinalScriptSig <- ss
+ psbt.Inputs.[index].FinalScriptWitness <- dummyTX.Inputs.[index].WitScript
+ psbt.Inputs.[index].ClearForFinalize()
+ Ok(psbt)
+ // p2wsh
+ else if PayToWitScriptHashTemplate.Instance.CheckScriptPubKey(spk) then
+ match Miniscript.fromScript psbtin.WitnessScript with
+ | Error msg ->
+ let errorMsg = "Failed to parse p2wsh as a Miniscript: " + msg
+ Error(PSBTFinalizationException(errorMsg))
+ | Ok ms ->
+ match ms.Satisfy(PSBTExtension.keyFn psbtin psbtin.WitnessScript, hashFn, age) with
+ | Error fCase ->
+ let msg = sprintf "Failed to satisfy p2wsh script: %A" fCase
+ Error(PSBTFinalizationException(msg))
+ | Ok items ->
+ let pushes = items |> List.toArray |> Array.map(fun i -> i.ToPushOps())
+ let ss = if isP2SH then Script(Op.GetPushOp(spk.ToBytes())) else Script.Empty
+ dummyTX.Inputs.[index].WitScript <- PayToWitScriptHashTemplate.Instance.GenerateWitScript(pushes, psbtin.WitnessScript)
+ if not (ctx.VerifyScript(ss, dummyTX, index, prevOut)) then
+ let msg = sprintf "Script verification failed for following p2wsh;\nErrorCode: %s\nScript:%s\nPushItems: %A"
+ (ctx.Error.ToString())
+ (psbtin.WitnessScript.ToString())
+ items
+ Error (PSBTFinalizationException(msg))
+ else
+ psbt.Inputs.[index].FinalScriptWitness <- dummyTX.Inputs.[index].WitScript
+ psbt.Inputs.[index].FinalScriptSig <- ss
+ psbt.Inputs.[index].ClearForFinalize()
+ Ok(psbt)
+ else
+ let msg = sprintf "Unknown type of script %s" (spk.ToString())
+ Error(PSBTFinalizationException(msg))
+
+ static member private isBareP2SH (psbtin: PSBTInput) =
+ (isNull psbtin.WitnessScript) && (PayToWitTemplate.Instance.CheckScriptPubKey(psbtin.RedeemScript) |> not)
+
+ []
+ static member FinalizeIndex(psbt: PSBT,
+ index: int,
+ [)>] hashFn: Func,
+ [] age: uint32) =
+ let psbtin = psbt.Inputs.[index]
+ let txin: TxIn = psbt.tx.Inputs.[index]
+ if psbtin.IsFinalized() then
+ Ok(psbt)
+ else if isNull (psbtin.GetOutput(txin.PrevOut)) then
+ Error(PSBTFinalizationException("Can not fiinlize PSBTInput without utxo"))
+ else
+ let prevOut: TxOut = psbtin.GetOutput(txin.PrevOut)
+ let dummyTX = psbt.tx.Clone()
+ let sigHash = if psbtin.SighashType = SigHash.Undefined then SigHash.All else psbtin.SighashType
+ let mutable context = ScriptEvaluationContext()
+ context.SigHash <- sigHash
+
+ let spk = prevOut.ScriptPubKey
+ let tryCheckWitness = PSBTExtension.tryCheckWitness hashFn age psbt sigHash dummyTX index prevOut context
+
+ // p2pkh
+ if (PayToPubkeyHashTemplate.Instance.CheckScriptPubKey(spk)) then
+ match PSBTExtension.getSig psbtin.PartialSigs with
+ | None -> Error(PSBTFinalizationException("No signature for p2pkh"))
+ | Some sigPair ->
+ let txSig = TransactionSignature(snd sigPair.Value, sigHash)
+ let ss = PayToPubkeyHashTemplate.Instance.GenerateScriptSig(txSig, fst sigPair.Value)
+ if not (context.VerifyScript(ss, dummyTX, index, prevOut)) then
+ let errorMsg = sprintf "Script verification failed for p2pkh %s" (context.Error.ToString())
+ Error(PSBTFinalizationException(errorMsg))
+ else
+ psbtin.FinalScriptSig <- ss
+ psbt.Inputs.[index].ClearForFinalize()
+ Ok(psbt)
+ // p2sh
+ else if spk.IsPayToScriptHash then
+ if isNull psbtin.RedeemScript then
+ Error(PSBTFinalizationException("no redeem scirpt for p2sh"))
+ else if PSBTExtension.isBareP2SH psbtin then
+ match Miniscript.fromScript psbtin.RedeemScript with
+ | Error msg ->
+ let msg = "Failed to parse p2sh as a Miniscript: " + msg
+ Error(PSBTFinalizationException(msg))
+ | Ok ms ->
+ match ms.Satisfy(PSBTExtension.keyFn psbtin psbtin.RedeemScript, hashFn, age) with
+ | Error fCase ->
+ let msg = sprintf "Failed to satisfy p2sh redeem script: %A" fCase
+ Error(PSBTFinalizationException(msg))
+ | Ok items ->
+ let pushes = items |> List.toArray |> Array.map(fun i -> i.ToPushOps())
+ let ss = PayToScriptHashTemplate.Instance.GenerateScriptSig(pushes, psbtin.RedeemScript)
+ if not (context.VerifyScript(ss, dummyTX, index, prevOut)) then
+ let msg = sprintf "Script verification failed for following p2sh;\nErrorCode: %s\nScriptWithPushItems: %s\nScript:%s\nPushItems: %A"
+ (context.Error.ToString())
+ (ss.ToString())
+ (psbtin.RedeemScript.ToString())
+ items
+ Error (PSBTFinalizationException(msg))
+ else
+ psbtin.FinalScriptSig <- ss
+ psbt.Inputs.[index].ClearForFinalize()
+ Ok(psbt)
+ else
+ // p2sh-p2wpkh, p2sh-p2wsh
+ tryCheckWitness true (psbtin.RedeemScript)
+ else
+ // p2wpkh, p2wsh
+ tryCheckWitness false (spk)
+
+
+ []
+ static member FinalizeIndexUnsafe(psbt: PSBT,
+ index: int,
+ [)>] hashFn: Func,
+ [] age: uint32) =
+
+ match psbt.FinalizeIndex(index, hashFn, age) with
+ | Ok psbt -> psbt
+ | Error e -> raise e
+
+ // Finalize all inputs.
+ []
+ static member Finalize(psbt: PSBT,
+ [)>] hashFn: Func,
+ [] age: uint32) =
+ let inline resultFolder (acc) (r): Result =
+ match acc, r with
+ | Error e1 , Error e2 -> Error(e1 @ e2)
+ | Error e, Ok _ -> Error e
+ | Ok _, Error e -> Error e
+ | Ok _, Ok psbt2 -> Ok psbt2
+
+ let r = seq { 0 .. psbt.Inputs.Count - 1 }
+ |> Seq.map(fun i -> psbt.FinalizeIndex(i, hashFn, age))
+ |> Seq.map(Result.mapError(fun e -> [e]))
+ |> Seq.reduce resultFolder
+ |> Result.mapError(fun es -> AggregateException(es |> List.map(fun e -> e :> exn)))
+ r
+ []
+ static member FinalizeUnsafe(psbt: PSBT,
+ [)>] hashFn: Func,
+ [] age: uint32) =
+ match psbt.Finalize(hashFn, age) with
+ | Ok psbt -> psbt
+ | Error e -> raise e
\ No newline at end of file
diff --git a/NBitcoin.Miniscript/Satisfy.fs b/NBitcoin.Miniscript/Satisfy.fs
new file mode 100644
index 0000000000..65cbba945b
--- /dev/null
+++ b/NBitcoin.Miniscript/Satisfy.fs
@@ -0,0 +1,320 @@
+namespace NBitcoin.Miniscript
+
+open NBitcoin
+open System
+
+type SignatureProvider = PubKey -> TransactionSignature option
+type PreImageHash = uint256
+type PreImage = uint256
+type PreImageProvider = PreImageHash -> PreImage option
+
+type ProviderSet = (SignatureProvider option * PreImageProvider option * LockTime option)
+
+type CSVOffset = BlockHeight of uint32 | UnixTime of DateTimeOffset
+type FailureCase =
+ | MissingSig of PubKey list
+ | MissingHash of uint256
+ | NotMatured of CSVOffset
+ | LockTimeTypeMismatch
+ | Nested of FailureCase list
+ | CurrentTimeNotSpecified
+
+type SatisfiedItem =
+ | PreImage of uint256
+ | Signature of TransactionSignature
+ | RawPush of byte[]
+ with member this.ToBytes(): byte array =
+ match this with
+ | RawPush i -> i
+ | PreImage i -> i.ToBytes()
+ | Signature i -> i.ToBytes()
+ member this.ToPushOps(): Op =
+ Op.GetPushOp(this.ToBytes())
+
+type SatisfactionResult = Result
+
+module internal Satisfy =
+ open NBitcoin.Miniscript.AST
+ open NBitcoin.Miniscript.Utils
+ open NBitcoin
+ open System
+
+ let satisfyCost (res: SatisfiedItem list): int =
+ res |> List.fold(fun a b -> 1 + b.ToBytes().Length + a) 0
+
+ let (>>=) xR f = Result.bind f xR
+
+ // ------- helpers --------
+ let satisfyCheckSig (maybeKeyFn: SignatureProvider option) k =
+ match maybeKeyFn with
+ | None -> Error(MissingSig([k]))
+ | Some keyFn ->
+ match keyFn k with
+ | None -> Error (MissingSig [k])
+ | Some(txSig) -> Ok([Signature(txSig)])
+
+ let satisfyCheckMultisig (maybeKeyFn: SignatureProvider option) (m, pks) =
+ match maybeKeyFn with
+ | None -> Error(MissingSig(pks |> List.ofArray))
+ | Some keyFn ->
+ let maybeSigList = pks
+ |> Array.map(keyFn)
+ |> Array.toList
+
+ let sigList = maybeSigList |> List.choose(id) |> List.map(Signature)
+
+ if sigList.Length >= (int32 m) then
+ Ok([RawPush [||]] @ sigList)
+ else
+ let sigNotFoundPks = maybeSigList
+ |> List.zip (pks |> Array.toList)
+ |> List.choose(fun (pk, maybeSig) ->
+ if maybeSig.IsNone then Some(pk) else None)
+ Error(MissingSig(sigNotFoundPks))
+
+ let satisfyHashEqual (maybeHashFn: PreImageProvider option) h =
+ match maybeHashFn with
+ | None -> Error(MissingHash(h))
+ | Some fn ->
+ match fn h with
+ | None -> Error(MissingHash h)
+ | Some v -> Ok([PreImage(v)])
+
+ let satisfyCSVCore (age: LockTime) (t: LockTime) =
+ let offset = (int32 t.Value) - (int32 age.Value)
+ if (age.IsHeightLock && t.IsHeightLock) then
+ if (offset > 0) then
+ Error(NotMatured(BlockHeight (uint32 offset)))
+ else
+ Ok([])
+ else if (age.IsTimeLock && t.IsTimeLock) then
+ if (offset > 0) then
+ Error(NotMatured(UnixTime(DateTimeOffset.FromUnixTimeSeconds(int64 (offset)))))
+ else
+ Ok([])
+ else
+ Error(LockTimeTypeMismatch)
+
+ let satisfyCSV (age: LockTime option) (t: LockTime) =
+ match age with
+ | None -> Error(CurrentTimeNotSpecified)
+ | Some a -> satisfyCSVCore a t
+
+ let rec satisfyThreshold (providers) (k, e, ws): SatisfactionResult =
+ let keyFn, hashFn, age = providers
+ let flatten l = List.collect id l
+ let wsList = ws |> Array.toList
+
+ let wResult = wsList
+ |> List.rev
+ |> List.map(satisfyW providers)
+ let wOkList = wResult
+ |> List.filter(fun wr -> match wr with | Ok w -> true;| _ -> false)
+ |> List.map(fun wr -> match wr with | Ok w -> w; | _ -> failwith "unreachable")
+
+ let wErrorList = wResult
+ |> List.filter(fun wr -> match wr with | Error w -> true;| _ -> false)
+ |> List.map(fun wr -> match wr with | Error e -> e; | _ -> failwith "unreachable")
+
+ let eResult = satisfyE (keyFn, hashFn, age) e |> List.singleton
+ let eOkList = eResult
+ |> List.filter(fun wr -> match wr with | Ok w -> true;| _ -> false)
+ |> List.map(fun wr -> match wr with | Ok w -> w; | _ -> failwith "unreachable")
+
+ let eErrorList = eResult
+ |> List.filter(fun wr -> match wr with | Error w -> true;| _ -> false)
+ |> List.map(fun wr -> match wr with | Error e -> e; | _ -> failwith "unreachable")
+
+
+ let satisfiedTotal = wOkList.Length + eOkList.Length
+
+ if satisfiedTotal >= (int k) then
+ let dissatisfiedW = List.zip wsList wResult
+ |> List.choose(fun (w, wr) -> match wr with | Error _ -> Some(w); | _ -> None)
+ |> List.map(dissatisfyW)
+ let dissatisfiedE = match eResult.[0] with | Error _ -> [dissatisfyE e] | Ok _ -> []
+ Ok(flatten (wOkList @ eOkList @ dissatisfiedW @ dissatisfiedE))
+ else
+ Error(Nested(wErrorList @ eErrorList))
+
+ and satisfyAST providers (ast: AST) =
+ match ast.GetASTType() with
+ | EExpr -> satisfyE providers (ast.CastEUnsafe())
+ | FExpr -> satisfyF providers (ast.CastFUnsafe())
+ | WExpr -> satisfyW providers (ast.CastWUnsafe())
+ | QExpr -> satisfyQ providers (ast.CastQUnsafe())
+ | TExpr -> satisfyT providers (ast.CastTUnsafe())
+ | VExpr -> satisfyV providers (ast.CastVUnsafe())
+
+ and dissatisfyAST (ast: AST) =
+ match ast.GetASTType() with
+ | EExpr -> dissatisfyE (ast.CastEUnsafe())
+ | WExpr -> dissatisfyW (ast.CastWUnsafe())
+ | _ -> failwith "unreachable"
+
+ and satisfyParallelOr providers (l: AST, r: AST) =
+ match (satisfyAST providers l), (satisfyAST providers r) with
+ | Ok(lItems), Ok (rItems) -> // return the one has less cost
+ let lDissat = dissatisfyAST l
+ let rDissat = dissatisfyAST r
+ if (satisfyCost rDissat + satisfyCost lItems <= satisfyCost rItems + satisfyCost lDissat) then
+ Ok(rDissat @ lItems)
+ else
+ Ok(lDissat @ rItems)
+ | Ok(lItems), Error _ ->
+ let rDissat = dissatisfyAST r
+ Ok(lItems @ rDissat)
+ | Error _, Ok(rItems) ->
+ let lDissat = dissatisfyAST l
+ Ok(rItems @ lDissat)
+ | Error e1, Error e2 -> Error(Nested([e1; e2]))
+
+ and satisfyCascadeOr providers (l, r) =
+ match (satisfyAST providers l), (satisfyAST providers r) with
+ | Error e, Error _ -> Error e
+ | Ok lItems, Error _ -> Ok(lItems)
+ | Error _, Ok rItems ->
+ let lDissat = dissatisfyAST l
+ Ok(rItems @ lDissat)
+ | Ok lItems, Ok rItems ->
+ let lDissat = dissatisfyAST l
+ if satisfyCost lItems <= satisfyCost rItems + satisfyCost lDissat then
+ Ok(lItems)
+ else
+ Ok(rItems)
+
+ and satisfySwitchOr providers (l, r) =
+ match (satisfyAST providers l), (satisfyAST providers r) with
+ | Error e, Error _ -> Error e
+ | Ok lItems, Error _ -> Ok(lItems @ [RawPush([|byte 1uy|])])
+ | Error e, Ok rItems -> Ok(rItems @ [RawPush([||])])
+ | Ok lItems, Ok rItems -> // return the one has less cost
+ if satisfyCost(lItems) + 2 <= satisfyCost rItems + 1 then
+ Ok(lItems @ [RawPush([|byte 1uy|])])
+ else
+ Ok(rItems @ [RawPush([||])])
+
+ and satisfyE (providers: ProviderSet) (e: E) =
+ let keyFn, hashFn, age = providers
+ match e with
+ | E.CheckSig k -> satisfyCheckSig keyFn k
+ | E.CheckMultiSig(m, pks) -> satisfyCheckMultisig keyFn (m, pks)
+ | E.Time t -> satisfyCSV age t
+ | E.Threshold i ->
+ satisfyThreshold providers i
+ | E.ParallelAnd(e, w) ->
+ satisfyE providers e
+ >>= (fun eitem -> satisfyW providers w >>= (fun witem -> Ok(eitem @ witem)))
+ | E.CascadeAnd(e, f) ->
+ satisfyE providers e
+ >>= (fun eitem -> satisfyF providers f >>= (fun fitem -> Ok(eitem @ fitem)))
+ | E.ParallelOr(e, w) -> satisfyParallelOr providers (ETree(e), WTree(w))
+ | E.CascadeOr(e1, e2) -> satisfyCascadeOr providers (ETree(e1), ETree(e2))
+ | E.SwitchOrLeft(e, f) -> satisfySwitchOr providers (ETree(e), FTree(f))
+ | E.SwitchOrRight(e, f) -> satisfySwitchOr providers (ETree(e), FTree(f))
+ | E.Likely f ->
+ satisfyF providers f |> Result.map(fun items -> items @ [RawPush([||])])
+ | E.Unlikely f ->
+ satisfyF providers f |> Result.map(fun items -> items @ [RawPush([|byte 1uy|])])
+
+ and satisfyW (providers: ProviderSet) w: SatisfactionResult =
+ let keyFn, hashFn, age = providers
+ match w with
+ | W.CheckSig pk -> satisfyCheckSig keyFn pk
+ | W.HashEqual h -> satisfyHashEqual hashFn h
+ | W.Time t ->
+ satisfyCSV age t |> Result.map(fun items -> items @ [RawPush([| byte 1uy |])])
+ | W.CastE e -> satisfyE providers e
+
+ and satisfyT (providers) t =
+ let (keyFn, hashFn, age) = providers
+ match t with
+ | T.Time t -> Ok([RawPush([||])])
+ | T.HashEqual h -> satisfyHashEqual hashFn h
+ | T.And(v, t) ->
+ let rRes = satisfyT providers t
+ let lRes = satisfyV providers v
+ rRes >>= (fun rItems -> lRes >>= fun(lItems) -> Ok(rItems @ lItems))
+ | T.ParallelOr(e, w) -> satisfyParallelOr providers (ETree(e), WTree(w))
+ | T.CascadeOr(e, t) -> satisfyCascadeOr providers (ETree(e), TTree(t))
+ | T.CascadeOrV(e, v) -> satisfyCascadeOr providers (ETree(e), VTree(v))
+ | T.SwitchOr(t1, t2) -> satisfySwitchOr providers (TTree(t1), TTree(t2))
+ | T.SwitchOrV(v1, v2) -> satisfySwitchOr providers (VTree(v1), VTree(v2))
+ | T.DelayedOr(q1, q2) -> satisfySwitchOr providers (QTree(q1), QTree(q2))
+ | T.CastE e -> satisfyE providers e
+
+ and satisfyQ (providers) q =
+ let (keyFn, hashFn, age) = providers
+ match q with
+ | Q.Pubkey pk -> satisfyCheckSig (keyFn) pk
+ | Q.And(l, r) ->
+ let rRes = satisfyQ providers r
+ let lRes = satisfyV providers l
+ rRes >>= (fun rItems -> lRes >>= fun(lItems) -> Ok(rItems @ lItems))
+ | Q.Or(l, r) -> satisfySwitchOr providers (QTree(l), QTree(r))
+
+ and satisfyF (providers) f =
+ let (keyFn, hashFn, age) = providers
+ match f with
+ | F.CheckSig pk -> satisfyCheckSig keyFn pk
+ | F.CheckMultiSig(m, pks) -> satisfyCheckMultisig keyFn (m, pks)
+ | F.Time t -> satisfyCSV age t
+ | F.HashEqual h -> satisfyHashEqual hashFn h
+ | F.Threshold i -> satisfyThreshold providers i
+ | F.And(v ,f) ->
+ let rRes = satisfyF providers f
+ let lRes = satisfyV providers v
+ rRes >>= (fun rItems -> lRes >>= fun(lItems) -> Ok(rItems @ lItems))
+ | F.CascadeOr(e, v) -> satisfyCascadeOr providers (ETree(e), VTree(v))
+ | F.SwitchOr(f1, f2) -> satisfySwitchOr providers (FTree(f1), FTree(f2))
+ | F.SwitchOrV(v1, v2) -> satisfySwitchOr providers (VTree(v1), VTree(v2))
+ | F.DelayedOr(q1, q2) -> satisfySwitchOr providers (QTree(q1), QTree(q2))
+
+ and satisfyV providers v =
+ let (keyFn, hashFn, age) = providers
+ match v with
+ | V.CheckSig pk -> satisfyCheckSig keyFn pk
+ | V.CheckMultiSig (m, pks) -> satisfyCheckMultisig keyFn (m, pks)
+ | V.Time t -> satisfyCSV age t
+ | V.HashEqual h -> satisfyHashEqual hashFn h
+ | V.Threshold i -> satisfyThreshold providers i
+ | V.And(v1, v2) ->
+ let rRes = satisfyV providers v2
+ let lRes = satisfyV providers v1
+ rRes >>= (fun rItems -> lRes >>= fun(lItems) -> Ok(rItems @ lItems))
+ | V.SwitchOr (v1, v2) -> satisfySwitchOr providers (VTree(v1), VTree(v2))
+ | V.SwitchOrT (t1, t2) -> satisfySwitchOr providers (TTree(t1), TTree(t2))
+ | V.CascadeOr(e, v) -> satisfyCascadeOr providers (ETree(e), VTree(v))
+ | V.DelayedOr(q1, q2) -> satisfySwitchOr providers (QTree(q1), QTree(q2))
+
+ and dissatisfyE (e: E): SatisfiedItem list =
+ match e with
+ | E.CheckSig pk -> [RawPush([||])]
+ | E.CheckMultiSig (m, pks) -> [RawPush[||]; RawPush[| byte(m + 1u)|]]
+ | E.Time t -> [RawPush([||])]
+ | E.Threshold (_, e, ws) ->
+ let wDissat = ws |> Array.toList |> List.rev |> List.map(dissatisfyW) |> List.collect id
+ let eDissat = dissatisfyE e
+ wDissat @ eDissat
+ | E.ParallelAnd (e, w) ->
+ (dissatisfyW w) @ (dissatisfyE e)
+ | E.CascadeAnd (e, _) ->
+ (dissatisfyE e)
+ | E.ParallelOr (e, w) ->
+ (dissatisfyW w) @ (dissatisfyE e)
+ | E.CascadeOr (e, e2) ->
+ (dissatisfyE e2) @ (dissatisfyE e)
+ | E.SwitchOrLeft (e, _) ->
+ (dissatisfyE e) @ [RawPush[| byte 1 |]]
+ | E.SwitchOrRight (e, _) ->
+ (dissatisfyE e) @ [RawPush[||]]
+ | E.Likely f -> [RawPush[| byte 1 |]]
+ | E.Unlikely f -> [RawPush[||]]
+
+ and dissatisfyW (w: W): SatisfiedItem list =
+ match w with
+ | W.CheckSig _ -> [RawPush[||]]
+ | W.HashEqual _ -> [RawPush[||]]
+ | W.Time _ -> [RawPush[||]]
+ | W.CastE e -> dissatisfyE e
+
diff --git a/NBitcoin.Miniscript/Utils/FuncConversion.fs b/NBitcoin.Miniscript/Utils/FuncConversion.fs
new file mode 100644
index 0000000000..5cc228891e
--- /dev/null
+++ b/NBitcoin.Miniscript/Utils/FuncConversion.fs
@@ -0,0 +1,8 @@
+namespace NBitcoin.Miniscript.Utils
+open System
+open System.Runtime.CompilerServices
+
+[]
+module FuncExtension =
+ type public CSharpFun =
+ static member internal ToFSharpFunc<'a> (action: Action<'a>) = fun a -> action.Invoke(a)
diff --git a/NBitcoin.Miniscript/Utils/Lib.fs b/NBitcoin.Miniscript/Utils/Lib.fs
new file mode 100644
index 0000000000..8e060df1b3
--- /dev/null
+++ b/NBitcoin.Miniscript/Utils/Lib.fs
@@ -0,0 +1,35 @@
+namespace NBitcoin.Miniscript.Utils
+
+[]
+module Utils =
+ open System
+ let inline (!>) (x:^a) : ^b = ((^a or ^b) : (static member op_Implicit : ^a -> ^b )x )
+
+ let resultFolder (acc : Result<'a seq, _>) (item : Result<'a, _>) =
+ match acc, item with
+ | Ok x, Ok y ->
+ Ok(seq {
+ yield! x
+ yield y
+ })
+ | Ok x, Error y -> Error y
+ | Error x, Ok y -> Error x
+ | Error x, Error y -> Error((AggregateException([|x; y|]) :> exn))
+
+
+ []
+ module List =
+ let rec traverseResult f list =
+ let (>>=) x f = Result.bind f x
+ let retn = Ok
+ let cons head tail = head :: tail
+
+ let initState = retn []
+ let folder head tail =
+ f head >>= (fun h ->
+ tail >>= (fun t ->
+ retn (cons h t)
+ )
+ )
+ List.foldBack folder list initState
+ let sequenceResult list = traverseResult id list
\ No newline at end of file
diff --git a/NBitcoin.Miniscript/Utils/Parser.fs b/NBitcoin.Miniscript/Utils/Parser.fs
new file mode 100644
index 0000000000..57fffb0738
--- /dev/null
+++ b/NBitcoin.Miniscript/Utils/Parser.fs
@@ -0,0 +1,167 @@
+namespace NBitcoin.Miniscript.Utils
+
+module Parser =
+ type ErrorMessage = string
+ type ParserName = string
+ type Position = int
+
+ type ParserError = ParserName * ErrorMessage * Position
+
+ let printParserError (pe: ParserError) =
+ let (name, msg, pos) = pe
+ sprintf "name: %s\nmsg: %s\nposition %d" name msg pos
+
+ type ParserResult<'a> = Result<'a, ParserError>
+
+ type Parser<'a, 'u> = {
+ parseFn: 'u -> ParserResult<'a * 'u>
+ name: ParserName
+ }
+ type Parser<'a> = Parser<'a, unit>
+
+
+ // combinators for parser. 1: Monad law
+ let bindP f p =
+ let innerFn input =
+ let r1 = p.parseFn input
+ match r1 with
+ | Ok(item, remainingInput) ->
+ let p2 = f item
+ p2.parseFn remainingInput
+ | Error e -> Error e
+ {parseFn=innerFn; name="unknown"}
+
+ let (>>=) p f = bindP f p
+
+ let returnP x =
+ let name = sprintf "%A" x
+ let innerFn input =
+ Ok (x, input)
+ {parseFn=innerFn; name=name}
+
+ // 2: Functor law
+ let mapP f =
+ bindP (f >> returnP)
+
+ let () = mapP
+ let (|>>) x f = mapP f x
+
+ // 3: Applicatives
+ let applyP fP xP =
+ fP >>= (fun f ->
+ xP >>= (f >> returnP))
+
+ let (<*>) = applyP
+ let lift2 f xP yP =
+ returnP f <*> xP <*> yP
+
+
+ // 4: parser specific things
+ /// get the label from a parser
+ let getName (parser) =
+ // get label
+ parser.name
+
+ /// update the label in the parser
+ let setName parser newName =
+ // change the inner function to use the new label
+ let newInnerFn input =
+ let result = parser.parseFn input
+ match result with
+ | Error (oldLabel,err,pos) ->
+ // if Failure, return new label
+ Error (newName,err,pos)
+ | ok -> ok
+ // return the Parser
+ {parseFn=newInnerFn; name=newName}
+
+ /// infix version of setLabel
+ let ( > ) = setName
+
+ let andThen (p1) (p2) =
+ let l = sprintf "%s andThen %s" (getName p1) (getName p2)
+ p1 >>= (fun p1R ->
+ p2 >>= (fun p2R ->
+ returnP (p1R, p2R)
+ )) > l
+
+ let (.>>.) = andThen
+
+ let run p input =
+ p.parseFn input
+
+ let orElse p1 p2 =
+ let name = sprintf "%s orElse %s" (getName p1) (getName p2)
+ let innerFn input =
+ let r1 = p1.parseFn input
+ match r1 with
+ | Ok _ -> r1
+ | Error e ->
+ let r2 = p2.parseFn input
+ r2
+ {parseFn=innerFn; name=name}
+
+ let (<|>) = orElse
+
+ let choice listOfParsers =
+ List.reduce (<|>) listOfParsers
+
+ let rec sequence parserlist =
+ let cons head tail = head::tail
+ let consP = lift2 cons
+ match parserlist with
+ | [] -> returnP []
+ | head::tail ->
+ consP head (sequence tail)
+
+ // parse zero or more occurancs o the specified parser
+ let rec star p input =
+ let firstResult = p.parseFn input
+ match firstResult with
+ | Error _ -> ([], input)
+ | Ok (firstValue, inputAfterFirstPlace) ->
+ let (subsequenceValues, remainingInput) =
+ star p inputAfterFirstPlace
+ let values = firstValue::subsequenceValues
+ (values, remainingInput)
+
+ // zero or more occurances
+ let many p =
+ let name = sprintf "many %s" (getName p)
+ let rec innerFn input =
+ Ok(star p input)
+ {parseFn=innerFn; name=name}
+
+ // one or more
+ let many1 p =
+ let name = sprintf "many1 %s" (getName p)
+ p >>= (fun head ->
+ many p >>= (fun tail ->
+ returnP (head::tail)
+ )) > name
+
+ let opt p =
+ let name = sprintf "opt %s" (getName p)
+ let some = p |>> Some
+ let none = returnP None
+ (some <|> none) > name
+
+ let (.>>) p1 p2 =
+ p1 .>>. p2
+ |> mapP (fun (a, b) -> a)
+
+ let (>>.) p1 p2 =
+ p1 .>>. p2
+ |> mapP (fun (a, b) -> b)
+
+ let createParserForwardedToRef<'a, 'u>() =
+ let dummyParser =
+ let innerFn input : ParserResult<'a * 'u> = failwith "unfixed forwarded parser"
+ {parseFn=innerFn; name="unknown"}
+ let parserRef = ref dummyParser
+ let innerFn input =
+ (!parserRef).parseFn input
+ let wrapperParser = {parseFn=innerFn; name="unknown"}
+ wrapperParser, parserRef
+
+
\ No newline at end of file
diff --git a/NBitcoin.Miniscript/fsc.props b/NBitcoin.Miniscript/fsc.props
new file mode 100644
index 0000000000..3fb4e62948
--- /dev/null
+++ b/NBitcoin.Miniscript/fsc.props
@@ -0,0 +1,21 @@
+
+
+
+
+ true
+ true
+ true
+
+
+ C:\Program Files (x86)\Microsoft SDKs\F#\4.1\Framework\v4.0
+ fsc.exe
+
+
+ /Library/Frameworks/Mono.framework/Versions/Current/Commands
+ fsharpc
+
+
+ /usr/bin
+ fsharpc
+
+
diff --git a/NBitcoin.Miniscript/netfx.props b/NBitcoin.Miniscript/netfx.props
new file mode 100644
index 0000000000..12a67e1e0c
--- /dev/null
+++ b/NBitcoin.Miniscript/netfx.props
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ true
+
+
+ /Library/Frameworks/Mono.framework/Versions/Current/lib/mono
+ /usr/lib/mono
+ /usr/local/lib/mono
+
+
+ $(BaseFrameworkPathOverrideForMono)/4.5-api
+ $(BaseFrameworkPathOverrideForMono)/4.5.1-api
+ $(BaseFrameworkPathOverrideForMono)/4.5.2-api
+ $(BaseFrameworkPathOverrideForMono)/4.6-api
+ $(BaseFrameworkPathOverrideForMono)/4.6.1-api
+ $(BaseFrameworkPathOverrideForMono)/4.6.2-api
+ $(BaseFrameworkPathOverrideForMono)/4.7-api
+ $(BaseFrameworkPathOverrideForMono)/4.7.1-api
+ true
+
+
+ $(FrameworkPathOverride)/Facades;$(AssemblySearchPaths)
+
+
diff --git a/NBitcoin.TestFramework/NBitcoin.TestFramework.csproj b/NBitcoin.TestFramework/NBitcoin.TestFramework.csproj
index fecdc402dc..ccc7ccdb35 100644
--- a/NBitcoin.TestFramework/NBitcoin.TestFramework.csproj
+++ b/NBitcoin.TestFramework/NBitcoin.TestFramework.csproj
@@ -2,7 +2,7 @@
1.6.35
- netstandard1.6;net452;netstandard2.0
+ netstandard1.6;net452;netstandard2.0;netcoreapp2.1
netstandard2.0
$(TargetFrameworkOverride)
NBitcoin.TestFramework
diff --git a/NBitcoin.Tests/Comparer.cs b/NBitcoin.Tests/Comparer.cs
deleted file mode 100644
index fe85b93ecc..0000000000
--- a/NBitcoin.Tests/Comparer.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Collections.Generic;
-using NBitcoin.BIP174;
-
-namespace NBitcoin.Tests
-{
- public static class Comparer
- {
- public class PSBTComparer : EqualityComparer
- {
- public override bool Equals(PSBT a, PSBT b) => a.Equals(b);
- public override int GetHashCode(PSBT psbt) => psbt.GetHashCode();
- }
- }
-}
\ No newline at end of file
diff --git a/NBitcoin.Tests/NBitcoin.Tests.csproj b/NBitcoin.Tests/NBitcoin.Tests.csproj
index 433aa1e31b..654c17f847 100644
--- a/NBitcoin.Tests/NBitcoin.Tests.csproj
+++ b/NBitcoin.Tests/NBitcoin.Tests.csproj
@@ -6,7 +6,7 @@
The C# Bitcoin Library
- net461;netcoreapp2.1
+ net461;netstandard2.0;netcoreapp2.1
netcoreapp2.1
@@ -97,9 +97,6 @@
PreserveNewest
-
- PreserveNewest
-
PreserveNewest
diff --git a/NBitcoin.Tests/PSBTComparer.cs b/NBitcoin.Tests/PSBTComparer.cs
new file mode 100644
index 0000000000..97ce81993e
--- /dev/null
+++ b/NBitcoin.Tests/PSBTComparer.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using NBitcoin.BIP174;
+
+namespace NBitcoin.Tests
+{
+ public class PSBTComparer : EqualityComparer
+ {
+ public override bool Equals(PSBT a, PSBT b) => a.Equals(b);
+ public override int GetHashCode(PSBT psbt) => psbt.GetHashCode();
+ }
+
+}
\ No newline at end of file
diff --git a/NBitcoin.Tests/PropertyTest/PSBTSerializationTest.cs b/NBitcoin.Tests/PropertyTest/PSBTSerializationTest.cs
index 8378e5224f..ae30ddc70f 100644
--- a/NBitcoin.Tests/PropertyTest/PSBTSerializationTest.cs
+++ b/NBitcoin.Tests/PropertyTest/PSBTSerializationTest.cs
@@ -8,7 +8,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using static NBitcoin.Tests.Comparer;
namespace NBitcoin.Tests.PropertyTest
{
diff --git a/NBitcoin.Tests/RPCClientTests.cs b/NBitcoin.Tests/RPCClientTests.cs
index 01b5919f2f..0bed829847 100644
--- a/NBitcoin.Tests/RPCClientTests.cs
+++ b/NBitcoin.Tests/RPCClientTests.cs
@@ -19,7 +19,6 @@
using NBitcoin.BIP174;
using FsCheck;
using NBitcoin.Tests.Generators;
-using static NBitcoin.Tests.Comparer;
namespace NBitcoin.Tests
{
@@ -31,14 +30,10 @@ public class RPCClientTests
{
const string TestAccount = "NBitcoin.RPCClientTests";
- public PSBTComparer PSBTComparerInstance { get; }
public ITestOutputHelper Output { get; }
public RPCClientTests(ITestOutputHelper output)
{
- Arb.Register();
- Arb.Register();
- PSBTComparerInstance = new PSBTComparer();
Output = output;
}
@@ -1289,295 +1284,6 @@ public async Task CanGenerateBlocks()
}
}
- [Fact]
- public void ShouldCreatePSBTAcceptableByRPCAsExpected()
- {
- using (var builder = NodeBuilderEx.Create())
- {
- var node = builder.CreateNode();
- node.Start();
- var client = node.CreateRPCClient();
-
- var keys = new Key[] {new Key(), new Key(), new Key() };
- var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(ki => ki.PubKey).ToArray());
- var funds = PSBTTests.CreateDummyFunds(Network.TestNet, keys, redeem);
-
- // case1: PSBT from already fully signed tx
- var tx = PSBTTests.CreateTxToSpendFunds(funds, keys, redeem, true, true);
- // PSBT without previous outputs but with finalized_script_witness will throw an error.
- var psbt = PSBT.FromTransaction(tx.Clone(), true);
- Assert.Throws(() => psbt.ToBase64());
-
- // after adding coins, will not throw an error.
- psbt.AddCoins(funds.SelectMany(f => f.Outputs.AsCoins()).ToArray());
- CheckPSBTIsAcceptableByRealRPC(psbt.ToBase64(), client);
-
- // but if we use rpc to convert tx to psbt, it will discard input scriptSig and ScriptWitness.
- // So it will be acceptable by any other rpc.
- psbt = PSBT.FromTransaction(tx.Clone());
- CheckPSBTIsAcceptableByRealRPC(psbt.ToBase64(), client);
-
- // case2: PSBT from tx with script (but without signatures)
- tx = PSBTTests.CreateTxToSpendFunds(funds, keys, redeem, true, false);
- psbt = PSBT.FromTransaction(tx, true);
- // it has witness_script but has no prevout so it will throw an error.
- Assert.Throws(() => psbt.ToBase64());
- // after adding coins, will not throw error.
- psbt.AddCoins(funds.SelectMany(f => f.Outputs.AsCoins()).ToArray());
- CheckPSBTIsAcceptableByRealRPC(psbt.ToBase64(), client);
-
- // case3: PSBT from tx without script nor signatures.
- tx = PSBTTests.CreateTxToSpendFunds(funds, keys, redeem, false, false);
- psbt = PSBT.FromTransaction(tx, true);
- // This time, it will not throw an error at the first place.
- // Since sanity check for witness input will not complain about witness-script-without-witnessUtxo
- CheckPSBTIsAcceptableByRealRPC(psbt.ToBase64(), client);
-
- var dummyKey = new Key();
- var dummyScript = new Script("OP_DUP " + "OP_HASH160 " + Op.GetPushOp(dummyKey.PubKey.Hash.ToBytes()) + " OP_EQUALVERIFY");
-
- // even after adding coins and scripts ...
- var psbtWithCoins = psbt.Clone().AddCoins(funds.SelectMany(f => f.Outputs.AsCoins()).ToArray());
- CheckPSBTIsAcceptableByRealRPC(psbtWithCoins.ToBase64(), client);
- psbtWithCoins.AddScript(redeem);
- CheckPSBTIsAcceptableByRealRPC(psbtWithCoins.ToBase64(), client);
- var tmp = psbtWithCoins.Clone().AddScript(dummyScript); // should not change with dummyScript
- Assert.Equal(psbtWithCoins, tmp, PSBTComparerInstance);
- // or txs and scripts.
- var psbtWithTXs = psbt.Clone().AddTransactions(funds);
- CheckPSBTIsAcceptableByRealRPC(psbtWithTXs.ToBase64(), client);
- psbtWithTXs.AddScript(redeem);
- CheckPSBTIsAcceptableByRealRPC(psbtWithTXs.ToBase64(), client);
- tmp = psbtWithTXs.Clone().AddScript(dummyScript);
- Assert.Equal(psbtWithTXs, tmp, PSBTComparerInstance);
-
- // Let's don't forget about hd KeyPath
- psbtWithTXs.AddKeyPath(keys[0].PubKey, Tuple.Create((uint)1234, KeyPath.Parse("m/1'/2/3")));
- psbtWithTXs.AddPathTo(3, keys[1].PubKey, 4321, KeyPath.Parse("m/3'/2/1"));
- psbtWithTXs.AddPathTo(0, keys[1].PubKey, 4321, KeyPath.Parse("m/3'/2/1"), false);
- CheckPSBTIsAcceptableByRealRPC(psbtWithTXs.ToBase64(), client);
-
- // What about after adding some signatures?
- psbtWithTXs.SignAll(keys);
- CheckPSBTIsAcceptableByRealRPC(psbtWithTXs.ToBase64(), client);
- tmp = psbtWithTXs.Clone().SignAll(dummyKey); // Try signing with unrelated key should not change anything
- Assert.Equal(psbtWithTXs, tmp, PSBTComparerInstance);
- // And finalization?
- psbtWithTXs.Finalize();
- CheckPSBTIsAcceptableByRealRPC(psbtWithTXs.ToBase64(), client);
- }
- return;
- }
-
- ///
- /// Just Check if the psbt is acceptable by bitcoin core rpc.
- ///
- ///
- ///
- private void CheckPSBTIsAcceptableByRealRPC(string base64, RPCClient client)
- => client.SendCommand(RPCOperations.decodepsbt, base64);
-
- [Fact]
- public void ShouldWalletProcessPSBTAndExtractMempoolAcceptableTX()
- {
- using (var builder = NodeBuilderEx.Create())
- {
- var node = builder.CreateNode();
- node.Start();
-
- var client = node.CreateRPCClient();
-
- // ensure the wallet has whole kinds of coins ...
- var addr = client.GetNewAddress();
- client.GenerateToAddress(101, addr);
- addr = client.GetNewAddress(new GetNewAddressRequest() { AddressType = AddressType.Bech32 });
- client.SendToAddress(addr, Money.Coins(15));
- addr = client.GetNewAddress(new GetNewAddressRequest() { AddressType = AddressType.P2SHSegwit });
- client.SendToAddress(addr, Money.Coins(15));
- var tmpaddr = new Key();
- client.GenerateToAddress(1, tmpaddr.PubKey.GetAddress(node.Network));
-
- // case 1: irrelevant psbt.
- var keys = new Key[] {new Key(), new Key(), new Key() };
- var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(ki => ki.PubKey).ToArray());
- var funds = PSBTTests.CreateDummyFunds(Network.TestNet, keys, redeem);
- var tx = PSBTTests.CreateTxToSpendFunds(funds, keys, redeem, true, true);
- var psbt = PSBT.FromTransaction(tx, true)
- .AddTransactions(funds)
- .AddScript(redeem);
- var case1Result = client.WalletProcessPSBT(psbt);
- // nothing must change for the psbt unrelated to the wallet.
- Assert.Equal(psbt, case1Result.PSBT, PSBTComparerInstance);
-
- // case 2: psbt relevant to the wallet. (but already finalized)
- var kOut = new Key();
- tx = builder.Network.CreateTransaction();
- tx.Outputs.Add(new TxOut(Money.Coins(45), kOut)); // This has to be big enough since the wallet must use whole kinds of address.
- var fundTxResult = client.FundRawTransaction(tx);
- Assert.Equal(3, fundTxResult.Transaction.Inputs.Count);
- var psbtFinalized = PSBT.FromTransaction(fundTxResult.Transaction, true);
- var result = client.WalletProcessPSBT(psbtFinalized, false);
- Assert.False(result.PSBT.CanExtractTX());
- result = client.WalletProcessPSBT(psbtFinalized, true);
- Assert.True(result.PSBT.CanExtractTX());
-
- // case 3a: psbt relevant to the wallet (and not finalized)
- var spendableCoins = client.ListUnspent().Where(c => c.IsSpendable).Select(c => c.AsCoin());
- tx = builder.Network.CreateTransaction();
- foreach (var coin in spendableCoins)
- tx.Inputs.Add(coin.Outpoint);
- tx.Outputs.Add(new TxOut(Money.Coins(45), kOut));
- var psbtUnFinalized = PSBT.FromTransaction(tx, true);
-
- var type = SigHash.All;
- // unsigned
- result = client.WalletProcessPSBT(psbtUnFinalized, false, type, bip32derivs: true);
- Assert.False(result.Complete);
- Assert.False(result.PSBT.CanExtractTX());
- var ex2 = Assert.Throws(
- () => result.PSBT.Finalize()
- );
- var errors2 = ex2.InnerExceptions;
- Assert.NotEmpty(errors2);
- foreach (var psbtin in result.PSBT.Inputs)
- {
- Assert.Equal(SigHash.Undefined, psbtin.SighashType);
- Assert.NotEmpty(psbtin.HDKeyPaths);
- }
-
- // signed
- result = client.WalletProcessPSBT(psbtUnFinalized, true, type);
- // does not throw
- result.PSBT.Finalize();
-
- var txResult = result.PSBT.ExtractTX();
- var acceptResult = client.TestMempoolAccept(txResult, true);
- Assert.True(acceptResult.IsAllowed, acceptResult.RejectReason);
- }
- }
-
- // refs: https://github.com/bitcoin/bitcoin/blob/df73c23f5fac031cc9b2ec06a74275db5ea322e3/doc/psbt.md#workflows
- // with 2 difference.
- // 1. one user (David) do not use bitcoin core (only NBitcoin)
- // 2. 4-of-4 instead of 2-of-3
- // 3. In version 0.17, `importmulti` can not handle witness script so only p2sh are considered here. TODO: fix
- [Fact]
- public void ShouldPerformMultisigProcessingWithCore()
- {
- using (var builder = NodeBuilderEx.Create())
- {
- if (!builder.NodeImplementation.Version.Contains("0.17"))
- throw new Exception("Test must be updated!");
- var nodeAlice = builder.CreateNode();
- var nodeBob = builder.CreateNode();
- var nodeCarol = builder.CreateNode();
- var nodeFunder = builder.CreateNode();
- var david = new Key();
- builder.StartAll();
-
- // prepare multisig script and watch with node.
- var nodes = new CoreNode[]{nodeAlice, nodeBob, nodeCarol};
- var clients = nodes.Select(n => n.CreateRPCClient()).ToArray();
- var addresses = clients.Select(c => c.GetNewAddress());
- var addrInfos = addresses.Select((a, i) => clients[i].GetAddressInfo(a));
- var pubkeys = new List { david.PubKey };
- pubkeys.AddRange(addrInfos.Select(i => i.PubKey).ToArray());
- var script = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(4, pubkeys.ToArray());
- var aMultiP2SH = script.Hash.ScriptPubKey;
- // var aMultiP2WSH = script.WitHash.ScriptPubKey;
- // var aMultiP2SH_P2WSH = script.WitHash.ScriptPubKey.Hash.ScriptPubKey;
- var multiAddresses = new BitcoinAddress[] { aMultiP2SH.GetDestinationAddress(builder.Network) };
- var importMultiObject = new ImportMultiAddress[] {
- new ImportMultiAddress()
- {
- ScriptPubKey = new ImportMultiAddress.ScriptPubKeyObject(multiAddresses[0]),
- RedeemScript = script.ToHex(),
- Internal = true,
- },
- /*
- new ImportMultiAddress()
- {
- ScriptPubKey = new ImportMultiAddress.ScriptPubKeyObject(aMultiP2WSH),
- RedeemScript = script.ToHex(),
- Internal = true,
- },
- new ImportMultiAddress()
- {
- ScriptPubKey = new ImportMultiAddress.ScriptPubKeyObject(aMultiP2SH_P2WSH),
- RedeemScript = script.WitHash.ScriptPubKey.ToHex(),
- Internal = true,
- },
- new ImportMultiAddress()
- {
- ScriptPubKey = new ImportMultiAddress.ScriptPubKeyObject(aMultiP2SH_P2WSH),
- RedeemScript = script.ToHex(),
- Internal = true,
- }
- */
- };
-
- for (var i = 0; i < clients.Length; i++)
- {
- var c = clients[i];
- Output.WriteLine($"Importing for {i}");
- c.ImportMulti(importMultiObject, false);
- }
-
- // pay from funder
- nodeFunder.Generate(103);
- var funderClient = nodeFunder.CreateRPCClient();
- funderClient.SendToAddress(aMultiP2SH, Money.Coins(40));
- // funderClient.SendToAddress(aMultiP2WSH, Money.Coins(40));
- // funderClient.SendToAddress(aMultiP2SH_P2WSH, Money.Coins(40));
- nodeFunder.Generate(1);
- foreach (var n in nodes)
- {
- nodeFunder.Sync(n, true);
- }
-
- // pay from multisig address
- // first carol creates psbt
- var carol = clients[2];
- // check if we have enough balance
- var info = carol.GetBlockchainInfoAsync().Result;
- Assert.Equal((ulong)104, info.Blocks);
- var balance = carol.GetBalance(0, true);
- // Assert.Equal(Money.Coins(120), balance);
- Assert.Equal(Money.Coins(40), balance);
-
- var aSend = new Key().PubKey.GetAddress(nodeAlice.Network);
- var outputs = new Dictionary();
- outputs.Add(aSend, Money.Coins(10));
- var fundOptions = new FundRawTransactionOptions() { SubtractFeeFromOutputs = new int[] {0}, IncludeWatching = true };
- PSBT psbt = carol.WalletCreateFundedPSBT(null, outputs, 0, fundOptions).PSBT;
- psbt = carol.WalletProcessPSBT(psbt).PSBT;
-
- // second, Bob checks and process psbt.
- var bob = clients[1];
- Assert.Contains(multiAddresses, a =>
- psbt.Inputs.Any(psbtin => psbtin.WitnessUtxo?.ScriptPubKey == a.ScriptPubKey) ||
- psbt.Inputs.Any(psbtin => (bool)psbtin.NonWitnessUtxo?.Outputs.Any(o => a.ScriptPubKey == o.ScriptPubKey))
- );
- var psbt1 = bob.WalletProcessPSBT(psbt.Clone()).PSBT;
-
- // at the same time, David may do the ;
- psbt.SignAll(david);
- var alice = clients[0];
- var psbt2 = alice.WalletProcessPSBT(psbt).PSBT;
-
- // not enough signatures
- Assert.Throws(() => psbt.Finalize());
-
- // So let's combine.
- var psbtCombined = psbt1.Combine(psbt2);
-
- // Finally, anyone can finalize and broadcast the psbt.
- var tx = psbtCombined.Finalize().ExtractTX();
- var result = alice.TestMempoolAccept(tx);
- Assert.True(result.IsAllowed, result.RejectReason);
- }
- }
-
[Fact]
///
diff --git a/NBitcoin.sln b/NBitcoin.sln
index 65922f5a63..3b8e340633 100644
--- a/NBitcoin.sln
+++ b/NBitcoin.sln
@@ -20,6 +20,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NBitcoin.TestFramework", "N
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NBitcoin.Bench", "NBitcoin.Bench\NBitcoin.Bench.csproj", "{153FEA8A-FA98-4ADD-8570-5FD88631C489}"
EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NBitcoin.Miniscript", "NBitcoin.Miniscript\NBitcoin.Miniscript.fsproj", "{F00D2B30-F9DA-40AC-AC4C-B1765A2FD7BA}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NBitcoin.Miniscript.Tests", "NBitcoin.Miniscript.Tests", "{961EC18E-0301-4917-B91C-EBE7AFF6F7A7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NBitcoin.Miniscript.Tests.CSharp", "NBitcoin.Miniscript.Tests\CSharp\NBitcoin.Miniscript.Tests.CSharp.csproj", "{C890F563-03B6-43AB-83CF-E7EF865DC9F6}"
+EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NBitcoin.Miniscript.Tests.FSharp", "NBitcoin.Miniscript.Tests\FSharp\NBitcoin.Miniscript.Tests.FSharp.fsproj", "{59132A30-5C07-4617-836C-36D8CF621C6E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -46,6 +54,18 @@ Global
{153FEA8A-FA98-4ADD-8570-5FD88631C489}.Debug|Any CPU.Build.0 = Debug|Any CPU
{153FEA8A-FA98-4ADD-8570-5FD88631C489}.Release|Any CPU.ActiveCfg = Release|Any CPU
{153FEA8A-FA98-4ADD-8570-5FD88631C489}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F00D2B30-F9DA-40AC-AC4C-B1765A2FD7BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F00D2B30-F9DA-40AC-AC4C-B1765A2FD7BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F00D2B30-F9DA-40AC-AC4C-B1765A2FD7BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F00D2B30-F9DA-40AC-AC4C-B1765A2FD7BA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C890F563-03B6-43AB-83CF-E7EF865DC9F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C890F563-03B6-43AB-83CF-E7EF865DC9F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C890F563-03B6-43AB-83CF-E7EF865DC9F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C890F563-03B6-43AB-83CF-E7EF865DC9F6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {59132A30-5C07-4617-836C-36D8CF621C6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {59132A30-5C07-4617-836C-36D8CF621C6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {59132A30-5C07-4617-836C-36D8CF621C6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {59132A30-5C07-4617-836C-36D8CF621C6E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -53,4 +73,8 @@ Global
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A1246380-43E4-4710-99A7-F7524458155E}
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {C890F563-03B6-43AB-83CF-E7EF865DC9F6} = {961EC18E-0301-4917-B91C-EBE7AFF6F7A7}
+ {59132A30-5C07-4617-836C-36D8CF621C6E} = {961EC18E-0301-4917-B91C-EBE7AFF6F7A7}
+ EndGlobalSection
EndGlobal
diff --git a/NBitcoin/BIP174/PartiallySignedTransaction.cs b/NBitcoin/BIP174/PartiallySignedTransaction.cs
index 520c08fac1..7d8706ec26 100755
--- a/NBitcoin/BIP174/PartiallySignedTransaction.cs
+++ b/NBitcoin/BIP174/PartiallySignedTransaction.cs
@@ -548,115 +548,11 @@ internal bool Sign(int index, Transaction tx, Key[] keys, bool UseLowR = true)
public bool IsFinalized() => final_script_sig != null || final_script_witness != null;
- internal void Finalize(Transaction tx, int index)
- {
- if (tx == null)
- throw new ArgumentNullException(nameof(tx));
-
- if (IsFinalized())
- return;
-
- var prevout = GetOutput(tx.Inputs[index].PrevOut);
- if (prevout == null)
- throw new InvalidOperationException("Can not finalize PSBTInput without utxo");
-
- var dummyTx = tx.Clone(); // Since to run VerifyScript for witness input, we must modify tx.
- var context = new ScriptEvaluationContext() { SigHash = sighash_type == 0 ? SigHash.All : sighash_type};
- var nextScript = prevout.ScriptPubKey;
-
- // 1. p2pkh
- if (PayToPubkeyHashTemplate.Instance.CheckScriptPubKey(nextScript))
- {
- var sigPair = partial_sigs.First();
- var txSig = new TransactionSignature(sigPair.Value.Item2, sighash_type == 0 ? SigHash.All : (SigHash)sighash_type);
- var ss = PayToPubkeyHashTemplate.Instance.GenerateScriptSig(txSig, sigPair.Value.Item1);
- if (!context.VerifyScript(ss, dummyTx, index, prevout))
- throw new InvalidOperationException($"Failed to verify script in p2pkh! {context.Error}");
- final_script_sig = ss;
- }
-
- // 2. p2sh
- else if (nextScript.IsPayToScriptHash)
- {
- // bare p2sh
- if (witness_script == null && !PayToWitTemplate.Instance.CheckScriptPubKey(redeem_script))
- {
- var pushes = GetPushItems(redeem_script);
- var ss = PayToScriptHashTemplate.Instance.GenerateScriptSig(pushes, redeem_script);
- if (!context.VerifyScript(ss, dummyTx, index, prevout))
- throw new InvalidOperationException($"Failed to verify script in p2sh! {context.Error}");
- final_script_sig = ss;
- }
- // Why not create `final_script_sig` here? because if the following code throws an error, it will be left out dirty.
- nextScript = redeem_script;
- }
-
- // 3. p2wpkh
- if (PayToWitPubKeyHashTemplate.Instance.CheckScriptPubKey(nextScript))
- {
- var sigPair = partial_sigs.First();
- var txSig = new TransactionSignature(sigPair.Value.Item2, sighash_type == 0 ? SigHash.All : (SigHash)sighash_type);
- dummyTx.Inputs[index].WitScript = PayToWitPubKeyHashTemplate.Instance.GenerateWitScript(txSig, sigPair.Value.Item1);
- Script ss = null;
- if (prevout.ScriptPubKey.IsPayToScriptHash)
- ss = new Script(Op.GetPushOp(redeem_script.ToBytes()));
- if (!context.VerifyScript(ss ?? Script.Empty, dummyTx, index, prevout))
- throw new InvalidOperationException($"Failed to verify script in p2wpkh! {context.Error}");
-
- final_script_witness = dummyTx.Inputs[index].WitScript;
- final_script_sig = ss;
- }
-
- // 4. p2wsh
- else if (PayToWitScriptHashTemplate.Instance.CheckScriptPubKey(nextScript))
- {
- var pushes = GetPushItems(witness_script);
- dummyTx.Inputs[index].WitScript = PayToWitScriptHashTemplate.Instance.GenerateWitScript(pushes, witness_script);
- Script ss = null;
- if (prevout.ScriptPubKey.IsPayToScriptHash)
- ss = new Script(Op.GetPushOp(redeem_script.ToBytes()));
- if (!context.VerifyScript(ss ?? Script.Empty, dummyTx, index, prevout))
- throw new InvalidOperationException($"Failed to verify script in p2wsh! {context.Error}");
-
- final_script_witness = dummyTx.Inputs[index].WitScript;
- final_script_sig = ss;
- }
- if (IsFinalized())
- ClearForFinalize();
- }
-
- ///
- /// conovert partial sigs to suitable form for ScriptSig (or Witness).
- /// This will preserve the ordering of redeem script even if it did not follow bip67.
- ///
- ///
- ///
- private Op[] GetPushItems(Script redeem)
- {
- if (PayToMultiSigTemplate.Instance.CheckScriptPubKey(redeem))
- {
- var sigPushes = new List { OpcodeType.OP_0 };
- foreach (var pk in redeem.GetAllPubKeys())
- {
- if (!partial_sigs.TryGetValue(pk.Hash, out var sigPair))
- continue;
- var txSig = new TransactionSignature(sigPair.Item2, sighash_type == 0 ? SigHash.All : (SigHash)sighash_type);
- sigPushes.Add(Op.GetPushOp(txSig.ToBytes()));
- }
- // check sig is more than m in case of p2multisig.
- var multiSigParam = PayToMultiSigTemplate.Instance.ExtractScriptPubKeyParameters(redeem);
- var numSigs = sigPushes.Count - 1;
- if (multiSigParam != null && numSigs < multiSigParam.SignatureCount)
- throw new InvalidOperationException("Not enough signatures to finalize.");
- return sigPushes.ToArray();
- }
- throw new InvalidOperationException("PSBT does not know how to finalize this type of script!");
- }
///
/// This will not clear utxos since tx extractor might want to check the validity
///
- private void ClearForFinalize()
+ internal void ClearForFinalize()
{
this.redeem_script = null;
this.witness_script = null;
@@ -1392,7 +1288,7 @@ private void Initialize()
///
/// It tries to preserve signatures and scripts from ScriptSig (or Witness Script) iff preserveInputProp is true.
/// Due to policy in sanity checking, input with witness script and without witness_utxo can not be serialized.
- /// So if you specify true, make sure you will run `TryAddTransaction` or `AddCoins` right after this and give witness_utxo to the input.
+ /// So if you specify true, make sure you will run `AddTransactions` or `AddCoins` right after this and give witness_utxo to the input.
///
/// PSBT
public static PSBT FromTransaction(Transaction tx, bool preserveInputProp = false) => new PSBT(tx, preserveInputProp);
@@ -1501,39 +1397,6 @@ public PSBT CoinJoin(PSBT other)
return result;
}
- ///
- /// If this method throws an error, that is a bug.
- ///
- ///
- ///
- private PSBT Finalize(out InvalidOperationException[] errors)
- {
- var elist = new List ();
- for (var i = 0; i < Inputs.Count; i++)
- {
- var psbtin = Inputs[i];
- try
- {
- psbtin.Finalize(tx, i);
- }
- catch (InvalidOperationException e)
- {
- var exception = new InvalidOperationException($"Failed to finalize in input {i}", e);
- elist.Add(exception);
- }
- }
- errors = elist.ToArray();
- return this;
- }
-
- public PSBT Finalize()
- {
- Finalize(out var errors);
- if (errors.Length != 0)
- throw new AggregateException(errors);
- return this;
- }
-
///
/// Test vector in the bip174 specify to use a signer which follows RFC 6979.
/// So we must sign without [LowR value assured way](https://github.com/MetacoSA/NBitcoin/pull/510)
diff --git a/NBitcoin/NBitcoin.csproj b/NBitcoin/NBitcoin.csproj
index ed48c17c92..58f7d7a6ce 100644
--- a/NBitcoin/NBitcoin.csproj
+++ b/NBitcoin/NBitcoin.csproj
@@ -1,5 +1,5 @@
-
+
Metaco SA
Copyright © Metaco SA 2017
@@ -20,7 +20,7 @@
true
- net461;net452;netstandard1.3;netstandard1.1;netcoreapp2.1;netstandard2.0
+ net452;net461;netstandard1.1;netstandard1.3;netstandard2.0;netcoreapp2.1
netstandard2.0
$(TargetFrameworkOverride)
1591;1573;1572;1584;1570;3021
@@ -93,4 +93,28 @@
bin\$(Configuration)\$(TargetFramework)\NBitcoin.xml
+
+
+
+ true
+
+
+
+ true
+ Always
+ lib\netstandard2.0
+
+
+ true
+ Always
+ lib\netcoreapp2.1
+
+
+ true
+ Always
+ lib\net461
+
+
+
diff --git a/NBitcoin/Properties/AssemblyInfo.cs b/NBitcoin/Properties/AssemblyInfo.cs
index 543bcdf56f..d3172fac57 100644
--- a/NBitcoin/Properties/AssemblyInfo.cs
+++ b/NBitcoin/Properties/AssemblyInfo.cs
@@ -8,6 +8,9 @@
// associated with an assembly.
[assembly: InternalsVisibleTo("NBitcoin.Tests")]
[assembly: InternalsVisibleTo("NBitcoin.Altcoins")]
+[assembly: InternalsVisibleTo("NBitcoin.Miniscript")]
+[assembly: InternalsVisibleTo("NBitcoin.Miniscript.Tests.CSharp")]
+[assembly: InternalsVisibleTo("NBitcoin.Miniscript.Tests.FSharp")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/appveyor.yml b/appveyor.yml
index 0593d31af0..1b565f2be7 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -66,6 +66,8 @@ test_script:
Write-Host "[$env:configuration] STARTED dotnet test" -foregroundcolor "magenta"
cd $env:APPVEYOR_BUILD_FOLDER
dotnet test -c Release ./NBitcoin.Tests/NBitcoin.Tests.csproj --filter "RestClient=RestClient|RPCClient=RPCClient|Protocol=Protocol|Core=Core|UnitTest=UnitTest" -p:ParallelizeTestCollections=false -f net461
+ dotnet run -c Release --project ./NBitcoin.Miniscript.Tests/FSharp/NBitcoin.Miniscript.Tests.FSharp.fsproj -f net461
+ dotnet test -c Release ./NBitcoin.Miniscript.Tests/CSharp/NBitcoin.Miniscript.Tests.CSharp.csproj -p:ParallelizeTestCollections=false -f net461
Write-Host "[$env:configuration] FINISHED dotnet test" -foregroundcolor "magenta"
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }