Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update PSBT test #627

Closed
101 changes: 95 additions & 6 deletions NBitcoin.Tests/PSBTTests.cs
Expand Up @@ -8,11 +8,14 @@
using System.Collections.Generic;
using System.Linq;
using static NBitcoin.Tests.Comparer;
using Xunit.Abstractions;

namespace NBitcoin.Tests
{
public class PSBTTests
{
private readonly ITestOutputHelper Output;

private static JObject testdata { get; }
private static PSBTComparer ComparerInstance { get; }

Expand All @@ -21,6 +24,10 @@ static PSBTTests()
testdata = JObject.Parse(File.ReadAllText("data/psbt.json"));
ComparerInstance = new PSBTComparer();
}
public PSBTTests(ITestOutputHelper output)
{
Output = output;
}

[Fact]
[Trait("UnitTest", "UnitTest")]
Expand Down Expand Up @@ -138,8 +145,8 @@ public void CanUpdate()
Assert.Single(signedPSBTWithCoins.inputs[3].PartialSigs);
Assert.Single(signedPSBTWithCoins.inputs[4].PartialSigs);
Assert.Single(signedPSBTWithCoins.inputs[5].PartialSigs);
signedPSBTWithCoins.TryFinalize(out bool HasFinalizationSucceedForPSBTWithoutPrevTX);
Assert.False(HasFinalizationSucceedForPSBTWithoutPrevTX);
signedPSBTWithCoins.TryFinalize(out var finalizationErrors);
Assert.Equal(4, finalizationErrors.Length); // Only p2wpkh and p2sh-p2wpkh will succeed.

var psbtWithTXs = PSBT.FromTransaction(tx, true)
.AddTransactions(funds);
Expand Down Expand Up @@ -205,6 +212,88 @@ public void ShouldFailToSignForTestcaseInvalidForSigner()
}
}

[Fact]
[Trait("UnitTest", "UnitTest")]
public void ShouldCaptureExceptionInFinalization()
{
var keys = new Key[] { new Key(), new Key(), new Key() };
var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(k => k.PubKey).ToArray());
var network = Network.Main;
var funds = CreateDummyFunds(network, keys, redeem);

var tx = CreateTxToSpendFunds(funds, keys, redeem, false, false);
var psbt = PSBT.FromTransaction(tx);

psbt.TryFinalize(out var errors);
Assert.Equal(6, errors.Length);
}

[Fact]
[Trait("UnitTest", "UnitTest")]
public void AddingScriptCoinShouldResultMoreInfoThanAddingSeparatelyInCaseOfP2SH()
{
var keys = new Key[] { new Key(), new Key(), new Key() };
var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(k => k.PubKey).ToArray());
var network = Network.Main;
var funds = CreateDummyFunds(network, keys, redeem);

var tx = CreateTxToSpendFunds(funds, keys, redeem, false, false);
var psbt = PSBT.FromTransaction(tx);

// case 1: Check that it will result to more info by adding ScriptCoin in case of p2sh-p2wpkh
var coins1 = DummyFundsToCoins(funds, null, null); // without script
var scriptCoins2 = DummyFundsToCoins(funds, null, keys[0]); // only with p2sh-p2wpkh redeem.
var psbt1 = psbt.Clone().AddCoins(coins1).TryAddScript(redeem);
var psbt2 = psbt.Clone().AddCoins(scriptCoins2).TryAddScript(redeem);
for (int i = 0; i < 6; i++)
{
Output.WriteLine($"Testing {i}");
var a = psbt1.inputs[i];
var e = psbt2.inputs[i];
// Since there are no way psbt can know p2sh-p2wpkh is actually a witness input in case we add coins and scripts separately,
// coin will not be added to the inputs[4].
if (i == 4) // p2sh-p2wpkh
{
Assert.NotEqual(a.ToBytes(), e.ToBytes());
Assert.Null(a.RedeemScript);
Assert.Null(a.WitnessUtxo);
Assert.NotNull(e.RedeemScript);
Assert.NotNull(e.WitnessUtxo);
}
// but otherwise, it will be the same.
else
{
AssertEx.CollectionEquals(a.ToBytes(), e.ToBytes());
}
}

// case 2: bare p2sh and p2sh-pw2sh
var scriptCoins3 = DummyFundsToCoins(funds, redeem, keys[0]); // with full scripts.
var psbt3 = psbt.Clone().AddCoins(scriptCoins3);
for (int i = 0; i < 6; i++)
{
Output.WriteLine($"Testing {i}");
var a = psbt2.inputs[i];
var e = psbt3.inputs[i];
if (i == 2 || i == 5) // p2sh or p2sh-p2wsh
{
Assert.NotEqual<byte[]>(a.ToBytes(), e.ToBytes());
Assert.Null(a.WitnessUtxo);
Assert.Null(a.RedeemScript);
Assert.NotNull(e.RedeemScript);
if (i == 5) // p2sh-p2wsh
{
Assert.NotNull(e.WitnessUtxo);
Assert.NotNull(e.WitnessScript);
}
}
else
{
AssertEx.CollectionEquals(a.ToBytes(), e.ToBytes());
}
}
}

[Fact]
[Trait("UnitTest", "UnitTest")]
public void ShouldPassTheLongestTestInBIP174()
Expand Down Expand Up @@ -309,10 +398,10 @@ static internal ICoin[] DummyFundsToCoins(IEnumerable<Transaction> txs, Script r
var coins = new ICoin[barecoins.Length];
coins[0] = barecoins[0];
coins[1] = barecoins[1];
coins[2] = new ScriptCoin(barecoins[2], redeem); // p2sh
coins[3] = new ScriptCoin(barecoins[3], redeem); // p2wsh
coins[4] = new ScriptCoin(barecoins[4], key.PubKey.WitHash.ScriptPubKey); // p2sh-p2wpkh
coins[5] = new ScriptCoin(barecoins[5], redeem); // p2sh-p2wsh
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;
}

Expand Down
165 changes: 159 additions & 6 deletions NBitcoin.Tests/RPCClientTests.cs
Expand Up @@ -32,12 +32,14 @@ public class RPCClientTests
const string TestAccount = "NBitcoin.RPCClientTests";

public PSBTComparer PSBTComparerInstance { get; }
public ITestOutputHelper Output { get; }

public RPCClientTests()
public RPCClientTests(ITestOutputHelper output)
{
Arb.Register<PSBTGenerator>();
Arb.Register<SegwitTransactionGenerators>();
PSBTComparerInstance = new PSBTComparer();
Output = output;
}

[Fact]
Expand Down Expand Up @@ -1404,8 +1406,8 @@ public void ShouldWalletProcessPSBTAndExtractMempoolAcceptableTX()
result = client.WalletProcessPSBT(psbtUnFinalized, false, type, bip32derivs: true);
Assert.False(result.Complete);
Assert.False(result.PSBT.CanExtractTX());
result.PSBT.TryFinalize(out bool isFinalized2);
Assert.False(isFinalized2);
result.PSBT.TryFinalize(out var errors2);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should still Assert.False this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed the TryFinalize API to take out InvalidOperationException[] instead of out bool so that the user can see why (and which input) is failing to finalize (breaking change). So Assert.NotEmpty below is equal to previous Assert.False
Sorry that I have put too many changes in one PR, I will split.

Assert.NotEmpty(errors2);
foreach (var psbtin in result.PSBT.inputs)
{
Assert.Equal(SigHash.Undefined, psbtin.SighashType);
Expand All @@ -1414,10 +1416,161 @@ public void ShouldWalletProcessPSBTAndExtractMempoolAcceptableTX()

// signed
result = client.WalletProcessPSBT(psbtUnFinalized, true, type);
result.PSBT.TryFinalize(out bool isFinalized3);
Assert.True(isFinalized3);
result.PSBT.TryFinalize(out var errors3);
Assert.Empty(errors3);
var txResult = result.PSBT.ExtractTX();
client.TestMempoolAccept(txResult, true);
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not fixed in 0.17.1 ? :(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried but its not fixed :(

[Fact]
public void ShouldPerformMultisigProcessingWithCore()
{
using (var builder = NodeBuilderEx.Create())
{
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<PubKey> { 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<BitcoinAddress, Money>();
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.TrySignAll(david);
var alice = clients[0];
var psbt2 = alice.WalletProcessPSBT(psbt).PSBT;
psbt2.TryFinalize(out var errors);
Assert.NotEmpty(errors); // not enough signature.

// 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]
/// <summary>
/// For p2sh, p2wsh, p2sh-p2wsh, we must also test the case for `solvable` to the wallet.
/// For that, both script and the address must be imported by `importmulti`.
/// but importmulti can not handle witness script(in v0.17).
/// TODO: add test for solvable scripts.
/// </summary>
public void ShouldGetAddressInfo()
{
using (var builder = NodeBuilderEx.Create())
{
var client = builder.CreateNode(true).CreateRPCClient();
var addrLegacy = client.GetNewAddress(new GetNewAddressRequest() { AddressType = AddressType.Legacy });
var addrBech32 = client.GetNewAddress(new GetNewAddressRequest() { AddressType = AddressType.Bech32 });
var addrP2SHSegwit = client.GetNewAddress(new GetNewAddressRequest() { AddressType = AddressType.P2SHSegwit });
var pubkeys = new PubKey[] { new Key().PubKey, new Key().PubKey, new Key().PubKey };
var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(2, pubkeys);
client.ImportAddress(redeem.Hash);
client.ImportAddress(redeem.WitHash);
client.ImportAddress(redeem.WitHash.ScriptPubKey.Hash);

Assert.NotNull(client.GetAddressInfo(addrLegacy));
Assert.NotNull(client.GetAddressInfo(addrBech32));
Assert.NotNull(client.GetAddressInfo(addrP2SHSegwit));
Assert.NotNull(client.GetAddressInfo(redeem.Hash));
Assert.NotNull(client.GetAddressInfo(redeem.WitHash));
Assert.NotNull(client.GetAddressInfo(redeem.WitHash.ScriptPubKey.Hash));
}
}

Expand Down