Merge pull request #3655 from carlaKC/fundingmgr-optionupfrontshutdown

Add Option Upfront Shutdown
This commit is contained in:
Olaoluwa Osuntokun 2019-12-04 21:05:24 -08:00 committed by GitHub
commit 183797f102
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 826 additions and 81 deletions

@ -64,3 +64,10 @@ linters:
issues:
# Only show newly introduced problems.
new-from-rev: 01f696afce2f9c0d4ed854edefa3846891d01d8a
exclude-rules:
# Exclude gosec from running for tests so that tests with weak randomness
# (math/rand) will pass the linter.
- path: _test\.go
linters:
- gosec

@ -1,6 +1,7 @@
package lnd
import (
"bytes"
"fmt"
"github.com/btcsuite/btcd/txscript"
@ -26,6 +27,12 @@ var (
// ErrInvalidState is returned when the closing state machine receives
// a message while it is in an unknown 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
@ -83,6 +90,9 @@ type chanCloseCfg struct {
// forward payments.
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
// machine should cease all progress and shutdown.
quit chan struct{}
@ -263,6 +273,40 @@ func (c *channelCloser) CloseRequest() *htlcswitch.ChanClose {
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.
// 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
@ -283,9 +327,18 @@ func (c *channelCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, b
"instead have %v", spew.Sdump(msg))
}
// Next, we'll note the other party's preference for their
// delivery address. We'll use this when we craft the closure
// transaction.
// 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
}
// 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
// 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))
}
// 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.
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)
}
})
}
}

@ -43,6 +43,16 @@ var (
// funding flow.
chanInfoKey = []byte("chan-info-key")
// localUpfrontShutdownKey can be accessed within the bucket for a channel
// (identified by its chanPoint). This key stores an optional upfront
// shutdown script for the local peer.
localUpfrontShutdownKey = []byte("local-upfront-shutdown-key")
// remoteUpfrontShutdownKey can be accessed within the bucket for a channel
// (identified by its chanPoint). This key stores an optional upfront
// shutdown script for the remote peer.
remoteUpfrontShutdownKey = []byte("remote-upfront-shutdown-key")
// chanCommitmentKey can be accessed within the sub-bucket for a
// particular channel. This key stores the up to date commitment state
// for a particular channel party. Appending a 0 to the end of this key
@ -551,6 +561,16 @@ type OpenChannel struct {
// method on the ChanType field.
FundingTxn *wire.MsgTx
// LocalShutdownScript is set to a pre-set script if the channel was opened
// by the local node with option_upfront_shutdown_script set. If the option
// was not set, the field is empty.
LocalShutdownScript lnwire.DeliveryAddress
// RemoteShutdownScript is set to a pre-set script if the channel was opened
// by the remote node with option_upfront_shutdown_script set. If the option
// was not set, the field is empty.
RemoteShutdownScript lnwire.DeliveryAddress
// TODO(roasbeef): eww
Db *DB
@ -2573,7 +2593,60 @@ func putChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error {
return err
}
return chanBucket.Put(chanInfoKey, w.Bytes())
if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil {
return err
}
// Finally, add optional shutdown scripts for the local and remote peer if
// they are present.
if err := putOptionalUpfrontShutdownScript(
chanBucket, localUpfrontShutdownKey, channel.LocalShutdownScript,
); err != nil {
return err
}
return putOptionalUpfrontShutdownScript(
chanBucket, remoteUpfrontShutdownKey, channel.RemoteShutdownScript,
)
}
// putOptionalUpfrontShutdownScript adds a shutdown script under the key
// provided if it has a non-zero length.
func putOptionalUpfrontShutdownScript(chanBucket *bbolt.Bucket, key []byte,
script []byte) error {
// If the script is empty, we do not need to add anything.
if len(script) == 0 {
return nil
}
var w bytes.Buffer
if err := WriteElement(&w, script); err != nil {
return err
}
return chanBucket.Put(key, w.Bytes())
}
// getOptionalUpfrontShutdownScript reads the shutdown script stored under the
// key provided if it is present. Upfront shutdown scripts are optional, so the
// function returns with no error if the key is not present.
func getOptionalUpfrontShutdownScript(chanBucket *bbolt.Bucket, key []byte,
script *lnwire.DeliveryAddress) error {
// Return early if the bucket does not exit, a shutdown script was not set.
bs := chanBucket.Get(key)
if bs == nil {
return nil
}
var tempScript []byte
r := bytes.NewReader(bs)
if err := ReadElement(r, &tempScript); err != nil {
return err
}
*script = tempScript
return nil
}
func serializeChanCommit(w io.Writer, c *ChannelCommitment) error {
@ -2696,7 +2769,16 @@ func fetchChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error {
channel.Packager = NewChannelPackager(channel.ShortChannelID)
return nil
// Finally, read the optional shutdown scripts.
if err := getOptionalUpfrontShutdownScript(
chanBucket, localUpfrontShutdownKey, &channel.LocalShutdownScript,
); err != nil {
return err
}
return getOptionalUpfrontShutdownScript(
chanBucket, remoteUpfrontShutdownKey, &channel.RemoteShutdownScript,
)
}
func deserializeChanCommit(r io.Reader) (ChannelCommitment, error) {

@ -345,6 +345,101 @@ func TestOpenChannelPutGetDelete(t *testing.T) {
}
}
// TestOptionalShutdown tests the reading and writing of channels with and
// without optional shutdown script fields.
func TestOptionalShutdown(t *testing.T) {
local := lnwire.DeliveryAddress([]byte("local shutdown script"))
remote := lnwire.DeliveryAddress([]byte("remote shutdown script"))
if _, err := rand.Read(remote); err != nil {
t.Fatalf("Could not create random script: %v", err)
}
tests := []struct {
name string
modifyChannel func(channel *OpenChannel)
expectedLocal lnwire.DeliveryAddress
expectedRemote lnwire.DeliveryAddress
}{
{
name: "no shutdown scripts",
modifyChannel: func(channel *OpenChannel) {},
},
{
name: "local shutdown script",
modifyChannel: func(channel *OpenChannel) {
channel.LocalShutdownScript = local
},
expectedLocal: local,
},
{
name: "remote shutdown script",
modifyChannel: func(channel *OpenChannel) {
channel.RemoteShutdownScript = remote
},
expectedRemote: remote,
},
{
name: "both scripts set",
modifyChannel: func(channel *OpenChannel) {
channel.LocalShutdownScript = local
channel.RemoteShutdownScript = remote
},
expectedLocal: local,
expectedRemote: remote,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
cdb, cleanUp, err := makeTestDB()
if err != nil {
t.Fatalf("unable to make test database: %v", err)
}
defer cleanUp()
// Create the test channel state, then add an additional fake HTLC
// before syncing to disk.
state, err := createTestChannelState(cdb)
if err != nil {
t.Fatalf("unable to create channel state: %v", err)
}
test.modifyChannel(state)
// Write channels to Db.
addr := &net.TCPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 18556,
}
if err := state.SyncPending(addr, 101); err != nil {
t.Fatalf("unable to save and serialize channel state: %v", err)
}
openChannels, err := cdb.FetchOpenChannels(state.IdentityPub)
if err != nil {
t.Fatalf("unable to fetch open channel: %v", err)
}
if len(openChannels) != 1 {
t.Fatalf("Expected one channel open, got: %v", len(openChannels))
}
if !bytes.Equal(openChannels[0].LocalShutdownScript, test.expectedLocal) {
t.Fatalf("Expected local: %x, got: %x", test.expectedLocal,
openChannels[0].LocalShutdownScript)
}
if !bytes.Equal(openChannels[0].RemoteShutdownScript, test.expectedRemote) {
t.Fatalf("Expected remote: %x, got: %x", test.expectedRemote,
openChannels[0].RemoteShutdownScript)
}
})
}
}
func assertCommitmentEqual(t *testing.T, a, b *ChannelCommitment) {
if !reflect.DeepEqual(a, b) {
_, _, line, _ := runtime.Caller(1)

@ -323,6 +323,8 @@ type config struct {
net tor.Net
EnableUpfrontShutdown bool `long:"enable-upfront-shutdown" description:"If true, option upfront shutdown script will be enabled. If peers that we open channels with support this feature, we will automatically set the script to which cooperative closes should be paid out to on channel open. This offers the partial protection of a channel peer diconnecting from us if cooperative close is attempted with a different script."`
Routing *routing.Conf `group:"routing" namespace:"routing"`
Workers *lncfg.Workers `group:"workers" namespace:"workers"`

@ -29,4 +29,8 @@ var defaultSetDesc = setDesc{
SetNodeAnn: {}, // N
SetLegacyGlobal: {},
},
lnwire.UpfrontShutdownScriptOptional: {
SetInit: {}, // I
SetNodeAnn: {}, // N
},
}

@ -9,6 +9,7 @@ import (
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/coreos/bbolt"
@ -93,6 +94,11 @@ var (
// blocks pass without confirmation.
ErrConfirmationTimeout = errors.New("timeout waiting for funding " +
"confirmation")
// errUpfrontShutdownScriptNotSupported is returned if an upfront shutdown
// script is set for a peer that does not support the feature bit.
errUpfrontShutdownScriptNotSupported = errors.New("peer does not support" +
"option upfront shutdown script")
)
// reservationWithCtx encapsulates a pending channel reservation. This wrapper
@ -1261,10 +1267,33 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) {
return
}
// Check whether the peer supports upfront shutdown, and get a new wallet
// address if our node is configured to set shutdown addresses by default.
// A nil address is set in place of user input, because this channel open
// was not initiated by the user.
shutdown, err := getUpfrontShutdownScript(
fmsg.peer, nil,
func() (lnwire.DeliveryAddress, error) {
addr, err := f.cfg.Wallet.NewAddress(lnwallet.WitnessPubKey, false)
if err != nil {
return nil, err
}
return txscript.PayToAddrScript(addr)
},
)
if err != nil {
f.failFundingFlow(
fmsg.peer, fmsg.msg.PendingChannelID,
fmt.Errorf("getUpfrontShutdownScript error: %v", err),
)
return
}
reservation.SetOurUpfrontShutdown(shutdown)
fndgLog.Infof("Requiring %v confirmations for pendingChan(%x): "+
"amt=%v, push_amt=%v, tweakless=%v", numConfsReq,
"amt=%v, push_amt=%v, tweakless=%v, upfrontShutdown=%x", numConfsReq,
fmsg.msg.PendingChannelID, amt, msg.PushAmount,
tweaklessCommitment)
tweaklessCommitment, msg.UpfrontShutdownScript)
// Generate our required constraints for the remote party.
remoteCsvDelay := f.cfg.RequiredRemoteDelay(amt)
@ -1324,6 +1353,7 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) {
PubKey: copyPubKey(msg.HtlcPoint),
},
},
UpfrontShutdown: msg.UpfrontShutdownScript,
}
err = reservation.ProcessSingleContribution(remoteContribution)
if err != nil {
@ -1341,20 +1371,21 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) {
// contribution in the next message of the workflow.
ourContribution := reservation.OurContribution()
fundingAccept := lnwire.AcceptChannel{
PendingChannelID: msg.PendingChannelID,
DustLimit: ourContribution.DustLimit,
MaxValueInFlight: maxValue,
ChannelReserve: chanReserve,
MinAcceptDepth: uint32(numConfsReq),
HtlcMinimum: minHtlc,
CsvDelay: remoteCsvDelay,
MaxAcceptedHTLCs: maxHtlcs,
FundingKey: ourContribution.MultiSigKey.PubKey,
RevocationPoint: ourContribution.RevocationBasePoint.PubKey,
PaymentPoint: ourContribution.PaymentBasePoint.PubKey,
DelayedPaymentPoint: ourContribution.DelayBasePoint.PubKey,
HtlcPoint: ourContribution.HtlcBasePoint.PubKey,
FirstCommitmentPoint: ourContribution.FirstCommitmentPoint,
PendingChannelID: msg.PendingChannelID,
DustLimit: ourContribution.DustLimit,
MaxValueInFlight: maxValue,
ChannelReserve: chanReserve,
MinAcceptDepth: uint32(numConfsReq),
HtlcMinimum: minHtlc,
CsvDelay: remoteCsvDelay,
MaxAcceptedHTLCs: maxHtlcs,
FundingKey: ourContribution.MultiSigKey.PubKey,
RevocationPoint: ourContribution.RevocationBasePoint.PubKey,
PaymentPoint: ourContribution.PaymentBasePoint.PubKey,
DelayedPaymentPoint: ourContribution.DelayBasePoint.PubKey,
HtlcPoint: ourContribution.HtlcBasePoint.PubKey,
FirstCommitmentPoint: ourContribution.FirstCommitmentPoint,
UpfrontShutdownScript: ourContribution.UpfrontShutdown,
}
if err := fmsg.peer.SendMessage(true, &fundingAccept); err != nil {
fndgLog.Errorf("unable to send funding response to peer: %v", err)
@ -1465,6 +1496,7 @@ func (f *fundingManager) handleFundingAccept(fmsg *fundingAcceptMsg) {
PubKey: copyPubKey(msg.HtlcPoint),
},
},
UpfrontShutdown: msg.UpfrontShutdownScript,
}
err = resCtx.reservation.ProcessContribution(remoteContribution)
if err != nil {
@ -2741,6 +2773,49 @@ func (f *fundingManager) initFundingWorkflow(peer lnpeer.Peer, req *openChanReq)
}
}
// getUpfrontShutdownScript takes a user provided script and a getScript
// function which can be used to generate an upfront shutdown script. If our
// peer does not support the feature, this function will error if a non-zero
// script was provided by the user, and return an empty script otherwise. If
// our peer does support the feature, we will return the user provided script
// if non-zero, or a freshly generated script if our node is configured to set
// upfront shutdown scripts automatically.
func getUpfrontShutdownScript(peer lnpeer.Peer, script lnwire.DeliveryAddress,
getScript func() (lnwire.DeliveryAddress, error)) (lnwire.DeliveryAddress,
error) {
// Check whether the remote peer supports upfront shutdown scripts.
remoteUpfrontShutdown := peer.RemoteFeatures().HasFeature(
lnwire.UpfrontShutdownScriptOptional,
)
// If the peer does not support upfront shutdown scripts, and one has been
// provided, return an error because the feature is not supported.
if !remoteUpfrontShutdown && len(script) != 0 {
return nil, errUpfrontShutdownScriptNotSupported
}
// If the peer does not support upfront shutdown, return an empty address.
if !remoteUpfrontShutdown {
return nil, nil
}
// If the user has provided an script and the peer supports the feature,
// return it. Note that user set scripts override the enable upfront
// shutdown flag.
if len(script) > 0 {
return script, nil
}
// If we do not have setting of upfront shutdown script enabled, return
// an empty script.
if !cfg.EnableUpfrontShutdown {
return nil, nil
}
return getScript()
}
// handleInitFundingMsg creates a channel reservation within the daemon's
// wallet, then sends a funding request to the remote peer kicking off the
// funding workflow.
@ -2826,6 +2901,27 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) {
return
}
// Check whether the peer supports upfront shutdown, and get an address which
// should be used (either a user specified address or a new address from the
// wallet if our node is configured to set shutdown address by default).
shutdown, err := getUpfrontShutdownScript(
msg.peer, msg.openChanReq.shutdownScript,
func() (lnwire.DeliveryAddress, error) {
addr, err := f.cfg.Wallet.NewAddress(lnwallet.WitnessPubKey, false)
if err != nil {
return nil, err
}
return txscript.PayToAddrScript(addr)
},
)
if err != nil {
msg.err <- err
return
}
// Set our upfront shutdown address in the existing reservation.
reservation.SetOurUpfrontShutdown(shutdown)
// Now that we have successfully reserved funds for this channel in the
// wallet, we can fetch the final channel capacity. This is done at
// this point since the final capacity might change in case of
@ -2886,24 +2982,25 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) {
"tweakless=%v", msg.peer.Address(), chanID, tweaklessCommitment)
fundingOpen := lnwire.OpenChannel{
ChainHash: *f.cfg.Wallet.Cfg.NetParams.GenesisHash,
PendingChannelID: chanID,
FundingAmount: capacity,
PushAmount: msg.pushAmt,
DustLimit: ourContribution.DustLimit,
MaxValueInFlight: maxValue,
ChannelReserve: chanReserve,
HtlcMinimum: minHtlc,
FeePerKiloWeight: uint32(commitFeePerKw),
CsvDelay: remoteCsvDelay,
MaxAcceptedHTLCs: maxHtlcs,
FundingKey: ourContribution.MultiSigKey.PubKey,
RevocationPoint: ourContribution.RevocationBasePoint.PubKey,
PaymentPoint: ourContribution.PaymentBasePoint.PubKey,
HtlcPoint: ourContribution.HtlcBasePoint.PubKey,
DelayedPaymentPoint: ourContribution.DelayBasePoint.PubKey,
FirstCommitmentPoint: ourContribution.FirstCommitmentPoint,
ChannelFlags: channelFlags,
ChainHash: *f.cfg.Wallet.Cfg.NetParams.GenesisHash,
PendingChannelID: chanID,
FundingAmount: capacity,
PushAmount: msg.pushAmt,
DustLimit: ourContribution.DustLimit,
MaxValueInFlight: maxValue,
ChannelReserve: chanReserve,
HtlcMinimum: minHtlc,
FeePerKiloWeight: uint32(commitFeePerKw),
CsvDelay: remoteCsvDelay,
MaxAcceptedHTLCs: maxHtlcs,
FundingKey: ourContribution.MultiSigKey.PubKey,
RevocationPoint: ourContribution.RevocationBasePoint.PubKey,
PaymentPoint: ourContribution.PaymentBasePoint.PubKey,
HtlcPoint: ourContribution.HtlcBasePoint.PubKey,
DelayedPaymentPoint: ourContribution.DelayBasePoint.PubKey,
FirstCommitmentPoint: ourContribution.FirstCommitmentPoint,
ChannelFlags: channelFlags,
UpfrontShutdownScript: shutdown,
}
if err := msg.peer.SendMessage(true, &fundingOpen); err != nil {
e := fmt.Errorf("Unable to send funding request message: %v",

@ -149,6 +149,7 @@ type testNode struct {
mockNotifier *mockNotifier
testDir string
shutdownChannel chan struct{}
remoteFeatures []lnwire.FeatureBit
remotePeer *testNode
sendMessage func(lnwire.Message) error
@ -189,7 +190,9 @@ func (n *testNode) LocalFeatures() *lnwire.FeatureVector {
}
func (n *testNode) RemoteFeatures() *lnwire.FeatureVector {
return lnwire.NewFeatureVector(nil, nil)
return lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(n.remoteFeatures...), nil,
)
}
func (n *testNode) AddNewChannel(channel *channeldb.OpenChannel,
@ -2948,3 +2951,92 @@ func TestFundingManagerFundAll(t *testing.T) {
}
}
}
// TestGetUpfrontShutdown tests different combinations of inputs for getting a
// shutdown script. It varies whether the peer has the feature set, whether
// the user has provided a script and our local configuration to test that
// GetUpfrontShutdownScript returns the expected outcome.
func TestGetUpfrontShutdownScript(t *testing.T) {
upfrontScript := []byte("upfront script")
generatedScript := []byte("generated script")
getScript := func() (lnwire.DeliveryAddress, error) {
return generatedScript, nil
}
tests := []struct {
name string
getScript func() (lnwire.DeliveryAddress, error)
upfrontScript lnwire.DeliveryAddress
peerEnabled bool
localEnabled bool
expectedScript lnwire.DeliveryAddress
expectedErr error
}{
{
name: "peer disabled, no shutdown",
getScript: getScript,
},
{
name: "peer disabled, upfront provided",
upfrontScript: upfrontScript,
expectedErr: errUpfrontShutdownScriptNotSupported,
},
{
name: "peer enabled, upfront provided",
upfrontScript: upfrontScript,
peerEnabled: true,
expectedScript: upfrontScript,
},
{
name: "peer enabled, local disabled",
peerEnabled: true,
},
{
name: "local enabled, no upfront script",
getScript: getScript,
peerEnabled: true,
localEnabled: true,
expectedScript: generatedScript,
},
{
name: "local enabled, upfront script",
peerEnabled: true,
upfrontScript: upfrontScript,
localEnabled: true,
expectedScript: upfrontScript,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
var mockPeer testNode
// If the remote peer in the test should support upfront shutdown,
// add the feature bit.
if test.peerEnabled {
mockPeer.remoteFeatures = []lnwire.FeatureBit{
lnwire.UpfrontShutdownScriptOptional,
}
}
// Set the command line option in config as needed.
cfg = &config{EnableUpfrontShutdown: test.localEnabled}
addr, err := getUpfrontShutdownScript(
&mockPeer, test.upfrontScript, test.getScript,
)
if err != test.expectedErr {
t.Fatalf("got: %v, expected error: %v", err, test.expectedErr)
}
if !bytes.Equal(addr, test.expectedScript) {
t.Fatalf("expected address: %x, got: %x",
test.expectedScript, addr)
}
})
}
}

@ -4918,6 +4918,18 @@ func (lc *LightningChannel) ShortChanID() lnwire.ShortChannelID {
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 lc.channelState.LocalShutdownScript
}
// 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 lc.channelState.RemoteShutdownScript
}
// 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
// HTLC is being applied to their commitment transaction or ours.

@ -44,6 +44,10 @@ type ChannelContribution struct {
// such as the min HTLC, and also all the keys which will be used for
// the duration of the channel.
*channeldb.ChannelConfig
// UpfrontShutdown is an optional address to which the channel should be
// paid out to on cooperative close.
UpfrontShutdown lnwire.DeliveryAddress
}
// toChanConfig returns the raw channel configuration generated by a node's
@ -531,6 +535,14 @@ func (r *ChannelReservation) FundingOutpoint() *wire.OutPoint {
return &r.partialState.FundingOutpoint
}
// SetOurUpfrontShutdown sets the upfront shutdown address on our contribution.
func (r *ChannelReservation) SetOurUpfrontShutdown(shutdown lnwire.DeliveryAddress) {
r.Lock()
defer r.Unlock()
r.ourContribution.UpfrontShutdown = shutdown
}
// Capacity returns the channel capacity for this reservation.
func (r *ChannelReservation) Capacity() btcutil.Amount {
r.RLock()

@ -1217,6 +1217,13 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
// rebroadcast on startup in case we fail.
res.partialState.FundingTxn = fundingTx
// Set optional upfront shutdown scripts on the channel state so that they
// are persisted. These values may be nil.
res.partialState.LocalShutdownScript =
res.ourContribution.UpfrontShutdown
res.partialState.RemoteShutdownScript =
res.theirContribution.UpfrontShutdown
// Add the complete funding transaction to the DB, in its open bucket
// which will be used for the lifetime of this channel.
nodeAddr := res.nodeAddr
@ -1376,6 +1383,13 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
return
}
// Set optional upfront shutdown scripts on the channel state so that they
// are persisted. These values may be nil.
chanState.LocalShutdownScript =
pendingReservation.ourContribution.UpfrontShutdown
chanState.RemoteShutdownScript =
pendingReservation.theirContribution.UpfrontShutdown
// Add the complete funding transaction to the DB, in it's open bucket
// which will be used for the lifetime of this channel.
chanState.LocalChanCfg = pendingReservation.ourContribution.toChanConfig()

@ -86,6 +86,12 @@ type AcceptChannel struct {
// base point in order to derive the revocation keys that are placed
// within the commitment transaction of the sender.
FirstCommitmentPoint *btcec.PublicKey
// UpfrontShutdownScript is the script to which the channel funds should
// be paid when mutually closing the channel. This field is optional, and
// and has a length prefix, so a zero will be written if it is not set
// and its length followed by the script will be written if it is set.
UpfrontShutdownScript DeliveryAddress
}
// A compile time check to ensure AcceptChannel implements the lnwire.Message
@ -113,6 +119,7 @@ func (a *AcceptChannel) Encode(w io.Writer, pver uint32) error {
a.DelayedPaymentPoint,
a.HtlcPoint,
a.FirstCommitmentPoint,
a.UpfrontShutdownScript,
)
}
@ -122,7 +129,8 @@ func (a *AcceptChannel) Encode(w io.Writer, pver uint32) error {
//
// This is part of the lnwire.Message interface.
func (a *AcceptChannel) Decode(r io.Reader, pver uint32) error {
return ReadElements(r,
// Read all the mandatory fields in the accept message.
err := ReadElements(r,
a.PendingChannelID[:],
&a.DustLimit,
&a.MaxValueInFlight,
@ -138,6 +146,17 @@ func (a *AcceptChannel) Decode(r io.Reader, pver uint32) error {
&a.HtlcPoint,
&a.FirstCommitmentPoint,
)
if err != nil {
return err
}
// Check for the optional upfront shutdown script field. If it is not there,
// silence the EOF error.
err = ReadElement(r, &a.UpfrontShutdownScript)
if err != nil && err != io.EOF {
return err
}
return nil
}
// MsgType returns the MessageType code which uniquely identifies this message
@ -154,5 +173,10 @@ func (a *AcceptChannel) MsgType() MessageType {
// This is part of the lnwire.Message interface.
func (a *AcceptChannel) MaxPayloadLength(uint32) uint32 {
// 32 + (8 * 4) + (4 * 1) + (2 * 2) + (33 * 6)
return 270
var length uint32 = 270 // base length
// Upfront shutdown script max length.
length += 2 + deliveryAddressMaxSize
return length
}

@ -0,0 +1,71 @@
package lnwire
import (
"bytes"
"testing"
"github.com/btcsuite/btcd/btcec"
)
// TestDecodeAcceptChannel tests decoding of an accept channel wire message with
// and without the optional upfront shutdown script.
func TestDecodeAcceptChannel(t *testing.T) {
tests := []struct {
name string
shutdownScript DeliveryAddress
}{
{
name: "no upfront shutdown script",
shutdownScript: nil,
},
{
name: "empty byte array",
shutdownScript: []byte{},
},
{
name: "upfront shutdown script set",
shutdownScript: []byte("example"),
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
priv, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
t.Fatalf("cannot create privkey: %v", err)
}
pk := priv.PubKey()
encoded := &AcceptChannel{
PendingChannelID: [32]byte{},
FundingKey: pk,
RevocationPoint: pk,
PaymentPoint: pk,
DelayedPaymentPoint: pk,
HtlcPoint: pk,
FirstCommitmentPoint: pk,
UpfrontShutdownScript: test.shutdownScript,
}
buf := &bytes.Buffer{}
if _, err := WriteMessage(buf, encoded, 0); err != nil {
t.Fatalf("cannot write message: %v", err)
}
msg, err := ReadMessage(buf, 0)
if err != nil {
t.Fatalf("cannot read message: %v", err)
}
decoded := msg.(*AcceptChannel)
if !bytes.Equal(
decoded.UpfrontShutdownScript, encoded.UpfrontShutdownScript,
) {
t.Fatalf("decoded script: %x does not equal encoded script: %x",
decoded.UpfrontShutdownScript, encoded.UpfrontShutdownScript)
}
})
}
}

@ -42,6 +42,16 @@ const (
// connection is established.
InitialRoutingSync FeatureBit = 3
// UpfrontShutdownScriptRequired is a feature bit which indicates that a
// peer *requires* that the remote peer accept an upfront shutdown script to
// which payout is enforced on cooperative closes.
UpfrontShutdownScriptRequired FeatureBit = 4
// UpfrontShutdownScriptOptional is an optional feature bit which indicates
// that the peer will accept an upfront shutdown script to which payout is
// enforced on cooperative closes.
UpfrontShutdownScriptOptional FeatureBit = 5
// GossipQueriesRequired is a feature bit that indicates that the
// receiving peer MUST know of the set of features that allows nodes to
// more efficiently query the network view of peers on the network for
@ -89,15 +99,17 @@ const (
// feature bits must be assigned a name in this mapping, and feature bit pairs
// must be assigned together for correct behavior.
var Features = map[FeatureBit]string{
DataLossProtectRequired: "data-loss-protect",
DataLossProtectOptional: "data-loss-protect",
InitialRoutingSync: "initial-routing-sync",
GossipQueriesRequired: "gossip-queries",
GossipQueriesOptional: "gossip-queries",
TLVOnionPayloadRequired: "tlv-onion",
TLVOnionPayloadOptional: "tlv-onion",
StaticRemoteKeyOptional: "static-remote-key",
StaticRemoteKeyRequired: "static-remote-key",
DataLossProtectRequired: "data-loss-protect",
DataLossProtectOptional: "data-loss-protect",
InitialRoutingSync: "initial-routing-sync",
UpfrontShutdownScriptRequired: "upfront-shutdown-script",
UpfrontShutdownScriptOptional: "upfront-shutdown-script",
GossipQueriesRequired: "gossip-queries",
GossipQueriesOptional: "gossip-queries",
TLVOnionPayloadRequired: "tlv-onion",
TLVOnionPayloadOptional: "tlv-onion",
StaticRemoteKeyOptional: "static-remote-key",
StaticRemoteKeyRequired: "static-remote-key",
}
// RawFeatureVector represents a set of feature bits as defined in BOLT-09. A

@ -816,8 +816,8 @@ func ReadElement(r io.Reader, element interface{}) error {
}
length := binary.BigEndian.Uint16(addrLen[:])
var addrBytes [34]byte
if length > 34 {
var addrBytes [deliveryAddressMaxSize]byte
if length > deliveryAddressMaxSize {
return fmt.Errorf("Cannot read %d bytes into addrBytes", length)
}
if _, err = io.ReadFull(r, addrBytes[:length]); err != nil {

@ -67,6 +67,15 @@ func randRawKey() ([33]byte, error) {
return n, nil
}
func randDeliveryAddress(r *rand.Rand) (DeliveryAddress, error) {
// Generate size minimum one. Empty scripts should be tested specifically.
size := r.Intn(deliveryAddressMaxSize) + 1
da := DeliveryAddress(make([]byte, size))
_, err := r.Read(da)
return da, err
}
func randRawFeatureVector(r *rand.Rand) *RawFeatureVector {
featureVec := NewRawFeatureVector()
for i := 0; i < 10000; i++ {
@ -241,6 +250,9 @@ func TestEmptyMessageUnknownType(t *testing.T) {
// TestLightningWireProtocol uses the testing/quick package to create a series
// of fuzz tests to attempt to break a primary scenario which is implemented as
// property based testing scenario.
//
// Debug help: when the message payload can reach a size larger than the return
// value of MaxPayloadLength, the test can panic without a helpful message.
func TestLightningWireProtocol(t *testing.T) {
t.Parallel()
@ -353,6 +365,17 @@ func TestLightningWireProtocol(t *testing.T) {
return
}
// 1/2 chance empty upfront shutdown script.
if r.Intn(2) == 0 {
req.UpfrontShutdownScript, err = randDeliveryAddress(r)
if err != nil {
t.Fatalf("unable to generate delivery address: %v", err)
return
}
} else {
req.UpfrontShutdownScript = []byte{}
}
v[0] = reflect.ValueOf(req)
},
MsgAcceptChannel: func(v []reflect.Value, r *rand.Rand) {
@ -403,6 +426,17 @@ func TestLightningWireProtocol(t *testing.T) {
return
}
// 1/2 chance empty upfront shutdown script.
if r.Intn(2) == 0 {
req.UpfrontShutdownScript, err = randDeliveryAddress(r)
if err != nil {
t.Fatalf("unable to generate delivery address: %v", err)
return
}
} else {
req.UpfrontShutdownScript = []byte{}
}
v[0] = reflect.ValueOf(req)
},
MsgFundingCreated: func(v []reflect.Value, r *rand.Rand) {

@ -122,6 +122,12 @@ type OpenChannel struct {
// Currently, the least significant bit of this bit field indicates the
// initiator of the channel wishes to advertise this channel publicly.
ChannelFlags FundingFlag
// UpfrontShutdownScript is the script to which the channel funds should
// be paid when mutually closing the channel. This field is optional, and
// and has a length prefix, so a zero will be written if it is not set
// and its length followed by the script will be written if it is set.
UpfrontShutdownScript DeliveryAddress
}
// A compile time check to ensure OpenChannel implements the lnwire.Message
@ -153,6 +159,7 @@ func (o *OpenChannel) Encode(w io.Writer, pver uint32) error {
o.HtlcPoint,
o.FirstCommitmentPoint,
o.ChannelFlags,
o.UpfrontShutdownScript,
)
}
@ -162,7 +169,7 @@ func (o *OpenChannel) Encode(w io.Writer, pver uint32) error {
//
// This is part of the lnwire.Message interface.
func (o *OpenChannel) Decode(r io.Reader, pver uint32) error {
return ReadElements(r,
if err := ReadElements(r,
o.ChainHash[:],
o.PendingChannelID[:],
&o.FundingAmount,
@ -181,7 +188,18 @@ func (o *OpenChannel) Decode(r io.Reader, pver uint32) error {
&o.HtlcPoint,
&o.FirstCommitmentPoint,
&o.ChannelFlags,
)
); err != nil {
return err
}
// Check for the optional upfront shutdown script field. If it is not there,
// silence the EOF error.
err := ReadElement(r, &o.UpfrontShutdownScript)
if err != nil && err != io.EOF {
return err
}
return nil
}
// MsgType returns the MessageType code which uniquely identifies this message
@ -198,5 +216,10 @@ func (o *OpenChannel) MsgType() MessageType {
// This is part of the lnwire.Message interface.
func (o *OpenChannel) MaxPayloadLength(uint32) uint32 {
// (32 * 2) + (8 * 6) + (4 * 1) + (2 * 2) + (33 * 6) + 1
return 319
var length uint32 = 319 // base length
// Upfront shutdown script max length.
length += 2 + deliveryAddressMaxSize
return length
}

@ -22,6 +22,15 @@ type Shutdown struct {
// p2wpkh.
type DeliveryAddress []byte
// deliveryAddressMaxSize is the maximum expected size in bytes of a
// DeliveryAddress based on the types of scripts we know.
// Following are the known scripts and their sizes in bytes.
// - pay to witness script hash: 34
// - pay to pubkey hash: 25
// - pay to script hash: 22
// - pay to witness pubkey hash: 22.
const deliveryAddressMaxSize = 34
// NewShutdown creates a new Shutdown message.
func NewShutdown(cid ChannelID, addr DeliveryAddress) *Shutdown {
return &Shutdown{
@ -71,11 +80,8 @@ func (s *Shutdown) MaxPayloadLength(pver uint32) uint32 {
// Len - 2 bytes
length += 2
// ScriptPubKey - 34 bytes for pay to witness script hash
length += 34
// NOTE: pay to pubkey hash is 25 bytes, pay to script hash is 22
// bytes, and pay to witness pubkey hash is 22 bytes in length.
// ScriptPubKey - maximum delivery address size.
length += deliveryAddressMaxSize
return length
}

54
peer.go

@ -2074,13 +2074,18 @@ func (p *peer) fetchActiveChanCloser(chanID lnwire.ChannelID) (*channelCloser, e
"channel w/ active htlcs")
}
// We'll create a valid closing state machine in order to
// respond to the initiated cooperative channel closure.
deliveryAddr, err := p.genDeliveryScript()
if err != nil {
peerLog.Errorf("unable to gen delivery script: %v", err)
return nil, fmt.Errorf("close addr unavailable")
// We'll create a valid closing state machine in order to respond to the
// initiated cooperative channel closure. First, we set the delivery
// 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 {
peerLog.Errorf("unable to gen delivery script: %v", err)
return nil, fmt.Errorf("close addr unavailable")
}
}
// In order to begin fee negotiations, we'll first compute our
@ -2105,9 +2110,12 @@ func (p *peer) fetchActiveChanCloser(chanID lnwire.ChannelID) (*channelCloser, e
unregisterChannel: p.server.htlcSwitch.RemoveLink,
broadcastTx: p.server.cc.wallet.PublishTransaction,
disableChannel: p.server.chanStatusMgr.RequestDisable,
quit: p.quit,
disconnect: func() error {
return p.server.DisconnectPeer(p.IdentityKey())
},
quit: p.quit,
},
deliveryAddr,
deliveryScript,
feePerKw,
uint32(startingHeight),
nil,
@ -2140,14 +2148,19 @@ func (p *peer) handleLocalCloseReq(req *htlcswitch.ChanClose) {
// out this channel on-chain, so we execute the cooperative channel
// closure workflow.
case htlcswitch.CloseRegular:
// First, we'll fetch a fresh delivery address that we'll use
// to send the funds to in the case of a successful
// negotiation.
deliveryAddr, err := p.genDeliveryScript()
if err != nil {
peerLog.Errorf(err.Error())
req.Err <- err
return
// First, we'll fetch a delivery script that we'll use to send the
// funds to in the case of a successful negotiation. 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 {
peerLog.Errorf(err.Error())
req.Err <- err
return
}
}
// Next, we'll create a new channel closer state machine to
@ -2165,9 +2178,12 @@ func (p *peer) handleLocalCloseReq(req *htlcswitch.ChanClose) {
unregisterChannel: p.server.htlcSwitch.RemoveLink,
broadcastTx: p.server.cc.wallet.PublishTransaction,
disableChannel: p.server.chanStatusMgr.RequestDisable,
quit: p.quit,
disconnect: func() error {
return p.server.DisconnectPeer(p.IdentityKey())
},
quit: p.quit,
},
deliveryAddr,
deliveryScript,
req.TargetFeePerKw,
uint32(startingHeight),
req,

@ -3088,6 +3088,10 @@ type openChanReq struct {
// output selected to fund the channel should satisfy.
minConfs int32
// shutdownScript is an optional upfront shutdown script for the channel.
// This value is optional, so may be nil.
shutdownScript lnwire.DeliveryAddress
// TODO(roasbeef): add ability to specify channel constraints as well
updates chan *lnrpc.OpenStatusUpdate