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) }