From c6263c2fe678055aad44236fd5f5ca7e7d0bef66 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 3 Apr 2023 09:43:46 +0200 Subject: [PATCH 01/12] multi/refactor: move node ann modification outside of sign method In preparation for moving feature bit modification inside of the feature manager, separate node modification from signing. --- netann/node_announcement.go | 15 ++++----------- server.go | 6 +++++- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/netann/node_announcement.go b/netann/node_announcement.go index 42296bf22..5b6f7a430 100644 --- a/netann/node_announcement.go +++ b/netann/node_announcement.go @@ -60,18 +60,11 @@ func NodeAnnSetTimestamp(nodeAnn *lnwire.NodeAnnouncement) { nodeAnn.Timestamp = newTimestamp } -// SignNodeAnnouncement applies the given modifies to the passed -// lnwire.NodeAnnouncement, then signs the resulting announcement. The provided -// update should be the most recent, valid update, otherwise the timestamp may -// not monotonically increase from the prior. +// SignNodeAnnouncement signs the lnwire.NodeAnnouncement provided, which +// should be the most recent, valid update, otherwise the timestamp may not +// monotonically increase from the prior. func SignNodeAnnouncement(signer lnwallet.MessageSigner, - keyLoc keychain.KeyLocator, nodeAnn *lnwire.NodeAnnouncement, - mods ...NodeAnnModifier) error { - - // Apply the requested changes to the node announcement. - for _, modifier := range mods { - modifier(nodeAnn) - } + keyLoc keychain.KeyLocator, nodeAnn *lnwire.NodeAnnouncement) error { // Create the DER-encoded ECDSA signature over the message digest. sig, err := SignAnnouncement(signer, keyLoc, nodeAnn) diff --git a/server.go b/server.go index caefe4a50..97aad7a02 100644 --- a/server.go +++ b/server.go @@ -2961,11 +2961,15 @@ func (s *server) genNodeAnnouncement(refresh bool, // propagates. modifiers = append(modifiers, netann.NodeAnnSetTimestamp) + // Apply the requested changes to the node announcement. + for _, modifier := range modifiers { + modifier(s.currentNodeAnn) + } + // Otherwise, we'll sign a new update after applying all of the passed // modifiers. err := netann.SignNodeAnnouncement( s.nodeSigner, s.identityKeyLoc, s.currentNodeAnn, - modifiers..., ) if err != nil { return lnwire.NodeAnnouncement{}, err From 3f9f0ea5d164b41c398898a43159dd84fbd3f953 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Wed, 5 Apr 2023 10:21:59 +0200 Subject: [PATCH 02/12] multi/refactor: separate methods for get and set node announcement In preparation for a more complex function signature for set node announcement, separate get and set so that readonly callers don't need to handle the extra arguments. --- discovery/gossiper.go | 22 +++++------ discovery/gossiper_test.go | 53 ++++++++++++++----------- funding/manager.go | 3 +- lnrpc/peersrpc/config_active.go | 2 +- lnrpc/peersrpc/peers_server.go | 6 +-- netann/host_ann.go | 9 +++-- peer/brontide.go | 6 +-- rpcserver.go | 13 ++----- server.go | 69 +++++++++++++++++---------------- subrpcserver_config.go | 2 +- 10 files changed, 94 insertions(+), 91 deletions(-) diff --git a/discovery/gossiper.go b/discovery/gossiper.go index 5251c06b2..72e5876ac 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -203,11 +203,14 @@ type Config struct { // notification for when it reconnects. NotifyWhenOffline func(peerPubKey [33]byte) <-chan struct{} - // SelfNodeAnnouncement is a function that fetches our own current node - // announcement, for use when determining whether we should update our - // peers about our presence on the network. If the refresh is true, a - // new and updated announcement will be returned. - SelfNodeAnnouncement func(refresh bool) (lnwire.NodeAnnouncement, error) + // FetchSelfAnnouncement retrieves our current node announcement, for + // use when determining whether we should update our peers about our + // presence in the network. + FetchSelfAnnouncement func() lnwire.NodeAnnouncement + + // UpdateSelfAnnouncement produces a new announcement for our node with + // an updated timestamp which can be broadcast to our peers. + UpdateSelfAnnouncement func() (lnwire.NodeAnnouncement, error) // ProofMatureDelta the number of confirmations which is needed before // exchange the channel announcement proofs. @@ -1660,12 +1663,7 @@ func (d *AuthenticatedGossiper) retransmitStaleAnns(now time.Time) error { } // We'll also check that our NodeAnnouncement is not too old. - currentNodeAnn, err := d.cfg.SelfNodeAnnouncement(false) - if err != nil { - return fmt.Errorf("unable to get current node announment: %v", - err) - } - + currentNodeAnn := d.cfg.FetchSelfAnnouncement() timestamp := time.Unix(int64(currentNodeAnn.Timestamp), 0) timeElapsed := now.Sub(timestamp) @@ -1673,7 +1671,7 @@ func (d *AuthenticatedGossiper) retransmitStaleAnns(now time.Time) error { // node announcement, refresh it and resend it. nodeAnnStr := "" if timeElapsed >= d.cfg.RebroadcastInterval { - newNodeAnn, err := d.cfg.SelfNodeAnnouncement(true) + newNodeAnn, err := d.cfg.UpdateSelfAnnouncement() if err != nil { return fmt.Errorf("unable to get refreshed node "+ "announcement: %v", err) diff --git a/discovery/gossiper_test.go b/discovery/gossiper_test.go index cc2fdef05..d364b80ec 100644 --- a/discovery/gossiper_test.go +++ b/discovery/gossiper_test.go @@ -775,7 +775,14 @@ func createTestCtx(t *testing.T, startHeight uint32) (*testCtx, error) { c := make(chan struct{}) return c }, - SelfNodeAnnouncement: func(bool) (lnwire.NodeAnnouncement, error) { + FetchSelfAnnouncement: func() lnwire.NodeAnnouncement { + return lnwire.NodeAnnouncement{ + Timestamp: testTimestamp, + } + }, + UpdateSelfAnnouncement: func() (lnwire.NodeAnnouncement, + error) { + return lnwire.NodeAnnouncement{ Timestamp: testTimestamp, }, nil @@ -1446,28 +1453,30 @@ func TestSignatureAnnouncementRetryAtStartup(t *testing.T) { return lnwire.ShortChannelID{}, fmt.Errorf("no peer alias") } + //nolint:lll gossiper := New(Config{ - Notifier: ctx.gossiper.cfg.Notifier, - Broadcast: ctx.gossiper.cfg.Broadcast, - NotifyWhenOnline: ctx.gossiper.reliableSender.cfg.NotifyWhenOnline, - NotifyWhenOffline: ctx.gossiper.reliableSender.cfg.NotifyWhenOffline, - SelfNodeAnnouncement: ctx.gossiper.cfg.SelfNodeAnnouncement, - Router: ctx.gossiper.cfg.Router, - TrickleDelay: trickleDelay, - RetransmitTicker: ticker.NewForce(retransmitDelay), - RebroadcastInterval: rebroadcastInterval, - ProofMatureDelta: proofMatureDelta, - WaitingProofStore: ctx.gossiper.cfg.WaitingProofStore, - MessageStore: ctx.gossiper.cfg.MessageStore, - RotateTicker: ticker.NewForce(DefaultSyncerRotationInterval), - HistoricalSyncTicker: ticker.NewForce(DefaultHistoricalSyncInterval), - NumActiveSyncers: 3, - MinimumBatchSize: 10, - SubBatchDelay: time.Second * 5, - IsAlias: isAlias, - SignAliasUpdate: signAliasUpdate, - FindBaseByAlias: findBaseByAlias, - GetAlias: getAlias, + Notifier: ctx.gossiper.cfg.Notifier, + Broadcast: ctx.gossiper.cfg.Broadcast, + NotifyWhenOnline: ctx.gossiper.reliableSender.cfg.NotifyWhenOnline, + NotifyWhenOffline: ctx.gossiper.reliableSender.cfg.NotifyWhenOffline, + FetchSelfAnnouncement: ctx.gossiper.cfg.FetchSelfAnnouncement, + UpdateSelfAnnouncement: ctx.gossiper.cfg.UpdateSelfAnnouncement, + Router: ctx.gossiper.cfg.Router, + TrickleDelay: trickleDelay, + RetransmitTicker: ticker.NewForce(retransmitDelay), + RebroadcastInterval: rebroadcastInterval, + ProofMatureDelta: proofMatureDelta, + WaitingProofStore: ctx.gossiper.cfg.WaitingProofStore, + MessageStore: ctx.gossiper.cfg.MessageStore, + RotateTicker: ticker.NewForce(DefaultSyncerRotationInterval), + HistoricalSyncTicker: ticker.NewForce(DefaultHistoricalSyncInterval), + NumActiveSyncers: 3, + MinimumBatchSize: 10, + SubBatchDelay: time.Second * 5, + IsAlias: isAlias, + SignAliasUpdate: signAliasUpdate, + FindBaseByAlias: findBaseByAlias, + GetAlias: getAlias, }, &keychain.KeyDescriptor{ PubKey: ctx.gossiper.selfKey, KeyLocator: ctx.gossiper.selfKeyLoc, diff --git a/funding/manager.go b/funding/manager.go index 3e72b896c..043ea4eb9 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -373,7 +373,8 @@ type Config struct { msg []byte, doubleHash bool) (*ecdsa.Signature, error) // CurrentNodeAnnouncement should return the latest, fully signed node - // announcement from the backing Lightning Network node. + // announcement from the backing Lightning Network node with a fresh + // timestamp. CurrentNodeAnnouncement func() (lnwire.NodeAnnouncement, error) // SendAnnouncement is used by the FundingManager to send announcement diff --git a/lnrpc/peersrpc/config_active.go b/lnrpc/peersrpc/config_active.go index 860e7627e..34da09699 100644 --- a/lnrpc/peersrpc/config_active.go +++ b/lnrpc/peersrpc/config_active.go @@ -18,7 +18,7 @@ import ( type Config struct { // GetNodeAnnouncement is used to send our retrieve the current // node announcement information. - GetNodeAnnouncement func() (lnwire.NodeAnnouncement, error) + GetNodeAnnouncement func() lnwire.NodeAnnouncement // ParseAddr parses an address from its string format to a net.Addr. ParseAddr func(addr string) (net.Addr, error) diff --git a/lnrpc/peersrpc/peers_server.go b/lnrpc/peersrpc/peers_server.go index 84516f7dc..5710f5fca 100644 --- a/lnrpc/peersrpc/peers_server.go +++ b/lnrpc/peersrpc/peers_server.go @@ -313,11 +313,7 @@ func (s *Server) UpdateNodeAnnouncement(_ context.Context, resp := &NodeAnnouncementUpdateResponse{} nodeModifiers := make([]netann.NodeAnnModifier, 0) - currentNodeAnn, err := s.cfg.GetNodeAnnouncement() - if err != nil { - return nil, fmt.Errorf("unable to get current node "+ - "announcement: %v", err) - } + currentNodeAnn := s.cfg.GetNodeAnnouncement() if len(req.FeatureUpdates) > 0 { features, ops, err := s.updateFeatures( diff --git a/netann/host_ann.go b/netann/host_ann.go index bc50289c1..767c3089c 100644 --- a/netann/host_ann.go +++ b/netann/host_ann.go @@ -168,15 +168,18 @@ func (h *HostAnnouncer) hostWatcher() { // NodeAnnUpdater describes a function that's able to update our current node // announcement on disk. It returns the updated node announcement given a set // of updates to be applied to the current node announcement. -type NodeAnnUpdater func(refresh bool, modifier ...NodeAnnModifier, +type NodeAnnUpdater func(modifier ...NodeAnnModifier, ) (lnwire.NodeAnnouncement, error) // IPAnnouncer is a factory function that generates a new function that uses // the passed annUpdater function to to announce new IP changes for a given // host. -func IPAnnouncer(annUpdater NodeAnnUpdater) func([]net.Addr, map[string]struct{}) error { +func IPAnnouncer(annUpdater NodeAnnUpdater) func([]net.Addr, + map[string]struct{}) error { + return func(newAddrs []net.Addr, oldAddrs map[string]struct{}) error { - _, err := annUpdater(true, func(currentNodeAnn *lnwire.NodeAnnouncement) { + _, err := annUpdater(func( + currentNodeAnn *lnwire.NodeAnnouncement) { // To ensure we don't duplicate any addresses, we'll // filter out the same of addresses we should no longer // advertise. diff --git a/peer/brontide.go b/peer/brontide.go index 82b11e310..08cad15de 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -263,8 +263,8 @@ type Config struct { // GenNodeAnnouncement is used to send our node announcement to the remote // on startup. - GenNodeAnnouncement func(bool, - ...netann.NodeAnnModifier) (lnwire.NodeAnnouncement, error) + GenNodeAnnouncement func(...netann.NodeAnnModifier) ( + lnwire.NodeAnnouncement, error) // PrunePersistentPeerConnection is used to remove all internal state // related to this peer in the server. @@ -1036,7 +1036,7 @@ func (p *Brontide) maybeSendNodeAnn(channels []*channeldb.OpenChannel) { return } - ourNodeAnn, err := p.cfg.GenNodeAnnouncement(false) + ourNodeAnn, err := p.cfg.GenNodeAnnouncement() if err != nil { p.log.Debugf("Unable to retrieve node announcement: %v", err) return diff --git a/rpcserver.go b/rpcserver.go index 528ca18d2..9cdcb9ee6 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -762,10 +762,6 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, return s.featureMgr.Get(feature.SetInvoiceAmp) } - getNodeAnnouncement := func() (lnwire.NodeAnnouncement, error) { - return s.genNodeAnnouncement(false) - } - parseAddr := func(addr string) (net.Addr, error) { return parseAddr(addr, r.cfg.net) } @@ -786,7 +782,7 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, routerBackend, s.nodeSigner, s.graphDB, s.chanStateDB, s.sweeper, tower, s.towerClient, s.anchorTowerClient, r.cfg.net.ResolveTCPAddr, genInvoiceFeatures, - genAmpInvoiceFeatures, getNodeAnnouncement, + genAmpInvoiceFeatures, s.getNodeAnnouncement, s.updateAndBrodcastSelfNode, parseAddr, rpcsLog, s.aliasMgr.GetPeerAlias, ) @@ -2910,11 +2906,8 @@ func (r *rpcServer) GetInfo(_ context.Context, // Check if external IP addresses were provided to lnd and use them // to set the URIs. - nodeAnn, err := r.server.genNodeAnnouncement(false) - if err != nil { - return nil, fmt.Errorf("unable to retrieve current fully signed "+ - "node announcement: %v", err) - } + nodeAnn := r.server.getNodeAnnouncement() + addrs := nodeAnn.Addresses uris := make([]string, len(addrs)) for i, addr := range addrs { diff --git a/server.go b/server.go index 97aad7a02..9957abbda 100644 --- a/server.go +++ b/server.go @@ -993,15 +993,18 @@ func newServer(cfg *Config, listenAddrs []net.Addr, } s.authGossiper = discovery.New(discovery.Config{ - Router: s.chanRouter, - Notifier: s.cc.ChainNotifier, - ChainHash: *s.cfg.ActiveNetParams.GenesisHash, - Broadcast: s.BroadcastMessage, - ChanSeries: chanSeries, - NotifyWhenOnline: s.NotifyWhenOnline, - NotifyWhenOffline: s.NotifyWhenOffline, - SelfNodeAnnouncement: func(refresh bool) (lnwire.NodeAnnouncement, error) { - return s.genNodeAnnouncement(refresh) + Router: s.chanRouter, + Notifier: s.cc.ChainNotifier, + ChainHash: *s.cfg.ActiveNetParams.GenesisHash, + Broadcast: s.BroadcastMessage, + ChanSeries: chanSeries, + NotifyWhenOnline: s.NotifyWhenOnline, + NotifyWhenOffline: s.NotifyWhenOffline, + FetchSelfAnnouncement: s.getNodeAnnouncement, + UpdateSelfAnnouncement: func() (lnwire.NodeAnnouncement, + error) { + + return s.genNodeAnnouncement() }, ProofMatureDelta: 0, TrickleDelay: time.Millisecond * time.Duration(cfg.TrickleDelay), @@ -1271,6 +1274,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, return ourPolicy, err } + //nolint:lll s.fundingMgr, err = funding.NewFundingManager(funding.Config{ NoWumboChans: !cfg.ProtocolOptions.Wumbo(), IDKey: nodeKeyDesc.PubKey, @@ -1283,8 +1287,10 @@ func newServer(cfg *Config, listenAddrs []net.Addr, Notifier: cc.ChainNotifier, FeeEstimator: cc.FeeEstimator, SignMessage: cc.MsgSigner.SignMessage, - CurrentNodeAnnouncement: func() (lnwire.NodeAnnouncement, error) { - return s.genNodeAnnouncement(true) + CurrentNodeAnnouncement: func() (lnwire.NodeAnnouncement, + error) { + + return s.genNodeAnnouncement() }, SendAnnouncement: s.authGossiper.ProcessLocalAnnouncement, NotifyWhenOnline: s.NotifyWhenOnline, @@ -2476,12 +2482,8 @@ out: // throughout the network. We'll only include addresses // that have a different IP from the previous one, as // the previous IP is no longer valid. - currentNodeAnn, err := s.genNodeAnnouncement(false) - if err != nil { - srvrLog.Debugf("Unable to retrieve current "+ - "node announcement: %v", err) - continue - } + currentNodeAnn := s.getNodeAnnouncement() + for _, addr := range currentNodeAnn.Addresses { host, _, err := net.SplitHostPort(addr.String()) if err != nil { @@ -2503,7 +2505,7 @@ out: // announcement with the updated addresses and broadcast // it to our peers. newNodeAnn, err := s.genNodeAnnouncement( - true, netann.NodeAnnSetAddrs(newAddrs), + netann.NodeAnnSetAddrs(newAddrs), ) if err != nil { srvrLog.Debugf("Unable to generate new node "+ @@ -2892,7 +2894,7 @@ func (s *server) createNewHiddenService() error { // Now that the onion service has been created, we'll add the onion // address it can be reached at to our list of advertised addresses. newNodeAnn, err := s.genNodeAnnouncement( - true, func(currentAnn *lnwire.NodeAnnouncement) { + func(currentAnn *lnwire.NodeAnnouncement) { currentAnn.Addresses = append(currentAnn.Addresses, addr) }, ) @@ -2942,20 +2944,22 @@ func (s *server) findChannel(node *btcec.PublicKey, chanID lnwire.ChannelID) ( return nil, fmt.Errorf("unable to find channel") } -// genNodeAnnouncement generates and returns the current fully signed node -// announcement. If refresh is true, then the time stamp of the announcement -// will be updated in order to ensure it propagates through the network. -func (s *server) genNodeAnnouncement(refresh bool, - modifiers ...netann.NodeAnnModifier) (lnwire.NodeAnnouncement, error) { - +// getNodeAnnouncement fetches the current, fully signed node announcement. +func (s *server) getNodeAnnouncement() lnwire.NodeAnnouncement { s.mu.Lock() defer s.mu.Unlock() - // If we don't need to refresh the announcement, then we can return a - // copy of our cached version. - if !refresh { - return *s.currentNodeAnn, nil - } + return *s.currentNodeAnn +} + +// genNodeAnnouncement generates and returns the current fully signed node +// announcement. The time stamp of the announcement will be updated in order +// to ensure it propagates through the network. +func (s *server) genNodeAnnouncement(modifiers ...netann.NodeAnnModifier) ( + lnwire.NodeAnnouncement, error) { + + s.mu.Lock() + defer s.mu.Unlock() // Always update the timestamp when refreshing to ensure the update // propagates. @@ -2966,8 +2970,7 @@ func (s *server) genNodeAnnouncement(refresh bool, modifier(s.currentNodeAnn) } - // Otherwise, we'll sign a new update after applying all of the passed - // modifiers. + // Sign a new update after applying all of the passed modifiers. err := netann.SignNodeAnnouncement( s.nodeSigner, s.identityKeyLoc, s.currentNodeAnn, ) @@ -2985,7 +2988,7 @@ func (s *server) genNodeAnnouncement(refresh bool, func (s *server) updateAndBrodcastSelfNode( modifiers ...netann.NodeAnnModifier) error { - newNodeAnn, err := s.genNodeAnnouncement(true, modifiers...) + newNodeAnn, err := s.genNodeAnnouncement(modifiers...) if err != nil { return fmt.Errorf("unable to generate new node "+ "announcement: %v", err) diff --git a/subrpcserver_config.go b/subrpcserver_config.go index 2723efaea..8de66d1ad 100644 --- a/subrpcserver_config.go +++ b/subrpcserver_config.go @@ -118,7 +118,7 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config, tcpResolver lncfg.TCPResolver, genInvoiceFeatures func() *lnwire.FeatureVector, genAmpInvoiceFeatures func() *lnwire.FeatureVector, - getNodeAnnouncement func() (lnwire.NodeAnnouncement, error), + getNodeAnnouncement func() lnwire.NodeAnnouncement, updateNodeAnnouncement func(modifiers ...netann.NodeAnnModifier) error, parseAddr func(addr string) (net.Addr, error), rpcLogger btclog.Logger, From 1a9d743e1863809e4500184dff2398853b05ca36 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 3 Apr 2023 13:31:53 +0200 Subject: [PATCH 03/12] lnwire: add feature vector update validation helpers --- lnwire/features.go | 57 +++++++++++++++++++++++ lnwire/features_test.go | 100 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/lnwire/features.go b/lnwire/features.go index 76dab3b05..21b7fbc02 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -3,6 +3,7 @@ package lnwire import ( "encoding/binary" "errors" + "fmt" "io" ) @@ -10,6 +11,11 @@ var ( // ErrFeaturePairExists signals an error in feature vector construction // where the opposing bit in a feature pair has already been set. ErrFeaturePairExists = errors.New("feature pair exists") + + // ErrFeatureStandard is returned when attempts to modify LND's known + // set of features are made. + ErrFeatureStandard = errors.New("feature is used in standard " + + "protocol set") ) // FeatureBit represents a feature that can be enabled in either a local or @@ -344,6 +350,57 @@ func (fv *RawFeatureVector) Merge(other *RawFeatureVector) error { return nil } +// ValidateUpdate checks whether a feature vector can safely be updated to the +// new feature vector provided, checking that it does not alter any of the +// "standard" features that are defined by LND. The new feature vector should +// be inclusive of all features in the original vector that it still wants to +// advertise, setting and unsetting updates as desired. +func (fv *RawFeatureVector) ValidateUpdate(other *RawFeatureVector) error { + // Run through the new set of features and check that we're not adding + // any feature bits that are defined but not set in LND. + for feature := range other.features { + if fv.IsSet(feature) { + continue + } + + if name, known := Features[feature]; known { + return fmt.Errorf("can't set feature "+ + "bit %d (%v): %w", feature, name, + ErrFeatureStandard) + } + } + + // Check that the new feature vector for this set does not unset any + // features that are standard in LND by comparing the features in our + // current set to the omitted values in the new set. + for feature := range fv.features { + if other.IsSet(feature) { + continue + } + + if name, known := Features[feature]; known { + return fmt.Errorf("can't unset feature "+ + "bit %d (%v): %w", feature, name, + ErrFeatureStandard) + } + } + + return nil +} + +// ValidatePairs checks each feature bit in a raw vector to ensure that the +// opposing bit is not set, validating that the vector has either the optional +// or required bit set, not both. +func (fv *RawFeatureVector) ValidatePairs() error { + for feature := range fv.features { + if _, ok := fv.features[feature^1]; ok { + return ErrFeaturePairExists + } + } + + return nil +} + // Clone makes a copy of a feature vector. func (fv *RawFeatureVector) Clone() *RawFeatureVector { newFeatures := NewRawFeatureVector() diff --git a/lnwire/features_test.go b/lnwire/features_test.go index a945eeb3e..84053b963 100644 --- a/lnwire/features_test.go +++ b/lnwire/features_test.go @@ -396,3 +396,103 @@ func TestIsEmptyFeatureVector(t *testing.T) { fv.Unset(StaticRemoteKeyOptional) require.True(t, fv.IsEmpty()) } + +// TestValidatePairs tests that feature vectors can only set the required or +// optional feature bit in a pair, not both. +func TestValidatePairs(t *testing.T) { + t.Parallel() + + rfv := NewRawFeatureVector( + StaticRemoteKeyOptional, + StaticRemoteKeyRequired, + ) + require.Equal(t, ErrFeaturePairExists, rfv.ValidatePairs()) + + rfv = NewRawFeatureVector( + StaticRemoteKeyOptional, + PaymentAddrRequired, + ) + require.Nil(t, rfv.ValidatePairs()) +} + +// TestValidateUpdate tests validation of an update to a feature vector. +func TestValidateUpdate(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + currentFeatures []FeatureBit + newFeatures []FeatureBit + err error + }{ + { + name: "defined feature bit set, can include", + currentFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + }, + newFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + }, + err: nil, + }, + { + name: "defined feature bit not already set", + currentFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + }, + newFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + PaymentAddrRequired, + }, + err: ErrFeatureStandard, + }, + { + name: "known feature missing", + currentFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + PaymentAddrRequired, + }, + newFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + }, + err: ErrFeatureStandard, + }, + { + name: "can set unknown feature", + currentFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + }, + newFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + FeatureBit(1001), + }, + err: nil, + }, + { + name: "can unset unknown feature", + currentFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + FeatureBit(1001), + }, + newFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + }, + err: nil, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + currentFV := NewRawFeatureVector( + testCase.currentFeatures..., + ) + newFV := NewRawFeatureVector(testCase.newFeatures...) + + err := currentFV.ValidateUpdate(newFV) + require.ErrorIs(t, err, testCase.err) + }) + } +} From 5a139fd29769d0a025740390eea9a0196ebc7f68 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 3 Apr 2023 14:07:23 +0200 Subject: [PATCH 04/12] feature: add update function for safe feature update Disallow update of any features that are defined by LND (since just updating the feature, but not the functionality will result in strange behavior). All known feature bits should be toggled using protocol config options. --- feature/manager.go | 42 ++++++++++++ feature/manager_internal_test.go | 110 +++++++++++++++++++++++++++++++ feature/set.go | 11 ++++ 3 files changed, 163 insertions(+) diff --git a/feature/manager.go b/feature/manager.go index 26a3d4a31..f788f9892 100644 --- a/feature/manager.go +++ b/feature/manager.go @@ -1,11 +1,16 @@ package feature import ( + "errors" "fmt" "github.com/lightningnetwork/lnd/lnwire" ) +// ErrUnknownSet is returned if a proposed feature vector contains a set that +// is unknown to LND. +var ErrUnknownSet = errors.New("unknown feature bit set") + // Config houses any runtime modifications to the default set descriptors. For // our purposes, this typically means disabling certain features to test legacy // protocol interoperability or functionality. @@ -198,3 +203,40 @@ func (m *Manager) ListSets() []Set { return sets } + +// UpdateFeatureSets accepts a map of new feature vectors for each of the +// manager's known sets, validates that the update can be applied and modifies +// the feature manager's internal state. If a set is not included in the update +// map, it is left unchanged. The feature vectors provided are expected to +// include the current set of features, updated with desired bits added/removed. +func (m *Manager) UpdateFeatureSets( + updates map[Set]*lnwire.RawFeatureVector) error { + + for set, newFeatures := range updates { + if !set.valid() { + return fmt.Errorf("%w: set: %d", ErrUnknownSet, set) + } + + if err := newFeatures.ValidatePairs(); err != nil { + return err + } + + if err := m.Get(set).ValidateUpdate(newFeatures); err != nil { + return err + } + + fv := lnwire.NewFeatureVector(newFeatures, lnwire.Features) + if err := ValidateDeps(fv); err != nil { + return err + } + } + + // Only update the current feature sets once every proposed set has + // passed validation so that we don't partially update any sets then + // fail out on a later set's validation. + for set, features := range updates { + m.SetRaw(set, features.Clone()) + } + + return nil +} diff --git a/feature/manager_internal_test.go b/feature/manager_internal_test.go index 8debddcbe..04422cd9b 100644 --- a/feature/manager_internal_test.go +++ b/feature/manager_internal_test.go @@ -135,3 +135,113 @@ func testManager(t *testing.T, test managerTest) { assertSet(lnwire.StaticRemoteKeyOptional) } } + +// TestUpdateFeatureSets tests validation of the update of various features in +// each of our sets, asserting that the feature set is not partially modified +// if one set in incorrectly specified. +func TestUpdateFeatureSets(t *testing.T) { + t.Parallel() + + // Use a reduced set description to make reasoning about our sets + // easier. + setDesc := setDesc{ + lnwire.DataLossProtectRequired: { + SetInit: {}, // I + SetNodeAnn: {}, // N + }, + lnwire.GossipQueriesOptional: { + SetNodeAnn: {}, // N + }, + } + + testCases := []struct { + name string + features map[Set]*lnwire.RawFeatureVector + err error + }{ + { + name: "unknown set", + features: map[Set]*lnwire.RawFeatureVector{ + setSentinel + 1: lnwire.NewRawFeatureVector(), + }, + err: ErrUnknownSet, + }, + { + name: "invalid pairwise feature", + features: map[Set]*lnwire.RawFeatureVector{ + SetNodeAnn: lnwire.NewRawFeatureVector( + lnwire.FeatureBit(1000), + lnwire.FeatureBit(1001), + ), + }, + err: lnwire.ErrFeaturePairExists, + }, + { + name: "error in one set", + features: map[Set]*lnwire.RawFeatureVector{ + SetNodeAnn: lnwire.NewRawFeatureVector( + lnwire.FeatureBit(1000), + lnwire.FeatureBit(1001), + ), + SetInit: lnwire.NewRawFeatureVector( + lnwire.DataLossProtectRequired, + ), + }, + err: lnwire.ErrFeaturePairExists, + }, + { + name: "update existing sets ok", + features: map[Set]*lnwire.RawFeatureVector{ + SetInit: lnwire.NewRawFeatureVector( + lnwire.DataLossProtectRequired, + lnwire.FeatureBit(1001), + ), + SetNodeAnn: lnwire.NewRawFeatureVector( + lnwire.DataLossProtectRequired, + lnwire.GossipQueriesOptional, + lnwire.FeatureBit(1000), + ), + }, + }, + { + name: "update new, valid set ok", + features: map[Set]*lnwire.RawFeatureVector{ + SetInvoice: lnwire.NewRawFeatureVector( + lnwire.FeatureBit(1001), + ), + }, + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + featureMgr, err := newManager(Config{}, setDesc) + require.NoError(t, err) + + err = featureMgr.UpdateFeatureSets(testCase.features) + require.ErrorIs(t, err, testCase.err) + + // Compare the feature manager's sets to the updated + // set if no error was hit, otherwise assert that it + // is unchanged. + expected := testCase.features + actual := featureMgr + if err != nil { + originalMgr, err := newManager( + Config{}, setDesc, + ) + require.NoError(t, err) + expected = originalMgr.fsets + } + + for set, expectedFeatures := range expected { + actualSet := actual.GetRaw(set) + require.True(t, + actualSet.Equals(expectedFeatures)) + } + }) + } +} diff --git a/feature/set.go b/feature/set.go index 435fac1a4..c70637031 100644 --- a/feature/set.go +++ b/feature/set.go @@ -26,8 +26,19 @@ const ( // SetInvoiceAmp identifies the features that should be advertised on // AMP invoices generated by the daemon. SetInvoiceAmp + + // setSentinel is used to mark the end of our known sets. This enum + // member must *always* be the last item in the iota list to ensure + // that validation works as expected. + setSentinel ) +// valid returns a boolean indicating whether a set value is one of our +// predefined feature sets. +func (s Set) valid() bool { + return s < setSentinel +} + // String returns a human-readable description of a Set. func (s Set) String() string { switch s { From 019127c4f4635cbb345cb5cfbb88856a848b0026 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 25 Apr 2023 11:01:52 -0400 Subject: [PATCH 05/12] multi: add restriction on maximum feature bit in invoices Base 32 encoded bolt 11 invoices only allow 10 bits to express the length of the feature vector in a tagged field, so there is a much lower limit on the values invoice custom features can hold. Other places in the protocol are theoretically limited by the maximum message size, but since we express a feature bit as u16 we don't need to be concerned about this. The decision is made to track maximum per-set in the feature manager, which is conceptually aware of sets and then validate in lnwire/features against some arbitrary maximum value provided to the caller to keep the base features package unaware of sets. --- feature/manager.go | 4 +++- feature/set.go | 25 +++++++++++++++++++++++++ lnwire/features.go | 39 +++++++++++++++++++++++++++------------ lnwire/features_test.go | 34 +++++++++++++++++++++++++++++++++- 4 files changed, 88 insertions(+), 14 deletions(-) diff --git a/feature/manager.go b/feature/manager.go index f788f9892..eaaa0294c 100644 --- a/feature/manager.go +++ b/feature/manager.go @@ -221,7 +221,9 @@ func (m *Manager) UpdateFeatureSets( return err } - if err := m.Get(set).ValidateUpdate(newFeatures); err != nil { + if err := m.Get(set).ValidateUpdate( + newFeatures, set.Maximum(), + ); err != nil { return err } diff --git a/feature/set.go b/feature/set.go index c70637031..9be2c4315 100644 --- a/feature/set.go +++ b/feature/set.go @@ -1,5 +1,11 @@ package feature +import ( + "math" + + "github.com/lightningnetwork/lnd/lnwire" +) + // Set is an enum identifying various feature sets, which separates the single // feature namespace into distinct categories depending what context a feature // vector is being used. @@ -56,3 +62,22 @@ func (s Set) String() string { return "SetUnknown" } } + +// Maximum returns the maximum allowable value for a feature bit in the context +// of a set. The maximum feature value we can express differs by set context +// because the amount of space available varies between protocol messages. In +// practice this should never be a problem (reasonably one would never hit +// these high ranges), but we enforce these maximums for the sake of sane +// validation. +func (s Set) Maximum() lnwire.FeatureBit { + switch s { + case SetInvoice, SetInvoiceAmp: + return lnwire.MaxBolt11Feature + + // The space available in other sets is > math.MaxUint16, so we just + // return the maximum value our expression of a feature bit allows so + // that any value will pass. + default: + return math.MaxUint16 + } +} diff --git a/lnwire/features.go b/lnwire/features.go index 21b7fbc02..dd0abc392 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -16,6 +16,10 @@ var ( // set of features are made. ErrFeatureStandard = errors.New("feature is used in standard " + "protocol set") + + // ErrFeatureBitMaximum is returned when a feature bit exceeds the + // maximum allowable value. + ErrFeatureBitMaximum = errors.New("feature bit exceeds allowed maximum") ) // FeatureBit represents a feature that can be enabled in either a local or @@ -227,17 +231,18 @@ const ( // TODO: Decide on actual feature bit value. ScriptEnforcedLeaseOptional FeatureBit = 2023 - // maxAllowedSize is a maximum allowed size of feature vector. + // MaxBolt11Feature is the maximum feature bit value allowed in bolt 11 + // invoices. // - // NOTE: Within the protocol, the maximum allowed message size is 65535 - // bytes for all messages. Accounting for the overhead within the feature - // message to signal the type of message, that leaves us with 65533 bytes - // for the init message itself. Next, we reserve 4 bytes to encode the - // lengths of both the local and global feature vectors, so 65529 bytes - // for the local and global features. Knocking off one byte for the sake - // of the calculation, that leads us to 32764 bytes for each feature - // vector, or 131056 different features. - maxAllowedSize = 32764 + // The base 32 encoded tagged fields in invoices are limited to 10 bits + // to express the length of the field's data. + //nolint:lll + // See: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md#tagged-fields + // + // With a maximum length field of 1023 (2^10 -1) and 5 bit encoding, + // the highest feature bit that can be expressed is: + // 1023 * 5 - 1 = 5114. + MaxBolt11Feature = 5114 ) // IsRequired returns true if the feature bit is even, and false otherwise. @@ -354,8 +359,12 @@ func (fv *RawFeatureVector) Merge(other *RawFeatureVector) error { // new feature vector provided, checking that it does not alter any of the // "standard" features that are defined by LND. The new feature vector should // be inclusive of all features in the original vector that it still wants to -// advertise, setting and unsetting updates as desired. -func (fv *RawFeatureVector) ValidateUpdate(other *RawFeatureVector) error { +// advertise, setting and unsetting updates as desired. Features in the vector +// are also checked against a maximum inclusive value, as feature vectors in +// different contexts have different maximum values. +func (fv *RawFeatureVector) ValidateUpdate(other *RawFeatureVector, + maximumValue FeatureBit) error { + // Run through the new set of features and check that we're not adding // any feature bits that are defined but not set in LND. for feature := range other.features { @@ -363,6 +372,12 @@ func (fv *RawFeatureVector) ValidateUpdate(other *RawFeatureVector) error { continue } + if feature > maximumValue { + return fmt.Errorf("can't set feature bit %d: %w %v", + feature, ErrFeatureBitMaximum, + maximumValue) + } + if name, known := Features[feature]; known { return fmt.Errorf("can't set feature "+ "bit %d (%v): %w", feature, name, diff --git a/lnwire/features_test.go b/lnwire/features_test.go index 84053b963..c77ddd4d1 100644 --- a/lnwire/features_test.go +++ b/lnwire/features_test.go @@ -2,6 +2,7 @@ package lnwire import ( "bytes" + "math" "reflect" "sort" "testing" @@ -423,6 +424,7 @@ func TestValidateUpdate(t *testing.T) { name string currentFeatures []FeatureBit newFeatures []FeatureBit + maximumValue FeatureBit err error }{ { @@ -479,6 +481,30 @@ func TestValidateUpdate(t *testing.T) { }, err: nil, }, + { + name: "at allowed maximum", + currentFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + }, + newFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + 100, + }, + maximumValue: 100, + err: nil, + }, + { + name: "above allowed maximum", + currentFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + }, + newFeatures: []FeatureBit{ + StaticRemoteKeyOptional, + 101, + }, + maximumValue: 100, + err: ErrFeatureBitMaximum, + }, } for _, testCase := range testCases { @@ -491,7 +517,13 @@ func TestValidateUpdate(t *testing.T) { ) newFV := NewRawFeatureVector(testCase.newFeatures...) - err := currentFV.ValidateUpdate(newFV) + // Set maximum value if not populated in the test case. + maximumValue := testCase.maximumValue + if testCase.maximumValue == 0 { + maximumValue = math.MaxUint16 + } + + err := currentFV.ValidateUpdate(newFV, maximumValue) require.ErrorIs(t, err, testCase.err) }) } From e41c65785f3064a1d2853fd820dc9c56d79aecdd Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 3 Apr 2023 11:27:14 +0200 Subject: [PATCH 06/12] multi: update node announcement features in feature manager first Move merge of our features into the feature manager, rather than updating our node announcement then retrospectively setting the feature manger's set to the new vector. This allows us to use the feature vector as our single source of truth for announcements. --- itest/lnd_channel_graph_test.go | 30 +++++++++++++--- lnrpc/peersrpc/config_active.go | 9 +++-- lnrpc/peersrpc/peers_server.go | 25 ++++++++------ netann/host_ann.go | 2 +- server.go | 61 +++++++++++++++++++++++---------- subrpcserver_config.go | 3 +- 6 files changed, 90 insertions(+), 40 deletions(-) diff --git a/itest/lnd_channel_graph_test.go b/itest/lnd_channel_graph_test.go index ed4dfa25a..73a9266b7 100644 --- a/itest/lnd_channel_graph_test.go +++ b/itest/lnd_channel_graph_test.go @@ -14,6 +14,7 @@ import ( "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" ) @@ -491,10 +492,29 @@ func testUpdateNodeAnnouncement(ht *lntest.HarnessTest) { ) } - // This feature bit is used to test that our endpoint sets/unsets - // feature bits properly. If the current FeatureBit is set by default - // update this one for another one unset by default at random. - featureBit := lnrpc.FeatureBit_WUMBO_CHANNELS_REQ + // Test that we can't modify a feature bit that is known to LND. + reservedFeatureBit := lnrpc.FeatureBit_WUMBO_CHANNELS_REQ + _, known := lnwire.Features[lnwire.FeatureBit(reservedFeatureBit)] + require.True(ht, known, "feature bit should be known to lnd") + + nodeAnnReq := &peersrpc.NodeAnnouncementUpdateRequest{ + FeatureUpdates: []*peersrpc.UpdateFeatureAction{ + { + Action: peersrpc.UpdateAction_ADD, + FeatureBit: reservedFeatureBit, + }, + }, + } + + // Node announcement should fail because we're not allowed to alter + // "known" feature bits. + dave.RPC.UpdateNodeAnnouncementErr(nodeAnnReq) + + // We also set an unknown feature bit (which we should be able to + // set/unset) and test that we can update it accordingly. + featureBit := lnrpc.FeatureBit(999) + _, known = lnwire.Features[lnwire.FeatureBit(featureBit)] + require.False(ht, known, "feature bit should be unknown to lnd") featureIdx := uint32(featureBit) _, ok := resp.Features[featureIdx] require.False(ht, ok, "unexpected feature bit enabled by default") @@ -566,7 +586,7 @@ func testUpdateNodeAnnouncement(ht *lntest.HarnessTest) { }, } - nodeAnnReq := &peersrpc.NodeAnnouncementUpdateRequest{ + nodeAnnReq = &peersrpc.NodeAnnouncementUpdateRequest{ Alias: newAlias, Color: newColor, AddressUpdates: updateAddressActions, diff --git a/lnrpc/peersrpc/config_active.go b/lnrpc/peersrpc/config_active.go index 34da09699..4a2f028e4 100644 --- a/lnrpc/peersrpc/config_active.go +++ b/lnrpc/peersrpc/config_active.go @@ -23,7 +23,10 @@ type Config struct { // ParseAddr parses an address from its string format to a net.Addr. ParseAddr func(addr string) (net.Addr, error) - // UpdateNodeAnnouncement updates our node announcement applying the - // given NodeAnnModifiers and broadcasts the new version to the network. - UpdateNodeAnnouncement func(...netann.NodeAnnModifier) error + // UpdateNodeAnnouncement updates and broadcasts our node announcement, + // setting the feature vector provided and applying the + // NodeAnnModifiers. If no feature updates are required, a nil feature + // vector should be provided. + UpdateNodeAnnouncement func(features *lnwire.RawFeatureVector, + mods ...netann.NodeAnnModifier) error } diff --git a/lnrpc/peersrpc/peers_server.go b/lnrpc/peersrpc/peers_server.go index 5710f5fca..6981f7949 100644 --- a/lnrpc/peersrpc/peers_server.go +++ b/lnrpc/peersrpc/peers_server.go @@ -315,20 +315,21 @@ func (s *Server) UpdateNodeAnnouncement(_ context.Context, currentNodeAnn := s.cfg.GetNodeAnnouncement() - if len(req.FeatureUpdates) > 0 { - features, ops, err := s.updateFeatures( - currentNodeAnn.Features, - req.FeatureUpdates, + nodeAnnFeatures := currentNodeAnn.Features + featureUpdates := len(req.FeatureUpdates) > 0 + if featureUpdates { + var ( + ops *lnrpc.Op + err error + ) + nodeAnnFeatures, ops, err = s.updateFeatures( + nodeAnnFeatures, req.FeatureUpdates, ) if err != nil { return nil, fmt.Errorf("error trying to update node "+ "features: %w", err) } resp.Ops = append(resp.Ops, ops) - nodeModifiers = append( - nodeModifiers, - netann.NodeAnnSetFeatures(features), - ) } if req.Color != "" { @@ -386,12 +387,14 @@ func (s *Server) UpdateNodeAnnouncement(_ context.Context, ) } - if len(nodeModifiers) == 0 { - return nil, fmt.Errorf("unable detect any new values to " + + if len(nodeModifiers) == 0 && !featureUpdates { + return nil, fmt.Errorf("unable to detect any new values to " + "update the node announcement") } - if err := s.cfg.UpdateNodeAnnouncement(nodeModifiers...); err != nil { + if err := s.cfg.UpdateNodeAnnouncement( + nodeAnnFeatures, nodeModifiers..., + ); err != nil { return nil, err } diff --git a/netann/host_ann.go b/netann/host_ann.go index 767c3089c..fdb48e839 100644 --- a/netann/host_ann.go +++ b/netann/host_ann.go @@ -178,7 +178,7 @@ func IPAnnouncer(annUpdater NodeAnnUpdater) func([]net.Addr, map[string]struct{}) error { return func(newAddrs []net.Addr, oldAddrs map[string]struct{}) error { - _, err := annUpdater(func( + _, err := annUpdater(nil, func( currentNodeAnn *lnwire.NodeAnnouncement) { // To ensure we don't duplicate any addresses, we'll // filter out the same of addresses we should no longer diff --git a/server.go b/server.go index 9957abbda..d70324d81 100644 --- a/server.go +++ b/server.go @@ -1004,7 +1004,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, UpdateSelfAnnouncement: func() (lnwire.NodeAnnouncement, error) { - return s.genNodeAnnouncement() + return s.genNodeAnnouncement(nil) }, ProofMatureDelta: 0, TrickleDelay: time.Millisecond * time.Duration(cfg.TrickleDelay), @@ -1290,7 +1290,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, CurrentNodeAnnouncement: func() (lnwire.NodeAnnouncement, error) { - return s.genNodeAnnouncement() + return s.genNodeAnnouncement(nil) }, SendAnnouncement: s.authGossiper.ProcessLocalAnnouncement, NotifyWhenOnline: s.NotifyWhenOnline, @@ -1631,8 +1631,15 @@ func newServer(cfg *Config, listenAddrs []net.Addr, cfg.net.ResolveTCPAddr, ) }, - AdvertisedIPs: advertisedIPs, - AnnounceNewIPs: netann.IPAnnouncer(s.genNodeAnnouncement), + AdvertisedIPs: advertisedIPs, + AnnounceNewIPs: netann.IPAnnouncer( + func(modifier ...netann.NodeAnnModifier) ( + lnwire.NodeAnnouncement, error) { + + return s.genNodeAnnouncement( + nil, modifier..., + ) + }), }) } @@ -2505,7 +2512,7 @@ out: // announcement with the updated addresses and broadcast // it to our peers. newNodeAnn, err := s.genNodeAnnouncement( - netann.NodeAnnSetAddrs(newAddrs), + nil, netann.NodeAnnSetAddrs(newAddrs), ) if err != nil { srvrLog.Debugf("Unable to generate new node "+ @@ -2894,7 +2901,7 @@ func (s *server) createNewHiddenService() error { // Now that the onion service has been created, we'll add the onion // address it can be reached at to our list of advertised addresses. newNodeAnn, err := s.genNodeAnnouncement( - func(currentAnn *lnwire.NodeAnnouncement) { + nil, func(currentAnn *lnwire.NodeAnnouncement) { currentAnn.Addresses = append(currentAnn.Addresses, addr) }, ) @@ -2955,12 +2962,31 @@ func (s *server) getNodeAnnouncement() lnwire.NodeAnnouncement { // genNodeAnnouncement generates and returns the current fully signed node // announcement. The time stamp of the announcement will be updated in order // to ensure it propagates through the network. -func (s *server) genNodeAnnouncement(modifiers ...netann.NodeAnnModifier) ( - lnwire.NodeAnnouncement, error) { +func (s *server) genNodeAnnouncement(features *lnwire.RawFeatureVector, + modifiers ...netann.NodeAnnModifier) (lnwire.NodeAnnouncement, error) { s.mu.Lock() defer s.mu.Unlock() + // First, try to update our feature manager with the updated set of + // features. + if features != nil { + proposedFeatures := map[feature.Set]*lnwire.RawFeatureVector{ + feature.SetNodeAnn: features, + } + err := s.featureMgr.UpdateFeatureSets(proposedFeatures) + if err != nil { + return lnwire.NodeAnnouncement{}, err + } + + // If we could successfully update our feature manager, add + // an update modifier to include these new features to our + // set. + modifiers = append( + modifiers, netann.NodeAnnSetFeatures(features), + ) + } + // Always update the timestamp when refreshing to ensure the update // propagates. modifiers = append(modifiers, netann.NodeAnnSetTimestamp) @@ -2985,10 +3011,10 @@ func (s *server) genNodeAnnouncement(modifiers ...netann.NodeAnnModifier) ( // applying the giving modifiers and updating the time stamp // to ensure it propagates through the network. Then it brodcasts // it to the network. -func (s *server) updateAndBrodcastSelfNode( +func (s *server) updateAndBrodcastSelfNode(features *lnwire.RawFeatureVector, modifiers ...netann.NodeAnnModifier) error { - newNodeAnn, err := s.genNodeAnnouncement(modifiers...) + newNodeAnn, err := s.genNodeAnnouncement(features, modifiers...) if err != nil { return fmt.Errorf("unable to generate new node "+ "announcement: %v", err) @@ -3006,9 +3032,7 @@ func (s *server) updateAndBrodcastSelfNode( selfNode.LastUpdate = time.Unix(int64(newNodeAnn.Timestamp), 0) selfNode.Addresses = newNodeAnn.Addresses selfNode.Alias = newNodeAnn.Alias.String() - selfNode.Features = lnwire.NewFeatureVector( - newNodeAnn.Features, lnwire.Features, - ) + selfNode.Features = s.featureMgr.Get(feature.SetNodeAnn) selfNode.Color = newNodeAnn.RGBColor selfNode.AuthSigBytes = newNodeAnn.Signature.ToSignatureBytes() @@ -3018,11 +3042,6 @@ func (s *server) updateAndBrodcastSelfNode( return fmt.Errorf("can't set self node: %v", err) } - // Update the feature bits for the SetNodeAnn in case they changed. - s.featureMgr.SetRaw( - feature.SetNodeAnn, selfNode.Features.RawFeatureVector, - ) - // Finally, propagate it to the nodes in the network. err = s.BroadcastMessage(nil, &newNodeAnn) if err != nil { @@ -3828,7 +3847,11 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, TowerClient: s.towerClient, AnchorTowerClient: s.anchorTowerClient, DisconnectPeer: s.DisconnectPeer, - GenNodeAnnouncement: s.genNodeAnnouncement, + GenNodeAnnouncement: func(...netann.NodeAnnModifier) ( + lnwire.NodeAnnouncement, error) { + + return s.genNodeAnnouncement(nil) + }, PongBuf: s.pongBuf, diff --git a/subrpcserver_config.go b/subrpcserver_config.go index 8de66d1ad..3e7e77bd9 100644 --- a/subrpcserver_config.go +++ b/subrpcserver_config.go @@ -119,7 +119,8 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config, genInvoiceFeatures func() *lnwire.FeatureVector, genAmpInvoiceFeatures func() *lnwire.FeatureVector, getNodeAnnouncement func() lnwire.NodeAnnouncement, - updateNodeAnnouncement func(modifiers ...netann.NodeAnnModifier) error, + updateNodeAnnouncement func(features *lnwire.RawFeatureVector, + modifiers ...netann.NodeAnnModifier) error, parseAddr func(addr string) (net.Addr, error), rpcLogger btclog.Logger, getAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error)) error { From 4e98f4da5e69f15ec6d6b5c7a785d566723dacce Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 4 Apr 2023 09:11:13 +0200 Subject: [PATCH 07/12] feature/refactor: unexport set raw --- feature/manager.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/feature/manager.go b/feature/manager.go index eaaa0294c..ecc7470da 100644 --- a/feature/manager.go +++ b/feature/manager.go @@ -181,8 +181,8 @@ func (m *Manager) GetRaw(set Set) *lnwire.RawFeatureVector { return lnwire.NewRawFeatureVector() } -// SetRaw sets a new raw feature vector for the given set. -func (m *Manager) SetRaw(set Set, raw *lnwire.RawFeatureVector) { +// setRaw sets a new raw feature vector for the given set. +func (m *Manager) setRaw(set Set, raw *lnwire.RawFeatureVector) { m.fsets[set] = raw } @@ -237,7 +237,7 @@ func (m *Manager) UpdateFeatureSets( // passed validation so that we don't partially update any sets then // fail out on a later set's validation. for set, features := range updates { - m.SetRaw(set, features.Clone()) + m.setRaw(set, features.Clone()) } return nil From 4e36e2fd2b24d8e81e7c3503315d9e7b2743ae35 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 3 Apr 2023 11:53:02 +0200 Subject: [PATCH 08/12] multi: add custom feature bits to config options --- feature/manager.go | 21 +++++++++++++++++++ lncfg/protocol_experimental_off.go | 10 +++++++++ lncfg/protocol_experimental_on.go | 33 ++++++++++++++++++++++++++++++ server.go | 2 ++ 4 files changed, 66 insertions(+) diff --git a/feature/manager.go b/feature/manager.go index ecc7470da..c1f036fd9 100644 --- a/feature/manager.go +++ b/feature/manager.go @@ -49,6 +49,10 @@ type Config struct { // NoAnySegwit unsets any bits that signal support for using other // segwit witness versions for co-op closes. NoAnySegwit bool + + // CustomFeatures is a set of custom features to advertise in each + // set. + CustomFeatures map[Set][]lnwire.FeatureBit } // Manager is responsible for generating feature vectors for different requested @@ -156,6 +160,23 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) { raw.Unset(lnwire.ShutdownAnySegwitRequired) } + for _, custom := range cfg.CustomFeatures[set] { + if custom > set.Maximum() { + return nil, fmt.Errorf("feature bit: %v "+ + "exceeds set: %v maximum: %v", custom, + set, set.Maximum()) + } + + if raw.IsSet(custom) { + return nil, fmt.Errorf("feature bit: %v "+ + "already set", custom) + } + + if err := raw.SafeSet(custom); err != nil { + return nil, fmt.Errorf("%w: could not set "+ + "feature: %d", err, custom) + } + } // Ensure that all of our feature sets properly set any // dependent features. fv := lnwire.NewFeatureVector(raw, lnwire.Features) diff --git a/lncfg/protocol_experimental_off.go b/lncfg/protocol_experimental_off.go index e88b4b52b..51c90ec38 100644 --- a/lncfg/protocol_experimental_off.go +++ b/lncfg/protocol_experimental_off.go @@ -3,6 +3,11 @@ package lncfg +import ( + "github.com/lightningnetwork/lnd/feature" + "github.com/lightningnetwork/lnd/lnwire" +) + // ExperimentalProtocol is a sub-config that houses any experimental protocol // features that also require a build-tag to activate. type ExperimentalProtocol struct { @@ -13,3 +18,8 @@ type ExperimentalProtocol struct { func (p ExperimentalProtocol) CustomMessageOverrides() []uint16 { return nil } + +// CustomFeatures returns a custom set of feature bits to advertise. +func (p ExperimentalProtocol) CustomFeatures() map[feature.Set][]lnwire.FeatureBit { + return map[feature.Set][]lnwire.FeatureBit{} +} diff --git a/lncfg/protocol_experimental_on.go b/lncfg/protocol_experimental_on.go index cf49890ae..bb23e83c2 100644 --- a/lncfg/protocol_experimental_on.go +++ b/lncfg/protocol_experimental_on.go @@ -3,12 +3,23 @@ package lncfg +import ( + "github.com/lightningnetwork/lnd/feature" + "github.com/lightningnetwork/lnd/lnwire" +) + // ExperimentalProtocol is a sub-config that houses any experimental protocol // features that also require a build-tag to activate. // //nolint:lll type ExperimentalProtocol struct { CustomMessage []uint16 `long:"custom-message" description:"allows the custom message apis to send and report messages with the protocol number provided that fall outside of the custom message number range."` + + CustomInit []uint16 `long:"custom-init" description:"custom feature bits to advertise in the node's init message"` + + CustomNodeAnn []uint16 `long:"custom-nodeann" description:"custom feature bits to advertise in the node's announcement message"` + + CustomInvoice []uint16 `long:"custom-invoice" description:"custom feature bits to advertise in the node's invoices"` } // CustomMessageOverrides returns the set of protocol messages that we override @@ -16,3 +27,25 @@ type ExperimentalProtocol struct { func (p ExperimentalProtocol) CustomMessageOverrides() []uint16 { return p.CustomMessage } + +// CustomFeatures returns a custom set of feature bits to advertise. +// +//nolint:lll +func (p ExperimentalProtocol) CustomFeatures() map[feature.Set][]lnwire.FeatureBit { + customFeatures := make(map[feature.Set][]lnwire.FeatureBit) + + setFeatures := func(set feature.Set, bits []uint16) { + for _, customFeature := range bits { + customFeatures[set] = append( + customFeatures[set], + lnwire.FeatureBit(customFeature), + ) + } + } + + setFeatures(feature.SetInit, p.CustomInit) + setFeatures(feature.SetNodeAnn, p.CustomNodeAnn) + setFeatures(feature.SetInvoice, p.CustomInvoice) + + return customFeatures +} diff --git a/server.go b/server.go index d70324d81..d698535e9 100644 --- a/server.go +++ b/server.go @@ -530,6 +530,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, readBufferPool, cfg.Workers.Read, pool.DefaultWorkerTimeout, ) + //nolint:lll featureMgr, err := feature.NewManager(feature.Config{ NoTLVOnion: cfg.ProtocolOptions.LegacyOnion(), NoStaticRemoteKey: cfg.ProtocolOptions.NoStaticRemoteKey(), @@ -540,6 +541,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, NoOptionScidAlias: !cfg.ProtocolOptions.ScidAlias(), NoZeroConf: !cfg.ProtocolOptions.ZeroConf(), NoAnySegwit: cfg.ProtocolOptions.NoAnySegwit(), + CustomFeatures: cfg.ProtocolOptions.ExperimentalProtocol.CustomFeatures(), }) if err != nil { return nil, err From fe7cad2695e5da99d73343a0e53a2550503ed141 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 3 Apr 2023 13:14:10 +0200 Subject: [PATCH 09/12] features: disallow unsetting of config-set features --- feature/manager.go | 42 +++++++++++++++++++++++++--- feature/manager_internal_test.go | 48 ++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/feature/manager.go b/feature/manager.go index c1f036fd9..e7db9899a 100644 --- a/feature/manager.go +++ b/feature/manager.go @@ -7,9 +7,15 @@ import ( "github.com/lightningnetwork/lnd/lnwire" ) -// ErrUnknownSet is returned if a proposed feature vector contains a set that -// is unknown to LND. -var ErrUnknownSet = errors.New("unknown feature bit set") +var ( + // ErrUnknownSet is returned if a proposed feature vector contains a + // set that is unknown to LND. + ErrUnknownSet = errors.New("unknown feature bit set") + + // ErrFeatureConfigured is returned if an attempt is made to unset a + // feature that was configured at startup. + ErrFeatureConfigured = errors.New("can't unset configured feature") +) // Config houses any runtime modifications to the default set descriptors. For // our purposes, this typically means disabling certain features to test legacy @@ -61,6 +67,11 @@ type Manager struct { // fsets is a static map of feature set to raw feature vectors. Requests // are fulfilled by cloning these internal feature vectors. fsets map[Set]*lnwire.RawFeatureVector + + // configFeatures is a set of custom features that were "hard set" in + // lnd's config that cannot be updated at runtime (as is the case with + // our "standard" features that are defined in LND). + configFeatures map[Set]*lnwire.FeatureVector } // NewManager creates a new feature Manager, applying any custom modifications @@ -99,6 +110,7 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) { } // Now, remove any features as directed by the config. + configFeatures := make(map[Set]*lnwire.FeatureVector) for set, raw := range fsets { if cfg.NoTLVOnion { raw.Unset(lnwire.TLVOnionPayloadOptional) @@ -177,6 +189,15 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) { "feature: %d", err, custom) } } + + // Track custom features separately so that we can check that + // they aren't unset in subsequent updates. If there is no + // entry for the set, the vector will just be empty. + configFeatures[set] = lnwire.NewFeatureVector( + lnwire.NewRawFeatureVector(cfg.CustomFeatures[set]...), + lnwire.Features, + ) + // Ensure that all of our feature sets properly set any // dependent features. fv := lnwire.NewFeatureVector(raw, lnwire.Features) @@ -188,7 +209,8 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) { } return &Manager{ - fsets: fsets, + fsets: fsets, + configFeatures: configFeatures, }, nil } @@ -248,6 +270,18 @@ func (m *Manager) UpdateFeatureSets( return err } + // If any features were configured for this set, ensure that + // they are still set in the new feature vector. + if cfgFeat, haveCfgFeat := m.configFeatures[set]; haveCfgFeat { + for feature := range cfgFeat.Features() { + if !newFeatures.IsSet(feature) { + return fmt.Errorf("%w: can't unset: "+ + "%d", ErrFeatureConfigured, + feature) + } + } + } + fv := lnwire.NewFeatureVector(newFeatures, lnwire.Features) if err := ValidateDeps(fv); err != nil { return err diff --git a/feature/manager_internal_test.go b/feature/manager_internal_test.go index 04422cd9b..ee716fba0 100644 --- a/feature/manager_internal_test.go +++ b/feature/manager_internal_test.go @@ -157,6 +157,7 @@ func TestUpdateFeatureSets(t *testing.T) { testCases := []struct { name string features map[Set]*lnwire.RawFeatureVector + config Config err error }{ { @@ -211,6 +212,49 @@ func TestUpdateFeatureSets(t *testing.T) { ), }, }, + { + name: "missing configured feature", + features: map[Set]*lnwire.RawFeatureVector{ + SetInit: lnwire.NewRawFeatureVector( + lnwire.DataLossProtectRequired, + ), + SetNodeAnn: lnwire.NewRawFeatureVector( + lnwire.DataLossProtectRequired, + lnwire.GossipQueriesOptional, + ), + }, + config: Config{ + CustomFeatures: map[Set][]lnwire.FeatureBit{ + SetInit: { + lnwire.FeatureBit(333), + }, + }, + }, + err: ErrFeatureConfigured, + }, + { + name: "valid", + features: map[Set]*lnwire.RawFeatureVector{ + SetInit: lnwire.NewRawFeatureVector( + lnwire.DataLossProtectRequired, + ), + SetNodeAnn: lnwire.NewRawFeatureVector( + lnwire.DataLossProtectRequired, + lnwire.GossipQueriesOptional, + lnwire.FeatureBit(500), + ), + SetInvoice: lnwire.NewRawFeatureVector( + lnwire.FeatureBit(333), + ), + }, + config: Config{ + CustomFeatures: map[Set][]lnwire.FeatureBit{ + SetInvoice: { + lnwire.FeatureBit(333), + }, + }, + }, + }, } for _, testCase := range testCases { @@ -218,7 +262,7 @@ func TestUpdateFeatureSets(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { t.Parallel() - featureMgr, err := newManager(Config{}, setDesc) + featureMgr, err := newManager(testCase.config, setDesc) require.NoError(t, err) err = featureMgr.UpdateFeatureSets(testCase.features) @@ -231,7 +275,7 @@ func TestUpdateFeatureSets(t *testing.T) { actual := featureMgr if err != nil { originalMgr, err := newManager( - Config{}, setDesc, + testCase.config, setDesc, ) require.NoError(t, err) expected = originalMgr.fsets From 019da315a8fe2cdb9d9873b9a00e476097f382b5 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 25 Apr 2023 09:38:41 -0400 Subject: [PATCH 10/12] itest: add coverage for custom features --- itest/list_on_test.go | 4 ++ itest/lnd_custom_features.go | 96 ++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 itest/lnd_custom_features.go diff --git a/itest/list_on_test.go b/itest/list_on_test.go index ade6a80f6..cc735afad 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -531,4 +531,8 @@ var allTestCases = []*lntest.TestCase{ Name: "htlc timeout resolver extract preimage local", TestFunc: testHtlcTimeoutResolverExtractPreimageLocal, }, + { + Name: "custom features", + TestFunc: testCustomFeatures, + }, } diff --git a/itest/lnd_custom_features.go b/itest/lnd_custom_features.go new file mode 100644 index 000000000..4867ca2d1 --- /dev/null +++ b/itest/lnd_custom_features.go @@ -0,0 +1,96 @@ +package itest + +import ( + "fmt" + + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/peersrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/stretchr/testify/require" +) + +// testCustomFeatures tests advertisement of custom features in various bolt 9 +// sets. For completeness, it also asserts that features aren't set in places +// where they aren't intended to be. +func testCustomFeatures(ht *lntest.HarnessTest) { + var ( + // Odd custom features so that we don't need to worry about + // issues connecting to peers. + customInit uint32 = 101 + customNodeAnn uint32 = 103 + customInvoice uint32 = 105 + ) + + // Restart Alice with custom feature bits configured. + extraArgs := []string{ + fmt.Sprintf("--protocol.custom-init=%v", customInit), + fmt.Sprintf("--protocol.custom-nodeann=%v", customNodeAnn), + fmt.Sprintf("--protocol.custom-invoice=%v", customInvoice), + } + ht.RestartNodeWithExtraArgs(ht.Alice, extraArgs) + + // Connect nodes and open a channel so that Alice will be included + // in Bob's graph. + ht.ConnectNodes(ht.Alice, ht.Bob) + chanPoint := ht.OpenChannel( + ht.Alice, ht.Bob, lntest.OpenChannelParams{Amt: 1000000}, + ) + + // Check that Alice's custom feature bit was sent to Bob in her init + // message. + peers := ht.Bob.RPC.ListPeers() + require.Len(ht, peers.Peers, 1) + require.Equal(ht, peers.Peers[0].PubKey, ht.Alice.PubKeyStr) + + _, customInitSet := peers.Peers[0].Features[customInit] + require.True(ht, customInitSet) + assertFeatureNotInSet( + ht, []uint32{customNodeAnn, customInvoice}, + peers.Peers[0].Features, + ) + + // Assert that Alice's custom feature bit is contained in the node + // announcement sent to Bob. + updates := ht.AssertNumNodeAnns(ht.Bob, ht.Alice.PubKeyStr, 1) + features := updates[len(updates)-1].Features + _, customFeature := features[customNodeAnn] + require.True(ht, customFeature) + assertFeatureNotInSet( + ht, []uint32{customInit, customInvoice}, features, + ) + + // Assert that Alice's custom feature bit is included in invoices. + invoice := ht.Alice.RPC.AddInvoice(&lnrpc.Invoice{}) + payReq := ht.Alice.RPC.DecodePayReq(invoice.PaymentRequest) + _, customInvoiceSet := payReq.Features[customInvoice] + require.True(ht, customInvoiceSet) + assertFeatureNotInSet( + ht, []uint32{customInit, customNodeAnn}, payReq.Features, + ) + + // Check that Alice can't unset configured features via the node + // announcement update API. This is only checked for node announcement + // because it is the only set that can be updated via the API. + nodeAnnReq := &peersrpc.NodeAnnouncementUpdateRequest{ + FeatureUpdates: []*peersrpc.UpdateFeatureAction{ + { + Action: peersrpc.UpdateAction_ADD, + FeatureBit: lnrpc.FeatureBit(customNodeAnn), + }, + }, + } + ht.Alice.RPC.UpdateNodeAnnouncementErr(nodeAnnReq) + + ht.CloseChannel(ht.Alice, chanPoint) +} + +// assertFeatureNotInSet checks that the features provided aren't contained in +// a feature map. +func assertFeatureNotInSet(ht *lntest.HarnessTest, features []uint32, + advertised map[uint32]*lnrpc.Feature) { + + for _, feature := range features { + _, featureSet := advertised[feature] + require.False(ht, featureSet) + } +} From ad6ccb415ff88015298a5902b676ffd247c200a5 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 25 Apr 2023 09:38:57 -0400 Subject: [PATCH 11/12] lncli: fix typo in updatenodeannouncement --- cmd/lncli/peersrpc_active.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/lncli/peersrpc_active.go b/cmd/lncli/peersrpc_active.go index 1636bc99a..5ac94f2c7 100644 --- a/cmd/lncli/peersrpc_active.go +++ b/cmd/lncli/peersrpc_active.go @@ -76,7 +76,7 @@ var updateNodeAnnouncementCommand = cli.Command{ }, cli.Int64SliceFlag{ Name: "feature_bit_remove", - Usage: "a feature bit that needs to be disabled" + + Usage: "a feature bit that needs to be disabled. " + "Can be set multiple times in the same command", }, }, From 820920a7498509d7ef4d62ef40b1071f7e6b47c9 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Wed, 5 Apr 2023 10:34:25 +0200 Subject: [PATCH 12/12] docs: add release notes --- docs/release-notes/release-notes-0.17.0.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/release-notes/release-notes-0.17.0.md b/docs/release-notes/release-notes-0.17.0.md index f9b55306f..c5eed2886 100644 --- a/docs/release-notes/release-notes-0.17.0.md +++ b/docs/release-notes/release-notes-0.17.0.md @@ -18,14 +18,25 @@ * [SendOutputs](https://github.com/lightningnetwork/lnd/pull/7631) now adheres to the anchor channel reserve requirement. +* The [UpdateNodeAnnouncement](https://github.com/lightningnetwork/lnd/pull/7568) + API can no longer be used to set/unset protocol features that are defined by + LND. + + Custom node announcement feature bits can also be specified in config using + the `dev` build tag and `--protocol.custom-nodeann`, `--protocol.custom-init` + and `--protocol.custom-invoice` flags to set feature bits for various feature + "sets", as defined in [BOLT 9](https://github.com/lightning/bolts/blob/master/09-features.md). + ## Misc * [Generate default macaroons independently](https://github.com/lightningnetwork/lnd/pull/7592) on wallet unlock or create. + # Contributors (Alphabetical Order) +* Carla Kirk-Cohen * Daniel McNally * Elle Mouton * hieblmi