multi: add p2tr tapscript signing capabilities
This commit is contained in:
7 files changed
+445
-104
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user