lnd/peer/brontide_test.go
Olaoluwa Osuntokun e5e134ddac peer+lnwallet/chancloser: advance the legacy closer from one goroutine
In this commit, we give the legacy ChanCloser a single owner, rather than
letting two goroutines advance it. The peer's channelManager drives the state
machine for the Shutdown and ClosingSigned messages that come off the wire, and
for local close requests. The link drives it as well: while we wait for the
channel to drain we register a flush hook, and the link invokes that hook from
its own goroutine, where it called BeginNegotiation directly. Nothing kept the
two apart, so the state field, the priorFeeOffers map, and the signing step
could all be touched at once. Under `go test -race` this shows up as a data race
on the state field.

Rather than reach for a lock, we route the flush through the channelManager. The
hook now only reports the channel ID over a new chanCloseFlushed channel, and
handleChanFlushed picks it up next to the close messages. Every transition, the
cached offer processing, the fee map, and the signing then happen on the one
goroutine, so the closer needs no synchronization of its own. We spell that out
on the type, since it's an invariant a new caller can break from the outside.

The report goes out from a fresh goroutine, which matters more than it looks.
The link may well be holding its own lock while it invokes the hook, and
channelManager reaches for that same lock in DisableAdds, so blocking on the
handoff would trade the race for a deadlock. The `go` in front of RemoveLink
just above it is there for the same reason.

We look the closer up with a plain map load rather than through
fetchActiveChanCloser, as that one builds a fresh closer when it doesn't find
an existing one, and a flush that lands after the negotiation was torn down has
no business starting a new negotiation.

One behavior change falls out of the move: the flush path now runs the same
finalization tail as the message path. It skipped that before, so a responder
that drained a cached offer would reach closeFinished and broadcast, but nothing
ran finalizeChanClosure until the next close message showed up, and having
already sent its final signature, there may not be one. The link == nil path
already ran the tail, so this makes all three paths agree.

The new test drives a close with a link that hands us the flush hook instead of
running it inline, so we can check that negotiation waits on the report, and
that a report for a channel we have no closer for is dropped.
2026-08-05 12:22:56 -07:00

2062 lines
59 KiB
Go

package peer
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/btcsuite/btcd/address/v2"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/chaincfg/v2"
"github.com/btcsuite/btcd/txscript/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/chanstate"
"github.com/lightningnetwork/lnd/contractcourt"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
var (
// p2SHAddress is a valid pay to script hash address.
p2SHAddress = "2NBFNJTktNa7GZusGbDbGKRZTxdK9VVez3n"
// p2wshAddress is a valid pay to witness script hash address.
p2wshAddress = "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"
)
// TestPeerChannelClosureShutdownResponseLinkRemoved tests the shutdown
// response we get if the link for the channel can't be found in the
// switch. This test was added due to a regression.
func TestPeerChannelClosureShutdownResponseLinkRemoved(t *testing.T) {
t.Parallel()
harness, err := createTestPeerWithChannel(t, noUpdate)
require.NoError(t, err, "unable to create test channels")
var (
alicePeer = harness.peer
bobChan = harness.channel
)
chanPoint := bobChan.ChannelPoint()
chanID := lnwire.NewChanIDFromOutPoint(chanPoint)
dummyDeliveryScript := genScript(t, p2wshAddress)
// We send a shutdown request to Alice. She will now be the responding
// node in this shutdown procedure. We first expect Alice to answer
// this shutdown request with a Shutdown message.
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: lnwire.NewShutdown(chanID, dummyDeliveryScript),
}
var msg lnwire.Message
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive shutdown message")
}
shutdownMsg, ok := msg.(*lnwire.Shutdown)
if !ok {
t.Fatalf("expected Shutdown message, got %T", msg)
}
require.NotEqualValues(t, shutdownMsg.Address, dummyDeliveryScript)
}
// TestPeerChannelClosureAcceptFeeResponder tests the shutdown responder's
// behavior if we can agree on the fee immediately.
func TestPeerChannelClosureAcceptFeeResponder(t *testing.T) {
t.Parallel()
harness, err := createTestPeerWithChannel(t, noUpdate)
require.NoError(t, err, "unable to create test channels")
var (
alicePeer = harness.peer
bobChan = harness.channel
mockSwitch = harness.mockSwitch
broadcastTxChan = harness.publishTx
notifier = harness.notifier
)
chanPoint := bobChan.ChannelPoint()
chanID := lnwire.NewChanIDFromOutPoint(chanPoint)
mockLink := newMockUpdateHandler(chanID)
mockSwitch.links = append(mockSwitch.links, mockLink)
dummyDeliveryScript := genScript(t, p2wshAddress)
// We send a shutdown request to Alice. She will now be the responding
// node in this shutdown procedure. We first expect Alice to answer
// this shutdown request with a Shutdown message.
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: lnwire.NewShutdown(chanID, dummyDeliveryScript),
}
var msg lnwire.Message
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive shutdown message")
}
shutdownMsg, ok := msg.(*lnwire.Shutdown)
if !ok {
t.Fatalf("expected Shutdown message, got %T", msg)
}
respDeliveryScript := shutdownMsg.Address
require.NotEqualValues(t, respDeliveryScript, dummyDeliveryScript)
// Alice will then send a ClosingSigned message, indicating her proposed
// closing transaction fee. Alice sends the ClosingSigned message as she is
// the initiator of the channel.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive ClosingSigned message")
}
respClosingSigned, ok := msg.(*lnwire.ClosingSigned)
if !ok {
t.Fatalf("expected ClosingSigned message, got %T", msg)
}
// We accept the fee, and send a ClosingSigned with the same fee back,
// so she knows we agreed.
aliceFee := respClosingSigned.FeeSatoshis
bobSig, _, _, err := bobChan.CreateCloseProposal(
aliceFee, dummyDeliveryScript, respDeliveryScript,
)
require.NoError(t, err, "error creating close proposal")
parsedSig, err := lnwire.NewSigFromSignature(bobSig)
require.NoError(t, err, "error parsing signature")
closingSigned := lnwire.NewClosingSigned(chanID, aliceFee, parsedSig)
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: closingSigned,
}
// Alice should now see that we agreed on the fee, and should broadcast the
// closing transaction.
select {
case <-broadcastTxChan:
case <-time.After(timeout):
t.Fatalf("closing tx not broadcast")
}
// Need to pull the remaining message off of Alice's outgoing queue.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive ClosingSigned message")
}
if _, ok := msg.(*lnwire.ClosingSigned); !ok {
t.Fatalf("expected ClosingSigned message, got %T", msg)
}
// Alice should be waiting in a goroutine for a confirmation.
notifier.ConfChan <- &chainntnfs.TxConfirmation{}
}
// TestPeerChannelClosureFlushDrivesNegotiation checks that a legacy cooperative
// close holds off on fee negotiation until the link reports that the channel
// has drained, and that the report is what carries the negotiation forward. The
// link notices the flush on its own goroutine, so it hands the channel to the
// channelManager rather than advancing the closer itself.
func TestPeerChannelClosureFlushDrivesNegotiation(t *testing.T) {
t.Parallel()
harness, err := createTestPeerWithChannel(t, noUpdate)
require.NoError(t, err, "unable to create test channels")
var (
alicePeer = harness.peer
bobChan = harness.channel
mockSwitch = harness.mockSwitch
broadcastTxChan = harness.publishTx
notifier = harness.notifier
)
chanPoint := bobChan.ChannelPoint()
chanID := lnwire.NewChanIDFromOutPoint(chanPoint)
// The link holds on to the flush hook rather than running it inline, so
// we get to say when the channel looks drained.
mockLink := newDeferredFlushUpdateHandler(chanID)
mockSwitch.links = append(mockSwitch.links, mockLink)
dummyDeliveryScript := genScript(t, p2wshAddress)
// We send a shutdown request to Alice, and expect her own Shutdown in
// response.
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: lnwire.NewShutdown(chanID, dummyDeliveryScript),
}
var msg lnwire.Message
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive shutdown message")
}
shutdownMsg, ok := msg.(*lnwire.Shutdown)
require.True(t, ok, "expected Shutdown message, got %T", msg)
respDeliveryScript := shutdownMsg.Address
// The channel hasn't drained yet, so Alice shouldn't have opened fee
// negotiation, even though she's the one that funded the channel.
select {
case outMsg := <-alicePeer.outgoingQueue:
t.Fatalf("negotiation started before the channel flushed: %T",
outMsg.msg)
case <-time.After(shortTimeout):
}
// A flush report for a channel we have no closer for should be dropped
// on the floor rather than start anything.
var unknownChanID lnwire.ChannelID
select {
case alicePeer.chanCloseFlushed <- unknownChanID:
case <-time.After(timeout):
t.Fatalf("channelManager not reading flush reports")
}
// Now we let the link report the flush, which is what should carry the
// negotiation into its fee phase.
select {
case hook := <-mockLink.flushHooks:
go hook()
case <-time.After(timeout):
t.Fatalf("no flush hook was registered")
}
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive ClosingSigned message")
}
respClosingSigned, ok := msg.(*lnwire.ClosingSigned)
require.True(t, ok, "expected ClosingSigned message, got %T", msg)
// We accept the fee, and send a ClosingSigned with the same fee back so
// she knows we agreed.
aliceFee := respClosingSigned.FeeSatoshis
bobSig, _, _, err := bobChan.CreateCloseProposal(
aliceFee, dummyDeliveryScript, respDeliveryScript,
)
require.NoError(t, err, "error creating close proposal")
parsedSig, err := lnwire.NewSigFromSignature(bobSig)
require.NoError(t, err, "error parsing signature")
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: lnwire.NewClosingSigned(chanID, aliceFee, parsedSig),
}
// Alice should now see that we agreed on the fee, and broadcast the
// closing transaction.
select {
case <-broadcastTxChan:
case <-time.After(timeout):
t.Fatalf("closing tx not broadcast")
}
// Need to pull the remaining message off of Alice's outgoing queue.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive ClosingSigned message")
}
_, ok = msg.(*lnwire.ClosingSigned)
require.True(t, ok, "expected ClosingSigned message, got %T", msg)
// Alice should be waiting in a goroutine for a confirmation.
notifier.ConfChan <- &chainntnfs.TxConfirmation{}
}
// TestPeerChannelClosureAcceptFeeInitiator tests the shutdown initiator's
// behavior if we can agree on the fee immediately.
func TestPeerChannelClosureAcceptFeeInitiator(t *testing.T) {
t.Parallel()
harness, err := createTestPeerWithChannel(t, noUpdate)
require.NoError(t, err, "unable to create test channels")
var (
bobChan = harness.channel
alicePeer = harness.peer
mockSwitch = harness.mockSwitch
broadcastTxChan = harness.publishTx
notifier = harness.notifier
)
chanPoint := bobChan.ChannelPoint()
chanID := lnwire.NewChanIDFromOutPoint(chanPoint)
mockLink := newMockUpdateHandler(chanID)
mockSwitch.links = append(mockSwitch.links, mockLink)
dummyDeliveryScript := genScript(t, p2wshAddress)
// We make Alice send a shutdown request.
updateChan := make(chan interface{}, 1)
errChan := make(chan error, 1)
closeCommand := &htlcswitch.ChanClose{
CloseType: contractcourt.CloseRegular,
ChanPoint: &chanPoint,
Updates: updateChan,
TargetFeePerKw: 12500,
Err: errChan,
}
alicePeer.localCloseChanReqs <- closeCommand
// We can now pull a Shutdown message off of Alice's outgoingQueue.
var msg lnwire.Message
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive shutdown request")
}
shutdownMsg, ok := msg.(*lnwire.Shutdown)
if !ok {
t.Fatalf("expected Shutdown message, got %T", msg)
}
aliceDeliveryScript := shutdownMsg.Address
require.NotEqualValues(t, aliceDeliveryScript, dummyDeliveryScript)
// Bob will respond with his own Shutdown message.
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: lnwire.NewShutdown(chanID,
dummyDeliveryScript),
}
// Alice will reply with a ClosingSigned here.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive closing signed message")
}
closingSignedMsg, ok := msg.(*lnwire.ClosingSigned)
if !ok {
t.Fatalf("expected to receive closing signed message, got %T", msg)
}
// Bob should reply with the exact same fee in his next ClosingSigned
// message.
bobFee := closingSignedMsg.FeeSatoshis
bobSig, _, _, err := bobChan.CreateCloseProposal(
bobFee, dummyDeliveryScript, aliceDeliveryScript,
)
require.NoError(t, err, "unable to create close proposal")
parsedSig, err := lnwire.NewSigFromSignature(bobSig)
require.NoError(t, err, "unable to parse signature")
closingSigned := lnwire.NewClosingSigned(shutdownMsg.ChannelID,
bobFee, parsedSig)
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: closingSigned,
}
// Alice should accept Bob's fee, broadcast the cooperative close tx, and
// send a ClosingSigned message back to Bob.
// Alice should now broadcast the closing transaction.
select {
case <-broadcastTxChan:
case <-time.After(timeout):
t.Fatalf("closing tx not broadcast")
}
// Alice should respond with the ClosingSigned they both agreed upon.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive closing signed message")
}
closingSignedMsg, ok = msg.(*lnwire.ClosingSigned)
if !ok {
t.Fatalf("expected ClosingSigned message, got %T", msg)
}
if closingSignedMsg.FeeSatoshis != bobFee {
t.Fatalf("expected ClosingSigned fee to be %v, instead got %v",
bobFee, closingSignedMsg.FeeSatoshis)
}
// Alice should be waiting on a single confirmation for the coop close tx.
notifier.ConfChan <- &chainntnfs.TxConfirmation{}
}
// TestPeerChannelClosureFeeNegotiationsResponder tests the shutdown
// responder's behavior in the case where we must do several rounds of fee
// negotiation before we agree on a fee.
func TestPeerChannelClosureFeeNegotiationsResponder(t *testing.T) {
t.Parallel()
harness, err := createTestPeerWithChannel(t, noUpdate)
require.NoError(t, err, "unable to create test channels")
var (
bobChan = harness.channel
alicePeer = harness.peer
mockSwitch = harness.mockSwitch
broadcastTxChan = harness.publishTx
notifier = harness.notifier
)
chanPoint := bobChan.ChannelPoint()
chanID := lnwire.NewChanIDFromOutPoint(chanPoint)
mockLink := newMockUpdateHandler(chanID)
mockSwitch.links = append(mockSwitch.links, mockLink)
// Bob sends a shutdown request to Alice. She will now be the responding
// node in this shutdown procedure. We first expect Alice to answer this
// Shutdown request with a Shutdown message.
dummyDeliveryScript := genScript(t, p2wshAddress)
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: lnwire.NewShutdown(chanID,
dummyDeliveryScript),
}
var msg lnwire.Message
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive shutdown message")
}
shutdownMsg, ok := msg.(*lnwire.Shutdown)
if !ok {
t.Fatalf("expected Shutdown message, got %T", msg)
}
aliceDeliveryScript := shutdownMsg.Address
require.NotEqualValues(t, aliceDeliveryScript, dummyDeliveryScript)
// As Alice is the channel initiator, she will send her ClosingSigned
// message.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive closing signed message")
}
aliceClosingSigned, ok := msg.(*lnwire.ClosingSigned)
if !ok {
t.Fatalf("expected ClosingSigned message, got %T", msg)
}
// Bob doesn't agree with the fee and will send one back that's 2.5x.
preferredRespFee := aliceClosingSigned.FeeSatoshis
increasedFee := btcutil.Amount(float64(preferredRespFee) * 2.5)
bobSig, _, _, err := bobChan.CreateCloseProposal(
increasedFee, dummyDeliveryScript, aliceDeliveryScript,
)
require.NoError(t, err, "error creating close proposal")
parsedSig, err := lnwire.NewSigFromSignature(bobSig)
require.NoError(t, err, "error parsing signature")
closingSigned := lnwire.NewClosingSigned(chanID, increasedFee, parsedSig)
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: closingSigned,
}
// Alice will now see the new fee we propose, but with current settings it
// won't accept it immediately as it differs too much by its ideal fee. We
// should get a new proposal back, which should have the average fee rate
// proposed.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive closing signed message")
}
aliceClosingSigned, ok = msg.(*lnwire.ClosingSigned)
if !ok {
t.Fatalf("expected ClosingSigned message, got %T", msg)
}
// The fee sent by Alice should be less than the fee Bob just sent as Alice
// should attempt to compromise.
aliceFee := aliceClosingSigned.FeeSatoshis
if aliceFee > increasedFee {
t.Fatalf("new fee should be less than our fee: new=%v, "+
"prior=%v", aliceFee, increasedFee)
}
lastFeeResponder := aliceFee
// We try negotiating a 2.1x fee, which should also be rejected.
increasedFee = btcutil.Amount(float64(preferredRespFee) * 2.1)
bobSig, _, _, err = bobChan.CreateCloseProposal(
increasedFee, dummyDeliveryScript, aliceDeliveryScript,
)
require.NoError(t, err, "error creating close proposal")
parsedSig, err = lnwire.NewSigFromSignature(bobSig)
require.NoError(t, err, "error parsing signature")
closingSigned = lnwire.NewClosingSigned(chanID, increasedFee, parsedSig)
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: closingSigned,
}
// Bob's latest proposal still won't be accepted and Alice should send over
// a new ClosingSigned message. It should be the average of what Bob and
// Alice each proposed last time.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive closing signed message")
}
aliceClosingSigned, ok = msg.(*lnwire.ClosingSigned)
if !ok {
t.Fatalf("expected ClosingSigned message, got %T", msg)
}
// Alice should inch towards Bob's fee, in order to compromise.
// Additionally, this fee should be less than the fee Bob sent before.
aliceFee = aliceClosingSigned.FeeSatoshis
if aliceFee < lastFeeResponder {
t.Fatalf("new fee should be greater than prior: new=%v, "+
"prior=%v", aliceFee, lastFeeResponder)
}
if aliceFee > increasedFee {
t.Fatalf("new fee should be less than Bob's fee: new=%v, "+
"prior=%v", aliceFee, increasedFee)
}
// Finally, Bob will accept the fee by echoing back the same fee that Alice
// just sent over.
bobSig, _, _, err = bobChan.CreateCloseProposal(
aliceFee, dummyDeliveryScript, aliceDeliveryScript,
)
require.NoError(t, err, "error creating close proposal")
parsedSig, err = lnwire.NewSigFromSignature(bobSig)
require.NoError(t, err, "error parsing signature")
closingSigned = lnwire.NewClosingSigned(chanID, aliceFee, parsedSig)
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: closingSigned,
}
// Alice will now see that Bob agreed on the fee, and broadcast the coop
// close transaction.
select {
case <-broadcastTxChan:
case <-time.After(timeout):
t.Fatalf("closing tx not broadcast")
}
// Alice should respond with the ClosingSigned they both agreed upon.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive closing signed message")
}
if _, ok := msg.(*lnwire.ClosingSigned); !ok {
t.Fatalf("expected to receive closing signed message, got %T", msg)
}
// Alice should be waiting on a single confirmation for the coop close tx.
notifier.ConfChan <- &chainntnfs.TxConfirmation{}
}
// TestPeerChannelClosureFeeNegotiationsInitiator tests the shutdown
// initiator's behavior in the case where we must do several rounds of fee
// negotiation before we agree on a fee.
func TestPeerChannelClosureFeeNegotiationsInitiator(t *testing.T) {
t.Parallel()
harness, err := createTestPeerWithChannel(t, noUpdate)
require.NoError(t, err, "unable to create test channels")
var (
alicePeer = harness.peer
bobChan = harness.channel
mockSwitch = harness.mockSwitch
broadcastTxChan = harness.publishTx
notifier = harness.notifier
)
chanPoint := bobChan.ChannelPoint()
chanID := lnwire.NewChanIDFromOutPoint(chanPoint)
mockLink := newMockUpdateHandler(chanID)
mockSwitch.links = append(mockSwitch.links, mockLink)
// We make the initiator send a shutdown request.
updateChan := make(chan interface{}, 1)
errChan := make(chan error, 1)
closeCommand := &htlcswitch.ChanClose{
CloseType: contractcourt.CloseRegular,
ChanPoint: &chanPoint,
Updates: updateChan,
TargetFeePerKw: 12500,
Err: errChan,
}
alicePeer.localCloseChanReqs <- closeCommand
// Alice should now send a Shutdown request to Bob.
var msg lnwire.Message
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive shutdown request")
}
shutdownMsg, ok := msg.(*lnwire.Shutdown)
if !ok {
t.Fatalf("expected Shutdown message, got %T", msg)
}
aliceDeliveryScript := shutdownMsg.Address
// Bob will answer the Shutdown message with his own Shutdown.
dummyDeliveryScript := genScript(t, p2wshAddress)
respShutdown := lnwire.NewShutdown(chanID, dummyDeliveryScript)
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: respShutdown,
}
// Alice should now respond with a ClosingSigned message with her ideal
// fee rate.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive closing signed")
}
closingSignedMsg, ok := msg.(*lnwire.ClosingSigned)
if !ok {
t.Fatalf("expected ClosingSigned message, got %T", msg)
}
idealFeeRate := closingSignedMsg.FeeSatoshis
lastReceivedFee := idealFeeRate
increasedFee := btcutil.Amount(float64(idealFeeRate) * 2.1)
lastSentFee := increasedFee
bobSig, _, _, err := bobChan.CreateCloseProposal(
increasedFee, dummyDeliveryScript, aliceDeliveryScript,
)
require.NoError(t, err, "error creating close proposal")
parsedSig, err := lnwire.NewSigFromSignature(bobSig)
require.NoError(t, err, "unable to parse signature")
closingSigned := lnwire.NewClosingSigned(chanID, increasedFee, parsedSig)
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: closingSigned,
}
// It still won't be accepted, and we should get a new proposal, the
// average of what we proposed, and what they proposed last time.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive closing signed")
}
closingSignedMsg, ok = msg.(*lnwire.ClosingSigned)
if !ok {
t.Fatalf("expected ClosingSigned message, got %T", msg)
}
aliceFee := closingSignedMsg.FeeSatoshis
if aliceFee < lastReceivedFee {
t.Fatalf("new fee should be greater than prior: new=%v, old=%v",
aliceFee, lastReceivedFee)
}
if aliceFee > lastSentFee {
t.Fatalf("new fee should be less than our fee: new=%v, old=%v",
aliceFee, lastSentFee)
}
lastReceivedFee = aliceFee
// We'll try negotiating a 1.5x fee, which should also be rejected.
increasedFee = btcutil.Amount(float64(idealFeeRate) * 1.5)
lastSentFee = increasedFee
bobSig, _, _, err = bobChan.CreateCloseProposal(
increasedFee, dummyDeliveryScript, aliceDeliveryScript,
)
require.NoError(t, err, "error creating close proposal")
parsedSig, err = lnwire.NewSigFromSignature(bobSig)
require.NoError(t, err, "error parsing signature")
closingSigned = lnwire.NewClosingSigned(chanID, increasedFee, parsedSig)
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: closingSigned,
}
// Alice won't accept Bob's new proposal, and Bob should receive a new
// proposal which is the average of what Bob proposed and Alice proposed
// last time.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive closing signed")
}
closingSignedMsg, ok = msg.(*lnwire.ClosingSigned)
if !ok {
t.Fatalf("expected ClosingSigned message, got %T", msg)
}
aliceFee = closingSignedMsg.FeeSatoshis
if aliceFee < lastReceivedFee {
t.Fatalf("new fee should be greater than prior: new=%v, old=%v",
aliceFee, lastReceivedFee)
}
if aliceFee > lastSentFee {
t.Fatalf("new fee should be less than Bob's fee: new=%v, old=%v",
aliceFee, lastSentFee)
}
// Bob will now accept their fee by sending back a ClosingSigned message
// with an identical fee.
bobSig, _, _, err = bobChan.CreateCloseProposal(
aliceFee, dummyDeliveryScript, aliceDeliveryScript,
)
require.NoError(t, err, "error creating close proposal")
parsedSig, err = lnwire.NewSigFromSignature(bobSig)
require.NoError(t, err, "error parsing signature")
closingSigned = lnwire.NewClosingSigned(chanID, aliceFee, parsedSig)
alicePeer.chanCloseMsgs <- &closeMsg{
cid: chanID,
msg: closingSigned,
}
// Wait for closing tx to be broadcasted.
select {
case <-broadcastTxChan:
case <-time.After(timeout):
t.Fatalf("closing tx not broadcast")
}
// Alice should respond with the ClosingSigned they both agreed upon.
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive closing signed message")
}
if _, ok := msg.(*lnwire.ClosingSigned); !ok {
t.Fatalf("expected to receive closing signed message, got %T", msg)
}
// Alice should be waiting on a single confirmation for the coop close tx.
notifier.ConfChan <- &chainntnfs.TxConfirmation{}
}
// TestChooseDeliveryScript tests that chooseDeliveryScript correctly errors
// when upfront and user set scripts that do not match are provided, allows
// matching values and returns appropriate values in the case where one or none
// are set.
func TestChooseDeliveryScript(t *testing.T) {
// generate non-zero scripts for testing.
script1 := genScript(t, p2SHAddress)
script2 := genScript(t, p2wshAddress)
tests := []struct {
name string
userScript lnwire.DeliveryAddress
shutdownScript lnwire.DeliveryAddress
expectedScript lnwire.DeliveryAddress
newAddr func() ([]byte, error)
expectedError error
}{
{
name: "Both set and equal",
userScript: script1,
shutdownScript: script1,
expectedScript: script1,
expectedError: nil,
},
{
name: "Both set and not equal",
userScript: script1,
shutdownScript: script2,
expectedScript: nil,
expectedError: chancloser.ErrUpfrontShutdownScriptMismatch,
},
{
name: "Only upfront script",
userScript: nil,
shutdownScript: script1,
expectedScript: script1,
expectedError: nil,
},
{
name: "Only user script",
userScript: script2,
shutdownScript: nil,
expectedScript: script2,
expectedError: nil,
},
{
name: "no script generate new one",
userScript: nil,
shutdownScript: nil,
expectedScript: script2,
newAddr: func() ([]byte, error) {
return script2, nil
},
expectedError: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
script, err := chooseDeliveryScript(
test.shutdownScript, test.userScript,
test.newAddr,
)
if err != test.expectedError {
t.Fatalf("Expected: %v, got: %v",
test.expectedError, err)
}
if !bytes.Equal(script, test.expectedScript) {
t.Fatalf("Expected: %x, got: %x",
test.expectedScript, script)
}
})
}
}
// TestCustomShutdownScript tests that the delivery script of a shutdown
// message can be set to a specified address. It checks that setting a close
// script fails for channels which have an upfront shutdown script already set.
func TestCustomShutdownScript(t *testing.T) {
script := genScript(t, p2SHAddress)
// setShutdown is a function which sets the upfront shutdown address for
// the local channel.
setShutdown := func(a, b *chanstate.OpenChannel) {
a.LocalShutdownScript = script
b.RemoteShutdownScript = script
}
tests := []struct {
name string
// update is a function used to set values on the channel set up for the
// test. It is used to set values for upfront shutdown addresses.
update func(a, b *chanstate.OpenChannel)
// userCloseScript is the address specified by the user.
userCloseScript lnwire.DeliveryAddress
// expectedScript is the address we expect to be set on the shutdown
// message.
expectedScript lnwire.DeliveryAddress
// expectedError is the error we expect, if any.
expectedError error
}{
{
name: "User set script",
update: noUpdate,
userCloseScript: script,
expectedScript: script,
},
{
name: "No user set script",
update: noUpdate,
},
{
name: "Shutdown set, no user script",
update: setShutdown,
expectedScript: script,
},
{
name: "Shutdown set, user script matches",
update: setShutdown,
userCloseScript: script,
expectedScript: script,
},
{
name: "Shutdown set, user script different",
update: setShutdown,
userCloseScript: []byte("different addr"),
expectedError: chancloser.ErrUpfrontShutdownScriptMismatch,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Open a channel.
harness, err := createTestPeerWithChannel(
t, test.update,
)
if err != nil {
t.Fatalf("unable to create test channels: %v", err)
}
var (
alicePeer = harness.peer
bobChan = harness.channel
mockSwitch = harness.mockSwitch
)
chanPoint := bobChan.ChannelPoint()
chanID := lnwire.NewChanIDFromOutPoint(chanPoint)
mockLink := newMockUpdateHandler(chanID)
mockSwitch.links = append(mockSwitch.links, mockLink)
// Request initiator to cooperatively close the channel,
// with a specified delivery address.
updateChan := make(chan interface{}, 1)
errChan := make(chan error, 1)
closeCommand := htlcswitch.ChanClose{
CloseType: contractcourt.CloseRegular,
ChanPoint: &chanPoint,
Updates: updateChan,
TargetFeePerKw: 12500,
DeliveryScript: test.userCloseScript,
Err: errChan,
}
// Send the close command for the correct channel and check that a
// shutdown message is sent.
alicePeer.localCloseChanReqs <- &closeCommand
var msg lnwire.Message
select {
case outMsg := <-alicePeer.outgoingQueue:
msg = outMsg.msg
case <-time.After(timeout):
t.Fatalf("did not receive shutdown message")
case err := <-errChan:
// Fail if we do not expect an error.
if test.expectedError != nil {
require.ErrorIs(
t, err, test.expectedError,
)
}
// Terminate the test early if have received an error, no
// further action is expected.
return
}
// Check that we have received a shutdown message.
shutdownMsg, ok := msg.(*lnwire.Shutdown)
if !ok {
t.Fatalf("expected shutdown message, got %T", msg)
}
// If the test has not specified an expected address, do not check
// whether the shutdown address matches. This covers the case where
// we expect shutdown to a random address and cannot match it.
if len(test.expectedScript) == 0 {
return
}
// Check that the Shutdown message includes the expected delivery
// script.
if !bytes.Equal(test.expectedScript, shutdownMsg.Address) {
t.Fatalf("expected delivery script: %x, got: %x",
test.expectedScript, shutdownMsg.Address)
}
})
}
}
// TestStaticRemoteDowngrade tests that we downgrade our static remote feature
// bit to optional if we have legacy channels with a peer. This ensures that
// we can stay connected to peers that don't support the feature bit that we
// have channels with.
func TestStaticRemoteDowngrade(t *testing.T) {
t.Parallel()
var (
// We set the same legacy feature bits for all tests, since
// these are not relevant to our test scenario
rawLegacy = lnwire.NewRawFeatureVector(
lnwire.UpfrontShutdownScriptOptional,
)
legacy = lnwire.NewFeatureVector(rawLegacy, nil)
legacyCombinedOptional = lnwire.NewRawFeatureVector(
lnwire.UpfrontShutdownScriptOptional,
lnwire.StaticRemoteKeyOptional,
)
rawFeatureOptional = lnwire.NewRawFeatureVector(
lnwire.StaticRemoteKeyOptional,
)
featureOptional = lnwire.NewFeatureVector(
rawFeatureOptional, nil,
)
rawFeatureRequired = lnwire.NewRawFeatureVector(
lnwire.StaticRemoteKeyRequired,
)
featureRequired = lnwire.NewFeatureVector(
rawFeatureRequired, nil,
)
)
tests := []struct {
name string
legacy bool
features *lnwire.FeatureVector
expectedInit *lnwire.Init
}{
{
name: "no legacy channel, static optional",
legacy: false,
features: featureOptional,
expectedInit: &lnwire.Init{
GlobalFeatures: rawLegacy,
Features: rawFeatureOptional,
},
},
{
name: "legacy channel, static optional",
legacy: true,
features: featureOptional,
expectedInit: &lnwire.Init{
GlobalFeatures: rawLegacy,
Features: rawFeatureOptional,
},
},
{
name: "no legacy channel, static required",
legacy: false,
features: featureRequired,
expectedInit: &lnwire.Init{
GlobalFeatures: rawLegacy,
Features: rawFeatureRequired,
},
},
// In this case we need to flip our required bit to optional,
// this should also propagate to the legacy set of feature bits
// so we have proper consistency: a bit isn't set to optional
// in one field and required in the other.
{
name: "legacy channel, static required",
legacy: true,
features: featureRequired,
expectedInit: &lnwire.Init{
GlobalFeatures: legacyCombinedOptional,
Features: rawFeatureOptional,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
params := createTestPeer(t)
var (
p = params.peer
mockConn = params.mockConn
writePool = p.cfg.WritePool
)
// Set feature bits.
p.cfg.LegacyFeatures = legacy
p.cfg.Features = test.features
var b bytes.Buffer
_, err := lnwire.WriteMessage(&b, test.expectedInit, 0)
require.NoError(t, err)
// Send our init message, assert that we write our
// expected message and shutdown our write pool.
require.NoError(t, p.sendInitMsg(test.legacy))
mockConn.assertWrite(b.Bytes())
require.NoError(t, writePool.Stop())
})
}
}
// genScript creates a script paying out to the address provided, which must
// be a valid address.
func genScript(t *testing.T, addr string) lnwire.DeliveryAddress {
// Generate an address which can be used for testing.
deliveryAddr, err := address.DecodeAddress(
addr, &chaincfg.TestNet3Params,
)
require.NoError(t, err, "invalid delivery address")
script, err := txscript.PayToAddrScript(deliveryAddr)
require.NoError(t, err, "cannot create script")
return script
}
// TestPeerCustomMessage tests custom message exchange between peers.
func TestPeerCustomMessage(t *testing.T) {
t.Parallel()
params := createTestPeer(t)
var (
mockConn = params.mockConn
alicePeer = params.peer
receivedCustomChan = params.customChan
remoteKey = alicePeer.PubKey()
)
// Start peer.
startPeerDone := startPeer(t, mockConn, alicePeer)
_, err := fn.RecvOrTimeout(startPeerDone, 2*timeout)
require.NoError(t, err)
// Send a custom message.
customMsg, err := lnwire.NewCustom(
lnwire.MessageType(40000), []byte{1, 2, 3},
)
require.NoError(t, err)
require.NoError(t, alicePeer.SendMessageLazy(false, customMsg))
// Verify that it is passed down to the noise layer correctly.
writtenMsg := <-mockConn.writtenMessages
require.Equal(t, []byte{0x9c, 0x40, 0x1, 0x2, 0x3}, writtenMsg)
// Receive a custom message.
receivedCustomMsg, err := lnwire.NewCustom(
lnwire.MessageType(40001), []byte{4, 5, 6},
)
require.NoError(t, err)
receivedData := []byte{0x9c, 0x41, 0x4, 0x5, 0x6}
mockConn.readMessages <- receivedData
// Verify that it is propagated up to the custom message handler.
receivedCustom := <-receivedCustomChan
require.Equal(t, remoteKey, receivedCustom.peer)
require.Equal(t, receivedCustomMsg, &receivedCustom.msg)
}
// TestPeerIgnoresPingWithoutPongReply ensures we keep the connection alive for
// pings using the BOLT 1 no-reply sentinel range.
func TestPeerIgnoresPingWithoutPongReply(t *testing.T) {
t.Parallel()
// Arrange: Start a peer using the mock connection so we can
// inject incoming pings and observe any outgoing responses.
params := createTestPeer(t)
var (
mockConn = params.mockConn
alicePeer = params.peer
)
startPeerDone := startPeer(t, mockConn, alicePeer)
_, err := fn.RecvOrTimeout(startPeerDone, 2*timeout)
require.NoError(t, err)
writePing := func(msg *lnwire.Ping) {
t.Helper()
var b bytes.Buffer
_, err := lnwire.WriteMessage(&b, msg, 0)
require.NoError(t, err)
select {
case mockConn.readMessages <- b.Bytes():
case <-time.After(timeout):
t.Fatal("timeout sending ping to peer")
}
}
// Act: Deliver a ping in the BOLT 1 no-reply range.
ignoredPayload := []byte{1, 2, 3}
writePing(&lnwire.Ping{
NumPongBytes: 65535,
PaddingBytes: ignoredPayload,
})
// Assert: The peer records the latest ping payload for observability.
require.Eventually(t, func() bool {
return bytes.Equal(
alicePeer.LastRemotePingPayload(), ignoredPayload,
)
}, timeout, 10*time.Millisecond)
// Assert: No pong is sent for the no-reply sentinel range.
select {
case rawMsg := <-mockConn.writtenMessages:
t.Fatalf("expected no pong reply, got %x", rawMsg)
case <-time.After(100 * time.Millisecond):
}
// Act: Send a normal ping afterward to prove the peer
// stayed connected and still handles standard ping/pong
// traffic.
writePing(&lnwire.Ping{NumPongBytes: 1})
rawMsg, err := fn.RecvOrTimeout(mockConn.writtenMessages, timeout)
require.NoError(t, err)
msg, err := lnwire.ReadMessage(bytes.NewReader(rawMsg), 0)
require.NoError(t, err)
// Assert: The follow-up ping receives the requested pong reply.
pong, ok := msg.(*lnwire.Pong)
require.True(t, ok)
require.Len(t, pong.PongBytes, 1)
}
// TestMessageSummaryPingIncludesNumPongBytes ensures the debug summary for a
// ping exposes the requested pong size, which makes ignored no-reply pings
// visible without requiring trace-level logging.
func TestMessageSummaryPingIncludesNumPongBytes(t *testing.T) {
t.Parallel()
// Arrange: Build a ping that uses the BOLT 1 no-reply sentinel range.
msg := &lnwire.Ping{
NumPongBytes: 65535,
PaddingBytes: []byte{1, 2, 3},
}
// Act: Generate the human-readable message summary.
summary := messageSummary(msg)
// Assert: The summary includes both the requested pong size and payload
// length so debug logs can explain why no pong was sent.
require.Equal(t, "num_pong_bytes=65535, len(ping_bytes)=3", summary)
}
// TestUpdateNextRevocation checks that the method `updateNextRevocation` is
// behave as expected.
func TestUpdateNextRevocation(t *testing.T) {
t.Parallel()
require := require.New(t)
harness, err := createTestPeerWithChannel(t, noUpdate)
require.NoError(err, "unable to create test channels")
bobChan := harness.channel
alicePeer := harness.peer
// testChannel is used to test the updateNextRevocation function.
testChannel := bobChan.State()
// Update the next revocation for a known channel should give us no
// error.
err = alicePeer.updateNextRevocation(testChannel)
require.NoError(err, "expected no error")
// Test an error is returned when the chanID cannot be found in
// `activeChannels` map.
testChannel.FundingOutpoint = wire.OutPoint{Index: 0}
err = alicePeer.updateNextRevocation(testChannel)
require.Error(err, "expected an error")
// Test an error is returned when the chanID's corresponding channel is
// nil.
testChannel.FundingOutpoint = wire.OutPoint{Index: 1}
chanID := lnwire.NewChanIDFromOutPoint(testChannel.FundingOutpoint)
alicePeer.activeChannels.Store(chanID, nil)
err = alicePeer.updateNextRevocation(testChannel)
require.Error(err, "expected an error")
// TODO(yy): should also test `InitNextRevocation` is called on
// `lnwallet.LightningWallet` once it's interfaced.
}
func assertMsgSent(t *testing.T, conn *mockMessageConn,
msgType lnwire.MessageType) {
t.Helper()
require := require.New(t)
rawMsg, err := fn.RecvOrTimeout(conn.writtenMessages, timeout)
require.NoError(err)
msgReader := bytes.NewReader(rawMsg)
msg, err := lnwire.ReadMessage(msgReader, 0)
require.NoError(err)
require.Equal(msgType, msg.MsgType())
}
// TestAlwaysSendChannelUpdate tests that each time we connect to the peer if
// an active channel, we always send the latest channel update.
func TestAlwaysSendChannelUpdate(t *testing.T) {
require := require.New(t)
var channel *chanstate.OpenChannel
channelIntercept := func(a, b *chanstate.OpenChannel) {
channel = a
}
harness, err := createTestPeerWithChannel(t, channelIntercept)
require.NoError(err, "unable to create test channels")
// Avoid the need to mock the channel graph by marking the channel
// borked. Borked channels still get a reestablish message sent on
// reconnect, while skipping channel graph checks and link creation.
require.NoError(channel.MarkBorked())
// Start the peer, which'll trigger the normal init and start up logic.
startPeerDone := startPeer(t, harness.mockConn, harness.peer)
_, err = fn.RecvOrTimeout(startPeerDone, 2*timeout)
require.NoError(err)
// Assert that we eventually send a channel update.
assertMsgSent(t, harness.mockConn, lnwire.MsgChannelReestablish)
assertMsgSent(t, harness.mockConn, lnwire.MsgChannelUpdate)
}
// TODO(yy): add test for `addActiveChannel` and `handleNewActiveChannel` once
// we have interfaced `lnwallet.LightningChannel` and
// `*contractcourt.ChainArbitrator`.
// TestHandleNewPendingChannel checks the method `handleNewPendingChannel`
// behaves as expected.
func TestHandleNewPendingChannel(t *testing.T) {
t.Parallel()
// Create three channel IDs for testing.
chanIDActive := lnwire.ChannelID{0}
chanIDNotExist := lnwire.ChannelID{1}
chanIDPending := lnwire.ChannelID{2}
testCases := []struct {
name string
chanID lnwire.ChannelID
// expectChanAdded specifies whether this chanID will be added
// to the peer's state.
expectChanAdded bool
}{
{
name: "noop on active channel",
chanID: chanIDActive,
expectChanAdded: false,
},
{
name: "noop on pending channel",
chanID: chanIDPending,
expectChanAdded: false,
},
{
name: "new channel should be added",
chanID: chanIDNotExist,
expectChanAdded: true,
},
}
for _, tc := range testCases {
// Create a request for testing.
errChan := make(chan error, 1)
req := &newChannelMsg{
channelID: tc.chanID,
err: errChan,
}
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
require := require.New(t)
// Create a test brontide.
dummyConfig := Config{}
peer := NewBrontide(dummyConfig)
// Create the test state.
peer.activeChannels.Store(
chanIDActive, &lnwallet.LightningChannel{},
)
peer.activeChannels.Store(chanIDPending, nil)
// Assert test state, we should have two channels
// store, one active and one pending.
numChans := 2
require.EqualValues(
numChans, peer.activeChannels.Len(),
)
// Call the method.
peer.handleNewPendingChannel(req)
// Add one if we expect this channel to be added.
if tc.expectChanAdded {
numChans++
}
// Assert the number of channels is correct.
require.Equal(numChans, peer.activeChannels.Len())
// Assert the request's error chan is closed.
err, ok := <-req.err
require.False(ok, "expect err chan to be closed")
require.NoError(err, "expect no error")
})
}
}
// TestHandleRemovePendingChannel checks the method
// `handleRemovePendingChannel` behaves as expected.
func TestHandleRemovePendingChannel(t *testing.T) {
t.Parallel()
// Create three channel IDs for testing.
chanIDActive := lnwire.ChannelID{0}
chanIDNotExist := lnwire.ChannelID{1}
chanIDPending := lnwire.ChannelID{2}
testCases := []struct {
name string
chanID lnwire.ChannelID
// expectDeleted specifies whether this chanID will be removed
// from the peer's state.
expectDeleted bool
}{
{
name: "noop on active channel",
chanID: chanIDActive,
expectDeleted: false,
},
{
name: "pending channel should be removed",
chanID: chanIDPending,
expectDeleted: true,
},
{
name: "noop on non-exist channel",
chanID: chanIDNotExist,
expectDeleted: false,
},
}
for _, tc := range testCases {
// Create a request for testing.
errChan := make(chan error, 1)
req := &newChannelMsg{
channelID: tc.chanID,
err: errChan,
}
// Create a test brontide.
dummyConfig := Config{}
peer := NewBrontide(dummyConfig)
// Create the test state.
peer.activeChannels.Store(
chanIDActive, &lnwallet.LightningChannel{},
)
peer.activeChannels.Store(chanIDPending, nil)
// Assert test state, we should have two channels store, one
// active and one pending.
require.Equal(t, 2, peer.activeChannels.Len())
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
require := require.New(t)
// Get the number of channels before mutating the
// state.
numChans := peer.activeChannels.Len()
// Call the method.
peer.handleRemovePendingChannel(req)
// Minus one if we expect this channel to be removed.
if tc.expectDeleted {
numChans--
}
// Assert the number of channels is correct.
require.Equal(numChans, peer.activeChannels.Len())
// Assert the request's error chan is closed.
err, ok := <-req.err
require.False(ok, "expect err chan to be closed")
require.NoError(err, "expect no error")
})
}
}
// TestStartupWriteMessageRace checks that no data race occurs when starting up
// a peer with an existing channel, while an outgoing message is queuing. Such
// a race occurred in https://github.com/lightningnetwork/lnd/issues/8184, where
// a channel reestablish message raced with another outgoing message.
//
// Note that races will only be detected with the Go race detector enabled.
func TestStartupWriteMessageRace(t *testing.T) {
t.Parallel()
// Use a callback to extract the channel created by
// createTestPeerWithChannel, so we can mark it borked below.
// We can't mark it borked within the callback, since the channel hasn't
// been saved to the DB yet when the callback executes.
var channel *chanstate.OpenChannel
getChannels := func(a, b *chanstate.OpenChannel) {
channel = a
}
// createTestPeerWithChannel creates a peer and a channel with that
// peer.
harness, err := createTestPeerWithChannel(t, getChannels)
require.NoError(t, err, "unable to create test channel")
peer := harness.peer
// Avoid the need to mock the channel graph by marking the channel
// borked. Borked channels still get a reestablish message sent on
// reconnect, while skipping channel graph checks and link creation.
require.NoError(t, channel.MarkBorked())
// Use a mock conn to detect read/write races on the conn.
mockConn := newMockConn(t, 2)
peer.cfg.Conn = mockConn
// Send a message while starting the peer. As the peer starts up, it
// should not trigger a data race between the sending of this message
// and the sending of the channel reestablish message.
var sendPingDone = make(chan struct{})
go func() {
require.NoError(t, peer.SendMessage(true, lnwire.NewPing(0)))
close(sendPingDone)
}()
// Start the peer. No data race should occur.
startPeerDone := startPeer(t, mockConn, peer)
// Ensure startup is complete.
_, err = fn.RecvOrTimeout(startPeerDone, 2*timeout)
require.NoError(t, err)
// Ensure messages were sent during startup.
<-sendPingDone
for i := 0; i < 2; i++ {
select {
case <-mockConn.writtenMessages:
default:
t.Fatalf("Failed to send all messages during startup")
}
}
}
// TestRemovePendingChannel checks that we are able to remove a pending channel
// successfully from the peers channel map. This also makes sure the
// removePendingChannel is initialized so we don't send to a nil channel and
// get stuck.
func TestRemovePendingChannel(t *testing.T) {
t.Parallel()
// createTestPeerWithChannel creates a peer and a channel.
harness, err := createTestPeerWithChannel(t, noUpdate)
require.NoError(t, err, "unable to create test channel")
peer := harness.peer
// Add a pending channel to the peer Alice.
errChan := make(chan error, 1)
pendingChanID := lnwire.ChannelID{1}
req := &newChannelMsg{
channelID: pendingChanID,
err: errChan,
}
select {
case peer.newPendingChannel <- req:
// Operation completed successfully
case <-time.After(timeout):
t.Fatalf("not able to remove pending channel")
}
// Make sure the channel was added as a pending channel.
// The peer was already created with one active channel therefore the
// `activeChannels` had already one channel prior to adding the new one.
// The `addedChannels` map only tracks new channels in the current life
// cycle therefore the initial channel is not part of it.
err = wait.NoError(func() error {
if peer.activeChannels.Len() == 2 &&
peer.addedChannels.Len() == 1 {
return nil
}
return fmt.Errorf("pending channel not successfully added")
}, wait.DefaultTimeout)
require.NoError(t, err)
// Now try to remove it, the errChan needs to be reopened because it was
// closed during the pending channel registration above.
errChan = make(chan error, 1)
req = &newChannelMsg{
channelID: pendingChanID,
err: errChan,
}
select {
case peer.removePendingChannel <- req:
// Operation completed successfully
case <-time.After(timeout):
t.Fatalf("not able to remove pending channel")
}
// Make sure the pending channel is successfully removed from both
// channel maps.
// The initial channel between the peer is still active at this point.
err = wait.NoError(func() error {
if peer.activeChannels.Len() == 1 &&
peer.addedChannels.Len() == 0 {
return nil
}
return fmt.Errorf("pending channel not successfully removed")
}, wait.DefaultTimeout)
require.NoError(t, err)
}
// mockAuxTrafficShaper is a mock implementation of htlcswitch.AuxTrafficShaper
// for testing the createHtlcValidator function.
type mockAuxTrafficShaper struct {
mock.Mock
}
// ShouldHandleTraffic returns the configured mock values.
func (m *mockAuxTrafficShaper) ShouldHandleTraffic(
cid lnwire.ShortChannelID,
fundingBlob, htlcBlob fn.Option[tlv.Blob]) (bool, error) {
args := m.Called(cid, fundingBlob, htlcBlob)
return args.Bool(0), args.Error(1)
}
// PaymentBandwidth returns the configured mock values.
func (m *mockAuxTrafficShaper) PaymentBandwidth(fundingBlob, htlcBlob,
commitmentBlob fn.Option[tlv.Blob], linkBandwidth,
htlcAmt lnwire.MilliSatoshi, htlcView lnwallet.AuxHtlcView,
peer route.Vertex) (lnwire.MilliSatoshi, error) {
args := m.Called(
fundingBlob, htlcBlob, commitmentBlob, linkBandwidth,
htlcAmt, htlcView, peer,
)
bw, _ := args.Get(0).(lnwire.MilliSatoshi)
return bw, args.Error(1)
}
// ProduceHtlcExtraData is part of the AuxTrafficShaper interface.
func (m *mockAuxTrafficShaper) ProduceHtlcExtraData(
totalAmount lnwire.MilliSatoshi,
htlcCustomRecords lnwire.CustomRecords,
peer route.Vertex) (lnwire.MilliSatoshi, lnwire.CustomRecords,
error) {
args := m.Called(totalAmount, htlcCustomRecords, peer)
amt, _ := args.Get(0).(lnwire.MilliSatoshi)
records, _ := args.Get(1).(lnwire.CustomRecords)
return amt, records, args.Error(2)
}
// IsCustomHTLC is part of the AuxTrafficShaper interface.
func (m *mockAuxTrafficShaper) IsCustomHTLC(
htlcRecords lnwire.CustomRecords) bool {
args := m.Called(htlcRecords)
return args.Bool(0)
}
// Compile-time check that mockAuxTrafficShaper implements AuxTrafficShaper.
var _ htlcswitch.AuxTrafficShaper = (*mockAuxTrafficShaper)(nil)
// TestCreateHtlcValidator tests that the HTLC validator created by
// createHtlcValidator respects the ShouldHandleTraffic check. When
// ShouldHandleTraffic returns false, the validator should return nil without
// calling PaymentBandwidth.
func TestCreateHtlcValidator(t *testing.T) {
t.Parallel()
// Create a minimal Brontide with just the identity key set.
privKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
peer := &Brontide{
cfg: Config{
Addr: &lnwire.NetAddress{
IdentityKey: privKey.PubKey(),
},
},
}
// Create a mock channel with minimal required fields.
dbChan := &chanstate.OpenChannel{
ShortChannelID: lnwire.NewShortChanIDFromInt(123),
}
anyArg := mock.Anything
testCases := []struct {
name string
setupMock func(*mockAuxTrafficShaper)
htlcAmount lnwire.MilliSatoshi
linkBw lnwire.MilliSatoshi
expectError bool
}{
{
name: "non-custom channel skips check",
setupMock: func(m *mockAuxTrafficShaper) {
m.On(
"ShouldHandleTraffic",
anyArg, anyArg, anyArg,
).Return(false, nil)
},
htlcAmount: 1000,
linkBw: 5000,
expectError: false,
},
{
name: "sufficient bandwidth",
setupMock: func(m *mockAuxTrafficShaper) {
m.On(
"ShouldHandleTraffic",
anyArg, anyArg, anyArg,
).Return(true, nil)
m.On(
"PaymentBandwidth",
anyArg, anyArg, anyArg,
anyArg, anyArg, anyArg,
anyArg,
).Return(
lnwire.MilliSatoshi(10000),
nil,
)
},
htlcAmount: 1000,
linkBw: 5000,
expectError: false,
},
{
name: "insufficient bandwidth",
setupMock: func(m *mockAuxTrafficShaper) {
m.On(
"ShouldHandleTraffic",
anyArg, anyArg, anyArg,
).Return(true, nil)
m.On(
"PaymentBandwidth",
anyArg, anyArg, anyArg,
anyArg, anyArg, anyArg,
anyArg,
).Return(
lnwire.MilliSatoshi(500),
nil,
)
},
htlcAmount: 1000,
linkBw: 5000,
expectError: true,
},
{
name: "ShouldHandleTraffic error",
setupMock: func(m *mockAuxTrafficShaper) {
m.On(
"ShouldHandleTraffic",
anyArg, anyArg, anyArg,
).Return(
false,
fmt.Errorf("shaper error"),
)
},
htlcAmount: 1000,
linkBw: 5000,
expectError: true,
},
{
name: "PaymentBandwidth error",
setupMock: func(m *mockAuxTrafficShaper) {
m.On(
"ShouldHandleTraffic",
anyArg, anyArg, anyArg,
).Return(true, nil)
m.On(
"PaymentBandwidth",
anyArg, anyArg, anyArg,
anyArg, anyArg, anyArg,
anyArg,
).Return(
lnwire.MilliSatoshi(0),
fmt.Errorf("bandwidth error"),
)
},
htlcAmount: 1000,
linkBw: 5000,
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
m := &mockAuxTrafficShaper{}
tc.setupMock(m)
validator := peer.createHtlcValidator(
dbChan, m,
)
err := validator.ValidateHtlc(
tc.htlcAmount, tc.linkBw,
nil, lnwallet.AuxHtlcView{},
)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
m.AssertExpectations(t)
})
}
}
// TestHasActiveChannels exercises the atomic active-channel counter that
// backs hasActiveChannels(). hasActiveChannels is on the hot path for
// every incoming onion message — the onion message ingress gate calls
// it per packet — so a correct, O(1) shadow of activeChannels is a
// load-bearing invariant. This test walks the three state transitions
// that have to keep numActiveChans in lockstep with activeChannels:
// initial emptiness, pending entries (nil values) that must not count,
// and pending-delete paths that must not decrement.
func TestHasActiveChannels(t *testing.T) {
t.Parallel()
peer := NewBrontide(Config{})
// Initial state: no channels, counter is zero, gate is closed.
require.False(t, peer.hasActiveChannels())
require.Equal(t, int32(0), peer.numActiveChans.Load())
// Simulate the loadActiveChannels active-channel path: the entry
// is stored and the counter is incremented in lockstep. After
// this, hasActiveChannels must flip to true because the peer now
// holds a non-pending channel.
activeID := lnwire.ChannelID{0x01}
peer.activeChannels.Store(activeID, &lnwallet.LightningChannel{})
peer.numActiveChans.Add(1)
require.True(t, peer.hasActiveChannels())
require.Equal(t, int32(1), peer.numActiveChans.Load())
// Simulate the loadActiveChannels pending path: the entry is
// stored as nil and the counter must NOT move. This is the
// invariant the onion message gate relies on — pending channels
// are cheap to open and get stuck, so they must not satisfy the
// Sybil-resistance gate on their own.
pendingID := lnwire.ChannelID{0x02}
peer.activeChannels.Store(pendingID, nil)
require.Equal(t, int32(1), peer.numActiveChans.Load())
require.True(t, peer.hasActiveChannels())
// handleRemovePendingChannel walks the pending-delete path. It
// uses LoadAndDelete and must skip the counter decrement when
// the previous value was nil (pending). If this invariant ever
// broke, the counter would underflow every time a pending
// channel was cancelled and hasActiveChannels would return the
// wrong answer until the next reconnect.
errChan := make(chan error, 1)
peer.handleRemovePendingChannel(&newChannelMsg{
channelID: pendingID,
err: errChan,
})
require.Equal(t, int32(1), peer.numActiveChans.Load())
require.True(t, peer.hasActiveChannels())
// The pending entry must have been removed from the map.
_, found := peer.activeChannels.Load(pendingID)
require.False(t, found)
// Drain the request error channel so the test leaves no loose
// ends. handleRemovePendingChannel closes the err chan via
// defer, so we expect a closed-channel receive here.
_, reqOk := <-errChan
require.False(t, reqOk)
// Finally, simulate WipeChannel's decrement path directly via
// LoadAndDelete. We cannot call WipeChannel in this
// dummy-config harness because it also calls
// p.cfg.Switch.RemoveLink, but the counter-maintenance half of
// WipeChannel is exactly the LoadAndDelete + conditional Add(-1)
// we exercise here.
prev, loaded := peer.activeChannels.LoadAndDelete(activeID)
require.True(t, loaded)
require.NotNil(t, prev)
peer.numActiveChans.Add(-1)
require.False(t, peer.hasActiveChannels())
require.Equal(t, int32(0), peer.numActiveChans.Load())
}
// TestRbfCoopCloseAllowed asserts that the per-channel RBF coop close
// predicate excludes aux channels (channel types carrying a tapscript root)
// even when both peers have negotiated the RBF coop close feature, while
// permitting it for all other channel types.
func TestRbfCoopCloseAllowed(t *testing.T) {
t.Parallel()
newPeer := func(local, remote *lnwire.RawFeatureVector) *Brontide {
return &Brontide{
cfg: Config{
Features: lnwire.NewFeatureVector(
local, lnwire.Features,
),
},
remoteFeatures: lnwire.NewFeatureVector(
remote, lnwire.Features,
),
}
}
var (
noBits = lnwire.NewRawFeatureVector()
rbfBit = lnwire.NewRawFeatureVector(
lnwire.RbfCoopCloseOptional,
)
stagingBit = lnwire.NewRawFeatureVector(
lnwire.RbfCoopCloseOptionalStaging,
)
overlayChan = chanstate.SimpleTaprootFeatureBit |
chanstate.TapscriptRootBit
)
tests := []struct {
name string
peer *Brontide
chanType chanstate.ChannelType
allowed bool
}{
{
name: "both signal, plain channel",
peer: newPeer(rbfBit, rbfBit),
chanType: chanstate.SingleFunderTweaklessBit,
allowed: true,
},
{
name: "both signal staging, plain channel",
peer: newPeer(stagingBit, stagingBit),
chanType: chanstate.SingleFunderTweaklessBit,
allowed: true,
},
{
name: "both signal, simple taproot channel",
peer: newPeer(rbfBit, rbfBit),
chanType: chanstate.SimpleTaprootFeatureBit,
allowed: true,
},
{
name: "both signal, aux (overlay) channel",
peer: newPeer(rbfBit, rbfBit),
chanType: overlayChan,
allowed: false,
},
{
name: "both signal staging, aux (overlay) channel",
peer: newPeer(stagingBit, stagingBit),
chanType: overlayChan,
allowed: false,
},
{
name: "only local signals, plain channel",
peer: newPeer(rbfBit, noBits),
chanType: chanstate.SingleFunderTweaklessBit,
allowed: false,
},
{
name: "neither signals, aux (overlay) channel",
peer: newPeer(noBits, noBits),
chanType: overlayChan,
allowed: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
require.Equal(
t, test.allowed,
test.peer.rbfCoopCloseAllowed(
test.chanType,
),
)
})
}
}