mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
82ba5bf0bf
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.
617 lines
16 KiB
Go
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())
|
|
}
|