mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 18:10:34 +01:00
1973 lines
62 KiB
Go
1973 lines
62 KiB
Go
package itest
|
|
|
|
import (
|
|
"bytes"
|
|
"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/lntest/node"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"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 (
|
|
hexDecode = func(keyStr string) []byte {
|
|
keyBytes, _ := hex.DecodeString(keyStr)
|
|
return keyBytes
|
|
}
|
|
dummyInternalKey, _ = btcec.ParsePubKey(hexDecode(
|
|
"03464805f5468e294d88cf15a3f06aef6c89d63ef1bd7b42db2e0c74c1ac" +
|
|
"eb90fe",
|
|
))
|
|
)
|
|
|
|
// testTaproot ensures that the daemon can send to and spend from taproot (p2tr)
|
|
// outputs.
|
|
func testTaproot(ht *lntest.HarnessTest) {
|
|
testTaprootSendCoinsKeySpendBip86(ht, ht.Alice)
|
|
testTaprootComputeInputScriptKeySpendBip86(ht, ht.Alice)
|
|
testTaprootSignOutputRawScriptSpend(ht, ht.Alice)
|
|
testTaprootSignOutputRawScriptSpend(
|
|
ht, ht.Alice, txscript.SigHashSingle,
|
|
)
|
|
testTaprootSignOutputRawKeySpendBip86(ht, ht.Alice)
|
|
testTaprootSignOutputRawKeySpendBip86(
|
|
ht, ht.Alice, txscript.SigHashSingle,
|
|
)
|
|
testTaprootSignOutputRawKeySpendRootHash(ht, ht.Alice)
|
|
|
|
muSig2Versions := []signrpc.MuSig2Version{
|
|
signrpc.MuSig2Version_MUSIG2_VERSION_V040,
|
|
signrpc.MuSig2Version_MUSIG2_VERSION_V100RC2,
|
|
}
|
|
for _, version := range muSig2Versions {
|
|
testTaprootMuSig2KeySpendBip86(ht, ht.Alice, version)
|
|
testTaprootMuSig2KeySpendRootHash(ht, ht.Alice, version)
|
|
testTaprootMuSig2ScriptSpend(ht, ht.Alice, version)
|
|
testTaprootMuSig2CombinedLeafKeySpend(ht, ht.Alice, version)
|
|
testMuSig2CombineKey(ht, ht.Alice, version)
|
|
}
|
|
|
|
testTaprootImportTapscriptFullTree(ht, ht.Alice)
|
|
testTaprootImportTapscriptPartialReveal(ht, ht.Alice)
|
|
testTaprootImportTapscriptRootHashOnly(ht, ht.Alice)
|
|
testTaprootImportTapscriptFullKey(ht, ht.Alice)
|
|
}
|
|
|
|
// 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(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode) {
|
|
|
|
// We'll start the test by sending Alice some coins, which she'll use to
|
|
// send to herself on a p2tr output.
|
|
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
|
|
|
|
// Let's create a p2tr address now.
|
|
p2trResp := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{
|
|
Type: AddrTypeTaprootPubkey,
|
|
})
|
|
|
|
// Assert this is a segwit v1 address that starts with bcrt1p.
|
|
require.Contains(
|
|
ht, p2trResp.Address, ht.Miner().ActiveNet.Bech32HRPSegwit+"1p",
|
|
)
|
|
|
|
// Send the coins from Alice's wallet to her own, but to the new p2tr
|
|
// address.
|
|
alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{
|
|
Addr: p2trResp.Address,
|
|
Amount: 0.5 * btcutil.SatoshiPerBitcoin,
|
|
TargetConf: 6,
|
|
})
|
|
|
|
txid := ht.AssertNumTxsInMempool(1)[0]
|
|
|
|
// Wait until bob has seen the tx and considers it as owned.
|
|
p2trOutputIndex := ht.GetOutputIndex(txid, p2trResp.Address)
|
|
op := &lnrpc.OutPoint{
|
|
TxidBytes: txid[:],
|
|
OutputIndex: uint32(p2trOutputIndex),
|
|
}
|
|
ht.AssertUTXOInWallet(alice, op, "")
|
|
|
|
// Mine a block to clean up the mempool.
|
|
ht.MineBlocksAndAssertNumTxes(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 = alice.RPC.NewAddress(&lnrpc.NewAddressRequest{
|
|
Type: lnrpc.AddressType_TAPROOT_PUBKEY,
|
|
})
|
|
|
|
alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{
|
|
Addr: p2trResp.Address,
|
|
SendAll: true,
|
|
TargetConf: 6,
|
|
})
|
|
|
|
// Make sure the coins sent to the address are confirmed correctly,
|
|
// including the confirmation notification.
|
|
confirmAddress(ht, alice, p2trResp.Address)
|
|
}
|
|
|
|
// testTaprootComputeInputScriptKeySpendBip86 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 testTaprootComputeInputScriptKeySpendBip86(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode) {
|
|
|
|
// We'll start the test by sending Alice some coins, which she'll use
|
|
// to send to herself on a p2tr output.
|
|
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
|
|
|
|
// Let's create a p2tr address now.
|
|
p2trAddr, p2trPkScript := newAddrWithScript(
|
|
ht, alice, lnrpc.AddressType_TAPROOT_PUBKEY,
|
|
)
|
|
|
|
// Send the coins from Alice's wallet to her own, but to the new p2tr
|
|
// address.
|
|
req := &lnrpc.SendCoinsRequest{
|
|
Addr: p2trAddr.String(),
|
|
Amount: testAmount,
|
|
TargetConf: 6,
|
|
}
|
|
alice.RPC.SendCoins(req)
|
|
|
|
// Wait until bob has seen the tx and considers it as owned.
|
|
txid := ht.AssertNumTxsInMempool(1)[0]
|
|
p2trOutputIndex := ht.GetOutputIndex(txid, p2trAddr.String())
|
|
op := &lnrpc.OutPoint{
|
|
TxidBytes: txid[:],
|
|
OutputIndex: uint32(p2trOutputIndex),
|
|
}
|
|
ht.AssertUTXOInWallet(alice, op, "")
|
|
|
|
p2trOutpoint := wire.OutPoint{
|
|
Hash: *txid,
|
|
Index: uint32(p2trOutputIndex),
|
|
}
|
|
|
|
// Mine a block to clean up the mempool.
|
|
ht.MineBlocksAndAssertNumTxes(1, 1)
|
|
|
|
// We'll send the coins back to a p2wkh address.
|
|
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
|
|
ht, 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 := 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(ht, tx.Serialize(&buf))
|
|
|
|
utxoInfo := []*signrpc.TxOut{{
|
|
PkScript: p2trPkScript,
|
|
Value: testAmount,
|
|
}}
|
|
signReq := &signrpc.SignReq{
|
|
RawTxBytes: buf.Bytes(),
|
|
SignDescs: []*signrpc.SignDescriptor{{
|
|
Output: utxoInfo[0],
|
|
InputIndex: 0,
|
|
Sighash: uint32(txscript.SigHashDefault),
|
|
}},
|
|
PrevOutputs: utxoInfo,
|
|
}
|
|
signResp := alice.RPC.ComputeInputScript(signReq)
|
|
|
|
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(
|
|
ht, 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(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode, sigHashType ...txscript.SigHashType) {
|
|
|
|
// For the next step, we need a public key. Let's use a special family
|
|
// for this.
|
|
req := &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily}
|
|
keyDesc := alice.RPC.DeriveNextKey(req)
|
|
|
|
leafSigningKey, err := btcec.ParsePubKey(keyDesc.RawKeyBytes)
|
|
require.NoError(ht, err)
|
|
|
|
// Let's create a taproot script output now. This is a hash lock with a
|
|
// simple preimage of "foobar".
|
|
leaf1 := testScriptHashLock(ht.T, []byte("foobar"))
|
|
|
|
// Let's add a second script output as well to test the partial reveal.
|
|
leaf2 := testScriptSchnorrSig(ht.T, leafSigningKey)
|
|
|
|
inclusionProof := leaf1.TapHash()
|
|
tapscript := input.TapscriptPartialReveal(
|
|
dummyInternalKey, leaf2, inclusionProof[:],
|
|
)
|
|
taprootKey, err := tapscript.TaprootKey()
|
|
require.NoError(ht, err)
|
|
|
|
// Send some coins to the generated tapscript address.
|
|
p2trOutpoint, p2trPkScript := sendToTaprootOutput(ht, alice, taprootKey)
|
|
|
|
// Spend the output again, this time back to a p2wkh address.
|
|
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
|
|
ht, 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 := estimator.Weight()
|
|
sigHash := txscript.SigHashDefault
|
|
if len(sigHashType) != 0 {
|
|
sigHash = sigHashType[0]
|
|
|
|
// If a non-default sighash is used, then we'll need to add an
|
|
// extra byte to account for the sighash that doesn't exist in
|
|
// the default case.
|
|
estimatedWeight++
|
|
}
|
|
|
|
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(ht, 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.
|
|
signReq := &signrpc.SignReq{
|
|
RawTxBytes: buf.Bytes(),
|
|
SignDescs: []*signrpc.SignDescriptor{{
|
|
Output: utxoInfo[0],
|
|
InputIndex: 0,
|
|
KeyDesc: keyDesc,
|
|
Sighash: uint32(sigHash),
|
|
WitnessScript: leaf2.Script,
|
|
SignMethod: signMethodTapscript,
|
|
}},
|
|
}
|
|
err = alice.RPC.SignOutputRawErr(signReq)
|
|
require.Contains(
|
|
ht, 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.
|
|
signReq = &signrpc.SignReq{
|
|
RawTxBytes: buf.Bytes(),
|
|
SignDescs: []*signrpc.SignDescriptor{{
|
|
Output: utxoInfo[0],
|
|
InputIndex: 0,
|
|
KeyDesc: keyDesc,
|
|
Sighash: uint32(sigHash),
|
|
WitnessScript: leaf2.Script,
|
|
}},
|
|
PrevOutputs: utxoInfo,
|
|
}
|
|
err = alice.RPC.SignOutputRawErr(signReq)
|
|
require.Contains(
|
|
ht, err.Error(), "selected sign method witness_v0 is not "+
|
|
"compatible with given pk script 5120",
|
|
)
|
|
|
|
// Do the actual signing now.
|
|
signReq = &signrpc.SignReq{
|
|
RawTxBytes: buf.Bytes(),
|
|
SignDescs: []*signrpc.SignDescriptor{{
|
|
Output: utxoInfo[0],
|
|
InputIndex: 0,
|
|
KeyDesc: keyDesc,
|
|
Sighash: uint32(sigHash),
|
|
WitnessScript: leaf2.Script,
|
|
SignMethod: signMethodTapscript,
|
|
}},
|
|
PrevOutputs: utxoInfo,
|
|
}
|
|
signResp := alice.RPC.SignOutputRaw(signReq)
|
|
|
|
// We can now assemble the witness stack.
|
|
controlBlockBytes, err := tapscript.ControlBlock.ToBytes()
|
|
require.NoError(ht, err)
|
|
|
|
sig := signResp.RawSigs[0]
|
|
if len(sigHashType) != 0 {
|
|
sig = append(sig, byte(sigHashType[0]))
|
|
}
|
|
tx.TxIn[0].Witness = wire.TxWitness{
|
|
sig, 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(
|
|
ht, 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(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode, sigHashType ...txscript.SigHashType) {
|
|
|
|
// For the next step, we need a public key. Let's use a special family
|
|
// for this.
|
|
req := &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily}
|
|
keyDesc := alice.RPC.DeriveNextKey(req)
|
|
|
|
internalKey, err := btcec.ParsePubKey(keyDesc.RawKeyBytes)
|
|
require.NoError(ht, 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(ht, alice, taprootKey)
|
|
|
|
// Spend the output again, this time back to a p2wkh address.
|
|
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
|
|
ht, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
|
|
)
|
|
|
|
sigHash := txscript.SigHashDefault
|
|
if len(sigHashType) != 0 {
|
|
sigHash = sigHashType[0]
|
|
}
|
|
|
|
// Create fee estimation for a p2tr input and p2wkh output.
|
|
feeRate := chainfee.SatPerKWeight(12500)
|
|
estimator := input.TxWeightEstimator{}
|
|
estimator.AddTaprootKeySpendInput(sigHash)
|
|
estimator.AddP2WKHOutput()
|
|
estimatedWeight := 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(ht, tx.Serialize(&buf))
|
|
|
|
utxoInfo := []*signrpc.TxOut{{
|
|
PkScript: p2trPkScript,
|
|
Value: testAmount,
|
|
}}
|
|
signReq := &signrpc.SignReq{
|
|
RawTxBytes: buf.Bytes(),
|
|
SignDescs: []*signrpc.SignDescriptor{{
|
|
Output: utxoInfo[0],
|
|
InputIndex: 0,
|
|
KeyDesc: keyDesc,
|
|
SingleTweak: dummyKeyTweak[:],
|
|
Sighash: uint32(sigHash),
|
|
SignMethod: signMethodBip86,
|
|
}},
|
|
PrevOutputs: utxoInfo,
|
|
}
|
|
signResp := alice.RPC.SignOutputRaw(signReq)
|
|
|
|
sig := signResp.RawSigs[0]
|
|
if len(sigHashType) != 0 {
|
|
sig = append(sig, byte(sigHash))
|
|
}
|
|
tx.TxIn[0].Witness = wire.TxWitness{sig}
|
|
|
|
// Serialize, weigh and publish the TX now, then make sure the
|
|
// coins are sent and confirmed to the final sweep destination address.
|
|
publishTxAndConfirmSweep(
|
|
ht, 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(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode) {
|
|
|
|
// For the next step, we need a public key. Let's use a special family
|
|
// for this.
|
|
req := &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily}
|
|
keyDesc := alice.RPC.DeriveNextKey(req)
|
|
|
|
internalKey, err := btcec.ParsePubKey(keyDesc.RawKeyBytes)
|
|
require.NoError(ht, 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(ht.T, []byte("foobar"))
|
|
|
|
rootHash := leaf1.TapHash()
|
|
taprootKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash[:])
|
|
|
|
// Send some coins to the generated tapscript address.
|
|
p2trOutpoint, p2trPkScript := sendToTaprootOutput(ht, alice, taprootKey)
|
|
|
|
// Spend the output again, this time back to a p2wkh address.
|
|
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
|
|
ht, 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 := 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(ht, tx.Serialize(&buf))
|
|
|
|
utxoInfo := []*signrpc.TxOut{{
|
|
PkScript: p2trPkScript,
|
|
Value: testAmount,
|
|
}}
|
|
signReq := &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,
|
|
}
|
|
signResp := alice.RPC.SignOutputRaw(signReq)
|
|
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(
|
|
ht, 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(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode, version signrpc.MuSig2Version) {
|
|
|
|
// 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(
|
|
ht, alice, version,
|
|
)
|
|
_, taprootKey, sessResp1, sessResp2, sessResp3 := createMuSigSessions(
|
|
ht, alice, taprootTweak, keyDesc1, keyDesc2, keyDesc3,
|
|
allPubKeys, version,
|
|
)
|
|
|
|
// Send some coins to the generated tapscript address.
|
|
p2trOutpoint, p2trPkScript := sendToTaprootOutput(ht, alice, taprootKey)
|
|
|
|
// Spend the output again, this time back to a p2wkh address.
|
|
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
|
|
ht, 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 := 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(ht, 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(ht, 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.
|
|
signReq := &signrpc.MuSig2SignRequest{
|
|
SessionId: sessResp1.SessionId,
|
|
MessageDigest: sigHash,
|
|
}
|
|
alice.RPC.MuSig2Sign(signReq)
|
|
|
|
signReq = &signrpc.MuSig2SignRequest{
|
|
SessionId: sessResp2.SessionId,
|
|
MessageDigest: sigHash,
|
|
Cleanup: true,
|
|
}
|
|
signResp2 := alice.RPC.MuSig2Sign(signReq)
|
|
|
|
signReq = &signrpc.MuSig2SignRequest{
|
|
SessionId: sessResp3.SessionId,
|
|
MessageDigest: sigHash,
|
|
Cleanup: true,
|
|
}
|
|
signResp3 := alice.RPC.MuSig2Sign(signReq)
|
|
|
|
// Luckily only one of the signers needs to combine the signature, so
|
|
// let's do that now.
|
|
combineReq := &signrpc.MuSig2CombineSigRequest{
|
|
SessionId: sessResp1.SessionId,
|
|
OtherPartialSignatures: [][]byte{
|
|
signResp2.LocalPartialSignature,
|
|
signResp3.LocalPartialSignature,
|
|
},
|
|
}
|
|
combineResp := alice.RPC.MuSig2CombineSig(combineReq)
|
|
require.Equal(ht, true, combineResp.HaveAllSignatures)
|
|
require.NotEmpty(ht, combineResp.FinalSignature)
|
|
|
|
sig, err := schnorr.ParseSignature(combineResp.FinalSignature)
|
|
require.NoError(ht, err)
|
|
require.True(ht, sig.Verify(sigHash, taprootKey))
|
|
|
|
tx.TxIn[0].Witness = wire.TxWitness{
|
|
combineResp.FinalSignature,
|
|
}
|
|
|
|
// Serialize, weigh and publish the TX now, then make sure the
|
|
// coins are sent and confirmed to the final sweep destination address.
|
|
publishTxAndConfirmSweep(
|
|
ht, 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(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode, version signrpc.MuSig2Version) {
|
|
|
|
// 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(ht.T, []byte("foobar"))
|
|
rootHash := leaf1.TapHash()
|
|
taprootTweak := &signrpc.TaprootTweakDesc{
|
|
ScriptRoot: rootHash[:],
|
|
}
|
|
|
|
keyDesc1, keyDesc2, keyDesc3, allPubKeys := deriveSigningKeys(
|
|
ht, alice, version,
|
|
)
|
|
_, taprootKey, sessResp1, sessResp2, sessResp3 := createMuSigSessions(
|
|
ht, alice, taprootTweak, keyDesc1, keyDesc2, keyDesc3,
|
|
allPubKeys, version,
|
|
)
|
|
|
|
// Send some coins to the generated tapscript address.
|
|
p2trOutpoint, p2trPkScript := sendToTaprootOutput(ht, alice, taprootKey)
|
|
|
|
// Spend the output again, this time back to a p2wkh address.
|
|
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
|
|
ht, 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 := 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(ht, 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(ht, 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.
|
|
req := &signrpc.MuSig2SignRequest{
|
|
SessionId: sessResp1.SessionId,
|
|
MessageDigest: sigHash,
|
|
}
|
|
alice.RPC.MuSig2Sign(req)
|
|
|
|
req = &signrpc.MuSig2SignRequest{
|
|
SessionId: sessResp2.SessionId,
|
|
MessageDigest: sigHash,
|
|
Cleanup: true,
|
|
}
|
|
signResp2 := alice.RPC.MuSig2Sign(req)
|
|
|
|
req = &signrpc.MuSig2SignRequest{
|
|
SessionId: sessResp3.SessionId,
|
|
MessageDigest: sigHash,
|
|
Cleanup: true,
|
|
}
|
|
signResp3 := alice.RPC.MuSig2Sign(req)
|
|
|
|
// Luckily only one of the signers needs to combine the signature, so
|
|
// let's do that now.
|
|
combineReq := &signrpc.MuSig2CombineSigRequest{
|
|
SessionId: sessResp1.SessionId,
|
|
OtherPartialSignatures: [][]byte{
|
|
signResp2.LocalPartialSignature,
|
|
signResp3.LocalPartialSignature,
|
|
},
|
|
}
|
|
combineResp := alice.RPC.MuSig2CombineSig(combineReq)
|
|
require.Equal(ht, true, combineResp.HaveAllSignatures)
|
|
require.NotEmpty(ht, combineResp.FinalSignature)
|
|
|
|
sig, err := schnorr.ParseSignature(combineResp.FinalSignature)
|
|
require.NoError(ht, err)
|
|
require.True(ht, sig.Verify(sigHash, taprootKey))
|
|
|
|
tx.TxIn[0].Witness = wire.TxWitness{
|
|
combineResp.FinalSignature,
|
|
}
|
|
|
|
// Serialize, weigh and publish the TX now, then make sure the
|
|
// coins are sent and confirmed to the final sweep destination address.
|
|
publishTxAndConfirmSweep(
|
|
ht, 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(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode, version signrpc.MuSig2Version) {
|
|
|
|
// 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(ht.T, []byte("foobar"))
|
|
rootHash := leaf1.TapHash()
|
|
taprootTweak := &signrpc.TaprootTweakDesc{
|
|
ScriptRoot: rootHash[:],
|
|
}
|
|
|
|
keyDesc1, keyDesc2, keyDesc3, allPubKeys := deriveSigningKeys(
|
|
ht, alice, version,
|
|
)
|
|
internalKey, taprootKey, _, _, _ := createMuSigSessions(
|
|
ht, alice, taprootTweak, keyDesc1, keyDesc2, keyDesc3,
|
|
allPubKeys, version,
|
|
)
|
|
|
|
// 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(ht, alice, taprootKey)
|
|
|
|
// Spend the output again, this time back to a p2wkh address.
|
|
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
|
|
ht, 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(
|
|
lntypes.WeightUnit(len([]byte("foobar"))+len(leaf1.Script)+1),
|
|
tapscript,
|
|
)
|
|
estimator.AddP2WKHOutput()
|
|
estimatedWeight := 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(ht, 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(
|
|
ht, 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(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode, version signrpc.MuSig2Version) {
|
|
|
|
// 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(
|
|
ht, alice, version,
|
|
)
|
|
req := &signrpc.MuSig2CombineKeysRequest{
|
|
AllSignerPubkeys: allPubKeys,
|
|
Version: version,
|
|
}
|
|
combineResp := alice.RPC.MuSig2CombineKeys(req)
|
|
combinedPubKey, err := schnorr.ParsePubKey(combineResp.CombinedKey)
|
|
require.NoError(ht, 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(ht.T, combinedPubKey)
|
|
tapscript := input.TapscriptPartialReveal(dummyInternalKey, leaf, nil)
|
|
taprootKey, err := tapscript.TaprootKey()
|
|
require.NoError(ht, err)
|
|
|
|
// Send some coins to the generated tapscript address.
|
|
p2trOutpoint, p2trPkScript := sendToTaprootOutput(ht, alice, taprootKey)
|
|
|
|
// Spend the output again, this time back to a p2wkh address.
|
|
p2wkhAddr, p2wkhPkScript := newAddrWithScript(
|
|
ht, 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 := 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(ht, tx.Serialize(&buf))
|
|
|
|
utxoInfo := []*signrpc.TxOut{{
|
|
PkScript: p2trPkScript,
|
|
Value: testAmount,
|
|
}}
|
|
|
|
// Do the actual signing now.
|
|
_, _, sessResp1, sessResp2, sessResp3 := createMuSigSessions(
|
|
ht, alice, nil, keyDesc1, keyDesc2, keyDesc3, allPubKeys,
|
|
version,
|
|
)
|
|
require.NoError(ht, 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(ht, 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.
|
|
signReq := &signrpc.MuSig2SignRequest{
|
|
SessionId: sessResp1.SessionId,
|
|
MessageDigest: sigHash,
|
|
}
|
|
alice.RPC.MuSig2Sign(signReq)
|
|
|
|
signReq = &signrpc.MuSig2SignRequest{
|
|
SessionId: sessResp2.SessionId,
|
|
MessageDigest: sigHash,
|
|
Cleanup: true,
|
|
}
|
|
signResp2 := alice.RPC.MuSig2Sign(signReq)
|
|
|
|
// Before we have all partial signatures, we shouldn't get a final
|
|
// signature back.
|
|
combineReq := &signrpc.MuSig2CombineSigRequest{
|
|
SessionId: sessResp1.SessionId,
|
|
OtherPartialSignatures: [][]byte{
|
|
signResp2.LocalPartialSignature,
|
|
},
|
|
}
|
|
combineSigResp := alice.RPC.MuSig2CombineSig(combineReq)
|
|
require.False(ht, combineSigResp.HaveAllSignatures)
|
|
require.Empty(ht, combineSigResp.FinalSignature)
|
|
|
|
signReq = &signrpc.MuSig2SignRequest{
|
|
SessionId: sessResp3.SessionId,
|
|
MessageDigest: sigHash,
|
|
}
|
|
signResp3 := alice.RPC.MuSig2Sign(signReq)
|
|
|
|
// We manually clean up session 3, just to make sure that works as well.
|
|
cleanReq := &signrpc.MuSig2CleanupRequest{
|
|
SessionId: sessResp3.SessionId,
|
|
}
|
|
alice.RPC.MuSig2Cleanup(cleanReq)
|
|
|
|
// A second call to that cleaned up session should now fail with a
|
|
// specific error.
|
|
signReq = &signrpc.MuSig2SignRequest{
|
|
SessionId: sessResp3.SessionId,
|
|
MessageDigest: sigHash,
|
|
}
|
|
err = alice.RPC.MuSig2SignErr(signReq)
|
|
require.Contains(ht, err.Error(), "not found")
|
|
|
|
// Luckily only one of the signers needs to combine the signature, so
|
|
// let's do that now.
|
|
combineReq = &signrpc.MuSig2CombineSigRequest{
|
|
SessionId: sessResp1.SessionId,
|
|
OtherPartialSignatures: [][]byte{
|
|
signResp3.LocalPartialSignature,
|
|
},
|
|
}
|
|
combineResp1 := alice.RPC.MuSig2CombineSig(combineReq)
|
|
require.Equal(ht, true, combineResp1.HaveAllSignatures)
|
|
require.NotEmpty(ht, combineResp1.FinalSignature)
|
|
|
|
sig, err := schnorr.ParseSignature(combineResp1.FinalSignature)
|
|
require.NoError(ht, err)
|
|
require.True(ht, sig.Verify(sigHash, combinedPubKey))
|
|
|
|
// We can now assemble the witness stack.
|
|
controlBlockBytes, err := tapscript.ControlBlock.ToBytes()
|
|
require.NoError(ht, err)
|
|
|
|
tx.TxIn[0].Witness = wire.TxWitness{
|
|
combineResp1.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(
|
|
ht, 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(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode) {
|
|
|
|
// For the next step, we need a public key. Let's use a special family
|
|
// for this.
|
|
_, internalKey, derivationPath := deriveInternalKey(ht, alice)
|
|
|
|
// Let's create a taproot script output now. This is a hash lock with a
|
|
// simple preimage of "foobar".
|
|
leaf1 := testScriptHashLock(ht.T, []byte("foobar"))
|
|
|
|
// Let's add a second script output as well to test the partial reveal.
|
|
leaf2 := testScriptSchnorrSig(ht.T, internalKey)
|
|
|
|
tapscript := input.TapscriptFullTree(internalKey, leaf1, leaf2)
|
|
tree := txscript.AssembleTaprootScriptTree(leaf1, leaf2)
|
|
rootHash := tree.RootNode.TapHash()
|
|
taprootKey, err := tapscript.TaprootKey()
|
|
require.NoError(ht, err)
|
|
|
|
// Import the scripts and make sure we get the same address back as we
|
|
// calculated ourselves.
|
|
req := &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,
|
|
}},
|
|
},
|
|
},
|
|
}
|
|
importResp := alice.RPC.ImportTapscript(req)
|
|
|
|
calculatedAddr, err := btcutil.NewAddressTaproot(
|
|
schnorr.SerializePubKey(taprootKey), harnessNetParams,
|
|
)
|
|
require.NoError(ht, err)
|
|
require.Equal(ht, calculatedAddr.String(), importResp.P2TrAddress)
|
|
|
|
// Send some coins to the generated tapscript address.
|
|
p2trOutpoint, p2trPkScript := sendToTaprootOutput(ht, alice, taprootKey)
|
|
p2trOutputRPC := &lnrpc.OutPoint{
|
|
TxidBytes: p2trOutpoint.Hash[:],
|
|
OutputIndex: p2trOutpoint.Index,
|
|
}
|
|
ht.AssertUTXOInWallet(alice, p2trOutputRPC, "imported")
|
|
ht.AssertWalletAccountBalance(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(
|
|
ht, alice, utxo, p2trOutpoint, internalKey, derivationPath,
|
|
rootHash[:],
|
|
)
|
|
}
|
|
|
|
// testTaprootImportTapscriptPartialReveal tests importing p2tr script addresses
|
|
// for which we only know part of the tree.
|
|
func testTaprootImportTapscriptPartialReveal(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode) {
|
|
|
|
// For the next step, we need a public key. Let's use a special family
|
|
// for this.
|
|
_, internalKey, derivationPath := deriveInternalKey(ht, alice)
|
|
|
|
// Let's create a taproot script output now. This is a hash lock with a
|
|
// simple preimage of "foobar".
|
|
leaf1 := testScriptHashLock(ht.T, []byte("foobar"))
|
|
|
|
// Let's add a second script output as well to test the partial reveal.
|
|
leaf2 := testScriptSchnorrSig(ht.T, internalKey)
|
|
leaf2Hash := leaf2.TapHash()
|
|
|
|
tapscript := input.TapscriptPartialReveal(
|
|
internalKey, leaf1, leaf2Hash[:],
|
|
)
|
|
rootHash := tapscript.ControlBlock.RootHash(leaf1.Script)
|
|
taprootKey, err := tapscript.TaprootKey()
|
|
require.NoError(ht, err)
|
|
|
|
// Import the scripts and make sure we get the same address back as we
|
|
// calculated ourselves.
|
|
req := &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[:],
|
|
},
|
|
},
|
|
}
|
|
importResp := alice.RPC.ImportTapscript(req)
|
|
|
|
calculatedAddr, err := btcutil.NewAddressTaproot(
|
|
schnorr.SerializePubKey(taprootKey), harnessNetParams,
|
|
)
|
|
require.NoError(ht, err)
|
|
require.Equal(ht, calculatedAddr.String(), importResp.P2TrAddress)
|
|
|
|
// Send some coins to the generated tapscript address.
|
|
p2trOutpoint, p2trPkScript := sendToTaprootOutput(ht, alice, taprootKey)
|
|
|
|
p2trOutputRPC := &lnrpc.OutPoint{
|
|
TxidBytes: p2trOutpoint.Hash[:],
|
|
OutputIndex: p2trOutpoint.Index,
|
|
}
|
|
ht.AssertUTXOInWallet(alice, p2trOutputRPC, "imported")
|
|
ht.AssertWalletAccountBalance(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(
|
|
ht, alice, utxo, p2trOutpoint, internalKey, derivationPath,
|
|
rootHash,
|
|
)
|
|
}
|
|
|
|
// testTaprootImportTapscriptRootHashOnly tests importing p2tr script addresses
|
|
// for which we only know the root hash.
|
|
func testTaprootImportTapscriptRootHashOnly(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode) {
|
|
|
|
// For the next step, we need a public key. Let's use a special family
|
|
// for this.
|
|
_, internalKey, derivationPath := deriveInternalKey(ht, alice)
|
|
|
|
// Let's create a taproot script output now. This is a hash lock with a
|
|
// simple preimage of "foobar".
|
|
leaf1 := testScriptHashLock(ht.T, []byte("foobar"))
|
|
rootHash := leaf1.TapHash()
|
|
|
|
tapscript := input.TapscriptRootHashOnly(internalKey, rootHash[:])
|
|
taprootKey, err := tapscript.TaprootKey()
|
|
require.NoError(ht, err)
|
|
|
|
// Import the scripts and make sure we get the same address back as we
|
|
// calculated ourselves.
|
|
req := &walletrpc.ImportTapscriptRequest{
|
|
InternalPublicKey: schnorr.SerializePubKey(internalKey),
|
|
Script: &walletrpc.ImportTapscriptRequest_RootHashOnly{
|
|
RootHashOnly: rootHash[:],
|
|
},
|
|
}
|
|
importResp := alice.RPC.ImportTapscript(req)
|
|
|
|
calculatedAddr, err := btcutil.NewAddressTaproot(
|
|
schnorr.SerializePubKey(taprootKey), harnessNetParams,
|
|
)
|
|
require.NoError(ht, err)
|
|
require.Equal(ht, calculatedAddr.String(), importResp.P2TrAddress)
|
|
|
|
// Send some coins to the generated tapscript address.
|
|
p2trOutpoint, p2trPkScript := sendToTaprootOutput(ht, alice, taprootKey)
|
|
|
|
p2trOutputRPC := &lnrpc.OutPoint{
|
|
TxidBytes: p2trOutpoint.Hash[:],
|
|
OutputIndex: p2trOutpoint.Index,
|
|
}
|
|
ht.AssertUTXOInWallet(alice, p2trOutputRPC, "imported")
|
|
ht.AssertWalletAccountBalance(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(
|
|
ht, alice, utxo, p2trOutpoint, internalKey, derivationPath,
|
|
rootHash[:],
|
|
)
|
|
}
|
|
|
|
// testTaprootImportTapscriptFullKey tests importing p2tr script addresses for
|
|
// which we only know the full Taproot key.
|
|
func testTaprootImportTapscriptFullKey(ht *lntest.HarnessTest,
|
|
alice *node.HarnessNode) {
|
|
|
|
// For the next step, we need a public key. Let's use a special family
|
|
// for this.
|
|
_, internalKey, derivationPath := deriveInternalKey(ht, alice)
|
|
|
|
// Let's create a taproot script output now. This is a hash lock with a
|
|
// simple preimage of "foobar".
|
|
leaf1 := testScriptHashLock(ht.T, []byte("foobar"))
|
|
|
|
tapscript := input.TapscriptFullTree(internalKey, leaf1)
|
|
rootHash := leaf1.TapHash()
|
|
taprootKey, err := tapscript.TaprootKey()
|
|
require.NoError(ht, err)
|
|
|
|
// Import the scripts and make sure we get the same address back as we
|
|
// calculated ourselves.
|
|
req := &walletrpc.ImportTapscriptRequest{
|
|
InternalPublicKey: schnorr.SerializePubKey(taprootKey),
|
|
Script: &walletrpc.ImportTapscriptRequest_FullKeyOnly{
|
|
FullKeyOnly: true,
|
|
},
|
|
}
|
|
importResp := alice.RPC.ImportTapscript(req)
|
|
|
|
calculatedAddr, err := btcutil.NewAddressTaproot(
|
|
schnorr.SerializePubKey(taprootKey), harnessNetParams,
|
|
)
|
|
require.NoError(ht, err)
|
|
require.Equal(ht, calculatedAddr.String(), importResp.P2TrAddress)
|
|
|
|
// Send some coins to the generated tapscript address.
|
|
p2trOutpoint, p2trPkScript := sendToTaprootOutput(ht, alice, taprootKey)
|
|
|
|
p2trOutputRPC := &lnrpc.OutPoint{
|
|
TxidBytes: p2trOutpoint.Hash[:],
|
|
OutputIndex: p2trOutpoint.Index,
|
|
}
|
|
ht.AssertUTXOInWallet(alice, p2trOutputRPC, "imported")
|
|
ht.AssertWalletAccountBalance(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(
|
|
ht, 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(ht *lntest.HarnessTest,
|
|
hn *node.HarnessNode, utxo *wire.TxOut, outPoint wire.OutPoint,
|
|
internalKey *btcec.PublicKey, derivationPath []uint32,
|
|
rootHash []byte) {
|
|
|
|
_, sweepPkScript := newAddrWithScript(
|
|
ht, hn, 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(ht, 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(ht, packet.Serialize(&buf))
|
|
|
|
// Sign the manually funded PSBT now.
|
|
signResp := hn.RPC.SignPsbt(&walletrpc.SignPsbtRequest{
|
|
FundedPsbt: buf.Bytes(),
|
|
})
|
|
|
|
signedPacket, err := psbt.NewFromRawBytes(
|
|
bytes.NewReader(signResp.SignedPsbt), false,
|
|
)
|
|
require.NoError(ht, err)
|
|
|
|
// We should be able to finalize the PSBT and extract the sweep TX now.
|
|
err = psbt.MaybeFinalizeAll(signedPacket)
|
|
require.NoError(ht, err)
|
|
|
|
sweepTx, err := psbt.Extract(signedPacket)
|
|
require.NoError(ht, err)
|
|
|
|
buf.Reset()
|
|
err = sweepTx.Serialize(&buf)
|
|
require.NoError(ht, err)
|
|
|
|
// Publish the sweep transaction and then mine it as well.
|
|
hn.RPC.PublishTransaction(&walletrpc.Transaction{
|
|
TxHex: buf.Bytes(),
|
|
})
|
|
|
|
// Mine one block which should contain the sweep transaction.
|
|
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
|
sweepTxHash := sweepTx.TxHash()
|
|
ht.AssertTxInBlock(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(ht *lntest.HarnessTest, node *node.HarnessNode,
|
|
addrType lnrpc.AddressType) (btcutil.Address, []byte) {
|
|
|
|
p2wkhResp := node.RPC.NewAddress(&lnrpc.NewAddressRequest{
|
|
Type: addrType,
|
|
})
|
|
p2wkhAddr, err := btcutil.DecodeAddress(
|
|
p2wkhResp.Address, harnessNetParams,
|
|
)
|
|
require.NoError(ht, err)
|
|
|
|
p2wkhPkScript, err := txscript.PayToAddrScript(p2wkhAddr)
|
|
require.NoError(ht, err)
|
|
|
|
return p2wkhAddr, p2wkhPkScript
|
|
}
|
|
|
|
// sendToTaprootOutput sends coins to a p2tr output of the given taproot key and
|
|
// mines a block to confirm the coins.
|
|
func sendToTaprootOutput(ht *lntest.HarnessTest, hn *node.HarnessNode,
|
|
taprootKey *btcec.PublicKey) (wire.OutPoint, []byte) {
|
|
|
|
tapScriptAddr, err := btcutil.NewAddressTaproot(
|
|
schnorr.SerializePubKey(taprootKey), harnessNetParams,
|
|
)
|
|
require.NoError(ht, err)
|
|
p2trPkScript, err := txscript.PayToAddrScript(tapScriptAddr)
|
|
require.NoError(ht, err)
|
|
|
|
// Send some coins to the generated tapscript address.
|
|
req := &lnrpc.SendCoinsRequest{
|
|
Addr: tapScriptAddr.String(),
|
|
Amount: testAmount,
|
|
TargetConf: 6,
|
|
}
|
|
hn.RPC.SendCoins(req)
|
|
|
|
// Wait until the TX is found in the mempool.
|
|
txid := ht.AssertNumTxsInMempool(1)[0]
|
|
p2trOutputIndex := ht.GetOutputIndex(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 := hn.RPC.GetTransactions(&lnrpc.GetTransactionsRequest{
|
|
StartHeight: 0,
|
|
EndHeight: -1,
|
|
})
|
|
require.NotEmpty(ht, 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(ht, outputDetail, "transaction not found in wallet")
|
|
require.Equal(
|
|
ht, lnrpc.OutputScriptType_SCRIPT_TYPE_WITNESS_V1_TAPROOT,
|
|
outputDetail.OutputType,
|
|
)
|
|
|
|
// Clear the mempool.
|
|
ht.MineBlocksAndAssertNumTxes(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(ht *lntest.HarnessTest, node *node.HarnessNode,
|
|
tx *wire.MsgTx, estimatedWeight lntypes.WeightUnit,
|
|
spendRequest *chainrpc.SpendRequest, sweepAddr string) {
|
|
|
|
ht.Helper()
|
|
|
|
// 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 := ht.CurrentHeight()
|
|
|
|
// For a Taproot output we cannot leave the outpoint empty. Let's make
|
|
// sure the API returns the correct error here.
|
|
req := &chainrpc.SpendRequest{
|
|
Script: spendRequest.Script,
|
|
HeightHint: currentHeight,
|
|
}
|
|
spendClient := node.RPC.RegisterSpendNtfn(req)
|
|
|
|
// The error is only thrown when trying to read a message.
|
|
_, err := spendClient.Recv()
|
|
require.Contains(
|
|
ht, err.Error(),
|
|
"cannot register witness v1 spend request without outpoint",
|
|
)
|
|
|
|
// Now try again, this time with the outpoint set.
|
|
req = &chainrpc.SpendRequest{
|
|
Outpoint: spendRequest.Outpoint,
|
|
Script: spendRequest.Script,
|
|
HeightHint: currentHeight,
|
|
}
|
|
spendClient = node.RPC.RegisterSpendNtfn(req)
|
|
|
|
var buf bytes.Buffer
|
|
require.NoError(ht, 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.EqualValues(ht, estimatedWeight, txWeight)
|
|
|
|
txReq := &walletrpc.Transaction{
|
|
TxHex: buf.Bytes(),
|
|
}
|
|
node.RPC.PublishTransaction(txReq)
|
|
|
|
// Make sure the coins sent to the address are confirmed correctly,
|
|
// including the confirmation notification.
|
|
confirmAddress(ht, node, sweepAddr)
|
|
|
|
// We now expect our spend event to go through.
|
|
spendMsg, err := spendClient.Recv()
|
|
require.NoError(ht, err)
|
|
spend := spendMsg.GetSpend()
|
|
require.NotNil(ht, spend)
|
|
require.Equal(ht, spend.SpendingHeight, 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(ht *lntest.HarnessTest, hn *node.HarnessNode,
|
|
addrString string) {
|
|
|
|
// Wait until the tx that sends to the address is found.
|
|
txid := ht.AssertNumTxsInMempool(1)[0]
|
|
|
|
// Wait until bob has seen the tx and considers it as owned.
|
|
addrOutputIndex := ht.GetOutputIndex(txid, addrString)
|
|
op := &lnrpc.OutPoint{
|
|
TxidBytes: txid[:],
|
|
OutputIndex: uint32(addrOutputIndex),
|
|
}
|
|
ht.AssertUTXOInWallet(hn, 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(ht, err)
|
|
addrPkScript, err := txscript.PayToAddrScript(parsedAddr)
|
|
require.NoError(ht, err)
|
|
|
|
currentHeight := ht.CurrentHeight()
|
|
req := &chainrpc.ConfRequest{
|
|
Script: addrPkScript,
|
|
Txid: txid[:],
|
|
HeightHint: currentHeight,
|
|
NumConfs: 1,
|
|
IncludeBlock: true,
|
|
}
|
|
confClient := hn.RPC.RegisterConfirmationsNtfn(req)
|
|
|
|
// Mine another block to clean up the mempool.
|
|
ht.MineBlocksAndAssertNumTxes(1, 1)
|
|
|
|
// We now expect our confirmation to go through, and also that the
|
|
// block was specified.
|
|
confMsg, err := confClient.Recv()
|
|
require.NoError(ht, err)
|
|
conf := confMsg.GetConf()
|
|
require.NotNil(ht, conf)
|
|
require.Equal(ht, conf.BlockHeight, currentHeight+1)
|
|
require.NotNil(ht, conf.RawBlock)
|
|
|
|
// We should also be able to decode the raw block.
|
|
var blk wire.MsgBlock
|
|
require.NoError(ht, 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(ht *lntest.HarnessTest, node *node.HarnessNode,
|
|
version signrpc.MuSig2Version) (*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.
|
|
req := &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily}
|
|
keyDesc1 := node.RPC.DeriveNextKey(req)
|
|
pubKey1, err := btcec.ParsePubKey(keyDesc1.RawKeyBytes)
|
|
require.NoError(ht, err)
|
|
|
|
keyDesc2 := node.RPC.DeriveNextKey(req)
|
|
pubKey2, err := btcec.ParsePubKey(keyDesc2.RawKeyBytes)
|
|
require.NoError(ht, err)
|
|
|
|
keyDesc3 := node.RPC.DeriveNextKey(req)
|
|
pubKey3, err := btcec.ParsePubKey(keyDesc3.RawKeyBytes)
|
|
require.NoError(ht, 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.
|
|
var allPubKeys [][]byte
|
|
switch version {
|
|
case signrpc.MuSig2Version_MUSIG2_VERSION_V040:
|
|
allPubKeys = [][]byte{
|
|
schnorr.SerializePubKey(pubKey1),
|
|
schnorr.SerializePubKey(pubKey2),
|
|
schnorr.SerializePubKey(pubKey3),
|
|
}
|
|
|
|
case signrpc.MuSig2Version_MUSIG2_VERSION_V100RC2:
|
|
allPubKeys = [][]byte{
|
|
pubKey1.SerializeCompressed(),
|
|
pubKey2.SerializeCompressed(),
|
|
pubKey3.SerializeCompressed(),
|
|
}
|
|
}
|
|
|
|
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(ht *lntest.HarnessTest, node *node.HarnessNode,
|
|
taprootTweak *signrpc.TaprootTweakDesc,
|
|
keyDesc1, keyDesc2, keyDesc3 *signrpc.KeyDescriptor,
|
|
allPubKeys [][]byte, version signrpc.MuSig2Version) (*btcec.PublicKey,
|
|
*btcec.PublicKey, *signrpc.MuSig2SessionResponse,
|
|
*signrpc.MuSig2SessionResponse, *signrpc.MuSig2SessionResponse) {
|
|
|
|
// Make sure that when not specifying a version we get an error, since
|
|
// it is mandatory.
|
|
err := node.RPC.MuSig2CreateSessionErr(&signrpc.MuSig2SessionRequest{})
|
|
require.ErrorContains(ht, err, "unknown MuSig2 version")
|
|
|
|
// Create the actual session with the version specified.
|
|
sessResp1 := node.RPC.MuSig2CreateSession(&signrpc.MuSig2SessionRequest{
|
|
KeyLoc: keyDesc1.KeyLoc,
|
|
AllSignerPubkeys: allPubKeys,
|
|
TaprootTweak: taprootTweak,
|
|
Version: version,
|
|
})
|
|
require.Equal(ht, version, sessResp1.Version)
|
|
|
|
// Make sure the version is returned correctly.
|
|
require.Equal(ht, version, sessResp1.Version)
|
|
|
|
// 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(ht, 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(ht, 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(
|
|
ht, schnorr.SerializePubKey(expectedCombinedKey),
|
|
schnorr.SerializePubKey(combinedKey),
|
|
)
|
|
}
|
|
|
|
// Same with the combine keys RPC, no version specified should give us
|
|
// an error.
|
|
err = node.RPC.MuSig2CombineKeysErr(&signrpc.MuSig2CombineKeysRequest{})
|
|
require.ErrorContains(ht, err, "unknown MuSig2 version")
|
|
|
|
// We should also get the same keys when just calling the
|
|
// MuSig2CombineKeys RPC.
|
|
combineReq := &signrpc.MuSig2CombineKeysRequest{
|
|
AllSignerPubkeys: allPubKeys,
|
|
TaprootTweak: taprootTweak,
|
|
Version: version,
|
|
}
|
|
combineResp := node.RPC.MuSig2CombineKeys(combineReq)
|
|
require.Equal(
|
|
ht, schnorr.SerializePubKey(expectedCombinedKey),
|
|
combineResp.CombinedKey,
|
|
)
|
|
require.Equal(
|
|
ht, schnorr.SerializePubKey(internalKey),
|
|
combineResp.TaprootInternalKey,
|
|
)
|
|
require.Equal(ht, version, combineResp.Version)
|
|
|
|
// Everything is good so far, let's continue with creating the signing
|
|
// session for the other two participants.
|
|
req := &signrpc.MuSig2SessionRequest{
|
|
KeyLoc: keyDesc2.KeyLoc,
|
|
AllSignerPubkeys: allPubKeys,
|
|
OtherSignerPublicNonces: [][]byte{
|
|
sessResp1.LocalPublicNonces,
|
|
},
|
|
TaprootTweak: taprootTweak,
|
|
Version: version,
|
|
}
|
|
sessResp2 := node.RPC.MuSig2CreateSession(req)
|
|
require.Equal(ht, sessResp1.CombinedKey, sessResp2.CombinedKey)
|
|
require.Equal(ht, version, sessResp2.Version)
|
|
|
|
req = &signrpc.MuSig2SessionRequest{
|
|
KeyLoc: keyDesc3.KeyLoc,
|
|
AllSignerPubkeys: allPubKeys,
|
|
OtherSignerPublicNonces: [][]byte{
|
|
sessResp1.LocalPublicNonces,
|
|
sessResp2.LocalPublicNonces,
|
|
},
|
|
TaprootTweak: taprootTweak,
|
|
Version: version,
|
|
}
|
|
sessResp3 := node.RPC.MuSig2CreateSession(req)
|
|
require.Equal(ht, sessResp2.CombinedKey, sessResp3.CombinedKey)
|
|
require.Equal(ht, version, sessResp3.Version)
|
|
require.Equal(ht, true, sessResp3.HaveAllNonces)
|
|
|
|
// We need to distribute the rest of the nonces.
|
|
nonceReq := &signrpc.MuSig2RegisterNoncesRequest{
|
|
SessionId: sessResp1.SessionId,
|
|
OtherSignerPublicNonces: [][]byte{
|
|
sessResp2.LocalPublicNonces,
|
|
sessResp3.LocalPublicNonces,
|
|
},
|
|
}
|
|
nonceResp1 := node.RPC.MuSig2RegisterNonces(nonceReq)
|
|
require.True(ht, nonceResp1.HaveAllNonces)
|
|
|
|
nonceReq = &signrpc.MuSig2RegisterNoncesRequest{
|
|
SessionId: sessResp2.SessionId,
|
|
OtherSignerPublicNonces: [][]byte{
|
|
sessResp3.LocalPublicNonces,
|
|
},
|
|
}
|
|
nonceResp2 := node.RPC.MuSig2RegisterNonces(nonceReq)
|
|
require.True(ht, nonceResp2.HaveAllNonces)
|
|
|
|
return internalKey, combinedKey, sessResp1, sessResp2, sessResp3
|
|
}
|
|
|
|
// 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(ht *lntest.HarnessTest) {
|
|
// We'll start by making two new nodes, and funding a channel between
|
|
// them.
|
|
carol := ht.NewNode("Carol", nil)
|
|
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
|
|
|
|
dave := ht.NewNode("Dave", nil)
|
|
ht.EnsureConnected(carol, dave)
|
|
|
|
chanAmt := funding.MaxBtcFundingAmount
|
|
pushAmt := btcutil.Amount(100000)
|
|
satPerVbyte := btcutil.Amount(1)
|
|
|
|
// We'll now open a channel between Carol and Dave.
|
|
chanPoint := ht.OpenChannel(
|
|
carol, dave, lntest.OpenChannelParams{
|
|
Amt: chanAmt,
|
|
PushAmt: pushAmt,
|
|
SatPerVByte: satPerVbyte,
|
|
},
|
|
)
|
|
|
|
// We'll now close out the channel and obtain the closing TXID.
|
|
closingTxid := ht.CloseChannel(carol, chanPoint)
|
|
|
|
// assertTaprootDeliveryUsed returns true if a Taproot addr was used in
|
|
// the co-op close transaction.
|
|
assertTaprootDeliveryUsed := func(closingTxid *chainhash.Hash) bool {
|
|
tx := ht.GetRawTransaction(closingTxid)
|
|
for _, txOut := range tx.MsgTx().TxOut {
|
|
if !txscript.IsPayToTaproot(txOut.PkScript) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// We expect that the closing transaction only has P2TR addresses.
|
|
require.True(ht, assertTaprootDeliveryUsed(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 := ht.NewNode("Eve", eveArgs)
|
|
ht.EnsureConnected(carol, eve)
|
|
|
|
// We'll now open up a chanel again between Carol and Eve.
|
|
chanPoint = ht.OpenChannel(
|
|
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 = ht.CloseChannel(carol, chanPoint)
|
|
require.False(ht, assertTaprootDeliveryUsed(closingTxid),
|
|
"taproot addr shouldn't be used!")
|
|
}
|
|
|
|
// testMuSig2CombineKey makes sure that combining a key with MuSig2 returns the
|
|
// correct result according to the MuSig2 version specified.
|
|
func testMuSig2CombineKey(ht *lntest.HarnessTest, alice *node.HarnessNode,
|
|
version signrpc.MuSig2Version) {
|
|
|
|
testVector040Key1 := hexDecode(
|
|
"F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE0" +
|
|
"36F9",
|
|
)
|
|
testVector040Key2 := hexDecode(
|
|
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502B" +
|
|
"A659",
|
|
)
|
|
testVector040Key3 := hexDecode(
|
|
"3590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038" +
|
|
"CA66",
|
|
)
|
|
|
|
testVector100Key1 := hexDecode(
|
|
"02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BC" +
|
|
"E036F9",
|
|
)
|
|
testVector100Key2 := hexDecode(
|
|
"03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B50" +
|
|
"2BA659",
|
|
)
|
|
testVector100Key3 := hexDecode(
|
|
"023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D0" +
|
|
"38CA66",
|
|
)
|
|
|
|
var allPubKeys [][]byte
|
|
switch version {
|
|
case signrpc.MuSig2Version_MUSIG2_VERSION_V040:
|
|
allPubKeys = [][]byte{
|
|
testVector040Key1, testVector040Key2, testVector040Key3,
|
|
}
|
|
|
|
case signrpc.MuSig2Version_MUSIG2_VERSION_V100RC2:
|
|
allPubKeys = [][]byte{
|
|
testVector100Key1, testVector100Key2, testVector100Key3,
|
|
}
|
|
}
|
|
|
|
resp := alice.RPC.MuSig2CombineKeys(&signrpc.MuSig2CombineKeysRequest{
|
|
AllSignerPubkeys: allPubKeys,
|
|
TaprootTweak: &signrpc.TaprootTweakDesc{
|
|
KeySpendOnly: true,
|
|
},
|
|
Version: version,
|
|
})
|
|
|
|
expectedFinalKey040 := hexDecode(
|
|
"5b257b4e785d61157ef5303051f45184bd5cb47bc4b4069ed4dd453645" +
|
|
"9cb83b",
|
|
)
|
|
expectedPreTweakKey040 := hexDecode(
|
|
"d70cd69a2647f7390973df48cbfa2ccc407b8b2d60b08c5f1641185c79" +
|
|
"98a290",
|
|
)
|
|
|
|
expectedFinalKey100 := hexDecode(
|
|
"79e6c3e628c9bfbce91de6b7fb28e2aec7713d377cf260ab599dcbc40e54" +
|
|
"2312",
|
|
)
|
|
expectedPreTweakKey100 := hexDecode(
|
|
"789d937bade6673538f3e28d8368dda4d0512f94da44cf477a505716d26a" +
|
|
"1575",
|
|
)
|
|
|
|
switch version {
|
|
case signrpc.MuSig2Version_MUSIG2_VERSION_V040:
|
|
require.Equal(ht, expectedFinalKey040, resp.CombinedKey)
|
|
require.Equal(
|
|
ht, expectedPreTweakKey040, resp.TaprootInternalKey,
|
|
)
|
|
|
|
case signrpc.MuSig2Version_MUSIG2_VERSION_V100RC2:
|
|
require.Equal(ht, expectedFinalKey100, resp.CombinedKey)
|
|
require.Equal(
|
|
ht, expectedPreTweakKey100, resp.TaprootInternalKey,
|
|
)
|
|
}
|
|
}
|