lnd/lntest/itest/lnd_taproot_test.go
Oliver Gugger c45d0f6b07
itest: add integration tests for tapscript import
We add basic integration test coverage for importing Tapscript
addresses. Note that FundPsbt is not supported with those types of
imported keys, so we need to manually fill in all information in the
PSBT to be able to sweep the funds again.
2022-08-25 09:21:52 +02:00

1967 lines
61 KiB
Go

package itest
import (
"bytes"
"context"
"crypto/sha256"
"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/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
"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"
)
const (
testTaprootKeyFamily = 77
testAmount = 800_000
signMethodBip86 = signrpc.SignMethod_SIGN_METHOD_TAPROOT_KEY_SPEND_BIP0086
signMethodRootHash = signrpc.SignMethod_SIGN_METHOD_TAPROOT_KEY_SPEND
signMethodTapscript = signrpc.SignMethod_SIGN_METHOD_TAPROOT_SCRIPT_SPEND
)
var (
dummyInternalKeyBytes, _ = hex.DecodeString(
"03464805f5468e294d88cf15a3f06aef6c89d63ef1bd7b42db2e0c74c1ac" +
"eb90fe",
)
dummyInternalKey, _ = btcec.ParsePubKey(dummyInternalKeyBytes)
)
// testTaproot ensures that the daemon can send to and spend from taproot (p2tr)
// outputs.
func testTaproot(net *lntest.NetworkHarness, t *harnessTest) {
ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, 2*defaultTimeout)
defer cancel()
testTaprootSendCoinsKeySpendBip86(ctxt, t, net.Alice, net)
testTaprootComputeInputScriptKeySpendBip86(ctxt, t, net.Alice, net)
testTaprootSignOutputRawScriptSpend(ctxt, t, net.Alice, net)
testTaprootSignOutputRawKeySpendBip86(ctxt, t, net.Alice, net)
testTaprootSignOutputRawKeySpendRootHash(ctxt, t, net.Alice, net)
testTaprootMuSig2KeySpendBip86(ctxt, t, net.Alice, net)
testTaprootMuSig2KeySpendRootHash(ctxt, t, net.Alice, net)
testTaprootMuSig2ScriptSpend(ctxt, t, net.Alice, net)
testTaprootMuSig2CombinedLeafKeySpend(ctxt, t, net.Alice, net)
testTaprootImportTapscriptFullTree(ctxt, t, net.Alice, net)
testTaprootImportTapscriptPartialReveal(ctxt, t, net.Alice, net)
testTaprootImportTapscriptRootHashOnly(ctxt, t, net.Alice, net)
testTaprootImportTapscriptFullKey(ctxt, t, net.Alice, net)
}
// testTaprootSendCoinsKeySpendBip86 tests sending to and spending from
// p2tr key spend only (BIP-0086) addresses through the SendCoins RPC which
// internally uses the ComputeInputScript method for signing.
func testTaprootSendCoinsKeySpendBip86(ctxt context.Context,
t *harnessTest, alice *lntest.HarnessNode, 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, alice)
// Let's create a p2tr address now.
p2trResp, err := alice.NewAddress(ctxt, &lnrpc.NewAddressRequest{
Type: lnrpc.AddressType_TAPROOT_PUBKEY,
})
require.NoError(t.t, err)
// Assert this is a segwit v1 address that starts with bcrt1p.
require.Contains(
t.t, p2trResp.Address, net.Miner.ActiveNet.Bech32HRPSegwit+"1p",
)
// Send the coins from Alice's wallet to her own, but to the new p2tr
// address.
_, err = alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
Addr: p2trResp.Address,
Amount: 0.5 * btcutil.SatoshiPerBitcoin,
})
require.NoError(t.t, err)
txid, err := waitForTxInMempool(net.Miner.Client, defaultTimeout)
require.NoError(t.t, err)
// Wait until bob has seen the tx and considers it as owned.
p2trOutputIndex := getOutputIndex(t, net.Miner, txid, p2trResp.Address)
op := &lnrpc.OutPoint{
TxidBytes: txid[:],
OutputIndex: uint32(p2trOutputIndex),
}
assertWalletUnspent(t, alice, op, "")
// Mine a block to clean up the mempool.
mineBlocks(t, net, 1, 1)
// Let's sweep the whole wallet to a new p2tr address, making sure we
// can sign transactions with v0 and v1 inputs.
p2trResp, err = alice.NewAddress(ctxt, &lnrpc.NewAddressRequest{
Type: lnrpc.AddressType_TAPROOT_PUBKEY,
})
require.NoError(t.t, err)
_, err = alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
Addr: p2trResp.Address,
SendAll: true,
})
require.NoError(t.t, err)
// Make sure the coins sent to the address are confirmed correctly,
// including the confirmation notification.
confirmAddress(ctxt, t, net, alice, p2trResp.Address)
}
// testTaprootComputeInputScriptKeySpendBip86 tests sending to and spending
// from p2tr key spend only (BIP-0086) addresses through the ComputeInputScript
// RPC.
func testTaprootComputeInputScriptKeySpendBip86(ctxt context.Context,
t *harnessTest, alice *lntest.HarnessNode, 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, alice)
// Let's create a p2tr address now.
p2trAddr, p2trPkScript := newAddrWithScript(
ctxt, t.t, alice, lnrpc.AddressType_TAPROOT_PUBKEY,
)
// Send the coins from Alice's wallet to her own, but to the new p2tr
// address.
_, err := alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
Addr: p2trAddr.String(),
Amount: testAmount,
})
require.NoError(t.t, err)
txid, err := waitForTxInMempool(net.Miner.Client, defaultTimeout)
require.NoError(t.t, err)
// Wait until bob has seen the tx and considers it as owned.
p2trOutputIndex := getOutputIndex(t, net.Miner, txid, p2trAddr.String())
op := &lnrpc.OutPoint{
TxidBytes: txid[:],
OutputIndex: uint32(p2trOutputIndex),
}
assertWalletUnspent(t, alice, op, "")
p2trOutpoint := wire.OutPoint{
Hash: *txid,
Index: uint32(p2trOutputIndex),
}
// Mine a block to clean up the mempool.
mineBlocks(t, net, 1, 1)
// We'll send the coins back to a p2wkh address.
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
ctxt, t.t, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
)
// Create fee estimation for a p2tr input and p2wkh output.
feeRate := chainfee.SatPerKWeight(12500)
estimator := input.TxWeightEstimator{}
estimator.AddTaprootKeySpendInput(txscript.SigHashDefault)
estimator.AddP2WKHOutput()
estimatedWeight := int64(estimator.Weight())
requiredFee := feeRate.FeeForWeight(estimatedWeight)
tx := wire.NewMsgTx(2)
tx.TxIn = []*wire.TxIn{{
PreviousOutPoint: p2trOutpoint,
}}
value := int64(testAmount - 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: testAmount,
}}
signResp, err := alice.SignerClient.ComputeInputScript(
ctxt, &signrpc.SignReq{
RawTxBytes: buf.Bytes(),
SignDescs: []*signrpc.SignDescriptor{{
Output: utxoInfo[0],
InputIndex: 0,
Sighash: uint32(txscript.SigHashDefault),
}},
PrevOutputs: utxoInfo,
},
)
require.NoError(t.t, err)
tx.TxIn[0].Witness = signResp.InputScripts[0].Witness
// Serialize, weigh and publish the TX now, then make sure the
// coins are sent and confirmed to the final sweep destination address.
publishTxAndConfirmSweep(
ctxt, t, net, alice, tx, estimatedWeight,
&chainrpc.SpendRequest{
Outpoint: &chainrpc.Outpoint{
Hash: p2trOutpoint.Hash[:],
Index: p2trOutpoint.Index,
},
Script: p2trPkScript,
},
p2wkhAddr.String(),
)
}
// testTaprootSignOutputRawScriptSpend tests sending to and spending from p2tr
// script addresses using the script path with the SignOutputRaw RPC.
func testTaprootSignOutputRawScriptSpend(ctxt context.Context, t *harnessTest,
alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
// For the next step, we need a public key. Let's use a special family
// for this.
keyDesc, err := alice.WalletKitClient.DeriveNextKey(
ctxt, &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily},
)
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)
inclusionProof := leaf1.TapHash()
tapscript := input.TapscriptPartialReveal(
dummyInternalKey, leaf2, inclusionProof[:],
)
taprootKey, err := tapscript.TaprootKey()
require.NoError(t.t, err)
// Send some coins to the generated tapscript address.
p2trOutpoint, p2trPkScript := sendToTaprootOutput(
ctxt, t, net, alice, taprootKey, testAmount,
)
// Spend the output again, this time back to a p2wkh address.
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
ctxt, t.t, 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: p2trOutpoint,
}}
value := int64(testAmount - 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: testAmount,
}}
// Before we actually sign, we want to make sure that we get an error
// when we try to sign for a Taproot output without specifying all UTXO
// information.
_, err = 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,
SignMethod: signMethodTapscript,
}},
},
)
require.Error(t.t, err)
require.Contains(
t.t, err.Error(), "error signing taproot output, transaction "+
"input 0 is missing its previous outpoint information",
)
// We also want to make sure we get an error when we don't specify the
// correct signing method.
_, err = 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.Error(t.t, err)
require.Contains(
t.t, err.Error(), "selected sign method witness_v0 is not "+
"compatible with given pk script 5120",
)
// Do the actual signing now.
signResp, err := 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,
SignMethod: signMethodTapscript,
}},
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,
}
// Serialize, weigh and publish the TX now, then make sure the
// coins are sent and confirmed to the final sweep destination address.
publishTxAndConfirmSweep(
ctxt, t, net, alice, tx, estimatedWeight,
&chainrpc.SpendRequest{
Outpoint: &chainrpc.Outpoint{
Hash: p2trOutpoint.Hash[:],
Index: p2trOutpoint.Index,
},
Script: p2trPkScript,
},
p2wkhAddr.String(),
)
}
// testTaprootSignOutputRawKeySpendBip86 tests that a tapscript address can
// also be spent using the key spend path through the SignOutputRaw RPC using a
// BIP0086 key spend only commitment.
func testTaprootSignOutputRawKeySpendBip86(ctxt context.Context,
t *harnessTest, alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
// For the next step, we need a public key. Let's use a special family
// for this.
keyDesc, err := alice.WalletKitClient.DeriveNextKey(
ctxt, &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily},
)
require.NoError(t.t, err)
internalKey, err := btcec.ParsePubKey(keyDesc.RawKeyBytes)
require.NoError(t.t, err)
// We want to make sure we can still use a tweaked key, even if it ends
// up being essentially double tweaked because of the taproot root hash.
dummyKeyTweak := sha256.Sum256([]byte("this is a key tweak"))
internalKey = input.TweakPubKeyWithTweak(internalKey, dummyKeyTweak[:])
// Our taproot key is a BIP0086 key spend only construction that just
// commits to the internal key and no root hash.
taprootKey := txscript.ComputeTaprootKeyNoScript(internalKey)
// Send some coins to the generated tapscript address.
p2trOutpoint, p2trPkScript := sendToTaprootOutput(
ctxt, t, net, alice, taprootKey, testAmount,
)
// Spend the output again, this time back to a p2wkh address.
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
ctxt, t.t, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
)
// Create fee estimation for a p2tr input and p2wkh output.
feeRate := chainfee.SatPerKWeight(12500)
estimator := input.TxWeightEstimator{}
estimator.AddTaprootKeySpendInput(txscript.SigHashDefault)
estimator.AddP2WKHOutput()
estimatedWeight := int64(estimator.Weight())
requiredFee := feeRate.FeeForWeight(estimatedWeight)
tx := wire.NewMsgTx(2)
tx.TxIn = []*wire.TxIn{{
PreviousOutPoint: p2trOutpoint,
}}
value := int64(testAmount - 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: testAmount,
}}
signResp, err := alice.SignerClient.SignOutputRaw(
ctxt, &signrpc.SignReq{
RawTxBytes: buf.Bytes(),
SignDescs: []*signrpc.SignDescriptor{{
Output: utxoInfo[0],
InputIndex: 0,
KeyDesc: keyDesc,
SingleTweak: dummyKeyTweak[:],
Sighash: uint32(txscript.SigHashDefault),
SignMethod: signMethodBip86,
}},
PrevOutputs: utxoInfo,
},
)
require.NoError(t.t, err)
tx.TxIn[0].Witness = wire.TxWitness{
signResp.RawSigs[0],
}
// Serialize, weigh and publish the TX now, then make sure the
// coins are sent and confirmed to the final sweep destination address.
publishTxAndConfirmSweep(
ctxt, t, net, alice, tx, estimatedWeight,
&chainrpc.SpendRequest{
Outpoint: &chainrpc.Outpoint{
Hash: p2trOutpoint.Hash[:],
Index: p2trOutpoint.Index,
},
Script: p2trPkScript,
},
p2wkhAddr.String(),
)
}
// testTaprootSignOutputRawKeySpendRootHash tests that a tapscript address can
// also be spent using the key spend path through the SignOutputRaw RPC using a
// tapscript root hash.
func testTaprootSignOutputRawKeySpendRootHash(ctxt context.Context,
t *harnessTest, alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
// For the next step, we need a public key. Let's use a special family
// for this.
keyDesc, err := alice.WalletKitClient.DeriveNextKey(
ctxt, &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily},
)
require.NoError(t.t, err)
internalKey, err := btcec.ParsePubKey(keyDesc.RawKeyBytes)
require.NoError(t.t, err)
// We want to make sure we can still use a tweaked key, even if it ends
// up being essentially double tweaked because of the taproot root hash.
dummyKeyTweak := sha256.Sum256([]byte("this is a key tweak"))
internalKey = input.TweakPubKeyWithTweak(internalKey, dummyKeyTweak[:])
// 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"))
rootHash := leaf1.TapHash()
taprootKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash[:])
// Send some coins to the generated tapscript address.
p2trOutpoint, p2trPkScript := sendToTaprootOutput(
ctxt, t, net, alice, taprootKey, testAmount,
)
// Spend the output again, this time back to a p2wkh address.
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
ctxt, t.t, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
)
// Create fee estimation for a p2tr input and p2wkh output.
feeRate := chainfee.SatPerKWeight(12500)
estimator := input.TxWeightEstimator{}
estimator.AddTaprootKeySpendInput(txscript.SigHashDefault)
estimator.AddP2WKHOutput()
estimatedWeight := int64(estimator.Weight())
requiredFee := feeRate.FeeForWeight(estimatedWeight)
tx := wire.NewMsgTx(2)
tx.TxIn = []*wire.TxIn{{
PreviousOutPoint: p2trOutpoint,
}}
value := int64(testAmount - 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: testAmount,
}}
signResp, err := alice.SignerClient.SignOutputRaw(
ctxt, &signrpc.SignReq{
RawTxBytes: buf.Bytes(),
SignDescs: []*signrpc.SignDescriptor{{
Output: utxoInfo[0],
InputIndex: 0,
KeyDesc: keyDesc,
SingleTweak: dummyKeyTweak[:],
Sighash: uint32(txscript.SigHashDefault),
TapTweak: rootHash[:],
SignMethod: signMethodRootHash,
}},
PrevOutputs: utxoInfo,
},
)
require.NoError(t.t, err)
tx.TxIn[0].Witness = wire.TxWitness{
signResp.RawSigs[0],
}
// Serialize, weigh and publish the TX now, then make sure the
// coins are sent and confirmed to the final sweep destination address.
publishTxAndConfirmSweep(
ctxt, t, net, alice, tx, estimatedWeight,
&chainrpc.SpendRequest{
Outpoint: &chainrpc.Outpoint{
Hash: p2trOutpoint.Hash[:],
Index: p2trOutpoint.Index,
},
Script: p2trPkScript,
},
p2wkhAddr.String(),
)
}
// testTaprootMuSig2KeySpendBip86 tests that a combined MuSig2 key can also be
// used as a BIP-0086 key spend only key.
func testTaprootMuSig2KeySpendBip86(ctxt context.Context, t *harnessTest,
alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
// We're not going to commit to a script. So our taproot tweak will be
// empty and just specify the necessary flag.
taprootTweak := &signrpc.TaprootTweakDesc{
KeySpendOnly: true,
}
keyDesc1, keyDesc2, keyDesc3, allPubKeys := deriveSigningKeys(
ctxt, t, alice,
)
_, taprootKey, sessResp1, sessResp2, sessResp3 := createMuSigSessions(
ctxt, t, alice, taprootTweak, keyDesc1, keyDesc2, keyDesc3,
allPubKeys,
)
// Send some coins to the generated tapscript address.
p2trOutpoint, p2trPkScript := sendToTaprootOutput(
ctxt, t, net, alice, taprootKey, testAmount,
)
// Spend the output again, this time back to a p2wkh address.
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
ctxt, t.t, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
)
// Create fee estimation for a p2tr input and p2wkh output.
feeRate := chainfee.SatPerKWeight(12500)
estimator := input.TxWeightEstimator{}
estimator.AddTaprootKeySpendInput(txscript.SigHashDefault)
estimator.AddP2WKHOutput()
estimatedWeight := int64(estimator.Weight())
requiredFee := feeRate.FeeForWeight(estimatedWeight)
tx := wire.NewMsgTx(2)
tx.TxIn = []*wire.TxIn{{
PreviousOutPoint: p2trOutpoint,
}}
value := int64(testAmount - 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: testAmount,
}}
// We now need to create the raw sighash of the transaction, as that
// will be the message we're signing collaboratively.
prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
utxoInfo[0].PkScript, utxoInfo[0].Value,
)
sighashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
sigHash, err := txscript.CalcTaprootSignatureHash(
sighashes, txscript.SigHashDefault, tx, 0, prevOutputFetcher,
)
require.NoError(t.t, err)
// Now that we have the transaction prepared, we need to start with the
// signing. We simulate all three parties here, so we need to do
// everything three times. But because we're going to use session 1 to
// combine everything, we don't need its response, as it will store its
// own signature.
_, err = alice.SignerClient.MuSig2Sign(
ctxt, &signrpc.MuSig2SignRequest{
SessionId: sessResp1.SessionId,
MessageDigest: sigHash,
},
)
require.NoError(t.t, err)
signResp2, err := alice.SignerClient.MuSig2Sign(
ctxt, &signrpc.MuSig2SignRequest{
SessionId: sessResp2.SessionId,
MessageDigest: sigHash,
Cleanup: true,
},
)
require.NoError(t.t, err)
signResp3, err := alice.SignerClient.MuSig2Sign(
ctxt, &signrpc.MuSig2SignRequest{
SessionId: sessResp3.SessionId,
MessageDigest: sigHash,
Cleanup: true,
},
)
require.NoError(t.t, err)
// Luckily only one of the signers needs to combine the signature, so
// let's do that now.
combineReq1, err := alice.SignerClient.MuSig2CombineSig(
ctxt, &signrpc.MuSig2CombineSigRequest{
SessionId: sessResp1.SessionId,
OtherPartialSignatures: [][]byte{
signResp2.LocalPartialSignature,
signResp3.LocalPartialSignature,
},
},
)
require.NoError(t.t, err)
require.Equal(t.t, true, combineReq1.HaveAllSignatures)
require.NotEmpty(t.t, combineReq1.FinalSignature)
sig, err := schnorr.ParseSignature(combineReq1.FinalSignature)
require.NoError(t.t, err)
require.True(t.t, sig.Verify(sigHash, taprootKey))
tx.TxIn[0].Witness = wire.TxWitness{
combineReq1.FinalSignature,
}
// Serialize, weigh and publish the TX now, then make sure the
// coins are sent and confirmed to the final sweep destination address.
publishTxAndConfirmSweep(
ctxt, t, net, alice, tx, estimatedWeight,
&chainrpc.SpendRequest{
Outpoint: &chainrpc.Outpoint{
Hash: p2trOutpoint.Hash[:],
Index: p2trOutpoint.Index,
},
Script: p2trPkScript,
},
p2wkhAddr.String(),
)
}
// testTaprootMuSig2KeySpendRootHash tests that a tapscript address can also be
// spent using a MuSig2 combined key.
func testTaprootMuSig2KeySpendRootHash(ctxt context.Context, t *harnessTest,
alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
// We're going to commit to a script as well. This is a hash lock with a
// simple preimage of "foobar". We need to know this upfront so, we can
// specify the taproot tweak with the root hash when creating the Musig2
// signing session.
leaf1 := testScriptHashLock(t.t, []byte("foobar"))
rootHash := leaf1.TapHash()
taprootTweak := &signrpc.TaprootTweakDesc{
ScriptRoot: rootHash[:],
}
keyDesc1, keyDesc2, keyDesc3, allPubKeys := deriveSigningKeys(
ctxt, t, alice,
)
_, taprootKey, sessResp1, sessResp2, sessResp3 := createMuSigSessions(
ctxt, t, alice, taprootTweak, keyDesc1, keyDesc2, keyDesc3,
allPubKeys,
)
// Send some coins to the generated tapscript address.
p2trOutpoint, p2trPkScript := sendToTaprootOutput(
ctxt, t, net, alice, taprootKey, testAmount,
)
// Spend the output again, this time back to a p2wkh address.
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
ctxt, t.t, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
)
// Create fee estimation for a p2tr input and p2wkh output.
feeRate := chainfee.SatPerKWeight(12500)
estimator := input.TxWeightEstimator{}
estimator.AddTaprootKeySpendInput(txscript.SigHashDefault)
estimator.AddP2WKHOutput()
estimatedWeight := int64(estimator.Weight())
requiredFee := feeRate.FeeForWeight(estimatedWeight)
tx := wire.NewMsgTx(2)
tx.TxIn = []*wire.TxIn{{
PreviousOutPoint: p2trOutpoint,
}}
value := int64(testAmount - 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: testAmount,
}}
// We now need to create the raw sighash of the transaction, as that
// will be the message we're signing collaboratively.
prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
utxoInfo[0].PkScript, utxoInfo[0].Value,
)
sighashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
sigHash, err := txscript.CalcTaprootSignatureHash(
sighashes, txscript.SigHashDefault, tx, 0, prevOutputFetcher,
)
require.NoError(t.t, err)
// Now that we have the transaction prepared, we need to start with the
// signing. We simulate all three parties here, so we need to do
// everything three times. But because we're going to use session 1 to
// combine everything, we don't need its response, as it will store its
// own signature.
_, err = alice.SignerClient.MuSig2Sign(
ctxt, &signrpc.MuSig2SignRequest{
SessionId: sessResp1.SessionId,
MessageDigest: sigHash,
},
)
require.NoError(t.t, err)
signResp2, err := alice.SignerClient.MuSig2Sign(
ctxt, &signrpc.MuSig2SignRequest{
SessionId: sessResp2.SessionId,
MessageDigest: sigHash,
Cleanup: true,
},
)
require.NoError(t.t, err)
signResp3, err := alice.SignerClient.MuSig2Sign(
ctxt, &signrpc.MuSig2SignRequest{
SessionId: sessResp3.SessionId,
MessageDigest: sigHash,
Cleanup: true,
},
)
require.NoError(t.t, err)
// Luckily only one of the signers needs to combine the signature, so
// let's do that now.
combineReq1, err := alice.SignerClient.MuSig2CombineSig(
ctxt, &signrpc.MuSig2CombineSigRequest{
SessionId: sessResp1.SessionId,
OtherPartialSignatures: [][]byte{
signResp2.LocalPartialSignature,
signResp3.LocalPartialSignature,
},
},
)
require.NoError(t.t, err)
require.Equal(t.t, true, combineReq1.HaveAllSignatures)
require.NotEmpty(t.t, combineReq1.FinalSignature)
sig, err := schnorr.ParseSignature(combineReq1.FinalSignature)
require.NoError(t.t, err)
require.True(t.t, sig.Verify(sigHash, taprootKey))
tx.TxIn[0].Witness = wire.TxWitness{
combineReq1.FinalSignature,
}
// Serialize, weigh and publish the TX now, then make sure the
// coins are sent and confirmed to the final sweep destination address.
publishTxAndConfirmSweep(
ctxt, t, net, alice, tx, estimatedWeight,
&chainrpc.SpendRequest{
Outpoint: &chainrpc.Outpoint{
Hash: p2trOutpoint.Hash[:],
Index: p2trOutpoint.Index,
},
Script: p2trPkScript,
},
p2wkhAddr.String(),
)
}
// testTaprootMuSig2ScriptSpend tests that a tapscript address with an internal
// key that is a MuSig2 combined key can also be spent using the script path.
func testTaprootMuSig2ScriptSpend(ctxt context.Context, t *harnessTest,
alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
// We're going to commit to a script and spend the output using the
// script. This is a hash lock with a simple preimage of "foobar". We
// need to know this upfront so, we can specify the taproot tweak with
// the root hash when creating the Musig2 signing session.
leaf1 := testScriptHashLock(t.t, []byte("foobar"))
rootHash := leaf1.TapHash()
taprootTweak := &signrpc.TaprootTweakDesc{
ScriptRoot: rootHash[:],
}
keyDesc1, keyDesc2, keyDesc3, allPubKeys := deriveSigningKeys(
ctxt, t, alice,
)
internalKey, taprootKey, _, _, _ := createMuSigSessions(
ctxt, t, alice, taprootTweak, keyDesc1, keyDesc2, keyDesc3,
allPubKeys,
)
// Because we know the internal key and the script we want to spend, we
// can now create the tapscript struct that's used for assembling the
// control block and fee estimation.
tapscript := input.TapscriptFullTree(internalKey, leaf1)
// Send some coins to the generated tapscript address.
p2trOutpoint, p2trPkScript := sendToTaprootOutput(
ctxt, t, net, alice, taprootKey, testAmount,
)
// Spend the output again, this time back to a p2wkh address.
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
ctxt, t.t, 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(
len([]byte("foobar"))+len(leaf1.Script)+1, tapscript,
)
estimator.AddP2WKHOutput()
estimatedWeight := int64(estimator.Weight())
requiredFee := feeRate.FeeForWeight(estimatedWeight)
tx := wire.NewMsgTx(2)
tx.TxIn = []*wire.TxIn{{
PreviousOutPoint: p2trOutpoint,
}}
value := int64(testAmount - requiredFee)
tx.TxOut = []*wire.TxOut{{
PkScript: p2wkhPkScript,
Value: value,
}}
// We can now assemble the witness stack.
controlBlockBytes, err := tapscript.ControlBlock.ToBytes()
require.NoError(t.t, err)
tx.TxIn[0].Witness = wire.TxWitness{
[]byte("foobar"),
leaf1.Script,
controlBlockBytes,
}
// Serialize, weigh and publish the TX now, then make sure the
// coins are sent and confirmed to the final sweep destination address.
publishTxAndConfirmSweep(
ctxt, t, net, alice, tx, estimatedWeight,
&chainrpc.SpendRequest{
Outpoint: &chainrpc.Outpoint{
Hash: p2trOutpoint.Hash[:],
Index: p2trOutpoint.Index,
},
Script: p2trPkScript,
},
p2wkhAddr.String(),
)
}
// testTaprootMuSig2CombinedLeafKeySpend tests that a MuSig2 combined key can be
// used for an OP_CHECKSIG inside a tap script leaf spend.
func testTaprootMuSig2CombinedLeafKeySpend(ctxt context.Context, t *harnessTest,
alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
// We're using the combined MuSig2 key in a script leaf. So we need to
// derive the combined key first, before we can build the script.
keyDesc1, keyDesc2, keyDesc3, allPubKeys := deriveSigningKeys(
ctxt, t, alice,
)
combineResp, err := alice.SignerClient.MuSig2CombineKeys(
ctxt, &signrpc.MuSig2CombineKeysRequest{
AllSignerPubkeys: allPubKeys,
},
)
require.NoError(t.t, err)
combinedPubKey, err := schnorr.ParsePubKey(combineResp.CombinedKey)
require.NoError(t.t, err)
// We're going to commit to a script and spend the output using the
// script. This is just an OP_CHECKSIG with the combined MuSig2 public
// key.
leaf := testScriptSchnorrSig(t.t, combinedPubKey)
tapscript := input.TapscriptPartialReveal(dummyInternalKey, leaf, nil)
taprootKey, err := tapscript.TaprootKey()
require.NoError(t.t, err)
// Send some coins to the generated tapscript address.
p2trOutpoint, p2trPkScript := sendToTaprootOutput(
ctxt, t, net, alice, taprootKey, testAmount,
)
// Spend the output again, this time back to a p2wkh address.
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
ctxt, t.t, 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: p2trOutpoint,
}}
value := int64(testAmount - 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: testAmount,
}}
// Do the actual signing now.
_, _, sessResp1, sessResp2, sessResp3 := createMuSigSessions(
ctxt, t, alice, nil, keyDesc1, keyDesc2, keyDesc3, allPubKeys,
)
require.NoError(t.t, err)
// We now need to create the raw sighash of the transaction, as that
// will be the message we're signing collaboratively.
prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
utxoInfo[0].PkScript, utxoInfo[0].Value,
)
sighashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
sigHash, err := txscript.CalcTapscriptSignaturehash(
sighashes, txscript.SigHashDefault, tx, 0, prevOutputFetcher,
leaf,
)
require.NoError(t.t, err)
// Now that we have the transaction prepared, we need to start with the
// signing. We simulate all three parties here, so we need to do
// everything three times. But because we're going to use session 1 to
// combine everything, we don't need its response, as it will store its
// own signature.
_, err = alice.SignerClient.MuSig2Sign(
ctxt, &signrpc.MuSig2SignRequest{
SessionId: sessResp1.SessionId,
MessageDigest: sigHash,
},
)
require.NoError(t.t, err)
signResp2, err := alice.SignerClient.MuSig2Sign(
ctxt, &signrpc.MuSig2SignRequest{
SessionId: sessResp2.SessionId,
MessageDigest: sigHash,
Cleanup: true,
},
)
require.NoError(t.t, err)
// Before we have all partial signatures, we shouldn't get a final
// signature back.
combineSigResp, err := alice.SignerClient.MuSig2CombineSig(
ctxt, &signrpc.MuSig2CombineSigRequest{
SessionId: sessResp1.SessionId,
OtherPartialSignatures: [][]byte{
signResp2.LocalPartialSignature,
},
},
)
require.NoError(t.t, err)
require.False(t.t, combineSigResp.HaveAllSignatures)
require.Empty(t.t, combineSigResp.FinalSignature)
signResp3, err := alice.SignerClient.MuSig2Sign(
ctxt, &signrpc.MuSig2SignRequest{
SessionId: sessResp3.SessionId,
MessageDigest: sigHash,
},
)
require.NoError(t.t, err)
// We manually clean up session 3, just to make sure that works as well.
_, err = alice.SignerClient.MuSig2Cleanup(
ctxt, &signrpc.MuSig2CleanupRequest{
SessionId: sessResp3.SessionId,
},
)
require.NoError(t.t, err)
// A second call to that cleaned up session should now fail with a
// specific error.
_, err = alice.SignerClient.MuSig2Sign(
ctxt, &signrpc.MuSig2SignRequest{
SessionId: sessResp3.SessionId,
MessageDigest: sigHash,
},
)
require.Error(t.t, err)
require.Contains(t.t, err.Error(), "not found")
// Luckily only one of the signers needs to combine the signature, so
// let's do that now.
combineReq1, err := alice.SignerClient.MuSig2CombineSig(
ctxt, &signrpc.MuSig2CombineSigRequest{
SessionId: sessResp1.SessionId,
OtherPartialSignatures: [][]byte{
signResp3.LocalPartialSignature,
},
},
)
require.NoError(t.t, err)
require.Equal(t.t, true, combineReq1.HaveAllSignatures)
require.NotEmpty(t.t, combineReq1.FinalSignature)
sig, err := schnorr.ParseSignature(combineReq1.FinalSignature)
require.NoError(t.t, err)
require.True(t.t, sig.Verify(sigHash, combinedPubKey))
// We can now assemble the witness stack.
controlBlockBytes, err := tapscript.ControlBlock.ToBytes()
require.NoError(t.t, err)
tx.TxIn[0].Witness = wire.TxWitness{
combineReq1.FinalSignature,
leaf.Script,
controlBlockBytes,
}
// Serialize, weigh and publish the TX now, then make sure the
// coins are sent and confirmed to the final sweep destination address.
publishTxAndConfirmSweep(
ctxt, t, net, alice, tx, estimatedWeight,
&chainrpc.SpendRequest{
Outpoint: &chainrpc.Outpoint{
Hash: p2trOutpoint.Hash[:],
Index: p2trOutpoint.Index,
},
Script: p2trPkScript,
},
p2wkhAddr.String(),
)
}
// testTaprootImportTapscriptScriptSpend tests importing p2tr script addresses
// using the script path with the full tree known.
func testTaprootImportTapscriptFullTree(ctxt context.Context, t *harnessTest,
alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
// For the next step, we need a public key. Let's use a special family
// for this.
_, internalKey, derivationPath := deriveInternalKey(ctxt, t, alice)
// 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, internalKey)
tapscript := input.TapscriptFullTree(internalKey, leaf1, leaf2)
tree := txscript.AssembleTaprootScriptTree(leaf1, leaf2)
rootHash := tree.RootNode.TapHash()
taprootKey, err := tapscript.TaprootKey()
require.NoError(t.t, err)
// Import the scripts and make sure we get the same address back as we
// calculated ourselves.
importResp, err := alice.WalletKitClient.ImportTapscript(
ctxt, &walletrpc.ImportTapscriptRequest{
InternalPublicKey: schnorr.SerializePubKey(internalKey),
Script: &walletrpc.ImportTapscriptRequest_FullTree{
FullTree: &walletrpc.TapscriptFullTree{
AllLeaves: []*walletrpc.TapLeaf{{
LeafVersion: uint32(
leaf1.LeafVersion,
),
Script: leaf1.Script,
}, {
LeafVersion: uint32(
leaf2.LeafVersion,
),
Script: leaf2.Script,
}},
},
},
},
)
require.NoError(t.t, err)
calculatedAddr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(taprootKey), harnessNetParams,
)
require.NoError(t.t, err)
require.Equal(t.t, calculatedAddr.String(), importResp.P2TrAddress)
// Send some coins to the generated tapscript address.
p2trOutpoint, p2trPkScript := sendToTaprootOutput(
ctxt, t, net, alice, taprootKey, testAmount,
)
p2trOutputRPC := &lnrpc.OutPoint{
TxidBytes: p2trOutpoint.Hash[:],
OutputIndex: p2trOutpoint.Index,
}
assertWalletUnspent(t, alice, p2trOutputRPC, "imported")
assertAccountBalance(t.t, alice, "imported", testAmount, 0)
// Funding a PSBT from an imported script is not yet possible. So we
// basically need to add all information manually for the wallet to be
// able to sign for it.
utxo := &wire.TxOut{
Value: testAmount,
PkScript: p2trPkScript,
}
clearWalletImportedTapscriptBalance(
ctxt, t, alice, utxo, p2trOutpoint, internalKey, derivationPath,
rootHash[:],
)
}
// testTaprootImportTapscriptPartialReveal tests importing p2tr script addresses
// for which we only know part of the tree.
func testTaprootImportTapscriptPartialReveal(ctxt context.Context,
t *harnessTest, alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
// For the next step, we need a public key. Let's use a special family
// for this.
_, internalKey, derivationPath := deriveInternalKey(ctxt, t, alice)
// 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, internalKey)
leaf2Hash := leaf2.TapHash()
tapscript := input.TapscriptPartialReveal(
internalKey, leaf1, leaf2Hash[:],
)
rootHash := tapscript.ControlBlock.RootHash(leaf1.Script)
taprootKey, err := tapscript.TaprootKey()
require.NoError(t.t, err)
// Import the scripts and make sure we get the same address back as we
// calculated ourselves.
importResp, err := alice.WalletKitClient.ImportTapscript(
ctxt, &walletrpc.ImportTapscriptRequest{
InternalPublicKey: schnorr.SerializePubKey(internalKey),
Script: &walletrpc.ImportTapscriptRequest_PartialReveal{
PartialReveal: &walletrpc.TapscriptPartialReveal{
RevealedLeaf: &walletrpc.TapLeaf{
LeafVersion: uint32(leaf1.LeafVersion),
Script: leaf1.Script,
},
FullInclusionProof: leaf2Hash[:],
},
},
},
)
require.NoError(t.t, err)
calculatedAddr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(taprootKey), harnessNetParams,
)
require.NoError(t.t, err)
require.Equal(t.t, calculatedAddr.String(), importResp.P2TrAddress)
// Send some coins to the generated tapscript address.
p2trOutpoint, p2trPkScript := sendToTaprootOutput(
ctxt, t, net, alice, taprootKey, testAmount,
)
p2trOutputRPC := &lnrpc.OutPoint{
TxidBytes: p2trOutpoint.Hash[:],
OutputIndex: p2trOutpoint.Index,
}
assertWalletUnspent(t, alice, p2trOutputRPC, "imported")
assertAccountBalance(t.t, alice, "imported", testAmount, 0)
// Funding a PSBT from an imported script is not yet possible. So we
// basically need to add all information manually for the wallet to be
// able to sign for it.
utxo := &wire.TxOut{
Value: testAmount,
PkScript: p2trPkScript,
}
clearWalletImportedTapscriptBalance(
ctxt, t, alice, utxo, p2trOutpoint, internalKey, derivationPath,
rootHash,
)
}
// testTaprootImportTapscriptRootHashOnly tests importing p2tr script addresses
// for which we only know the root hash.
func testTaprootImportTapscriptRootHashOnly(ctxt context.Context,
t *harnessTest, alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
// For the next step, we need a public key. Let's use a special family
// for this.
_, internalKey, derivationPath := deriveInternalKey(ctxt, t, alice)
// 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"))
rootHash := leaf1.TapHash()
tapscript := input.TapscriptRootHashOnly(internalKey, rootHash[:])
taprootKey, err := tapscript.TaprootKey()
require.NoError(t.t, err)
// Import the scripts and make sure we get the same address back as we
// calculated ourselves.
importResp, err := alice.WalletKitClient.ImportTapscript(
ctxt, &walletrpc.ImportTapscriptRequest{
InternalPublicKey: schnorr.SerializePubKey(internalKey),
Script: &walletrpc.ImportTapscriptRequest_RootHashOnly{
RootHashOnly: rootHash[:],
},
},
)
require.NoError(t.t, err)
calculatedAddr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(taprootKey), harnessNetParams,
)
require.NoError(t.t, err)
require.Equal(t.t, calculatedAddr.String(), importResp.P2TrAddress)
// Send some coins to the generated tapscript address.
p2trOutpoint, p2trPkScript := sendToTaprootOutput(
ctxt, t, net, alice, taprootKey, testAmount,
)
p2trOutputRPC := &lnrpc.OutPoint{
TxidBytes: p2trOutpoint.Hash[:],
OutputIndex: p2trOutpoint.Index,
}
assertWalletUnspent(t, alice, p2trOutputRPC, "imported")
assertAccountBalance(t.t, alice, "imported", testAmount, 0)
// Funding a PSBT from an imported script is not yet possible. So we
// basically need to add all information manually for the wallet to be
// able to sign for it.
utxo := &wire.TxOut{
Value: testAmount,
PkScript: p2trPkScript,
}
clearWalletImportedTapscriptBalance(
ctxt, t, alice, utxo, p2trOutpoint, internalKey, derivationPath,
rootHash[:],
)
}
// testTaprootImportTapscriptFullKey tests importing p2tr script addresses for
// which we only know the full Taproot key.
func testTaprootImportTapscriptFullKey(ctxt context.Context, t *harnessTest,
alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
// For the next step, we need a public key. Let's use a special family
// for this.
_, internalKey, derivationPath := deriveInternalKey(ctxt, t, alice)
// 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"))
tapscript := input.TapscriptFullTree(internalKey, leaf1)
rootHash := leaf1.TapHash()
taprootKey, err := tapscript.TaprootKey()
require.NoError(t.t, err)
// Import the scripts and make sure we get the same address back as we
// calculated ourselves.
importResp, err := alice.WalletKitClient.ImportTapscript(
ctxt, &walletrpc.ImportTapscriptRequest{
InternalPublicKey: schnorr.SerializePubKey(taprootKey),
Script: &walletrpc.ImportTapscriptRequest_FullKeyOnly{
FullKeyOnly: true,
},
},
)
require.NoError(t.t, err)
calculatedAddr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(taprootKey), harnessNetParams,
)
require.NoError(t.t, err)
require.Equal(t.t, calculatedAddr.String(), importResp.P2TrAddress)
// Send some coins to the generated tapscript address.
p2trOutpoint, p2trPkScript := sendToTaprootOutput(
ctxt, t, net, alice, taprootKey, testAmount,
)
p2trOutputRPC := &lnrpc.OutPoint{
TxidBytes: p2trOutpoint.Hash[:],
OutputIndex: p2trOutpoint.Index,
}
assertWalletUnspent(t, alice, p2trOutputRPC, "imported")
assertAccountBalance(t.t, alice, "imported", testAmount, 0)
// Funding a PSBT from an imported script is not yet possible. So we
// basically need to add all information manually for the wallet to be
// able to sign for it.
utxo := &wire.TxOut{
Value: testAmount,
PkScript: p2trPkScript,
}
clearWalletImportedTapscriptBalance(
ctxt, t, alice, utxo, p2trOutpoint, internalKey, derivationPath,
rootHash[:],
)
}
// clearWalletImportedTapscriptBalance manually assembles and then attempts to
// sign a TX to sweep funds from an imported tapscript address.
func clearWalletImportedTapscriptBalance(ctx context.Context, t *harnessTest,
node *lntest.HarnessNode, utxo *wire.TxOut, outPoint wire.OutPoint,
internalKey *btcec.PublicKey, derivationPath []uint32,
rootHash []byte) {
_, sweepPkScript := newAddrWithScript(
ctx, t.t, node, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
)
output := &wire.TxOut{
PkScript: sweepPkScript,
Value: utxo.Value - 1000,
}
packet, err := psbt.New(
[]*wire.OutPoint{&outPoint}, []*wire.TxOut{output}, 2, 0,
[]uint32{0},
)
require.NoError(t.t, err)
// We have everything we need to know to sign the PSBT.
in := &packet.Inputs[0]
in.Bip32Derivation = []*psbt.Bip32Derivation{{
PubKey: internalKey.SerializeCompressed(),
Bip32Path: derivationPath,
}}
in.TaprootBip32Derivation = []*psbt.TaprootBip32Derivation{{
XOnlyPubKey: schnorr.SerializePubKey(internalKey),
Bip32Path: derivationPath,
}}
in.SighashType = txscript.SigHashDefault
in.TaprootMerkleRoot = rootHash
in.WitnessUtxo = utxo
var buf bytes.Buffer
require.NoError(t.t, packet.Serialize(&buf))
// Sign the manually funded PSBT now.
signResp, err := node.WalletKitClient.SignPsbt(
ctx, &walletrpc.SignPsbtRequest{
FundedPsbt: buf.Bytes(),
},
)
require.NoError(t.t, err)
signedPacket, err := psbt.NewFromRawBytes(
bytes.NewReader(signResp.SignedPsbt), false,
)
require.NoError(t.t, err)
// We should be able to finalize the PSBT and extract the sweep TX now.
err = psbt.MaybeFinalizeAll(signedPacket)
require.NoError(t.t, err)
sweepTx, err := psbt.Extract(signedPacket)
require.NoError(t.t, err)
buf.Reset()
err = sweepTx.Serialize(&buf)
require.NoError(t.t, err)
// Publish the sweep transaction and then mine it as well.
_, err = node.WalletKitClient.PublishTransaction(
ctx, &walletrpc.Transaction{
TxHex: buf.Bytes(),
},
)
require.NoError(t.t, err)
// Mine one block which should contain the sweep transaction.
block := mineBlocks(t, t.lndHarness, 1, 1)[0]
sweepTxHash := sweepTx.TxHash()
assertTxInBlock(t, block, &sweepTxHash)
}
// 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) {
newAddrResp, err := node.NewAddress(ctx, &lnrpc.NewAddressRequest{
Type: addrType,
})
require.NoError(t, err)
addr, err := btcutil.DecodeAddress(
newAddrResp.Address, harnessNetParams,
)
require.NoError(t, err)
pkScript, err := txscript.PayToAddrScript(addr)
require.NoError(t, err)
return addr, pkScript
}
// sendToTaprootOutput sends coins to a p2tr output of the given taproot key and
// mines a block to confirm the coins.
func sendToTaprootOutput(ctx context.Context, t *harnessTest,
net *lntest.NetworkHarness, node *lntest.HarnessNode,
taprootKey *btcec.PublicKey, amt int64) (wire.OutPoint, []byte) {
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 = node.SendCoins(ctx, &lnrpc.SendCoinsRequest{
Addr: tapScriptAddr.String(),
Amount: amt,
})
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(),
)
p2trOutpoint := wire.OutPoint{
Hash: *txid,
Index: uint32(p2trOutputIndex),
}
// Make sure the transaction is recognized by our wallet and has the
// correct output type.
var outputDetail *lnrpc.OutputDetail
walletTxns, err := node.GetTransactions(
ctx, &lnrpc.GetTransactionsRequest{
StartHeight: 0,
EndHeight: -1,
},
)
require.NoError(t.t, err)
require.NotEmpty(t.t, walletTxns.Transactions)
for _, tx := range walletTxns.Transactions {
if tx.TxHash != txid.String() {
continue
}
for outputIdx, out := range tx.OutputDetails {
if out.Address != tapScriptAddr.String() {
continue
}
outputDetail = tx.OutputDetails[outputIdx]
break
}
}
require.NotNil(t.t, outputDetail, "transaction not found in wallet")
require.Equal(
t.t, lnrpc.OutputScriptType_SCRIPT_TYPE_WITNESS_V1_TAPROOT,
outputDetail.OutputType,
)
// Clear the mempool.
mineBlocks(t, net, 1, 1)
return p2trOutpoint, p2trPkScript
}
// publishTxAndConfirmSweep is a helper function that publishes a transaction
// after checking its weight against an estimate. After asserting the given
// spend request, the given sweep address' balance is verified to be seen as
// funds belonging to the wallet.
func publishTxAndConfirmSweep(ctx context.Context, t *harnessTest,
net *lntest.NetworkHarness, node *lntest.HarnessNode, tx *wire.MsgTx,
estimatedWeight int64, spendRequest *chainrpc.SpendRequest,
sweepAddr string) {
// Before we publish the tx that spends the p2tr transaction, we want to
// register a spend listener that we expect to fire after mining the
// block.
_, currentHeight, err := net.Miner.Client.GetBestBlock()
require.NoError(t.t, err)
// For a Taproot output we cannot leave the outpoint empty. Let's make
// sure the API returns the correct error here.
spendClient, err := node.ChainClient.RegisterSpendNtfn(
ctx, &chainrpc.SpendRequest{
Script: spendRequest.Script,
HeightHint: uint32(currentHeight),
},
)
require.NoError(t.t, err)
// The error is only thrown when trying to read a message.
_, err = spendClient.Recv()
require.Contains(
t.t, err.Error(),
"cannot register witness v1 spend request without outpoint",
)
// Now try again, this time with the outpoint set.
spendClient, err = node.ChainClient.RegisterSpendNtfn(
ctx, &chainrpc.SpendRequest{
Outpoint: spendRequest.Outpoint,
Script: spendRequest.Script,
HeightHint: uint32(currentHeight),
},
)
require.NoError(t.t, err)
var buf bytes.Buffer
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, estimatedWeight, txWeight)
_, err = node.WalletKitClient.PublishTransaction(
ctx, &walletrpc.Transaction{
TxHex: buf.Bytes(),
},
)
require.NoError(t.t, err)
// Make sure the coins sent to the address are confirmed correctly,
// including the confirmation notification.
confirmAddress(ctx, t, net, node, sweepAddr)
// We now expect our spend event to go through.
spendMsg, err := spendClient.Recv()
require.NoError(t.t, err)
spend := spendMsg.GetSpend()
require.NotNil(t.t, spend)
require.Equal(t.t, spend.SpendingHeight, uint32(currentHeight+1))
}
// confirmAddress makes sure that a transaction in the mempool spends funds to
// the given address. It also checks that a confirmation notification for the
// address is triggered when the transaction is mined.
func confirmAddress(ctx context.Context, t *harnessTest,
net *lntest.NetworkHarness, node *lntest.HarnessNode,
addrString string) {
// Wait until the tx that sends to the address is found.
txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
require.NoError(t.t, err)
// Wait until bob has seen the tx and considers it as owned.
addrOutputIndex := getOutputIndex(t, net.Miner, txid, addrString)
op := &lnrpc.OutPoint{
TxidBytes: txid[:],
OutputIndex: uint32(addrOutputIndex),
}
assertWalletUnspent(t, node, op, "")
// Before we confirm the transaction, let's register a confirmation
// listener for it, which we expect to fire after mining a block.
parsedAddr, err := btcutil.DecodeAddress(addrString, harnessNetParams)
require.NoError(t.t, err)
addrPkScript, err := txscript.PayToAddrScript(parsedAddr)
require.NoError(t.t, err)
_, currentHeight, err := net.Miner.Client.GetBestBlock()
require.NoError(t.t, err)
// We'll register for a conf notification, and also request the block
// that included it as well.
confClient, err := node.ChainClient.RegisterConfirmationsNtfn(
ctx, &chainrpc.ConfRequest{
Script: addrPkScript,
Txid: txid[:],
HeightHint: uint32(currentHeight),
NumConfs: 1,
IncludeBlock: true,
},
)
require.NoError(t.t, err)
// Mine another block to clean up the mempool.
mineBlocks(t, net, 1, 1)
// We now expect our confirmation to go through, and also that the
// block was specified.
confMsg, err := confClient.Recv()
require.NoError(t.t, err)
conf := confMsg.GetConf()
require.NotNil(t.t, conf)
require.Equal(t.t, conf.BlockHeight, uint32(currentHeight+1))
require.NotNil(t.t, conf.RawBlock)
// We should also be able to decode the raw block.
var blk wire.MsgBlock
require.NoError(t.t, blk.Deserialize(bytes.NewReader(conf.RawBlock)))
}
// deriveSigningKeys derives three signing keys and returns their descriptors,
// as well as the public keys in the Schnorr serialized format.
func deriveSigningKeys(ctx context.Context, t *harnessTest,
node *lntest.HarnessNode) (*signrpc.KeyDescriptor,
*signrpc.KeyDescriptor, *signrpc.KeyDescriptor, [][]byte) {
// For muSig2 we need multiple keys. We derive three of them from the
// same wallet, just so we know we can also sign for them again.
keyDesc1, err := node.WalletKitClient.DeriveNextKey(
ctx, &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily},
)
require.NoError(t.t, err)
pubKey1, err := btcec.ParsePubKey(keyDesc1.RawKeyBytes)
require.NoError(t.t, err)
keyDesc2, err := node.WalletKitClient.DeriveNextKey(
ctx, &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily},
)
require.NoError(t.t, err)
pubKey2, err := btcec.ParsePubKey(keyDesc2.RawKeyBytes)
require.NoError(t.t, err)
keyDesc3, err := node.WalletKitClient.DeriveNextKey(
ctx, &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily},
)
require.NoError(t.t, err)
pubKey3, err := btcec.ParsePubKey(keyDesc3.RawKeyBytes)
require.NoError(t.t, err)
// Now that we have all three keys we can create three sessions, one
// for each of the signers. This would of course normally not happen on
// the same node.
allPubKeys := [][]byte{
schnorr.SerializePubKey(pubKey1),
schnorr.SerializePubKey(pubKey2),
schnorr.SerializePubKey(pubKey3),
}
return keyDesc1, keyDesc2, keyDesc3, allPubKeys
}
// createMuSigSessions creates a MuSig2 session with three keys that are
// combined into a single key. The same node is used for the three signing
// participants but a separate key is generated for each session. So the result
// should be the same as if it were three different nodes.
func createMuSigSessions(ctx context.Context, t *harnessTest,
node *lntest.HarnessNode, taprootTweak *signrpc.TaprootTweakDesc,
keyDesc1, keyDesc2, keyDesc3 *signrpc.KeyDescriptor,
allPubKeys [][]byte) (*btcec.PublicKey, *btcec.PublicKey,
*signrpc.MuSig2SessionResponse, *signrpc.MuSig2SessionResponse,
*signrpc.MuSig2SessionResponse) {
sessResp1, err := node.SignerClient.MuSig2CreateSession(
ctx, &signrpc.MuSig2SessionRequest{
KeyLoc: keyDesc1.KeyLoc,
AllSignerPubkeys: allPubKeys,
TaprootTweak: taprootTweak,
},
)
require.NoError(t.t, err)
// Now that we have the three keys in a combined form, we want to make
// sure the tweaking for the taproot key worked correctly. We first need
// to parse the combined key without any tweaks applied to it. That will
// be our internal key. Once we know that, we can tweak it with the
// tapHash of the script root hash. We should arrive at the same result
// as the API.
combinedKey, err := schnorr.ParsePubKey(sessResp1.CombinedKey)
require.NoError(t.t, err)
// When combining the key without creating a session, we expect the same
// combined key to be created.
expectedCombinedKey := combinedKey
// Without a tweak, the internal key is equal to the combined key.
internalKey := combinedKey
// If there is a tweak, then there is the internal, pre-tweaked combined
// key and the taproot key which is fully tweaked.
if taprootTweak != nil {
internalKey, err = schnorr.ParsePubKey(
sessResp1.TaprootInternalKey,
)
require.NoError(t.t, err)
// We now know the taproot key. The session with the tweak
// applied should produce the same key!
expectedCombinedKey = txscript.ComputeTaprootOutputKey(
internalKey, taprootTweak.ScriptRoot,
)
require.Equal(
t.t, schnorr.SerializePubKey(expectedCombinedKey),
schnorr.SerializePubKey(combinedKey),
)
}
// We should also get the same keys when just calling the
// MuSig2CombineKeys RPC.
combineResp, err := node.SignerClient.MuSig2CombineKeys(
ctx, &signrpc.MuSig2CombineKeysRequest{
AllSignerPubkeys: allPubKeys,
TaprootTweak: taprootTweak,
},
)
require.NoError(t.t, err)
require.Equal(
t.t, schnorr.SerializePubKey(expectedCombinedKey),
combineResp.CombinedKey,
)
require.Equal(
t.t, schnorr.SerializePubKey(internalKey),
combineResp.TaprootInternalKey,
)
// Everything is good so far, let's continue with creating the signing
// session for the other two participants.
sessResp2, err := node.SignerClient.MuSig2CreateSession(
ctx, &signrpc.MuSig2SessionRequest{
KeyLoc: keyDesc2.KeyLoc,
AllSignerPubkeys: allPubKeys,
OtherSignerPublicNonces: [][]byte{
sessResp1.LocalPublicNonces,
},
TaprootTweak: taprootTweak,
},
)
require.NoError(t.t, err)
require.Equal(t.t, sessResp1.CombinedKey, sessResp2.CombinedKey)
sessResp3, err := node.SignerClient.MuSig2CreateSession(
ctx, &signrpc.MuSig2SessionRequest{
KeyLoc: keyDesc3.KeyLoc,
AllSignerPubkeys: allPubKeys,
OtherSignerPublicNonces: [][]byte{
sessResp1.LocalPublicNonces,
sessResp2.LocalPublicNonces,
},
TaprootTweak: taprootTweak,
},
)
require.NoError(t.t, err)
require.Equal(t.t, sessResp2.CombinedKey, sessResp3.CombinedKey)
require.Equal(t.t, true, sessResp3.HaveAllNonces)
// We need to distribute the rest of the nonces.
nonceResp1, err := node.SignerClient.MuSig2RegisterNonces(
ctx, &signrpc.MuSig2RegisterNoncesRequest{
SessionId: sessResp1.SessionId,
OtherSignerPublicNonces: [][]byte{
sessResp2.LocalPublicNonces,
sessResp3.LocalPublicNonces,
},
},
)
require.NoError(t.t, err)
require.Equal(t.t, true, nonceResp1.HaveAllNonces)
nonceResp2, err := node.SignerClient.MuSig2RegisterNonces(
ctx, &signrpc.MuSig2RegisterNoncesRequest{
SessionId: sessResp2.SessionId,
OtherSignerPublicNonces: [][]byte{
sessResp3.LocalPublicNonces,
},
},
)
require.NoError(t.t, err)
require.Equal(t.t, true, nonceResp2.HaveAllNonces)
return internalKey, combinedKey, sessResp1, sessResp2, sessResp3
}
// assertTaprootDeliveryUsed returns true if a Taproot addr was used in the
// co-op close transaction.
func assertTaprootDeliveryUsed(net *lntest.NetworkHarness,
t *harnessTest, closingTxid *chainhash.Hash) bool {
tx, err := net.Miner.Client.GetRawTransaction(closingTxid)
require.NoError(t.t, err, "unable to get closing tx")
for _, txOut := range tx.MsgTx().TxOut {
if !txscript.IsPayToTaproot(txOut.PkScript) {
return false
}
}
return true
}
// testTaprootCoopClose asserts that if both peers signal ShutdownAnySegwit,
// then a taproot closing addr is used. Otherwise, we shouldn't expect one to
// be used.
func testTaprootCoopClose(net *lntest.NetworkHarness, t *harnessTest) {
// We'll start by making two new nodes, and funding a channel between
// them.
carol := net.NewNode(t.t, "Carol", nil)
defer shutdownAndAssert(net, t, carol)
net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol)
dave := net.NewNode(t.t, "Dave", nil)
defer shutdownAndAssert(net, t, dave)
net.EnsureConnected(t.t, carol, dave)
chanAmt := funding.MaxBtcFundingAmount
pushAmt := btcutil.Amount(100000)
satPerVbyte := btcutil.Amount(1)
// We'll now open a channel between Carol and Dave.
chanPoint := openChannelAndAssert(
t, net, carol, dave,
lntest.OpenChannelParams{
Amt: chanAmt,
PushAmt: pushAmt,
SatPerVByte: satPerVbyte,
},
)
// We'll now close out the channel and obtain the closing TXID.
closingTxid := closeChannelAndAssert(t, net, carol, chanPoint, false)
// We expect that the closing transaction only has P2TR addresses.
require.True(t.t, assertTaprootDeliveryUsed(net, t, closingTxid),
"taproot addr not used!")
// Now we'll bring Eve into the mix, Eve is running older software that
// doesn't understand Taproot.
eveArgs := []string{"--protocol.no-any-segwit"}
eve := net.NewNode(t.t, "Eve", eveArgs)
defer shutdownAndAssert(net, t, eve)
net.EnsureConnected(t.t, carol, eve)
// We'll now open up a chanel again between Carol and Eve.
chanPoint = openChannelAndAssert(
t, net, carol, eve,
lntest.OpenChannelParams{
Amt: chanAmt,
PushAmt: pushAmt,
SatPerVByte: satPerVbyte,
},
)
// We'll now close out this channel and expect that no Taproot
// addresses are used in the co-op close transaction.
closingTxid = closeChannelAndAssert(t, net, carol, chanPoint, false)
require.False(t.t, assertTaprootDeliveryUsed(net, t, closingTxid),
"taproot addr shouldn't be used!")
}