multi: add p2tr tapscript signing capabilities

This commit is contained in:
Oliver Gugger committed 2022-03-24 18:02:39 +01:00
1 parent 5109c55721
commit 78db46be7e
7 files changed
+445 -104

No files matched your search

+222
View File
@@ -1,11 +1,23 @@
package itest
import (
"bytes"
"context"
"encoding/hex"
"testing"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
)
@@ -16,6 +28,15 @@ func testTaproot(net *lntest.NetworkHarness, t *harnessTest) {
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
testTaprootKeySpend(ctxt, t, net)
testTaprootScriptSpend(ctxt, t, net)
}
// testTaprootKeySpend tests sending to and spending from p2tr key spend only
// (BIP-0086) addresses.
func testTaprootKeySpend(ctxt context.Context, t *harnessTest,
net *lntest.NetworkHarness) {
// We'll start the test by sending Alice some coins, which she'll use to
// send to herself on a p2tr output.
net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, net.Alice)
@@ -81,3 +102,204 @@ func testTaproot(net *lntest.NetworkHarness, t *harnessTest) {
// Mine another block to clean up the mempool.
mineBlocks(t, net, 1, 1)
}
// testTaprootScriptSpend tests sending to and spending from p2tr script
// addresses.
func testTaprootScriptSpend(ctxt context.Context, t *harnessTest,
net *lntest.NetworkHarness) {
// For the next step, we need a public key. Let's use a special family
// for this.
const taprootKeyFamily = 77
keyDesc, err := net.Alice.WalletKitClient.DeriveNextKey(
ctxt, &walletrpc.KeyReq{
KeyFamily: taprootKeyFamily,
},
)
require.NoError(t.t, err)
leafSigningKey, err := btcec.ParsePubKey(keyDesc.RawKeyBytes)
require.NoError(t.t, err)
// Let's create a taproot script output now. This is a hash lock with a
// simple preimage of "foobar".
leaf1 := testScriptHashLock(t.t, []byte("foobar"))
// Let's add a second script output as well to test the partial reveal.
leaf2 := testScriptSchnorrSig(t.t, leafSigningKey)
dummyInternalKeyBytes, _ := hex.DecodeString(
"03464805f5468e294d88cf15a3f06aef6c89d63ef1bd7b42db2e0c74c1ac" +
"eb90fe",
)
dummyInternalKey, _ := btcec.ParsePubKey(dummyInternalKeyBytes)
tapscript := input.TapscriptPartialReveal(
dummyInternalKey, leaf2, leaf1.TapHash(),
)
taprootKey, err := tapscript.TaprootKey()
require.NoError(t.t, err)
tapScriptAddr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(taprootKey), harnessNetParams,
)
require.NoError(t.t, err)
p2trPkScript, err := txscript.PayToAddrScript(tapScriptAddr)
require.NoError(t.t, err)
// Send some coins to the generated tapscript address.
_, err = net.Alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
Addr: tapScriptAddr.String(),
Amount: 800_000,
})
require.NoError(t.t, err)
// Wait until the TX is found in the mempool.
txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
require.NoError(t.t, err)
p2trOutputIndex := getOutputIndex(
t, net.Miner, txid, tapScriptAddr.String(),
)
// Clear the mempool.
mineBlocks(t, net, 1, 1)
// Spend the output again, this time back to a p2wkh address.
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
ctxt, t.t, net.Alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
)
// Create fee estimation for a p2tr input and p2wkh output.
feeRate := chainfee.SatPerKWeight(12500)
estimator := input.TxWeightEstimator{}
estimator.AddTapscriptInput(
input.TaprootSignatureWitnessSize, tapscript,
)
estimator.AddP2WKHOutput()
estimatedWeight := int64(estimator.Weight())
requiredFee := feeRate.FeeForWeight(estimatedWeight)
tx := wire.NewMsgTx(2)
tx.TxIn = []*wire.TxIn{{
PreviousOutPoint: wire.OutPoint{
Hash: *txid,
Index: uint32(p2trOutputIndex),
},
}}
value := int64(800_000 - requiredFee)
tx.TxOut = []*wire.TxOut{{
PkScript: p2wkhPkScript,
Value: value,
}}
var buf bytes.Buffer
require.NoError(t.t, tx.Serialize(&buf))
utxoInfo := []*signrpc.TxOut{{
PkScript: p2trPkScript,
Value: 800_000,
}}
signResp, err := net.Alice.SignerClient.SignOutputRaw(
ctxt, &signrpc.SignReq{
RawTxBytes: buf.Bytes(),
SignDescs: []*signrpc.SignDescriptor{{
Output: utxoInfo[0],
InputIndex: 0,
KeyDesc: keyDesc,
Sighash: uint32(txscript.SigHashDefault),
WitnessScript: leaf2.Script,
}},
PrevOutputs: utxoInfo,
},
)
require.NoError(t.t, err)
// We can now assemble the witness stack.
controlBlockBytes, err := tapscript.ControlBlock.ToBytes()
require.NoError(t.t, err)
tx.TxIn[0].Witness = wire.TxWitness{
signResp.RawSigs[0],
leaf2.Script,
controlBlockBytes,
}
buf.Reset()
require.NoError(t.t, tx.Serialize(&buf))
// Since Schnorr signatures are fixed size, we must be able to estimate
// the size of this transaction exactly.
txWeight := blockchain.GetTransactionWeight(btcutil.NewTx(tx))
require.Equal(t.t, txWeight, estimatedWeight)
_, err = net.Alice.WalletKitClient.PublishTransaction(
ctxt, &walletrpc.Transaction{
TxHex: buf.Bytes(),
},
)
require.NoError(t.t, err)
// Wait until the spending tx is found.
txid, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
require.NoError(t.t, err)
p2wpkhOutputIndex := getOutputIndex(
t, net.Miner, txid, p2wkhAddr.String(),
)
op := &lnrpc.OutPoint{
TxidBytes: txid[:],
OutputIndex: uint32(p2wpkhOutputIndex),
}
assertWalletUnspent(t, net.Alice, op)
// Mine another block to clean up the mempool and to make sure the spend
// tx is actually included in a block.
mineBlocks(t, net, 1, 1)
}
// testScriptHashLock returns a simple bitcoin script that locks the funds to
// a hash lock of the given preimage.
func testScriptHashLock(t *testing.T, preimage []byte) txscript.TapLeaf {
builder := txscript.NewScriptBuilder()
builder.AddOp(txscript.OP_DUP)
builder.AddOp(txscript.OP_HASH160)
builder.AddData(btcutil.Hash160(preimage))
builder.AddOp(txscript.OP_EQUALVERIFY)
script1, err := builder.Script()
require.NoError(t, err)
return txscript.NewBaseTapLeaf(script1)
}
// testScriptSchnorrSig returns a simple bitcoin script that locks the funds to
// a Schnorr signature of the given public key.
func testScriptSchnorrSig(t *testing.T,
pubKey *btcec.PublicKey) txscript.TapLeaf {
builder := txscript.NewScriptBuilder()
builder.AddData(schnorr.SerializePubKey(pubKey))
builder.AddOp(txscript.OP_CHECKSIG)
script2, err := builder.Script()
require.NoError(t, err)
return txscript.NewBaseTapLeaf(script2)
}
// newAddrWithScript returns a new address and its pkScript.
func newAddrWithScript(ctx context.Context, t *testing.T,
node *lntest.HarnessNode, addrType lnrpc.AddressType) (btcutil.Address,
[]byte) {
p2wkhResp, err := node.NewAddress(ctx, &lnrpc.NewAddressRequest{
Type: addrType,
})
require.NoError(t, err)
p2wkhAddr, err := btcutil.DecodeAddress(
p2wkhResp.Address, harnessNetParams,
)
require.NoError(t, err)
p2wkhPkScript, err := txscript.PayToAddrScript(p2wkhAddr)
require.NoError(t, err)
return p2wkhAddr, p2wkhPkScript
}