From 8826db32e8ff6da7dac5de853bf387a56146b092 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 9 Aug 2018 10:03:13 +0200 Subject: [PATCH 01/12] server: let gossiper hand policy update to router ProcessLocalAnnouncement will attempt to call UpdateEdge with the new policy. If we call it manually before handing it to the gossiper, that call will fail with "Outdated" and the announcement won't propagate. --- server.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/server.go b/server.go index bf7700920..c2d896b41 100644 --- a/server.go +++ b/server.go @@ -2976,22 +2976,6 @@ func extractChannelUpdate(info *channeldb.ChannelEdgeInfo, // applyChannelUpdate applies the channel update to the different sub-systems of // the server. func (s *server) applyChannelUpdate(update *lnwire.ChannelUpdate) error { - newChannelPolicy := &channeldb.ChannelEdgePolicy{ - SigBytes: update.Signature.ToSignatureBytes(), - ChannelID: update.ShortChannelID.ToUint64(), - LastUpdate: time.Unix(int64(update.Timestamp), 0), - Flags: update.Flags, - TimeLockDelta: update.TimeLockDelta, - MinHTLC: update.HtlcMinimumMsat, - FeeBaseMSat: lnwire.MilliSatoshi(update.BaseFee), - FeeProportionalMillionths: lnwire.MilliSatoshi(update.FeeRate), - } - - err := s.chanRouter.UpdateEdge(newChannelPolicy) - if err != nil && !routing.IsError(err, routing.ErrIgnored) { - return err - } - pubKey := s.identityPriv.PubKey() errChan := s.authGossiper.ProcessLocalAnnouncement(update, pubKey) select { From 304577f61784095006469802a8a3e8a218176898 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 9 Aug 2018 10:03:13 +0200 Subject: [PATCH 02/12] server: make sure timestamp always increases when creating new ChannelUpdate --- server.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server.go b/server.go index c2d896b41..8eb040508 100644 --- a/server.go +++ b/server.go @@ -2899,7 +2899,12 @@ func (s *server) disableChannel(op wire.OutPoint) error { // We must now update the message's timestamp and generate a new // signature. - chanUpdate.Timestamp = uint32(time.Now().Unix()) + newTimestamp := uint32(time.Now().Unix()) + if newTimestamp <= chanUpdate.Timestamp { + // Timestamp must increase for message to propagate. + newTimestamp = chanUpdate.Timestamp + 1 + } + chanUpdate.Timestamp = newTimestamp chanUpdateMsg, err := chanUpdate.DataToSign() if err != nil { From 940d0735f8a65238ffc7ad4a80770dee202366bf Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 9 Aug 2018 10:03:13 +0200 Subject: [PATCH 03/12] lnd_test: use dave to listen for ChannelUpdates in testSendUpdateDisableChannel This commit changes the test testSendUpdateDisableChannel to use Dave to listen for channel updates, as was originally intended. Since a ChannelUpdate won't propagate in the network if the channel is already closed (if the closing transaction has been mined), we change the closing procedure to initiate the closing process, wait for the ChannelUpdates, then finally mining the closing transaction. This avoids a propagation race between the mined block and the ChannelUpdate. --- lnd_test.go | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/lnd_test.go b/lnd_test.go index 82dd6a6d8..e08374f88 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -10925,7 +10925,7 @@ func testSendUpdateDisableChannel(net *lntest.NetworkHarness, t *harnessTest) { t.Fatalf("unable to connect bob to dave: %v", err) } - daveUpdates, quit := subscribeGraphNotifications(t, ctxb, net.Alice) + daveUpdates, quit := subscribeGraphNotifications(t, ctxb, dave) defer close(quit) // We should expect to see a channel update with the default routing @@ -10940,16 +10940,30 @@ func testSendUpdateDisableChannel(net *lntest.NetworkHarness, t *harnessTest) { // Close Alice's channels with Bob and Carol cooperatively and // unilaterally respectively. ctxt, _ = context.WithTimeout(ctxb, timeout) - closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAliceBob, false) - ctxt, _ = context.WithTimeout(ctxb, timeout) - closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAliceCarol, true) + _, _, err = net.CloseChannel(ctxt, net.Alice, chanPointAliceBob, false) + if err != nil { + t.Fatalf("unable to close channel: %v", err) + } - // Now that the channels have been closed, we should receive an update - // marking each as disabled. + ctxt, _ = context.WithTimeout(ctxb, timeout) + _, _, err = net.CloseChannel(ctxt, net.Alice, chanPointAliceCarol, true) + if err != nil { + t.Fatalf("unable to close channel: %v", err) + } + + // Now that the channel close processes have been started, we should + // receive an update marking each as disabled. waitForChannelUpdate( t, daveUpdates, net.Alice.PubKeyStr, expectedPolicy, chanPointAliceBob, chanPointAliceCarol, ) + + // Finally, close the channels by mining the closing transactions. + _, err = waitForNTxsInMempool(net.Miner.Node, 2, timeout) + if err != nil { + t.Fatalf("expected transactions not found in mempool: %v", err) + } + mineBlocks(t, net, 1) } type testCase struct { From 887722f38ffc4b2953284f898305a3b943694e06 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 9 Aug 2018 10:03:14 +0200 Subject: [PATCH 04/12] htlcswitch/switch: add method HasActiveLink --- htlcswitch/switch.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/htlcswitch/switch.go b/htlcswitch/switch.go index ecb56a5f5..74e0a2787 100644 --- a/htlcswitch/switch.go +++ b/htlcswitch/switch.go @@ -1902,6 +1902,16 @@ func (s *Switch) getLinkByShortID(chanID lnwire.ShortChannelID) (ChannelLink, er return link, nil } +// HasActiveLink returns true if the given channel ID has a link in the link +// index. +func (s *Switch) HasActiveLink(chanID lnwire.ChannelID) bool { + s.indexMtx.RLock() + defer s.indexMtx.RUnlock() + + _, ok := s.linkIndex[chanID] + return ok +} + // RemoveLink purges the switch of any link associated with chanID. If a pending // or active link is not found, this method does nothing. Otherwise, the method // returns after the link has been completely shutdown. From b5bfdd72b59659f735e02a33c191ed336b7e22ba Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 9 Aug 2018 10:03:14 +0200 Subject: [PATCH 05/12] server+peer: rename disableChannel->announceChanStatus Now takes a boolean whether to disable/activate, and keeps track of the last update sent to avoid sending duplicates. --- peer.go | 14 ++++++++++---- server.go | 50 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/peer.go b/peer.go index 6ce9302d9..c1faf6ddc 100644 --- a/peer.go +++ b/peer.go @@ -1690,8 +1690,11 @@ func (p *peer) fetchActiveChanCloser(chanID lnwire.ChannelID) (*channelCloser, e channel: channel, unregisterChannel: p.server.htlcSwitch.RemoveLink, broadcastTx: p.server.cc.wallet.PublishTransaction, - disableChannel: p.server.disableChannel, - quit: p.quit, + disableChannel: func(op wire.OutPoint) error { + return p.server.announceChanStatus(op, + true) + }, + quit: p.quit, }, deliveryAddr, feePerKw, @@ -1750,8 +1753,11 @@ func (p *peer) handleLocalCloseReq(req *htlcswitch.ChanClose) { channel: channel, unregisterChannel: p.server.htlcSwitch.RemoveLink, broadcastTx: p.server.cc.wallet.PublishTransaction, - disableChannel: p.server.disableChannel, - quit: p.quit, + disableChannel: func(op wire.OutPoint) error { + return p.server.announceChanStatus(op, + true) + }, + quit: p.quit, }, deliveryAddr, req.TargetFeePerKw, diff --git a/server.go b/server.go index 8eb040508..2a472b5de 100644 --- a/server.go +++ b/server.go @@ -166,6 +166,11 @@ type server struct { // changed since last start. currentNodeAnn *lnwire.NodeAnnouncement + // sendDisabled is used to keep track of the disabled flag of the last + // sent ChannelUpdate from announceChanStatus. + sentDisabled map[wire.OutPoint]bool + sentDisabledMtx sync.Mutex + quit chan struct{} wg sync.WaitGroup @@ -275,6 +280,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, inboundPeers: make(map[string]*peer), outboundPeers: make(map[string]*peer), peerConnectedListeners: make(map[string][]chan<- lnpeer.Peer), + sentDisabled: make(map[wire.OutPoint]bool), globalFeatures: lnwire.NewFeatureVector(globalFeatures, lnwire.GlobalFeatures), @@ -676,7 +682,9 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, return ErrServerShuttingDown } }, - DisableChannel: s.disableChannel, + DisableChannel: func(op wire.OutPoint) error { + return s.announceChanStatus(op, true) + }, }, chanDB) s.breachArbiter = newBreachArbiter(&BreachConfig{ @@ -2883,10 +2891,23 @@ func (s *server) fetchNodeAdvertisedAddr(pub *btcec.PublicKey) (net.Addr, error) return node.Addresses[0], nil } -// disableChannel disables a channel, resulting in it not being able to forward -// payments. This is done by sending a new channel update across the network -// with the disabled flag set. -func (s *server) disableChannel(op wire.OutPoint) error { +// announceChanStatus disables a channel if disabled=true, otherwise activates +// it. This is done by sending a new channel update across the network with the +// disabled flag set accordingly. The result of disabling the channel is it not +// being able to forward payments. +func (s *server) announceChanStatus(op wire.OutPoint, disabled bool) error { + s.sentDisabledMtx.Lock() + defer s.sentDisabledMtx.Unlock() + + // If we have already sent out an update reflecting the current status, + // skip this channel. + alreadyDisabled, ok := s.sentDisabled[op] + if ok && alreadyDisabled == disabled { + return nil + } + + srvrLog.Debugf("Announcing channel(%v) disabled=%v", op, disabled) + // Retrieve the latest update for this channel. We'll use this // as our starting point to send the new update. chanUpdate, err := s.fetchLastChanUpdateByOutPoint(op) @@ -2894,8 +2915,13 @@ func (s *server) disableChannel(op wire.OutPoint) error { return err } - // Set the bit responsible for marking a channel as disabled. - chanUpdate.Flags |= lnwire.ChanUpdateDisabled + if disabled { + // Set the bit responsible for marking a channel as disabled. + chanUpdate.Flags |= lnwire.ChanUpdateDisabled + } else { + // Clear the bit responsible for marking a channel as disabled. + chanUpdate.Flags &= ^lnwire.ChanUpdateDisabled + } // We must now update the message's timestamp and generate a new // signature. @@ -2922,7 +2948,15 @@ func (s *server) disableChannel(op wire.OutPoint) error { } // Once signed, we'll send the new update to all of our peers. - return s.applyChannelUpdate(chanUpdate) + if err := s.applyChannelUpdate(chanUpdate); err != nil { + return err + } + + // We'll keep track of the status set in the last update we sent, to + // avoid sending updates if nothing has changed. + s.sentDisabled[op] = disabled + + return nil } // fetchLastChanUpdateByOutPoint fetches the latest update for a channel from From 9e44b38eee4a17330636cecfd64aebd3ab635d90 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 9 Aug 2018 10:03:14 +0200 Subject: [PATCH 06/12] config: define InactiveChanTimeout --- config.go | 55 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/config.go b/config.go index 6e65afb7b..e32850a90 100644 --- a/config.go +++ b/config.go @@ -28,27 +28,28 @@ import ( ) const ( - defaultConfigFilename = "lnd.conf" - defaultDataDirname = "data" - defaultChainSubDirname = "chain" - defaultGraphSubDirname = "graph" - defaultTLSCertFilename = "tls.cert" - defaultTLSKeyFilename = "tls.key" - defaultAdminMacFilename = "admin.macaroon" - defaultReadMacFilename = "readonly.macaroon" - defaultInvoiceMacFilename = "invoice.macaroon" - defaultLogLevel = "info" - defaultLogDirname = "logs" - defaultLogFilename = "lnd.log" - defaultRPCPort = 10009 - defaultRESTPort = 8080 - defaultPeerPort = 9735 - defaultRPCHost = "localhost" - defaultMaxPendingChannels = 1 - defaultNoEncryptWallet = false - defaultTrickleDelay = 30 * 1000 - defaultMaxLogFiles = 3 - defaultMaxLogFileSize = 10 + defaultConfigFilename = "lnd.conf" + defaultDataDirname = "data" + defaultChainSubDirname = "chain" + defaultGraphSubDirname = "graph" + defaultTLSCertFilename = "tls.cert" + defaultTLSKeyFilename = "tls.key" + defaultAdminMacFilename = "admin.macaroon" + defaultReadMacFilename = "readonly.macaroon" + defaultInvoiceMacFilename = "invoice.macaroon" + defaultLogLevel = "info" + defaultLogDirname = "logs" + defaultLogFilename = "lnd.log" + defaultRPCPort = 10009 + defaultRESTPort = 8080 + defaultPeerPort = 9735 + defaultRPCHost = "localhost" + defaultMaxPendingChannels = 1 + defaultNoEncryptWallet = false + defaultTrickleDelay = 30 * 1000 + defaultInactiveChanTimeout = 20 * time.Minute + defaultMaxLogFiles = 3 + defaultMaxLogFileSize = 10 defaultTorSOCKSPort = 9050 defaultTorDNSHost = "soa.nodes.lightning.directory" @@ -226,7 +227,8 @@ type config struct { NoEncryptWallet bool `long:"noencryptwallet" description:"If set, wallet will be encrypted using the default passphrase."` - TrickleDelay int `long:"trickledelay" description:"Time in milliseconds between each release of announcements to the network"` + TrickleDelay int `long:"trickledelay" description:"Time in milliseconds between each release of announcements to the network"` + InactiveChanTimeout time.Duration `long:"inactivechantimeout" description:"If a channel has been inactive for the set time, send a ChannelUpdate disabling it."` Alias string `long:"alias" description:"The node alias. Used as a moniker by peers and intelligence services"` Color string `long:"color" description:"The color of the node in hex format (i.e. '#3399FF'). Used to customize node appearance in intelligence services"` @@ -299,10 +301,11 @@ func loadConfig() (*config, error) { MinChannelSize: int64(minChanFundingSize), MaxChannelSize: int64(maxFundingAmount), }, - TrickleDelay: defaultTrickleDelay, - Alias: defaultAlias, - Color: defaultColor, - MinChanSize: int64(minChanFundingSize), + TrickleDelay: defaultTrickleDelay, + InactiveChanTimeout: defaultInactiveChanTimeout, + Alias: defaultAlias, + Color: defaultColor, + MinChanSize: int64(minChanFundingSize), Tor: &torConfig{ SOCKS: defaultTorSOCKS, DNS: defaultTorDNS, From e9cc7492a9afa1441eaf71cef97ef0fcca8286dd Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 9 Aug 2018 10:03:14 +0200 Subject: [PATCH 07/12] server: add goroutine watchChannelStatus This commit adds a goroutine watchChannelStatus to the server, which will query the switch for the status of all open channels every InactiveChanTimeout / 4. If a channel's status has remained unchanged during the last InactiveChanTimeout it'll send out a ChannelUpdate setting the disabled bit accordingly. --- server.go | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/server.go b/server.go index 2a472b5de..125e04254 100644 --- a/server.go +++ b/server.go @@ -993,6 +993,11 @@ func (s *server) Start() error { srvrLog.Infof("Auto peer bootstrapping is disabled") } + // Start a goroutine that will periodically send out ChannelUpdates + // based on a channel's status. + s.wg.Add(1) + go s.watchChannelStatus() + return nil } @@ -3024,3 +3029,112 @@ func (s *server) applyChannelUpdate(update *lnwire.ChannelUpdate) error { return ErrServerShuttingDown } } + +// watchChannelStatus periodically queries the Switch for the status of the +// open channels, and sends out ChannelUpdates to the network indicating their +// active status. Currently we'll send out either a Disabled or Active update +// if the channel has been in the same status over a given amount of time. +// +// NOTE: This MUST be run as a goroutine. +func (s *server) watchChannelStatus() { + defer s.wg.Done() + + // A map with values activeStatus is used to keep track of the first + // time we saw a link changing to the current active status. + type activeStatus struct { + active bool + time time.Time + } + status := make(map[wire.OutPoint]activeStatus) + + // We'll check in on the channel statuses every 1/4 of the timeout. + unchangedTimeout := cfg.InactiveChanTimeout + tickerTimeout := unchangedTimeout / 4 + + if unchangedTimeout == 0 || tickerTimeout == 0 { + srvrLog.Debugf("Won't watch channel statuses") + return + } + + ticker := time.NewTicker(tickerTimeout) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + channels, err := s.chanDB.FetchAllOpenChannels() + if err != nil { + srvrLog.Errorf("Unable to fetch open "+ + "channels: %v", err) + continue + } + + // For each open channel, update the status. We'll copy + // the updated statuses to a new map, to avoid keeping + // the status of closed channels around. + newStatus := make(map[wire.OutPoint]activeStatus) + for _, c := range channels { + chanID := lnwire.NewChanIDFromOutPoint( + &c.FundingOutpoint) + + // Get the current active stauts from the + // Switch. + active := s.htlcSwitch.HasActiveLink(chanID) + + var currentStatus activeStatus + + // If this link is not in the map, or the + // status has changed, set an updated active + // status. + st, ok := status[c.FundingOutpoint] + if !ok || st.active != active { + currentStatus = activeStatus{ + active: active, + time: time.Now(), + } + } else { + // The status is unchanged, we'll keep + // it as is. + currentStatus = st + } + + newStatus[c.FundingOutpoint] = currentStatus + } + + // Set the status map to the map of new statuses. + status = newStatus + + // If no change in status has happened during the last + // interval, we'll send out an update. Note that we add + // the negative of the timeout to set our limit in the + // past. + limit := time.Now().Add(-unchangedTimeout) + + // We'll send out an update for all channels that have + // had their status unchanged for longer than the limit. + for op, st := range status { + disable := !st.active + + if st.time.Before(limit) { + // Before we attempt to announce the + // status of the channel, we remove it + // from the status map such that it + // will need a full unchaged interval + // before we attempt to announce its + // status again. + delete(status, op) + + err = s.announceChanStatus(op, disable) + if err != nil { + srvrLog.Errorf("Unable to "+ + "disable channel: %v", + err) + } + } + } + + case <-s.quit: + return + } + } +} From 5deffd228cb0e66ca88dc56b7dbd9f349344b45a Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 9 Aug 2018 10:03:14 +0200 Subject: [PATCH 08/12] peer: call disableChannel with disabled=false after loading channels This to ensure now active channel is available in the router, for path finding, as it might have been disabled if the peer has been offline a while. --- peer.go | 19 +++++++++++++++++++ server.go | 3 +++ 2 files changed, 22 insertions(+) diff --git a/peer.go b/peer.go index c1faf6ddc..f6366c805 100644 --- a/peer.go +++ b/peer.go @@ -318,6 +318,7 @@ func (p *peer) Start() error { // loadActiveChannels creates indexes within the peer for tracking all active // channels returned by the database. func (p *peer) loadActiveChannels(chans []*channeldb.OpenChannel) error { + var activeChans []wire.OutPoint for _, dbChan := range chans { lnChan, err := lnwallet.NewLightningChannel( p.server.cc.signer, p.server.witnessBeacon, dbChan, @@ -425,8 +426,26 @@ func (p *peer) loadActiveChannels(chans []*channeldb.OpenChannel) error { p.activeChanMtx.Lock() p.activeChannels[chanID] = lnChan p.activeChanMtx.Unlock() + + activeChans = append(activeChans, *chanPoint) } + // As a final measure we launch a goroutine that will ensure the + // channels are not currently disabled, as that will make us skip it + // during path finding. + go func() { + for _, chanPoint := range activeChans { + // Set the channel disabled=false by sending out a new + // ChannelUpdate. If this channel is already active, + // the update won't be sent. + err := p.server.announceChanStatus(chanPoint, false) + if err != nil { + peerLog.Errorf("unable to send out active "+ + "channel update: %v", err) + } + } + }() + return nil } diff --git a/server.go b/server.go index 125e04254..c3a4379a6 100644 --- a/server.go +++ b/server.go @@ -3112,6 +3112,9 @@ func (s *server) watchChannelStatus() { // We'll send out an update for all channels that have // had their status unchanged for longer than the limit. + // NOTE: We also make sure to activate any channel when + // we connect to a peer, to make them available for + // path finding immediately. for op, st := range status { disable := !st.active From 4581aa313c069f909d6cef14092d3c03ce535409 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 9 Aug 2018 10:03:14 +0200 Subject: [PATCH 09/12] server: handle case where both policies are not present in fetchLastChanUpdateByOutPoint --- server.go | 46 +++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/server.go b/server.go index c3a4379a6..0de0fdde8 100644 --- a/server.go +++ b/server.go @@ -2964,32 +2964,48 @@ func (s *server) announceChanStatus(op wire.OutPoint, disabled bool) error { return nil } -// fetchLastChanUpdateByOutPoint fetches the latest update for a channel from -// our point of view. -func (s *server) fetchLastChanUpdateByOutPoint(op wire.OutPoint) (*lnwire.ChannelUpdate, error) { +// fetchLastChanUpdateByOutPoint fetches the latest policy for our direction of +// a channel, and crafts a new ChannelUpdate with this policy. Returns an error +// in case our ChannelEdgePolicy is not found in the database. +func (s *server) fetchLastChanUpdateByOutPoint(op wire.OutPoint) ( + *lnwire.ChannelUpdate, error) { + + // Get the edge info and policies for this channel from the graph. graph := s.chanDB.ChannelGraph() info, edge1, edge2, err := graph.FetchChannelEdgesByOutpoint(&op) if err != nil { return nil, err } - if edge1 == nil || edge2 == nil { - return nil, fmt.Errorf("unable to find channel(%v)", op) + // Helper function to extract the owner of the given policy. + owner := func(edge *channeldb.ChannelEdgePolicy) []byte { + var pubKey *btcec.PublicKey + switch { + case edge.Flags&lnwire.ChanUpdateDirection == 0: + pubKey, _ = info.NodeKey1() + case edge.Flags&lnwire.ChanUpdateDirection == 1: + pubKey, _ = info.NodeKey2() + } + + // If pubKey was not found, just return nil. + if pubKey == nil { + return nil + } + + return pubKey.SerializeCompressed() } - // If we're the outgoing node on the first edge, then that - // means the second edge is our policy. Otherwise, the first - // edge is our policy. - var local *channeldb.ChannelEdgePolicy - + // Extract the channel update from the policy we own, if any. ourPubKey := s.identityPriv.PubKey().SerializeCompressed() - if bytes.Equal(edge1.Node.PubKeyBytes[:], ourPubKey) { - local = edge2 - } else { - local = edge1 + if edge1 != nil && bytes.Equal(ourPubKey, owner(edge1)) { + return extractChannelUpdate(info, edge1) } - return extractChannelUpdate(info, local) + if edge2 != nil && bytes.Equal(ourPubKey, owner(edge2)) { + return extractChannelUpdate(info, edge2) + } + + return nil, fmt.Errorf("unable to find channel(%v)", op) } // extractChannelUpdate retrieves a lnwire.ChannelUpdate message from an edge's From 53d319ad104b4efceceddbef1cab958c04808a03 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 9 Aug 2018 10:03:15 +0200 Subject: [PATCH 10/12] lnd_test: use shutdownAndAssert --- lnd_test.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lnd_test.go b/lnd_test.go index e08374f88..e8b058493 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -10643,6 +10643,8 @@ func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) { if err != nil { t.Fatalf("unable to create carol's node: %v", err) } + defer shutdownAndAssert(net, t, carol) + ctxt, _ = context.WithTimeout(ctxb, timeout) if err := net.ConnectNodes(ctxt, carol, net.Alice); err != nil { t.Fatalf("unable to connect carol to alice: %v", err) @@ -10663,6 +10665,8 @@ func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) { if err != nil { t.Fatalf("unable to create dave's node: %v", err) } + defer shutdownAndAssert(net, t, dave) + ctxt, _ = context.WithTimeout(ctxb, timeout) if err := net.ConnectNodes(ctxt, dave, net.Bob); err != nil { t.Fatalf("unable to connect dave to bob: %v", err) @@ -10873,13 +10877,6 @@ func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) { closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointBobDave, false) ctxt, _ = context.WithTimeout(ctxb, timeout) closeChannelAndAssert(ctxt, t, net, carol, chanPointCarolDave, false) - - if err := net.ShutdownNode(carol); err != nil { - t.Fatalf("unable to shut down carol: %v", err) - } - if err := net.ShutdownNode(dave); err != nil { - t.Fatalf("unable to shut down dave: %v", err) - } } // testSendUpdateDisableChannel ensures that a channel update with the disable From 52941cec532fd4234704f051b54dcdbedb6ad45b Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 9 Aug 2018 10:03:15 +0200 Subject: [PATCH 11/12] lntest: add SuspendNode --- lntest/harness.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lntest/harness.go b/lntest/harness.go index 60c0f4ef4..ff82a052d 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -532,7 +532,7 @@ func (n *NetworkHarness) DisconnectNodes(ctx context.Context, a, b *HarnessNode) return nil } -// RestartNode attempts to restart a lightning node by shutting it down +// RestartNode attempts to restart a lightning node by shutting it down // cleanly, then restarting the process. This function is fully blocking. Upon // restart, the RPC connection to the node will be re-attempted, continuing iff // the connection attempt is successful. If the callback parameter is non-nil, @@ -556,6 +556,20 @@ func (n *NetworkHarness) RestartNode(node *HarnessNode, callback func() error) e return node.start(n.lndErrorChan) } +// SuspendNode stops the given node and returns a callback that can be used to +// start it again. +func (n *NetworkHarness) SuspendNode(node *HarnessNode) (func() error, error) { + if err := node.stop(); err != nil { + return nil, err + } + + restart := func() error { + return node.start(n.lndErrorChan) + } + + return restart, nil +} + // ShutdownNode stops an active lnd process and returns when the process has // exited and any temporary directories have been cleaned up. func (n *NetworkHarness) ShutdownNode(node *HarnessNode) error { From 7b1ac0a979069796c3f1a20b9be658b53a5394c8 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 9 Aug 2018 10:03:15 +0200 Subject: [PATCH 12/12] lnd test: check inactive channel disables in testSendUpdateDisableChannel --- lnd_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/lnd_test.go b/lnd_test.go index e8b058493..4abd09837 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -10902,6 +10902,7 @@ func testSendUpdateDisableChannel(net *lntest.NetworkHarness, t *harnessTest) { t.Fatalf("unable to create carol's node: %v", err) } defer shutdownAndAssert(net, t, carol) + if err := net.ConnectNodes(ctxb, net.Alice, carol); err != nil { t.Fatalf("unable to connect alice to carol: %v", err) } @@ -10910,6 +10911,34 @@ func testSendUpdateDisableChannel(net *lntest.NetworkHarness, t *harnessTest) { ctxt, t, net, net.Alice, carol, chanAmt, 0, false, ) + // We create a new node Eve that has an inactive channel timeout of + // just 2 seconds (down from the default 20m). It will be used to test + // channel updates for channels going inactive. + eve, err := net.NewNode("Eve", []string{"--inactivechantimeout=2s"}) + if err != nil { + t.Fatalf("unable to create eve's node: %v", err) + } + defer shutdownAndAssert(net, t, eve) + + // Give Eve some coins. + err = net.SendCoins(ctxb, btcutil.SatoshiPerBitcoin, eve) + if err != nil { + t.Fatalf("unable to send coins to eve: %v", err) + } + + // Connect Eve to Carol and Bob, and open a channel to carol. + if err := net.ConnectNodes(ctxb, eve, carol); err != nil { + t.Fatalf("unable to connect alice to carol: %v", err) + } + if err := net.ConnectNodes(ctxb, eve, net.Bob); err != nil { + t.Fatalf("unable to connect eve to bob: %v", err) + } + + ctxt, _ = context.WithTimeout(ctxb, timeout) + chanPointEveCarol := openChannelAndAssert( + ctxt, t, net, eve, carol, chanAmt, 0, false, + ) + // Launch a node for Dave which will connect to Bob in order to receive // graph updates from. This will ensure that the channel updates are // propagated throughout the network. @@ -10934,6 +10963,29 @@ func testSendUpdateDisableChannel(net *lntest.NetworkHarness, t *harnessTest) { Disabled: true, } + // Let Carol go offline. Since Eve has an inactive timeout of 2s, we + // expect her to send an update disabling the channel. + restartCarol, err := net.SuspendNode(carol) + if err != nil { + t.Fatalf("unable to suspend carol: %v", err) + } + waitForChannelUpdate( + t, daveUpdates, eve.PubKeyStr, expectedPolicy, + chanPointEveCarol, + ) + + // We restart Carol. Since the channel now becomes active again, Eve + // should send a ChannelUpdate setting the channel no longer disabled. + if err := restartCarol(); err != nil { + t.Fatalf("unable to restart carol: %v", err) + } + + expectedPolicy.Disabled = false + waitForChannelUpdate( + t, daveUpdates, eve.PubKeyStr, expectedPolicy, + chanPointEveCarol, + ) + // Close Alice's channels with Bob and Carol cooperatively and // unilaterally respectively. ctxt, _ = context.WithTimeout(ctxb, timeout) @@ -10950,6 +11002,7 @@ func testSendUpdateDisableChannel(net *lntest.NetworkHarness, t *harnessTest) { // Now that the channel close processes have been started, we should // receive an update marking each as disabled. + expectedPolicy.Disabled = true waitForChannelUpdate( t, daveUpdates, net.Alice.PubKeyStr, expectedPolicy, chanPointAliceBob, chanPointAliceCarol, @@ -10961,6 +11014,24 @@ func testSendUpdateDisableChannel(net *lntest.NetworkHarness, t *harnessTest) { t.Fatalf("expected transactions not found in mempool: %v", err) } mineBlocks(t, net, 1) + + // Also do this check for Eve's channel with Carol. + ctxt, _ = context.WithTimeout(ctxb, timeout) + _, _, err = net.CloseChannel(ctxt, eve, chanPointEveCarol, false) + if err != nil { + t.Fatalf("unable to close channel: %v", err) + } + + waitForChannelUpdate( + t, daveUpdates, eve.PubKeyStr, expectedPolicy, + chanPointEveCarol, + ) + + _, err = waitForNTxsInMempool(net.Miner.Node, 1, timeout) + if err != nil { + t.Fatalf("expected transactions not found in mempool: %v", err) + } + mineBlocks(t, net, 1) } type testCase struct {