From fe566e1755d7cb65ceb21964ac78fc280633dcc8 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Fri, 8 Nov 2019 05:29:16 -0800 Subject: [PATCH] feature: add new feature pkg to manage feature sets This commit introduces a feature.Manager, which derives feature vectors for various contexts within the daemon. The sets can be described via a staticly compiled format, which makes any runtime adjustments to the feature sets when the manager is initialized. --- feature/default_sets.go | 32 ++++++++ feature/manager.go | 97 +++++++++++++++++++++++ feature/manager_internal_test.go | 128 +++++++++++++++++++++++++++++++ feature/set.go | 41 ++++++++++ lnwire/features.go | 30 ++++++++ 5 files changed, 328 insertions(+) create mode 100644 feature/default_sets.go create mode 100644 feature/manager.go create mode 100644 feature/manager_internal_test.go create mode 100644 feature/set.go diff --git a/feature/default_sets.go b/feature/default_sets.go new file mode 100644 index 00000000..fe69226c --- /dev/null +++ b/feature/default_sets.go @@ -0,0 +1,32 @@ +package feature + +import "github.com/lightningnetwork/lnd/lnwire" + +// setDesc describes which feature bits should be advertised in which feature +// sets. +type setDesc map[lnwire.FeatureBit]map[Set]struct{} + +// defaultSetDesc are the default set descriptors for generating feature +// vectors. Each set is annotated with the corresponding identifier from BOLT 9 +// indicating where it should be advertised. +var defaultSetDesc = setDesc{ + lnwire.DataLossProtectRequired: { + SetInit: {}, // I + SetNodeAnn: {}, // N + }, + lnwire.GossipQueriesOptional: { + SetInit: {}, // I + SetNodeAnn: {}, // N + }, + lnwire.TLVOnionPayloadOptional: { + SetInit: {}, // I + SetNodeAnn: {}, // N + SetInvoice: {}, // 9 + SetLegacyGlobal: {}, + }, + lnwire.StaticRemoteKeyOptional: { + SetInit: {}, // I + SetNodeAnn: {}, // N + SetLegacyGlobal: {}, + }, +} diff --git a/feature/manager.go b/feature/manager.go new file mode 100644 index 00000000..f201cad1 --- /dev/null +++ b/feature/manager.go @@ -0,0 +1,97 @@ +package feature + +import ( + "fmt" + + "github.com/lightningnetwork/lnd/lnwire" +) + +// 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. +type Config struct { + // NoTLVOnion unsets any optional or required TLVOnionPaylod bits from + // all feature sets. + NoTLVOnion bool + + // NoStaticRemoteKey unsets any optional or required StaticRemoteKey + // bits from all feature sets. + NoStaticRemoteKey bool +} + +// Manager is responsible for generating feature vectors for different requested +// feature sets. +type Manager struct { + // fsets is a static map of feature set to raw feature vectors. Requests + // are fulfilled by cloning these interal feature vectors. + fsets map[Set]*lnwire.RawFeatureVector +} + +// NewManager creates a new feature Manager, applying any custom modifications +// to its feature sets before returning. +func NewManager(cfg Config) (*Manager, error) { + return newManager(cfg, defaultSetDesc) +} + +// newManager creates a new feeature Manager, applying any custom modifications +// to its feature sets before returning. This method accepts the setDesc as its +// own parameter so that it can be unit tested. +func newManager(cfg Config, desc setDesc) (*Manager, error) { + // First build the default feature vector for all known sets. + fsets := make(map[Set]*lnwire.RawFeatureVector) + for bit, sets := range desc { + for set := range sets { + // Fetch the feature vector for this set, allocating a + // new one if it doesn't exist. + fv, ok := fsets[set] + if !ok { + fv = lnwire.NewRawFeatureVector() + } + + // Set the configured bit on the feature vector, + // ensuring that we don't set two feature bits for the + // same pair. + err := fv.SafeSet(bit) + if err != nil { + return nil, fmt.Errorf("unable to set "+ + "%v in %v: %v", bit, set, err) + } + + // Write the updated feature vector under its set. + fsets[set] = fv + } + } + + // Now, remove any features as directed by the config. + for _, fv := range fsets { + if cfg.NoTLVOnion { + fv.Unset(lnwire.TLVOnionPayloadOptional) + fv.Unset(lnwire.TLVOnionPayloadRequired) + } + if cfg.NoStaticRemoteKey { + fv.Unset(lnwire.StaticRemoteKeyOptional) + fv.Unset(lnwire.StaticRemoteKeyRequired) + } + } + + return &Manager{ + fsets: fsets, + }, nil +} + +// GetRaw returns a raw feature vector for the passed set. If no set is known, +// an empty raw feature vector is returned. +func (m *Manager) GetRaw(set Set) *lnwire.RawFeatureVector { + if fv, ok := m.fsets[set]; ok { + return fv.Clone() + } + + return lnwire.NewRawFeatureVector() +} + +// Get returns a feature vector for the passed set. If no set is known, an empty +// feature vector is returned. +func (m *Manager) Get(set Set) *lnwire.FeatureVector { + raw := m.GetRaw(set) + return lnwire.NewFeatureVector(raw, lnwire.Features) +} diff --git a/feature/manager_internal_test.go b/feature/manager_internal_test.go new file mode 100644 index 00000000..9c97bd05 --- /dev/null +++ b/feature/manager_internal_test.go @@ -0,0 +1,128 @@ +package feature + +import ( + "reflect" + "testing" + + "github.com/lightningnetwork/lnd/lnwire" +) + +type managerTest struct { + name string + cfg Config +} + +const unknownFeature lnwire.FeatureBit = 30 + +var testSetDesc = setDesc{ + lnwire.DataLossProtectRequired: { + SetNodeAnn: {}, // I + }, + lnwire.TLVOnionPayloadOptional: { + SetInit: {}, // I + SetNodeAnn: {}, // N + }, + lnwire.StaticRemoteKeyOptional: { + SetInit: {}, // I + SetNodeAnn: {}, // N + }, +} + +var managerTests = []managerTest{ + { + name: "default", + cfg: Config{}, + }, + { + name: "no tlv", + cfg: Config{ + NoTLVOnion: true, + }, + }, + { + name: "no static remote key", + cfg: Config{ + NoStaticRemoteKey: true, + }, + }, + { + name: "no tlv or static remote key", + cfg: Config{ + NoTLVOnion: true, + NoStaticRemoteKey: true, + }, + }, +} + +// TestManager asserts basic initialazation and operation of a feature manager, +// including that the proper features are removed in response to config changes. +func TestManager(t *testing.T) { + for _, test := range managerTests { + test := test + t.Run(test.name, func(t *testing.T) { + testManager(t, test) + }) + } +} + +func testManager(t *testing.T, test managerTest) { + m, err := newManager(test.cfg, testSetDesc) + if err != nil { + t.Fatalf("unable to create feature manager: %v", err) + } + + sets := []Set{ + SetInit, + SetLegacyGlobal, + SetNodeAnn, + SetInvoice, + } + + for _, set := range sets { + raw := m.GetRaw(set) + fv := m.Get(set) + + fv2 := lnwire.NewFeatureVector(raw, lnwire.Features) + + if !reflect.DeepEqual(fv, fv2) { + t.Fatalf("mismatch Get vs GetRaw, raw: %v vs fv: %v", + fv2, fv) + } + + assertUnset := func(bit lnwire.FeatureBit) { + hasBit := fv.HasFeature(bit) || fv.HasFeature(bit^1) + if hasBit { + t.Fatalf("bit %v or %v is set", bit, bit^1) + } + } + + // Assert that the manager properly unset the configured feature + // bits from all sets. + if test.cfg.NoTLVOnion { + assertUnset(lnwire.TLVOnionPayloadOptional) + } + if test.cfg.NoStaticRemoteKey { + assertUnset(lnwire.StaticRemoteKeyOptional) + } + + assertUnset(unknownFeature) + } + + // Do same basic sanity checks on features that are always present. + nodeFeatures := m.Get(SetNodeAnn) + + assertSet := func(bit lnwire.FeatureBit) { + has := nodeFeatures.HasFeature(bit) + if !has { + t.Fatalf("node features don't advertised %v", bit) + } + } + + assertSet(lnwire.DataLossProtectOptional) + if !test.cfg.NoTLVOnion { + assertSet(lnwire.TLVOnionPayloadRequired) + } + if !test.cfg.NoStaticRemoteKey { + assertSet(lnwire.StaticRemoteKeyOptional) + } +} diff --git a/feature/set.go b/feature/set.go new file mode 100644 index 00000000..2ac2ce52 --- /dev/null +++ b/feature/set.go @@ -0,0 +1,41 @@ +package feature + +// 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. +type Set uint8 + +const ( + // SetInit identifies features that should be sent in an Init message to + // a remote peer. + SetInit Set = iota + + // SetLegacyGlobal identifies features that should be set in the legacy + // GlobalFeatures field of an Init message, which maintains backwards + // compatibility with nodes that haven't implemented flat features. + SetLegacyGlobal + + // SetNodeAnn identifies features that should be advertised on node + // announcements. + SetNodeAnn + + // SetInvoice identifies features that should be advertised on invoices + // generated by the daemon. + SetInvoice +) + +// String returns a human-readable description of a Set. +func (s Set) String() string { + switch s { + case SetInit: + return "SetInit" + case SetLegacyGlobal: + return "SetLegacyGlobal" + case SetNodeAnn: + return "SetNodeAnn" + case SetInvoice: + return "SetInvoice" + default: + return "SetUnknown" + } +} diff --git a/lnwire/features.go b/lnwire/features.go index 2b7d9084..6843086c 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -2,10 +2,17 @@ package lnwire import ( "encoding/binary" + "errors" "fmt" "io" ) +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") +) + // FeatureBit represents a feature that can be enabled in either a local or // global feature vector at a specific bit position. Feature bits follow the // "it's OK to be odd" rule, where features at even bit positions must be known @@ -136,6 +143,15 @@ func NewRawFeatureVector(bits ...FeatureBit) *RawFeatureVector { return fv } +// Clone makes a copy of a feature vector. +func (fv *RawFeatureVector) Clone() *RawFeatureVector { + newFeatures := NewRawFeatureVector() + for bit := range fv.features { + newFeatures.Set(bit) + } + return newFeatures +} + // IsSet returns whether a particular feature bit is enabled in the vector. func (fv *RawFeatureVector) IsSet(feature FeatureBit) bool { return fv.features[feature] @@ -146,6 +162,20 @@ func (fv *RawFeatureVector) Set(feature FeatureBit) { fv.features[feature] = true } +// SafeSet sets the chosen feature bit in the feature vector, but returns an +// error if the opposing feature bit is already set. This ensures both that we +// are creating properly structured feature vectors, and in some cases, that +// peers are sending properly encoded ones, i.e. it can't be both optional and +// required. +func (fv *RawFeatureVector) SafeSet(feature FeatureBit) error { + if _, ok := fv.features[feature^1]; ok { + return ErrFeaturePairExists + } + + fv.Set(feature) + return nil +} + // Unset marks a feature as disabled in the vector. func (fv *RawFeatureVector) Unset(feature FeatureBit) { delete(fv.features, feature)