From 2fb71727252ee6a28b3576c74fc274dd5bcbcd36 Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Tue, 3 Dec 2019 11:38:21 +0200 Subject: [PATCH 1/5] lnwire: Add upfront shutdown messages and feature bit This commit adds the feature bit and additional fields required in `open_channel` and `accept_channel` wire messages for `option_upfront_shutdown_script`. --- .golangci.yml | 7 ++++ lnwire/accept_channel.go | 28 +++++++++++++- lnwire/accept_channel_test.go | 71 +++++++++++++++++++++++++++++++++++ lnwire/features.go | 30 ++++++++++----- lnwire/lnwire.go | 4 +- lnwire/lnwire_test.go | 34 +++++++++++++++++ lnwire/open_channel.go | 29 ++++++++++++-- lnwire/shutdown.go | 16 +++++--- 8 files changed, 198 insertions(+), 21 deletions(-) create mode 100644 lnwire/accept_channel_test.go diff --git a/.golangci.yml b/.golangci.yml index 782fc895..2051938b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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 diff --git a/lnwire/accept_channel.go b/lnwire/accept_channel.go index ef1b2828..da9daa69 100644 --- a/lnwire/accept_channel.go +++ b/lnwire/accept_channel.go @@ -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 } diff --git a/lnwire/accept_channel_test.go b/lnwire/accept_channel_test.go new file mode 100644 index 00000000..a1ab2be4 --- /dev/null +++ b/lnwire/accept_channel_test.go @@ -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) + } + }) + } +} diff --git a/lnwire/features.go b/lnwire/features.go index c9a60b5b..5d96c87e 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -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 diff --git a/lnwire/lnwire.go b/lnwire/lnwire.go index e1f6a7dd..4e9871b2 100644 --- a/lnwire/lnwire.go +++ b/lnwire/lnwire.go @@ -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 { diff --git a/lnwire/lnwire_test.go b/lnwire/lnwire_test.go index b7604f5d..02023b02 100644 --- a/lnwire/lnwire_test.go +++ b/lnwire/lnwire_test.go @@ -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) { diff --git a/lnwire/open_channel.go b/lnwire/open_channel.go index ccd01ef6..f78cc26e 100644 --- a/lnwire/open_channel.go +++ b/lnwire/open_channel.go @@ -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 } diff --git a/lnwire/shutdown.go b/lnwire/shutdown.go index 3dabc7bc..94d10a90 100644 --- a/lnwire/shutdown.go +++ b/lnwire/shutdown.go @@ -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 } From cc7accea3ddd8175249964236e3dc6a730baaa4f Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 3 Dec 2019 11:38:29 +0200 Subject: [PATCH 2/5] mulit: Signal support for upfront shutdown This commit enables signalling for the optional upfront shutdown script feature bit. --- feature/default_sets.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/feature/default_sets.go b/feature/default_sets.go index fe69226c..5d285c0e 100644 --- a/feature/default_sets.go +++ b/feature/default_sets.go @@ -29,4 +29,8 @@ var defaultSetDesc = setDesc{ SetNodeAnn: {}, // N SetLegacyGlobal: {}, }, + lnwire.UpfrontShutdownScriptOptional: { + SetInit: {}, // I + SetNodeAnn: {}, // N + }, } From f86d63144c83107e8bdf0975571031e011e848c1 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 3 Dec 2019 11:38:29 +0200 Subject: [PATCH 3/5] 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. --- chancloser.go | 70 ++++++++++++++++++++++++++++++++++++++--- chancloser_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++++ lnwallet/channel.go | 12 +++++++ peer.go | 54 ++++++++++++++++++++------------ 4 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 chancloser_test.go diff --git a/chancloser.go b/chancloser.go index 12bd7560..f2b71d19 100644 --- a/chancloser.go +++ b/chancloser.go @@ -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 diff --git a/chancloser_test.go b/chancloser_test.go new file mode 100644 index 00000000..f7c481d0 --- /dev/null +++ b/chancloser_test.go @@ -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) + } + }) + } +} diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 57f6b370..a958dea8 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -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 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 // 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. diff --git a/peer.go b/peer.go index a007701b..d95a82ca 100644 --- a/peer.go +++ b/peer.go @@ -2068,13 +2068,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 @@ -2099,9 +2104,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, @@ -2134,14 +2142,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 @@ -2159,9 +2172,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, From 77222d8b691f71f0ca7f0f7e59cac70c587b5eb0 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 3 Dec 2019 11:38:29 +0200 Subject: [PATCH 4/5] channeldb: Add upfront shutdown script to OpenChannel This commit adds fields for upfront shutdown scripts set by the local and remote peer to the OpenChannel struct. These values are optional, so they are added with their own keys in the chanBucket in the DB. --- channeldb/channel.go | 86 ++++++++++++++++++++++++++++++++++- channeldb/channel_test.go | 95 +++++++++++++++++++++++++++++++++++++++ lnwallet/channel.go | 4 +- 3 files changed, 181 insertions(+), 4 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 70722dfa..696cc4ed 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -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) { diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 645b3030..931f2879 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -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) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index a958dea8..f040aecd 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -4921,13 +4921,13 @@ func (lc *LightningChannel) ShortChanID() lnwire.ShortChannelID { // 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 + 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 nil + return lc.channelState.RemoteShutdownScript } // genHtlcScript generates the proper P2WSH public key scripts for the HTLC From 9b35c349de3cb87b52658cece4d09339e4115ac3 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 3 Dec 2019 11:38:29 +0200 Subject: [PATCH 5/5] multi: Set upfront shutdown from open and accept chanel messages This commit gets upfront shutdown scripts from openchannel and acceptchannel wire messages sent from our peer and sets upfront shutdown scripts in our open and accept channel messages when the remote peer supports option upfront shutdown and we have the feature enabled. --- config.go | 2 + fundingmanager.go | 165 +++++++++++++++++++++++++++++++--------- fundingmanager_test.go | 94 ++++++++++++++++++++++- lnwallet/reservation.go | 12 +++ lnwallet/wallet.go | 14 ++++ server.go | 4 + 6 files changed, 256 insertions(+), 35 deletions(-) diff --git a/config.go b/config.go index 064315a0..01cf592f 100644 --- a/config.go +++ b/config.go @@ -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"` diff --git a/fundingmanager.go b/fundingmanager.go index e87dd9f1..ac89aeaa 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -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 { @@ -2738,6 +2770,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. @@ -2823,6 +2898,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 @@ -2883,24 +2979,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", diff --git a/fundingmanager_test.go b/fundingmanager_test.go index 80d3cd33..77bfbaa6 100644 --- a/fundingmanager_test.go +++ b/fundingmanager_test.go @@ -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) + } + + }) + } +} diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 19f49eea..b7b63da5 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -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() diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 16dc50f9..855260a5 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -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() diff --git a/server.go b/server.go index 605d951f..4c89aa47 100644 --- a/server.go +++ b/server.go @@ -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