public async Task CanBuildTaprootSingleSigTransactions() { using (var nodeBuilder = NodeBuilderEx.Create()) { var rpc = nodeBuilder.CreateNode().CreateRPCClient(); nodeBuilder.StartAll(); rpc.Generate(102); var change = new Key(); var rootKey = new ExtKey(); var accountKeyPath = new KeyPath("86'/0'/0'"); var accountRootKeyPath = new RootedKeyPath(rootKey.GetPublicKey().GetHDFingerPrint(), accountKeyPath); var accountKey = rootKey.Derive(accountKeyPath); var key = accountKey.Derive(new KeyPath("0/0")).PrivateKey; var address = key.PubKey.GetAddress(ScriptPubKeyType.TaprootBIP86, nodeBuilder.Network); var destination = new Key(); var amount = new Money(1, MoneyUnit.BTC); uint256 id = null; Transaction tx = null; ICoin coin = null; TransactionBuilder builder = null; var rate = new FeeRate(Money.Satoshis(1), 1); async Task RefreshCoin() { id = await rpc.SendToAddressAsync(address, Money.Coins(1)); tx = await rpc.GetRawTransactionAsync(id); coin = tx.Outputs.AsCoins().Where(o => o.ScriptPubKey == address.ScriptPubKey).Single(); builder = Network.Main.CreateTransactionBuilder(0); } await RefreshCoin(); var signedTx = builder .AddCoins(coin) .AddKeys(key) .Send(destination, amount) .SubtractFees() .SetChange(change) .SendEstimatedFees(rate) .BuildTransaction(true); rpc.SendRawTransaction(signedTx); await RefreshCoin(); // Let's try again, but this time with PSBT var psbt = builder .AddCoins(coin) .Send(destination, amount) .SubtractFees() .SetChange(change) .SendEstimatedFees(rate) .BuildPSBT(false); var tk = key.PubKey.GetTaprootFullPubKey(); psbt.Inputs[0].HDTaprootKeyPaths.Add(tk.OutputKey, new TaprootKeyPath(accountRootKeyPath.Derive(KeyPath.Parse("0/0")))); psbt.SignAll(ScriptPubKeyType.TaprootBIP86, accountKey, accountRootKeyPath); // Check if we can roundtrip psbt = CanRoundtripPSBT(psbt); psbt.Finalize(); rpc.SendRawTransaction(psbt.ExtractTransaction()); // Let's try again, but this time with BuildPSBT(true) await RefreshCoin(); psbt = builder .AddCoins(coin) .AddKeys(key) .Send(destination, amount) .SubtractFees() .SetChange(change) .SendEstimatedFees(rate) .BuildPSBT(true); psbt.Finalize(); rpc.SendRawTransaction(psbt.ExtractTransaction()); // Let's try again, this time with a merkle root var merkleRoot = RandomUtils.GetUInt256(); address = key.PubKey.GetTaprootFullPubKey(merkleRoot).GetAddress(nodeBuilder.Network); await RefreshCoin(); psbt = builder .AddCoins(coin) .AddKeys(key.CreateTaprootKeyPair(merkleRoot)) .Send(destination, amount) .SubtractFees() .SetChange(change) .SendEstimatedFees(rate) .BuildPSBT(true); Assert.NotNull(psbt.Inputs[0].TaprootMerkleRoot); Assert.NotNull(psbt.Inputs[0].TaprootInternalKey); Assert.NotNull(psbt.Inputs[0].TaprootKeySignature); psbt = CanRoundtripPSBT(psbt); psbt.Finalize(); rpc.SendRawTransaction(psbt.ExtractTransaction()); // Can we sign the PSBT separately? await RefreshCoin(); psbt = builder .AddCoins(coin) .Send(destination, amount) .SubtractFees() .SetChange(change) .SendEstimatedFees(rate) .BuildPSBT(false); var taprootKeyPair = key.CreateTaprootKeyPair(merkleRoot); psbt.Inputs[0].Sign(taprootKeyPair); Assert.NotNull(psbt.Inputs[0].TaprootMerkleRoot); Assert.NotNull(psbt.Inputs[0].TaprootInternalKey); Assert.NotNull(psbt.Inputs[0].TaprootKeySignature); // This line is useless, we just use it to test the PSBT roundtrip psbt.Inputs[0].HDTaprootKeyPaths.Add(taprootKeyPair.PubKey, new TaprootKeyPath(RootedKeyPath.Parse("12345678/86'/0'/0'/0/0"), new uint256[] { RandomUtils.GetUInt256() })); psbt = CanRoundtripPSBT(psbt); psbt.Finalize(); rpc.SendRawTransaction(psbt.ExtractTransaction()); // Can we sign the transaction separately? await RefreshCoin(); var coin1 = coin; await RefreshCoin(); var coin2 = coin; builder = Network.Main.CreateTransactionBuilder(0); signedTx = builder .AddCoins(coin1, coin2) .Send(destination, amount) .SubtractFees() .SetChange(change) .SendEstimatedFees(rate) .BuildTransaction(false); var unsignedTx = signedTx.Clone(); builder = Network.Main.CreateTransactionBuilder(0); builder.AddKeys(key.CreateTaprootKeyPair(merkleRoot)); builder.AddCoins(coin1); var ex = Assert.Throws <InvalidOperationException>(() => builder.SignTransactionInPlace(signedTx)); Assert.Contains("taproot", ex.Message); builder.AddCoin(coin2); builder.SignTransactionInPlace(signedTx); Assert.True(!WitScript.IsNullOrEmpty(signedTx.Inputs.FindIndexedInput(coin2.Outpoint).WitScript)); // Another solution is to set the precomputed transaction data. signedTx = unsignedTx; builder = Network.Main.CreateTransactionBuilder(0); builder.AddKeys(key.CreateTaprootKeyPair(merkleRoot)); builder.AddCoins(coin2); builder.SetSigningOptions(new SigningOptions() { PrecomputedTransactionData = signedTx.PrecomputeTransactionData(new ICoin[] { coin1, coin2 }) }); builder.SignTransactionInPlace(signedTx); Assert.True(!WitScript.IsNullOrEmpty(signedTx.Inputs.FindIndexedInput(coin2.Outpoint).WitScript)); // Let's check if we estimate precisely the size of a taproot transaction. await RefreshCoin(); signedTx = builder .AddCoins(coin) .AddKeys(key.CreateTaprootKeyPair(merkleRoot)) .Send(destination, amount) .SubtractFees() .SetChange(change) .SendEstimatedFees(rate) .BuildTransaction(false); var actualvsize = builder.EstimateSize(signedTx, true); builder.SignTransactionInPlace(signedTx); var expectedvsize = signedTx.GetVirtualSize(); // The estimator can't assume the sighash to be default // for all inputs, so we likely overestimate 1 bytes per input Assert.Equal(expectedvsize, actualvsize - 1); } }
public void CanSignPSBTWithRootAndAccountKey() { using (var nodeBuilder = NodeBuilderEx.Create()) { var rpc = nodeBuilder.CreateNode().CreateRPCClient(); nodeBuilder.StartAll(); rpc.Generate(102); uint hardenedFlag = 0U; retry: var masterKey = new ExtKey(); var accountKeyPath = new RootedKeyPath(masterKey, new KeyPath("49'/0'/0'")); var accountKey = masterKey.Derive(accountKeyPath); var addresses = Enumerable.Range(0, 5) .Select(i => { var addressPath = new KeyPath(new uint[] { 0U | hardenedFlag, (uint)i | hardenedFlag }); var fullAddressPath = accountKeyPath.Derive(addressPath); var address = accountKey.Derive(addressPath).GetPublicKey().WitHash.GetAddress(nodeBuilder.Network); return(new { FullAddressPath = fullAddressPath, AddressPath = addressPath, Address = address }); }).ToList(); var changeAddress = addresses.Last(); addresses = addresses.Take(addresses.Count - 1).ToList(); // Fund the addresses var coins = addresses.Select(async kra => { var id = await rpc.SendToAddressAsync(kra.Address, Money.Coins(1)); var tx = await rpc.GetRawTransactionAsync(id); return(tx.Outputs.AsCoins().Where(o => o.ScriptPubKey == kra.Address.ScriptPubKey).Single()); }).Select(t => t.Result).ToArray(); var destination = new Key().ScriptPubKey; var amount = new Money(1, MoneyUnit.BTC); var builder = Network.Main.CreateTransactionBuilder(); var fee = new Money(100_000L); var partiallySignedTx = builder .AddCoins(coins) .Send(destination, amount) .SetChange(changeAddress.Address) .SendFees(fee) .BuildPSBT(false); partiallySignedTx.AddKeyPath(masterKey, addresses.Concat(new[] { changeAddress }).Select(a => a.FullAddressPath.KeyPath).ToArray()); var expectedBalance = -amount - fee; var actualBalance = partiallySignedTx.GetBalance(ScriptPubKeyType.Segwit, accountKey, accountKeyPath); Assert.Equal(expectedBalance, actualBalance); actualBalance = partiallySignedTx.GetBalance(ScriptPubKeyType.Segwit, masterKey); Assert.Equal(expectedBalance, actualBalance); Assert.Equal(Money.Zero, partiallySignedTx.GetBalance(ScriptPubKeyType.Legacy, masterKey)); // You can sign with accountKey and keypath var memento = partiallySignedTx.Clone(); partiallySignedTx.SignAll(ScriptPubKeyType.Segwit, accountKey, accountKeyPath); Assert.True(partiallySignedTx.Inputs.All(i => i.PartialSigs.Count == 1)); partiallySignedTx.Finalize(); var partiallySignedTx2 = memento; // Or you can sign with the masterKey partiallySignedTx2.SignAll(ScriptPubKeyType.Segwit, masterKey); Assert.True(partiallySignedTx2.Inputs.All(i => i.PartialSigs.Count == 1)); partiallySignedTx2.Finalize(); Assert.Equal(partiallySignedTx, partiallySignedTx2); var signedTx = partiallySignedTx.ExtractTransaction(); rpc.SendRawTransaction(signedTx); var errors = builder.Check(signedTx); Assert.Empty(errors); if (hardenedFlag == 0) { hardenedFlag = 0x80000000; goto retry; } } }
public void Setup() { var seed = new ExtKey(); accPath = new KeyPath("87'/0'/0'").ToRootedKeyPath(seed.GetPublicKey().GetHDFingerPrint()); acc = seed.Derive(accPath.KeyPath).AsHDKeyCache(); var coins = Enumerable .Range(0, 1300) .Select(i => new Coin(RandomOutpoint(), new TxOut(Money.Coins(1.0m), acc.Derive(0).Derive((uint)i).GetPublicKey().GetScriptPubKey(ScriptPubKeyType.Segwit)))) .ToArray(); var tx = Transaction.Create(Network.Main); foreach (var c in coins) { tx.Inputs.Add(c.Outpoint); } tx.Outputs.Add(Money.Coins(1299.0m), new Key()); var psbt = PSBT.FromTransaction(tx, Network.Main); psbt.AddCoins(coins); for (int i = 0; i < coins.Length; i++) { psbt.Inputs[i].AddKeyPath(acc.Derive(0).Derive((uint)i).GetPublicKey(), accPath.Derive(0).Derive((uint)i)); } psbtStr = psbt.ToBase64(); psbt.SignAll(acc.AsHDScriptPubKey(ScriptPubKeyType.Segwit), acc, accPath); psbtSignedStr = psbt.ToBase64(); }