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.
This commit is contained in:
parent
90e36ca04b
commit
fe566e1755
32
feature/default_sets.go
Normal file
32
feature/default_sets.go
Normal file
@ -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
feature/manager.go
Normal file
97
feature/manager.go
Normal file
@ -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
feature/manager_internal_test.go
Normal file
128
feature/manager_internal_test.go
Normal file
@ -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
feature/set.go
Normal file
41
feature/set.go
Normal file
@ -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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user