diff --git a/channeldb/db.go b/channeldb/db.go index 93bb239bb65..aebc31a7362 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -447,6 +447,7 @@ var dbTopLevelBuckets = [][]byte{ outpointBucket, chanIDBucket, historicalChannelBucket, + nodeAnnouncementBucket, } // Wipe completely deletes all saved state within all used buckets within the diff --git a/channeldb/error.go b/channeldb/error.go index 859af974648..d9411cb6916 100644 --- a/channeldb/error.go +++ b/channeldb/error.go @@ -108,6 +108,16 @@ var ( // channel with a channel point that is already present in the // database. ErrChanAlreadyExists = fmt.Errorf("channel already exists") + + // ErrNodeAnnBucketNotFound is returned when the nodeannouncement + // bucket hasn't been created yet. + ErrNodeAnnBucketNotFound = fmt.Errorf("no node announcement bucket " + + "exist") + + // ErrNodeAnnNotFound is returned when we're unable to find the target + // node announcement. + ErrNodeAnnNotFound = fmt.Errorf("node announcement with target " + + "identity not found") ) // ErrTooManyExtraOpaqueBytes creates an error which should be returned if the diff --git a/channeldb/node.go b/channeldb/node.go new file mode 100644 index 00000000000..e63a4f458aa --- /dev/null +++ b/channeldb/node.go @@ -0,0 +1,222 @@ +package channeldb + +import ( + "bytes" + "image/color" + "io" + "net" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lnwire" +) + +var ( + // nodeAnnouncementBucket stores announcement config pertaining to node. + // This bucket allows one to query for persisted node announcement + // config and use it when starting orrestarting a node + nodeAnnouncementBucket = []byte("nab") +) + +// NodeAlias is a hex encoded UTF-8 string that may be displayed as an +// alternative to the node's ID. Notice that aliases are not unique and may be +// freely chosen by the node operators. +type NodeAlias [32]byte + +// String returns a utf8 string representation of the alias bytes. +func (n NodeAlias) String() string { + // Trim trailing zero-bytes for presentation + return string(bytes.Trim(n[:], "\x00")) +} + +type NodeAnnouncement struct { + // Alias is used to customize node's appearance in maps and + // graphs + Alias NodeAlias + + // Color represent the hexadecimal value that node operators can assign + // to their nodes. It's represented as a hex string. + Color color.RGBA + + // NodeID is a public key which is used as node identification. + NodeID [33]byte + + // Address includes two specification fields: 'ipv6' and 'port' on + // which the node is accepting incoming connections. + Addresses []net.Addr + + // Features is the list of protocol features this node supports. + Features *lnwire.RawFeatureVector +} + +// Sync performs a full database sync which writes the current up-to-date data +// within the struct to the database. +func (n *NodeAnnouncement) Sync(db *DB) error { + return kvdb.Update(db, func(tx kvdb.RwTx) error { + nodeAnnBucket := tx.ReadWriteBucket(nodeAnnouncementBucket) + if nodeAnnBucket == nil { + return ErrNodeAnnBucketNotFound + } + + return putNodeAnnouncement(nodeAnnBucket, n) + }, func() {}) +} + +// FetchNodeAnnouncement attempts to lookup the data for NodeAnnouncement based +// on a target identity public key. If a particular NodeAnnouncement for the +// passed identity public key cannot be found, then returns ErrNodeAnnNotFound +func (d *DB) FetchNodeAnnouncement(identity *btcec.PublicKey) (*NodeAnnouncement, error) { + var nodeAnnouncement *NodeAnnouncement + + err := kvdb.View(d, func(tx kvdb.RTx) error { + nodeAnn, err := fetchNodeAnnouncement(tx, identity) + if err != nil { + return err + } + + nodeAnnouncement = nodeAnn + return nil + }, func() { + nodeAnnouncement = nil + }) + + return nodeAnnouncement, err +} + +func fetchNodeAnnouncement(tx kvdb.RTx, targetPub *btcec.PublicKey) (*NodeAnnouncement, error) { + // First fetch the bucket for storing node announcement, bailing out + // early if it hasn't been created yet. + nodeAnnBucket := tx.ReadBucket(nodeAnnouncementBucket) + if nodeAnnBucket == nil { + return nil, ErrNodeAnnBucketNotFound + } + + // If a node announcement for that particular public key cannot be + // located, then exit early with ErrNodeAnnNotFound + pubkey := targetPub.SerializeCompressed() + nodeAnnBytes := nodeAnnBucket.Get(pubkey) + if nodeAnnBytes == nil { + return nil, ErrNodeAnnNotFound + } + + // FInally, decode and allocate a fresh NodeAnnouncement object to be + // returned to the caller + nodeAnnReader := bytes.NewReader(nodeAnnBytes) + return deserializeNodeAnnouncement(nodeAnnReader) + +} + +func (d *DB) PutNodeAnnouncement(pubkey [33]byte, alias [32]byte, color color.RGBA, + addresses []net.Addr, features *lnwire.RawFeatureVector) error { + nodeAnn := &NodeAnnouncement{ + Alias: alias, + Color: color, + NodeID: pubkey, + Addresses: addresses, + Features: features, + } + + return kvdb.Update(d, func(tx kvdb.RwTx) error { + nodeAnnBucket := tx.ReadWriteBucket(nodeAnnouncementBucket) + if nodeAnnBucket == nil { + return ErrNodeAnnBucketNotFound + } + + return putNodeAnnouncement(nodeAnnBucket, nodeAnn) + + }, func() {}) +} + +func putNodeAnnouncement(nodeAnnBucket kvdb.RwBucket, n *NodeAnnouncement) error { + var b bytes.Buffer + if err := serializeNodeAnnouncement(&b, n); err != nil { + return err + } + + nodePub := n.NodeID[:] + return nodeAnnBucket.Put(nodePub, b.Bytes()) +} + +func serializeNodeAnnouncement(w io.Writer, n *NodeAnnouncement) error { + // Serialize Alias + if _, err := w.Write([]byte(n.Alias[:])); err != nil { + return err + } + + // Serialize Color + // Write R + if _, err := w.Write([]byte{n.Color.R}); err != nil { + return err + } + // Write G + if _, err := w.Write([]byte{n.Color.G}); err != nil { + return err + } + // Write B + if _, err := w.Write([]byte{n.Color.B}); err != nil { + return err + } + + // Serialize NodeID + if _, err := w.Write(n.NodeID[:]); err != nil { + return err + } + + // Serialize Addresses + var addrBuffer bytes.Buffer + if err := lnwire.WriteNetAddrs(&addrBuffer, n.Addresses); err != nil { + return err + } + if _, err := w.Write(addrBuffer.Bytes()); err != nil { + return err + } + + // Serialize Features + var featsBuffer bytes.Buffer + if err := lnwire.WriteRawFeatureVector(&featsBuffer, n.Features); err != nil { + return err + } + if _, err := w.Write(featsBuffer.Bytes()); err != nil { + return err + } + + return nil +} + +func deserializeNodeAnnouncement(r io.Reader) (*NodeAnnouncement, error) { + var err error + nodeAnn := &NodeAnnouncement{} + + // Read Alias + aliasBuf := make([]byte, 32) + if _, err := io.ReadFull(r, aliasBuf); err != nil { + return nil, err + } + nodeAnn.Alias = [32]byte(aliasBuf) + + // Read Color + // colorBuf contains R, G, B, A (alpha), but the color.RGBA type only + // expects R, G, B, so we need to slice it. + colorBuf := make([]byte, 3) + if _, err := io.ReadFull(r, colorBuf); err != nil { + return nil, err + } + nodeAnn.Color = color.RGBA{colorBuf[0], colorBuf[1], colorBuf[2], 0} + + var pub [33]byte + if _, err := io.ReadFull(r, pub[:]); err != nil { + return nil, err + } + nodeAnn.NodeID = pub + + if err := lnwire.ReadElement(r, &nodeAnn.Addresses); err != nil { + return nil, err + } + + if err := lnwire.ReadElement(r, &nodeAnn.Features); err != nil { + return nil, err + } + + return nodeAnn, err + +} diff --git a/channeldb/node_test.go b/channeldb/node_test.go new file mode 100644 index 00000000000..8c2197abe4b --- /dev/null +++ b/channeldb/node_test.go @@ -0,0 +1,65 @@ +package channeldb + +import ( + "image/color" + "net" + "reflect" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +func TestNodeAnnouncementEncodeDecode(t *testing.T) { + t.Parallel() + + fullDB, err := MakeTestDB(t) + require.NoError(t, err, "unable to make test database") + + // We'll start by creating initial data to use for populating our test + // node announcement instance + var alias [32]byte + copy(alias[:], []byte("alice")) + color := color.RGBA{255, 255, 255, 0} + _, pub := btcec.PrivKeyFromBytes(key[:]) + address := []net.Addr{testAddr} + features := lnwire.RawFeatureVector{} + + nodeAnn := &NodeAnnouncement{ + Alias: alias, + Color: color, + NodeID: [33]byte(pub.SerializeCompressed()), + Addresses: address, + Features: &features, + } + if err := nodeAnn.Sync(fullDB); err != nil { + t.Fatalf("unable to sync node announcement: %v", err) + } + + // Fetch the current node announcement from the database, it should + // match the one we just persisted + persistedNodeAnn, err := fullDB.FetchNodeAnnouncement(pub) + require.NoError(t, err, "unable to fetch node announcement") + if nodeAnn.Alias != persistedNodeAnn.Alias { + t.Fatalf("node aliases don't match: expected %v, got %v", + nodeAnn.Alias.String(), persistedNodeAnn.Alias.String()) + } + + if nodeAnn.Color != persistedNodeAnn.Color { + t.Fatalf("node colors don't match: expected %v, got %v", + nodeAnn.Color, persistedNodeAnn.Color) + } + + if nodeAnn.NodeID != persistedNodeAnn.NodeID { + t.Fatalf("node nodeIds don't match: expected %v, got %v", + nodeAnn.NodeID, persistedNodeAnn.NodeID) + } + + // Verify that the addresses of the node announcements are the same. + if !reflect.DeepEqual(nodeAnn.Addresses, persistedNodeAnn.Addresses) { + t.Fatalf("node addresses don't match: expected %v, got %v", + nodeAnn.Addresses, persistedNodeAnn.Addresses) + } + +} diff --git a/lnrpc/peersrpc/config_active.go b/lnrpc/peersrpc/config_active.go index 4a2f028e454..f93d1ab70a9 100644 --- a/lnrpc/peersrpc/config_active.go +++ b/lnrpc/peersrpc/config_active.go @@ -6,6 +6,7 @@ package peersrpc import ( "net" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/netann" ) @@ -29,4 +30,6 @@ type Config struct { // vector should be provided. UpdateNodeAnnouncement func(features *lnwire.RawFeatureVector, mods ...netann.NodeAnnModifier) error + + ChanStateDB *channeldb.ChannelStateDB } diff --git a/lnrpc/peersrpc/peers_server.go b/lnrpc/peersrpc/peers_server.go index 14e039f615d..569ca7efd04 100644 --- a/lnrpc/peersrpc/peers_server.go +++ b/lnrpc/peersrpc/peers_server.go @@ -398,5 +398,10 @@ func (s *Server) UpdateNodeAnnouncement(_ context.Context, return nil, err } + nodeAnnouncement := s.cfg.GetNodeAnnouncement() + s.cfg.ChanStateDB.GetParentDB().PutNodeAnnouncement( + nodeAnnouncement.NodeID, nodeAnnouncement.Alias, + nodeAnnouncement.RGBColor, nodeAnnouncement.Addresses, nodeAnnouncement.Features) + return resp, nil } diff --git a/server.go b/server.go index 2979880a291..fb3868ed98d 100644 --- a/server.go +++ b/server.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "encoding/hex" "fmt" + "image/color" "math/big" prand "math/rand" "net" @@ -795,8 +796,18 @@ func newServer(cfg *Config, listenAddrs []net.Addr, return nil, err } + // Fetch the persisted node announcement from disk. This announcement + // contains information about our node, and will be used to construct + // our initial node. + pubkey := nodeKeyDesc.PubKey + persistedConfig, err := s.miscDB.FetchNodeAnnouncement(pubkey) + if err != nil { + srvrLog.Errorf("unable to fetch node announcement: %v\n", err) + } + selfAddrs := make([]net.Addr, 0, len(externalIPs)) selfAddrs = append(selfAddrs, externalIPs...) + selfAddrs = append(selfAddrs, persistedConfig.Addresses...) // As the graph can be obtained at anytime from the network, we won't // replicate it, and instead it'll only be stored locally. @@ -805,17 +816,28 @@ func newServer(cfg *Config, listenAddrs []net.Addr, // We'll now reconstruct a node announcement based on our current // configuration so we can send it out as a sort of heart beat within // the network. - // - // We'll start by parsing the node color from configuration. - color, err := lncfg.ParseHexColor(cfg.Color) - if err != nil { - srvrLog.Errorf("unable to parse color: %v\n", err) - return nil, err + var color color.RGBA + // Determine the source of the color. If the user-provided color is + // #3399FF (default) and there is a persisted node announcement, use the color + // from that. Otherwise, parse the user-provided color. + if cfg.Color == "#3399FF" && persistedConfig != nil { + color = persistedConfig.Color + } else { + var err error + color, err = lncfg.ParseHexColor(cfg.Color) + if err != nil { + srvrLog.Errorf("unable to parse color: %v\n", err) + return nil, err + } } - // If no alias is provided, default to first 10 characters of public - // key. + // If no alias is provided, check if one is stored in the database and + // use that. If still no alias is provided, default to the first 10 + // characters of the public key. alias := cfg.Alias + if alias == "" && persistedConfig != nil { + alias = persistedConfig.Alias.String() + } if alias == "" { alias = hex.EncodeToString(serializedPubKey[:10]) } @@ -823,12 +845,26 @@ func newServer(cfg *Config, listenAddrs []net.Addr, if err != nil { return nil, err } + + // + // If there is no persisted configuration, we will use the set of features + // provided by the feature manager. Otherwise, we will use the features + // stored in the persisted configuration. + var features *lnwire.FeatureVector + if persistedConfig != nil { + // Extract the features from the persisted configuration. + features = lnwire.NewFeatureVector(persistedConfig.Features, lnwire.Features) + } else { + // Get the set of features from the feature manager. + features = s.featureMgr.Get(feature.SetNodeAnn) + } + selfNode := &channeldb.LightningNode{ HaveNodeAnnouncement: true, LastUpdate: time.Now(), Addresses: selfAddrs, Alias: nodeAlias.String(), - Features: s.featureMgr.Get(feature.SetNodeAnn), + Features: features, Color: color, } copy(selfNode.PubKeyBytes[:], nodeKeyDesc.PubKey.SerializeCompressed()) diff --git a/subrpcserver_config.go b/subrpcserver_config.go index 6687f71a7d4..00c507a4b36 100644 --- a/subrpcserver_config.go +++ b/subrpcserver_config.go @@ -332,6 +332,10 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config, reflect.ValueOf(updateNodeAnnouncement), ) + subCfgValue.FieldByName("ChanStateDB").Set( + reflect.ValueOf(chanStateDB), + ) + default: return fmt.Errorf("unknown field: %v, %T", fieldName, cfg)