Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multi: Add ability to set custom node announcement feature bits in config #7168

Closed
4 changes: 2 additions & 2 deletions cmd/lncli/peersrpc_active.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func updateNodeAnnouncement(ctx *cli.Context) error {

if ctx.IsSet("feature_bit_add") {
change = true
for _, feature := range ctx.IntSlice("feature_bit_add") {
for _, feature := range ctx.Int64Slice("feature_bit_add") {
action := &peersrpc.UpdateFeatureAction{
Action: peersrpc.UpdateAction_ADD,
FeatureBit: lnrpc.FeatureBit(feature),
Expand All @@ -137,7 +137,7 @@ func updateNodeAnnouncement(ctx *cli.Context) error {

if ctx.IsSet("feature_bit_remove") {
change = true
for _, feature := range ctx.IntSlice("feature_bit_remove") {
for _, feature := range ctx.Int64Slice("feature_bit_remove") {
action := &peersrpc.UpdateFeatureAction{
Action: peersrpc.UpdateAction_REMOVE,
FeatureBit: lnrpc.FeatureBit(feature),
Expand Down
8 changes: 8 additions & 0 deletions docs/release-notes/release-notes-0.16.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ current gossip sync query status.
* [A bug has been fixed which could cause `lnd` to crash when parsing a
malformed HTLC intercept message](https://github.com/lightningnetwork/lnd/pull/7392).

* The [UpdateNodeAnnouncement](https://github.com/lightningnetwork/lnd/pull/7168)
API can no longer be used to set/unset protocol features that are defined by
LND. A bug in the `updatenodeannouncement` peers cli which did not allow
setting/unsetting of feature bits also has been fixed.

Custom node announcement feature bits can also be specified in config using
the `dev` build tag and `--protocol.custom-nodeann-feature` flag.

## Wallet

* [Allows Taproot public keys and tap scripts to be imported as watch-only
Expand Down
8 changes: 8 additions & 0 deletions lncfg/protocol_experimental_off.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package lncfg

import "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 {
Expand All @@ -13,3 +15,9 @@ type ExperimentalProtocol struct {
func (p ExperimentalProtocol) CustomMessageOverrides() []uint16 {
return nil
}

// CustomAnnFeatures returns a set of protocol feature bits to set in the node's
// announcement.
func (p ExperimentalProtocol) CustomAnnFeatures() []lnwire.FeatureBit {
return nil
}
15 changes: 15 additions & 0 deletions lncfg/protocol_experimental_on.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,31 @@

package lncfg

import "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."`

CustomFeature []uint64 `long:"custom-nodeann-feature" description:"a protocol feature bit to set in our node's announcement"`
}

// CustomMessageOverrides returns the set of protocol messages that we override
// to allow custom handling.
func (p ExperimentalProtocol) CustomMessageOverrides() []uint16 {
return p.CustomMessage
}

// CustomAnnFeatures returns a set of protocol feature bits to add to the node's
// announcement.
func (p ExperimentalProtocol) CustomAnnFeatures() []lnwire.FeatureBit {
features := make([]lnwire.FeatureBit, len(p.CustomFeature))
for i, f := range p.CustomFeature {
features[i] = lnwire.FeatureBit(f)
}

return features
}
4 changes: 4 additions & 0 deletions lnrpc/peersrpc/config_active.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ type Config struct {
// UpdateNodeAnnouncement updates our node announcement applying the
// given NodeAnnModifiers and broadcasts the new version to the network.
UpdateNodeAnnouncement func(...netann.NodeAnnModifier) error

// ConfigFeatures holds a set of features that are set in lnd's top
// level config.
ConfigFeatures []lnwire.FeatureBit
}
33 changes: 14 additions & 19 deletions lnrpc/peersrpc/peers_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,36 +248,25 @@ func (s *Server) updateFeatures(currentfeatures *lnwire.RawFeatureVector,
updates []*UpdateFeatureAction) (*lnwire.RawFeatureVector,
*lnrpc.Op, error) {

ops := &lnrpc.Op{Entity: "features"}
raw := currentfeatures.Clone()
var (
ops = &lnrpc.Op{Entity: "features"}
add []lnwire.FeatureBit
remove []lnwire.FeatureBit
)

for _, update := range updates {
bit := lnwire.FeatureBit(update.FeatureBit)

switch update.Action {
case UpdateAction_ADD:
if raw.IsSet(bit) {
return nil, nil, fmt.Errorf(
"invalid add action for bit %v, "+
"bit is already set",
update.FeatureBit,
)
}
raw.Set(bit)
add = append(add, bit)
ops.Actions = append(
carlaKC marked this conversation as resolved.
Show resolved Hide resolved
ops.Actions,
fmt.Sprintf("%s set", lnwire.Features[bit]),
)

case UpdateAction_REMOVE:
if !raw.IsSet(bit) {
return nil, nil, fmt.Errorf(
"invalid remove action for bit %v, "+
"bit is already unset",
update.FeatureBit,
)
}
raw.Unset(bit)
remove = append(remove, bit)
ops.Actions = append(
ops.Actions,
fmt.Sprintf("%s unset", lnwire.Features[bit]),
Expand All @@ -292,7 +281,13 @@ func (s *Server) updateFeatures(currentfeatures *lnwire.RawFeatureVector,
}
}

// Validate our new SetNodeAnn.
raw, err := lnwire.MergeFeatureVector(
currentfeatures, add, remove, s.cfg.ConfigFeatures,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could call this a more 'external' approach to managing the feature bits. Would this have been possible as an extension of feature.Manager too? Perhaps that would keep the logic a bit more in one place.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to take a look at moving this into the feature manager. The existing API only advertises custom features in gossip (not peer init), but I think that it makes sense to update it to do both. Seems like it'll simplify things a bit as well 🤞

if err != nil {
return nil, nil, err
}

fv := lnwire.NewFeatureVector(raw, lnwire.Features)
if err := feature.ValidateDeps(fv); err != nil {
return nil, nil, fmt.Errorf(
Expand Down
56 changes: 50 additions & 6 deletions lntest/itest/lnd_channel_graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/lightningnetwork/lnd/lntemp"
"github.com/lightningnetwork/lnd/lntemp/node"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -426,7 +427,7 @@ func testUpdateNodeAnnouncement(ht *lntemp.HarnessTest) {

var lndArgs []string

// Add some exta addresses to the default ones.
// Add some extra addresses to the default ones.
extraAddrs := []string{
"192.168.1.1:8333",
"[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8337",
Expand All @@ -437,6 +438,13 @@ func testUpdateNodeAnnouncement(ht *lntemp.HarnessTest) {
for _, addr := range extraAddrs {
lndArgs = append(lndArgs, "--externalip="+addr)
}

// Advertise a custom feature bit in our args at startup.
customFeature := lnwire.FeatureBit(1000)
lndArgs = append(lndArgs, fmt.Sprintf(
"--protocol.custom-nodeann-feature=%v", customFeature,
))

dave := ht.NewNode("Dave", lndArgs)

// assertNodeAnn is a helper closure that checks a given node update
Expand Down Expand Up @@ -478,10 +486,46 @@ func testUpdateNodeAnnouncement(ht *lntemp.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
// Assert that the custom feature bit set in our config args is set.
_, haveCustom := resp.Features[uint32(customFeature)]
require.True(ht, haveCustom, "configured feature bit not "+
"advertised: %v", resp.Features)

// Test that we can't modify features bits that are set in our config.
nodeAnnReq := &peersrpc.NodeAnnouncementUpdateRequest{
FeatureUpdates: []*peersrpc.UpdateFeatureAction{
{
Action: peersrpc.UpdateAction_ADD,
FeatureBit: lnrpc.FeatureBit(customFeature),
},
},
}
dave.RPC.UpdateNodeAnnouncementErr(nodeAnnReq)

// 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")
Expand Down Expand Up @@ -553,7 +597,7 @@ func testUpdateNodeAnnouncement(ht *lntemp.HarnessTest) {
},
}

nodeAnnReq := &peersrpc.NodeAnnouncementUpdateRequest{
nodeAnnReq = &peersrpc.NodeAnnouncementUpdateRequest{
Alias: newAlias,
Color: newColor,
AddressUpdates: updateAddressActions,
Expand Down
79 changes: 79 additions & 0 deletions lnwire/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,31 @@ 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")

// 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")

// ErrFeatureSet is returned when an attempt to set a feature bit that
// is already active is made.
ErrFeatureSet = errors.New("feature bit already set")

// ErrFeatureNotSet is returned when an attempt to un-set a feature bit
// this is not active is made.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%s/this/that

ErrFeatureNotSet = errors.New("feature bit not set")

// ErrFeatureConfigured is returned when an attempt to un-set a feature
// that was set in lnd's config is made.
ErrFeatureConfigured = errors.New("feature bit set in config")
)

// FeatureBit represents a feature that can be enabled in either a local or
Expand Down Expand Up @@ -621,3 +639,64 @@ func (fv *FeatureVector) Clone() *FeatureVector {
features := fv.RawFeatureVector.Clone()
return NewFeatureVector(features, fv.featureNames)
}

// MergeFeatureVector accepts a list of feature bits to add and remove,
// validates that they may be modified (are not known to LND, set/unset actions
// are logical for the current vector) and returns a merged vector. The merge
// function also accepts a map of feature bits that were set at a config level.
// These features are considered "hard-set", so will also trigger an error if
// we attempt to modify them.
func MergeFeatureVector(current *RawFeatureVector, add, remove []FeatureBit,
configured []FeatureBit) (*RawFeatureVector, error) {

if current == nil {
return nil, errors.New("nil feature vector not allowed")
}

configuredBits := make(map[FeatureBit]struct{}, len(configured))
for _, bit := range configured {
configuredBits[bit] = struct{}{}
}

// Make a copy of the current vector so that we don't mutate it.
raw := current.Clone()
for _, bit := range add {
if _, set := configuredBits[bit]; set {
return nil, fmt.Errorf("can't add feature bit %d: %w",
bit, ErrFeatureConfigured)
}

if name, known := Features[bit]; known {
return nil, fmt.Errorf("can't set feature "+
"bit %d (%v): %w", bit, name,
ErrFeatureStandard)
}

if raw.IsSet(bit) {
return nil, fmt.Errorf("invalid add action for bit "+
"%d: %w", bit, ErrFeatureSet)
}
raw.Set(bit)
}

for _, bit := range remove {
if _, set := configuredBits[bit]; set {
return nil, fmt.Errorf("can't remove feature bit %d: "+
"%w", bit, ErrFeatureConfigured)
}

if name, known := Features[bit]; known {
return nil, fmt.Errorf("can't unset feature "+
"bit %d (%v): %w", bit, name,
ErrFeatureStandard)
}

if !raw.IsSet(bit) {
return nil, fmt.Errorf("invalid remove action for "+
"bit %d: %w", bit, ErrFeatureNotSet)
}
raw.Unset(bit)
}

return raw, nil
}
Loading