lnd/lnwallet/chancloser/chancloser_test.go
Olaoluwa Osuntokun 82ba5bf0bf
lnwallet+peer: add tapscript root awareness to musig2 sessions
With this commit, the channel is now aware of if it's a musig2 channel, that also has a tapscript root. We'll need to always pass in the tapscript root each time we: make the funding output, sign a new state, and also verify a new state.
2024-08-28 11:52:31 +02:00

617 lines
16 KiB
Go

package chancloser
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
// TestMaybeMatchScript tests that the maybeMatchScript errors appropriately
// when an upfront shutdown script is set and the script provided does not
// match, and does not error in any other case.
func TestMaybeMatchScript(t *testing.T) {
t.Parallel()
pubHash := bytes.Repeat([]byte{0x0}, 20)
scriptHash := bytes.Repeat([]byte{0x0}, 32)
p2wkh, err := txscript.NewScriptBuilder().AddOp(txscript.OP_0).
AddData(pubHash).Script()
require.NoError(t, err)
p2wsh, err := txscript.NewScriptBuilder().AddOp(txscript.OP_0).
AddData(scriptHash).Script()
require.NoError(t, err)
p2tr, err := txscript.NewScriptBuilder().AddOp(txscript.OP_1).
AddData(scriptHash).Script()
require.NoError(t, err)
p2OtherV1, err := txscript.NewScriptBuilder().AddOp(txscript.OP_1).
AddData(pubHash).Script()
require.NoError(t, err)
invalidFork, err := txscript.NewScriptBuilder().AddOp(txscript.OP_NOP).
AddData(scriptHash).Script()
require.NoError(t, err)
type testCase struct {
name string
shutdownScript lnwire.DeliveryAddress
upfrontScript lnwire.DeliveryAddress
expectedErr error
}
tests := []testCase{
{
name: "no upfront shutdown set, script ok",
shutdownScript: p2wkh,
upfrontScript: []byte{},
expectedErr: nil,
},
{
name: "upfront shutdown set, script ok",
shutdownScript: p2wkh,
upfrontScript: p2wkh,
expectedErr: nil,
},
{
name: "upfront shutdown set, script not ok",
shutdownScript: p2wkh,
upfrontScript: p2wsh,
expectedErr: ErrUpfrontShutdownScriptMismatch,
},
{
name: "nil shutdown and empty upfront",
shutdownScript: nil,
upfrontScript: []byte{},
expectedErr: nil,
},
{
name: "p2tr is ok",
shutdownScript: p2tr,
},
{
name: "segwit v1 is ok",
shutdownScript: p2OtherV1,
},
{
name: "invalid script not allowed",
shutdownScript: invalidFork,
expectedErr: ErrInvalidShutdownScript,
},
}
// All future segwit softforks should also be ok.
futureForks := []byte{
txscript.OP_1, txscript.OP_2, txscript.OP_3, txscript.OP_4,
txscript.OP_5, txscript.OP_6, txscript.OP_7, txscript.OP_8,
txscript.OP_9, txscript.OP_10, txscript.OP_11, txscript.OP_12,
txscript.OP_13, txscript.OP_14, txscript.OP_15, txscript.OP_16,
}
for _, witnessVersion := range futureForks {
p2FutureFork, err := txscript.NewScriptBuilder().AddOp(witnessVersion).
AddData(scriptHash).Script()
require.NoError(t, err)
opString, err := txscript.DisasmString([]byte{witnessVersion})
require.NoError(t, err)
tests = append(tests, testCase{
name: fmt.Sprintf("witness_version=%v", opString),
shutdownScript: p2FutureFork,
})
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
err := validateShutdownScript(
func() error { return nil }, test.upfrontScript,
test.shutdownScript, &chaincfg.SimNetParams,
)
if err != test.expectedErr {
t.Fatalf("Error: %v, expected error: %v", err, test.expectedErr)
}
})
}
}
type mockChannel struct {
chanPoint wire.OutPoint
initiator bool
scid lnwire.ShortChannelID
chanType channeldb.ChannelType
localKey keychain.KeyDescriptor
remoteKey keychain.KeyDescriptor
}
func (m *mockChannel) ChannelPoint() wire.OutPoint {
return m.chanPoint
}
func (m *mockChannel) MarkCoopBroadcasted(*wire.MsgTx,
lntypes.ChannelParty) error {
return nil
}
func (m *mockChannel) MarkShutdownSent(*channeldb.ShutdownInfo) error {
return nil
}
func (m *mockChannel) IsInitiator() bool {
return m.initiator
}
func (m *mockChannel) ShortChanID() lnwire.ShortChannelID {
return m.scid
}
func (m *mockChannel) AbsoluteThawHeight() (uint32, error) {
return 0, nil
}
func (m *mockChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress {
return lnwire.DeliveryAddress{}
}
func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount,
localScript, remoteScript []byte,
_ ...lnwallet.ChanCloseOpt) (input.Signature, *chainhash.Hash,
btcutil.Amount, error) {
if m.chanType.IsTaproot() {
return lnwallet.NewMusigPartialSig(
&musig2.PartialSignature{
S: new(btcec.ModNScalar),
R: new(btcec.PublicKey),
},
lnwire.Musig2Nonce{}, lnwire.Musig2Nonce{}, nil,
fn.None[chainhash.Hash](),
), nil, 0, nil
}
return nil, nil, 0, nil
}
func (m *mockChannel) CompleteCooperativeClose(localSig,
remoteSig input.Signature, localScript, remoteScript []byte,
proposedFee btcutil.Amount,
_ ...lnwallet.ChanCloseOpt) (*wire.MsgTx, btcutil.Amount, error) {
return &wire.MsgTx{}, 0, nil
}
func (m *mockChannel) LocalBalanceDust() bool {
return false
}
func (m *mockChannel) RemoteBalanceDust() bool {
return false
}
func (m *mockChannel) ChanType() channeldb.ChannelType {
return m.chanType
}
func (m *mockChannel) FundingTxOut() *wire.TxOut {
return nil
}
func (m *mockChannel) MultiSigKeys() (
keychain.KeyDescriptor, keychain.KeyDescriptor) {
return m.localKey, m.remoteKey
}
func newMockTaprootChan(t *testing.T, initiator bool) *mockChannel {
taprootBits := channeldb.SimpleTaprootFeatureBit |
channeldb.AnchorOutputsBit |
channeldb.ZeroHtlcTxFeeBit |
channeldb.SingleFunderTweaklessBit
localKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
remoteKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
return &mockChannel{
chanPoint: wire.OutPoint{
Hash: chainhash.Hash{},
Index: 0,
},
initiator: initiator,
chanType: taprootBits,
localKey: keychain.KeyDescriptor{
PubKey: localKey.PubKey(),
},
remoteKey: keychain.KeyDescriptor{
PubKey: remoteKey.PubKey(),
},
}
}
type mockMusigSession struct {
}
func newMockMusigSession() *mockMusigSession {
return &mockMusigSession{}
}
func (m *mockMusigSession) ProposalClosingOpts() ([]lnwallet.ChanCloseOpt,
error) {
return nil, nil
}
func (m *mockMusigSession) CombineClosingOpts(localSig,
remoteSig lnwire.PartialSig,
) (input.Signature, input.Signature, []lnwallet.ChanCloseOpt, error) {
return &lnwallet.MusigPartialSig{}, &lnwallet.MusigPartialSig{}, nil,
nil
}
func (m *mockMusigSession) ClosingNonce() (*musig2.Nonces, error) {
return &musig2.Nonces{}, nil
}
func (m *mockMusigSession) InitRemoteNonce(nonce *musig2.Nonces) {
}
type mockCoopFeeEstimator struct {
targetFee btcutil.Amount
}
func (m *mockCoopFeeEstimator) EstimateFee(chanType channeldb.ChannelType,
localTxOut, remoteTxOut *wire.TxOut,
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
return m.targetFee
}
// TestMaxFeeClamp tests that if a max fee is specified, then it's used instead
// of the default max fee multiplier.
func TestMaxFeeClamp(t *testing.T) {
t.Parallel()
const (
absoluteFeeOneSatByte = 126
absoluteFeeTenSatByte = 1265
)
tests := []struct {
name string
idealFee chainfee.SatPerKWeight
inputMaxFee chainfee.SatPerKWeight
maxFee btcutil.Amount
}{
{
// No max fee specified, we should see 3x the ideal fee.
name: "no max fee",
idealFee: chainfee.SatPerKWeight(253),
maxFee: absoluteFeeOneSatByte * defaultMaxFeeMultiplier,
},
{
// Max fee specified, this should be used in place.
name: "max fee clamp",
idealFee: chainfee.SatPerKWeight(253),
inputMaxFee: chainfee.SatPerKWeight(2530),
// We should get the resulting absolute fee based on a
// factor of 10 sat/byte (our new max fee).
maxFee: absoluteFeeTenSatByte,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
channel := mockChannel{}
chanCloser := NewChanCloser(
ChanCloseCfg{
Channel: &channel,
MaxFee: test.inputMaxFee,
FeeEstimator: &SimpleCoopFeeEstimator{},
}, nil, test.idealFee, 0, nil, lntypes.Remote,
)
// We'll call initFeeBaseline early here since we need
// the populate these internal variables.
chanCloser.initFeeBaseline()
require.Equal(t, test.maxFee, chanCloser.maxFee)
})
}
}
// TestMaxFeeBailOut tests that once the negotiated fee rate rises above our
// maximum fee, we'll return an error and refuse to process a co-op close
// message.
func TestMaxFeeBailOut(t *testing.T) {
t.Parallel()
const (
absoluteFee = btcutil.Amount(1000)
idealFee = chainfee.SatPerKWeight(253)
)
for _, isInitiator := range []bool{true, false} {
isInitiator := isInitiator
t.Run(fmt.Sprintf("initiator=%v", isInitiator), func(t *testing.T) {
t.Parallel()
// First, we'll make our mock channel, and use that to
// instantiate our channel closer.
closeCfg := ChanCloseCfg{
Channel: &mockChannel{
initiator: isInitiator,
},
FeeEstimator: &mockCoopFeeEstimator{
targetFee: absoluteFee,
},
MaxFee: idealFee * 2,
}
chanCloser := NewChanCloser(
closeCfg, nil, idealFee, 0, nil, lntypes.Remote,
)
// We'll now force the channel state into the
// closeFeeNegotiation state so we can skip straight to
// the juicy part. We'll also set our last fee sent so
// we'll attempt to actually "negotiate" here.
chanCloser.state = closeFeeNegotiation
chanCloser.lastFeeProposal = absoluteFee
// Next, we'll make a ClosingSigned message that
// proposes a fee that's above the specified max fee.
//
// NOTE: We use the absoluteFee here since our mock
// always returns this fee for the CalcFee method which
// is used to translate a fee rate
// into an absolute fee amount in sats.
closeMsg := &lnwire.ClosingSigned{
FeeSatoshis: absoluteFee * 2,
}
_, err := chanCloser.ReceiveClosingSigned(*closeMsg)
switch isInitiator {
// If we're the initiator, then we expect an error at
// this point.
case true:
require.ErrorIs(
t, err, ErrProposalExceedsMaxFee,
)
// Otherwise, we expect things to fail for some other
// reason (invalid sig, etc).
case false:
require.NotErrorIs(
t, err, ErrProposalExceedsMaxFee,
)
}
})
}
}
// TestParseUpfrontShutdownAddress tests the we are able to parse the upfront
// shutdown address properly.
func TestParseUpfrontShutdownAddress(t *testing.T) {
t.Parallel()
var (
testnetAddress = "tb1qdfkmwwgdaa5dnezrlhtftvmj5qn2kwgp7n0z6r"
regtestAddress = "bcrt1q09crvvuj95x5nk64wsxf5n6ky0kr8358vpx4d8"
)
tests := []struct {
name string
address string
params chaincfg.Params
expectedErr string
}{
{
name: "invalid closing address",
address: "non-valid-address",
params: chaincfg.RegressionNetParams,
expectedErr: "invalid address",
},
{
name: "closing address from another net",
address: testnetAddress,
params: chaincfg.RegressionNetParams,
expectedErr: "not a regtest address",
},
{
name: "valid p2wkh closing address",
address: regtestAddress,
params: chaincfg.RegressionNetParams,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := ParseUpfrontShutdownAddress(
tc.address, &tc.params,
)
if tc.expectedErr != "" {
require.ErrorContains(t, err, tc.expectedErr)
return
}
require.NoError(t, err)
})
}
}
// TestTaprootFastClose tests that we are able to properly execute a fast close
// (skip negotiation) for taproot channels.
func TestTaprootFastClose(t *testing.T) {
t.Parallel()
aliceChan := newMockTaprootChan(t, true)
bobChan := newMockTaprootChan(t, false)
broadcastSignal := make(chan struct{}, 2)
idealFee := chainfee.SatPerKWeight(506)
// Next, we'll make a channel for Alice and Bob, with Alice being the
// initiator.
aliceCloser := NewChanCloser(
ChanCloseCfg{
Channel: aliceChan,
MusigSession: newMockMusigSession(),
BroadcastTx: func(_ *wire.MsgTx, _ string) error {
broadcastSignal <- struct{}{}
return nil
},
MaxFee: chainfee.SatPerKWeight(1000),
FeeEstimator: &SimpleCoopFeeEstimator{},
DisableChannel: func(wire.OutPoint) error {
return nil
},
}, nil, idealFee, 0, nil, lntypes.Local,
)
aliceCloser.initFeeBaseline()
bobCloser := NewChanCloser(
ChanCloseCfg{
Channel: bobChan,
MusigSession: newMockMusigSession(),
MaxFee: chainfee.SatPerKWeight(1000),
BroadcastTx: func(_ *wire.MsgTx, _ string) error {
broadcastSignal <- struct{}{}
return nil
},
FeeEstimator: &SimpleCoopFeeEstimator{},
DisableChannel: func(wire.OutPoint) error {
return nil
},
}, nil, idealFee, 0, nil, lntypes.Remote,
)
bobCloser.initFeeBaseline()
// With our set up complete, we'll now initialize the shutdown
// procedure kicked off by Alice.
msg, err := aliceCloser.ShutdownChan()
require.NoError(t, err)
require.NotNil(t, msg)
// Bob will then process this message. As he's the responder, he should
// only send the shutdown message back to Alice.
oShutdown, err := bobCloser.ReceiveShutdown(*msg)
require.NoError(t, err)
oClosingSigned, err := bobCloser.BeginNegotiation()
require.NoError(t, err)
tx, _ := bobCloser.ClosingTx()
require.Nil(t, tx)
require.True(t, oShutdown.IsSome())
require.True(t, oClosingSigned.IsNone())
// Alice should process the shutdown message, and create a closing
// signed of her own.
oShutdown, err = aliceCloser.ReceiveShutdown(oShutdown.UnwrapOrFail(t))
require.NoError(t, err)
oClosingSigned, err = aliceCloser.BeginNegotiation()
require.NoError(t, err)
tx, _ = aliceCloser.ClosingTx()
require.Nil(t, tx)
require.True(t, oShutdown.IsNone())
require.True(t, oClosingSigned.IsSome())
aliceClosingSigned := oClosingSigned.UnwrapOrFail(t)
// Next, Bob will process the closing signed message, and send back a
// new one that should match exactly the offer Alice sent.
oClosingSigned, err = bobCloser.ReceiveClosingSigned(aliceClosingSigned)
require.NoError(t, err)
tx, _ = bobCloser.ClosingTx()
require.NotNil(t, tx)
require.True(t, oClosingSigned.IsSome())
bobClosingSigned := oClosingSigned.UnwrapOrFail(t)
// At this point, Bob has accepted the offer, so he can broadcast the
// closing transaction, and considers the channel closed.
_, err = lnutils.RecvOrTimeout(broadcastSignal, time.Second*1)
require.NoError(t, err)
// Bob's fee proposal should exactly match Alice's initial fee.
require.Equal(
t, aliceClosingSigned.FeeSatoshis, bobClosingSigned.FeeSatoshis,
)
// If we modify Bob's offer, and try to have Alice process it, then she
// should reject it.
ogOffer := bobClosingSigned.FeeSatoshis
bobClosingSigned.FeeSatoshis /= 2
_, err = aliceCloser.ReceiveClosingSigned(bobClosingSigned)
require.Error(t, err)
require.Contains(t, err.Error(), "was not accepted")
// We'll now restore the original offer before passing it on to Alice.
bobClosingSigned.FeeSatoshis = ogOffer
// If we use the original offer, then Alice should accept this message,
// and finalize the shutdown process. We expect a message here as Alice
// will echo back the final message.
oClosingSigned, err = aliceCloser.ReceiveClosingSigned(bobClosingSigned)
require.NoError(t, err)
tx, _ = aliceCloser.ClosingTx()
require.NotNil(t, tx)
require.True(t, oClosingSigned.IsSome())
aliceClosingSigned = oClosingSigned.UnwrapOrFail(t)
// Alice should now also broadcast her closing transaction.
_, err = lnutils.RecvOrTimeout(broadcastSignal, time.Second*1)
require.NoError(t, err)
// Finally, Bob will process Alice's echo message, and conclude.
oClosingSigned, err = bobCloser.ReceiveClosingSigned(aliceClosingSigned)
require.NoError(t, err)
tx, _ = bobCloser.ClosingTx()
require.NotNil(t, tx)
require.True(t, oClosingSigned.IsNone())
}