multi: Set and check upfront shutdown address on close

This commit sets our close addresss to the address
specified by option upfront shutdown, if specified,
and disconnects from peers that fail to provide
their upfront shutdown address for coopertaive closes
of channels that were opened with the option set.
This commit is contained in:
carla 2019-12-03 11:38:29 +02:00
parent cc7accea3d
commit f86d63144c
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91
4 changed files with 189 additions and 23 deletions

@ -1,6 +1,7 @@
package lnd package lnd
import ( import (
"bytes"
"fmt" "fmt"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
@ -26,6 +27,12 @@ var (
// ErrInvalidState is returned when the closing state machine receives // ErrInvalidState is returned when the closing state machine receives
// a message while it is in an unknown state. // a message while it is in an unknown state.
ErrInvalidState = fmt.Errorf("invalid state") ErrInvalidState = fmt.Errorf("invalid state")
// errUpfrontShutdownScriptMismatch is returned when our peer sends us a
// shutdown message with a script that does not match the upfront shutdown
// script previously set.
errUpfrontShutdownScriptMismatch = fmt.Errorf("peer's shutdown " +
"script does not match upfront shutdown script")
) )
// closeState represents all the possible states the channel closer state // closeState represents all the possible states the channel closer state
@ -83,6 +90,9 @@ type chanCloseCfg struct {
// forward payments. // forward payments.
disableChannel func(wire.OutPoint) error disableChannel func(wire.OutPoint) error
// disconnect will disconnect from the remote peer in this close.
disconnect func() error
// quit is a channel that should be sent upon in the occasion the state // quit is a channel that should be sent upon in the occasion the state
// machine should cease all progress and shutdown. // machine should cease all progress and shutdown.
quit chan struct{} quit chan struct{}
@ -263,6 +273,40 @@ func (c *channelCloser) CloseRequest() *htlcswitch.ChanClose {
return c.closeReq return c.closeReq
} }
// maybeMatchScript attempts to match the script provided in our peer's
// shutdown message with the upfront shutdown script we have on record.
// If no upfront shutdown script was set, we do not need to enforce option
// upfront shutdown, so the function returns early. If an upfront script is
// set, we check whether it matches the script provided by our peer. If they
// do not match, we use the disconnect function provided to disconnect from
// the peer.
func maybeMatchScript(disconnect func() error,
upfrontScript, peerScript lnwire.DeliveryAddress) error {
// If no upfront shutdown script was set, return early because we do not
// need to enforce closure to a specific script.
if len(upfrontScript) == 0 {
return nil
}
// If an upfront shutdown script was provided, disconnect from the peer, as
// per BOLT 2, and return an error.
if !bytes.Equal(upfrontScript, peerScript) {
peerLog.Warnf("peer's script: %x does not match upfront "+
"shutdown script: %x", peerScript, upfrontScript)
// Disconnect from the peer because they have violated option upfront
// shutdown.
if err := disconnect(); err != nil {
return err
}
return errUpfrontShutdownScriptMismatch
}
return nil
}
// ProcessCloseMsg attempts to process the next message in the closing series. // ProcessCloseMsg attempts to process the next message in the closing series.
// This method will update the state accordingly and return two primary values: // This method will update the state accordingly and return two primary values:
// the next set of messages to be sent, and a bool indicating if the fee // the next set of messages to be sent, and a bool indicating if the fee
@ -283,9 +327,18 @@ func (c *channelCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, b
"instead have %v", spew.Sdump(msg)) "instead have %v", spew.Sdump(msg))
} }
// Next, we'll note the other party's preference for their // If the remote node opened the channel with option upfront shutdown
// delivery address. We'll use this when we craft the closure // script, check that the script they provided matches.
// transaction. if err := maybeMatchScript(
c.cfg.disconnect, c.cfg.channel.RemoteUpfrontShutdownScript(),
shutDownMsg.Address,
); err != nil {
return nil, false, err
}
// Once we have checked that the other party has not violated option
// upfront shutdown we set their preference for delivery address. We'll
// use this when we craft the closure transaction.
c.remoteDeliveryScript = shutDownMsg.Address c.remoteDeliveryScript = shutDownMsg.Address
// We'll generate a shutdown message of our own to send across // We'll generate a shutdown message of our own to send across
@ -333,7 +386,16 @@ func (c *channelCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, b
"instead have %v", spew.Sdump(msg)) "instead have %v", spew.Sdump(msg))
} }
// Now that we know this is a valid shutdown message, we'll // If the remote node opened the channel with option upfront shutdown
// script, check that the script they provided matches.
if err := maybeMatchScript(
c.cfg.disconnect, c.cfg.channel.RemoteUpfrontShutdownScript(),
shutDownMsg.Address,
); err != nil {
return nil, false, err
}
// Now that we know this is a valid shutdown message and address, we'll
// record their preferred delivery closing script. // record their preferred delivery closing script.
c.remoteDeliveryScript = shutDownMsg.Address c.remoteDeliveryScript = shutDownMsg.Address

76
chancloser_test.go Normal file

@ -0,0 +1,76 @@
package lnd
import (
"crypto/rand"
"testing"
"github.com/lightningnetwork/lnd/lnwire"
)
// randDeliveryAddress generates a random delivery address for testing.
func randDeliveryAddress(t *testing.T) lnwire.DeliveryAddress {
// Generate an address of maximum length.
da := lnwire.DeliveryAddress(make([]byte, 34))
_, err := rand.Read(da)
if err != nil {
t.Fatalf("cannot generate random address: %v", err)
}
return da
}
// 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) {
addr1 := randDeliveryAddress(t)
addr2 := randDeliveryAddress(t)
tests := []struct {
name string
shutdownScript lnwire.DeliveryAddress
upfrontScript lnwire.DeliveryAddress
expectedErr error
}{
{
name: "no upfront shutdown set, script ok",
shutdownScript: addr1,
upfrontScript: []byte{},
expectedErr: nil,
},
{
name: "upfront shutdown set, script ok",
shutdownScript: addr1,
upfrontScript: addr1,
expectedErr: nil,
},
{
name: "upfront shutdown set, script not ok",
shutdownScript: addr1,
upfrontScript: addr2,
expectedErr: errUpfrontShutdownScriptMismatch,
},
{
name: "nil shutdown and empty upfront",
shutdownScript: nil,
upfrontScript: []byte{},
expectedErr: nil,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
err := maybeMatchScript(
func() error { return nil }, test.upfrontScript,
test.shutdownScript,
)
if err != test.expectedErr {
t.Fatalf("Error: %v, expected error: %v", err, test.expectedErr)
}
})
}
}

@ -4918,6 +4918,18 @@ func (lc *LightningChannel) ShortChanID() lnwire.ShortChannelID {
return lc.channelState.ShortChanID() return lc.channelState.ShortChanID()
} }
// LocalUpfrontShutdownScript returns the local upfront shutdown script for the
// channel. If it was not set, an empty byte array is returned.
func (lc *LightningChannel) LocalUpfrontShutdownScript() lnwire.DeliveryAddress {
return nil
}
// RemoteUpfrontShutdownScript returns the remote upfront shutdown script for the
// channel. If it was not set, an empty byte array is returned.
func (lc *LightningChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress {
return nil
}
// genHtlcScript generates the proper P2WSH public key scripts for the HTLC // genHtlcScript generates the proper P2WSH public key scripts for the HTLC
// output modified by two-bits denoting if this is an incoming HTLC, and if the // output modified by two-bits denoting if this is an incoming HTLC, and if the
// HTLC is being applied to their commitment transaction or ours. // HTLC is being applied to their commitment transaction or ours.

36
peer.go

@ -2068,14 +2068,19 @@ func (p *peer) fetchActiveChanCloser(chanID lnwire.ChannelID) (*channelCloser, e
"channel w/ active htlcs") "channel w/ active htlcs")
} }
// We'll create a valid closing state machine in order to // We'll create a valid closing state machine in order to respond to the
// respond to the initiated cooperative channel closure. // initiated cooperative channel closure. First, we set the delivery
deliveryAddr, err := p.genDeliveryScript() // script that our funds will be paid out to. If an upfront shutdown script
// was set, we will use it. Otherwise, we get a fresh delivery script.
deliveryScript := channel.LocalUpfrontShutdownScript()
if len(deliveryScript) == 0 {
var err error
deliveryScript, err = p.genDeliveryScript()
if err != nil { if err != nil {
peerLog.Errorf("unable to gen delivery script: %v", err) peerLog.Errorf("unable to gen delivery script: %v", err)
return nil, fmt.Errorf("close addr unavailable") return nil, fmt.Errorf("close addr unavailable")
} }
}
// In order to begin fee negotiations, we'll first compute our // In order to begin fee negotiations, we'll first compute our
// target ideal fee-per-kw. We'll set this to a lax value, as // target ideal fee-per-kw. We'll set this to a lax value, as
@ -2099,9 +2104,12 @@ func (p *peer) fetchActiveChanCloser(chanID lnwire.ChannelID) (*channelCloser, e
unregisterChannel: p.server.htlcSwitch.RemoveLink, unregisterChannel: p.server.htlcSwitch.RemoveLink,
broadcastTx: p.server.cc.wallet.PublishTransaction, broadcastTx: p.server.cc.wallet.PublishTransaction,
disableChannel: p.server.chanStatusMgr.RequestDisable, disableChannel: p.server.chanStatusMgr.RequestDisable,
disconnect: func() error {
return p.server.DisconnectPeer(p.IdentityKey())
},
quit: p.quit, quit: p.quit,
}, },
deliveryAddr, deliveryScript,
feePerKw, feePerKw,
uint32(startingHeight), uint32(startingHeight),
nil, nil,
@ -2134,15 +2142,20 @@ func (p *peer) handleLocalCloseReq(req *htlcswitch.ChanClose) {
// out this channel on-chain, so we execute the cooperative channel // out this channel on-chain, so we execute the cooperative channel
// closure workflow. // closure workflow.
case htlcswitch.CloseRegular: case htlcswitch.CloseRegular:
// First, we'll fetch a fresh delivery address that we'll use // First, we'll fetch a delivery script that we'll use to send the
// to send the funds to in the case of a successful // funds to in the case of a successful negotiation. If an upfront
// negotiation. // shutdown script was set, we will use it. Otherwise, we get a fresh
deliveryAddr, err := p.genDeliveryScript() // delivery script.
deliveryScript := channel.LocalUpfrontShutdownScript()
if len(deliveryScript) == 0 {
var err error
deliveryScript, err = p.genDeliveryScript()
if err != nil { if err != nil {
peerLog.Errorf(err.Error()) peerLog.Errorf(err.Error())
req.Err <- err req.Err <- err
return return
} }
}
// Next, we'll create a new channel closer state machine to // Next, we'll create a new channel closer state machine to
// handle the close negotiation. // handle the close negotiation.
@ -2159,9 +2172,12 @@ func (p *peer) handleLocalCloseReq(req *htlcswitch.ChanClose) {
unregisterChannel: p.server.htlcSwitch.RemoveLink, unregisterChannel: p.server.htlcSwitch.RemoveLink,
broadcastTx: p.server.cc.wallet.PublishTransaction, broadcastTx: p.server.cc.wallet.PublishTransaction,
disableChannel: p.server.chanStatusMgr.RequestDisable, disableChannel: p.server.chanStatusMgr.RequestDisable,
disconnect: func() error {
return p.server.DisconnectPeer(p.IdentityKey())
},
quit: p.quit, quit: p.quit,
}, },
deliveryAddr, deliveryScript,
req.TargetFeePerKw, req.TargetFeePerKw,
uint32(startingHeight), uint32(startingHeight),
req, req,