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", }, }, 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/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 diff --git a/feature/manager.go b/feature/manager.go index 26a3d4a31..e7db9899a 100644 --- a/feature/manager.go +++ b/feature/manager.go @@ -1,11 +1,22 @@ package feature import ( + "errors" "fmt" "github.com/lightningnetwork/lnd/lnwire" ) +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 // protocol interoperability or functionality. @@ -44,6 +55,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 @@ -52,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 @@ -90,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) @@ -151,6 +172,32 @@ 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) + } + } + + // 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) @@ -162,7 +209,8 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) { } return &Manager{ - fsets: fsets, + fsets: fsets, + configFeatures: configFeatures, }, nil } @@ -176,8 +224,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 } @@ -198,3 +246,54 @@ 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, set.Maximum(), + ); err != nil { + 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 + } + } + + // 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..ee716fba0 100644 --- a/feature/manager_internal_test.go +++ b/feature/manager_internal_test.go @@ -135,3 +135,157 @@ 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 + config Config + 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), + ), + }, + }, + { + 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 { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + featureMgr, err := newManager(testCase.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( + testCase.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..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. @@ -26,8 +32,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 { @@ -45,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/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/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_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/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) + } +} 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/lnrpc/peersrpc/config_active.go b/lnrpc/peersrpc/config_active.go index 860e7627e..4a2f028e4 100644 --- a/lnrpc/peersrpc/config_active.go +++ b/lnrpc/peersrpc/config_active.go @@ -18,12 +18,15 @@ 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) - // 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 84516f7dc..6981f7949 100644 --- a/lnrpc/peersrpc/peers_server.go +++ b/lnrpc/peersrpc/peers_server.go @@ -313,26 +313,23 @@ 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( - 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 != "" { @@ -390,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/lnwire/features.go b/lnwire/features.go index 76dab3b05..dd0abc392 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -3,6 +3,7 @@ package lnwire import ( "encoding/binary" "errors" + "fmt" "io" ) @@ -10,6 +11,15 @@ 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") + + // 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 @@ -221,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. @@ -344,6 +355,67 @@ 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. 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 { + if fv.IsSet(feature) { + 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, + 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..c77ddd4d1 100644 --- a/lnwire/features_test.go +++ b/lnwire/features_test.go @@ -2,6 +2,7 @@ package lnwire import ( "bytes" + "math" "reflect" "sort" "testing" @@ -396,3 +397,134 @@ 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 + maximumValue 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, + }, + { + 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 { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + currentFV := NewRawFeatureVector( + testCase.currentFeatures..., + ) + newFV := NewRawFeatureVector(testCase.newFeatures...) + + // 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) + }) + } +} diff --git a/netann/host_ann.go b/netann/host_ann.go index bc50289c1..fdb48e839 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(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 // advertise. 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/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 caefe4a50..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 @@ -993,15 +995,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(nil) }, ProofMatureDelta: 0, TrickleDelay: time.Millisecond * time.Duration(cfg.TrickleDelay), @@ -1271,6 +1276,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 +1289,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(nil) }, SendAnnouncement: s.authGossiper.ProcessLocalAnnouncement, NotifyWhenOnline: s.NotifyWhenOnline, @@ -1625,8 +1633,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..., + ) + }), }) } @@ -2476,12 +2491,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 +2514,7 @@ out: // announcement with the updated addresses and broadcast // it to our peers. newNodeAnn, err := s.genNodeAnnouncement( - true, netann.NodeAnnSetAddrs(newAddrs), + nil, netann.NodeAnnSetAddrs(newAddrs), ) if err != nil { srvrLog.Debugf("Unable to generate new node "+ @@ -2892,7 +2903,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) { + nil, func(currentAnn *lnwire.NodeAnnouncement) { currentAnn.Addresses = append(currentAnn.Addresses, addr) }, ) @@ -2942,30 +2953,54 @@ func (s *server) findChannel(node *btcec.PublicKey, chanID lnwire.ChannelID) ( return nil, fmt.Errorf("unable to find channel") } +// getNodeAnnouncement fetches the current, fully signed node announcement. +func (s *server) getNodeAnnouncement() lnwire.NodeAnnouncement { + s.mu.Lock() + defer s.mu.Unlock() + + return *s.currentNodeAnn +} + // 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, +// announcement. The time stamp of the announcement will be updated in order +// to ensure it propagates through the network. +func (s *server) genNodeAnnouncement(features *lnwire.RawFeatureVector, modifiers ...netann.NodeAnnModifier) (lnwire.NodeAnnouncement, error) { 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 + // 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) - // Otherwise, we'll sign a new update after applying all of the passed - // modifiers. + // Apply the requested changes to the node announcement. + for _, modifier := range modifiers { + modifier(s.currentNodeAnn) + } + + // 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 @@ -2978,10 +3013,10 @@ func (s *server) genNodeAnnouncement(refresh bool, // 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(true, modifiers...) + newNodeAnn, err := s.genNodeAnnouncement(features, modifiers...) if err != nil { return fmt.Errorf("unable to generate new node "+ "announcement: %v", err) @@ -2999,9 +3034,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() @@ -3011,11 +3044,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 { @@ -3821,7 +3849,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 2723efaea..3e7e77bd9 100644 --- a/subrpcserver_config.go +++ b/subrpcserver_config.go @@ -118,8 +118,9 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config, tcpResolver lncfg.TCPResolver, genInvoiceFeatures func() *lnwire.FeatureVector, genAmpInvoiceFeatures func() *lnwire.FeatureVector, - getNodeAnnouncement func() (lnwire.NodeAnnouncement, error), - updateNodeAnnouncement func(modifiers ...netann.NodeAnnModifier) error, + getNodeAnnouncement func() lnwire.NodeAnnouncement, + 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 {