Skip to content

Commit

Permalink
feature: add new feature pkg to manage feature sets
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cfromknecht committed Nov 8, 2019
1 parent 90e36ca commit fe566e1
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 0 deletions.
32 changes: 32 additions & 0 deletions feature/default_sets.go
Original file line number Diff line number Diff line change
@@ -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: {},
},
}
97 changes: 97 additions & 0 deletions feature/manager.go
Original file line number Diff line number Diff line change
@@ -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)
}
128 changes: 128 additions & 0 deletions feature/manager_internal_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
41 changes: 41 additions & 0 deletions feature/set.go
Original file line number Diff line number Diff line change
@@ -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"
}
}
30 changes: 30 additions & 0 deletions lnwire/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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)
Expand Down

0 comments on commit fe566e1

Please sign in to comment.