What changed, and why it matters
This commit is a large internal refactoring of how Lightning Network node records are created in the LND codebase. It introduces a version field and a new constructor for node objects, then updates many tests and internal callers to use that constructor. There is no direct security fix or vulnerability being patched; it is structural preparation for future gossip protocol versions.
No immediate security action required. Treat as normal code-review item; verify that the refactor preserves serialization compatibility and that optional-field defaults do not change node-announcement validation behavior.
Security signals we found
Large refactor touching graph database serialization and node-announcement handling
Introduction of gossip-version field in node model
No new input validation, authentication, or memory-safety checks visible in diff
No vendor security disclosure or CVE references present in commit or supplied materials
Evidence from the diff
The change adds a Version field to graph/db/models.Node, a NodeV1Fields struct, and NewV1Node/NewV1ShellNode/NewShellNode constructors. It converts Color and Alias from direct values to fn.Option types, updates serialization/deserialization in KV and SQL stores to handle optional fields, and switches NodeUpdatesInHorizon iterators from value to pointer semantics. Call sites across tests, RPC server, and server.go are migrated to the new constructors. No bug fix, bounds check, or security-sensitive validation is added.
Changed components
graph/db/models/node.gograph/db/kv_store.gograph/db/sql_store.gograph/db/sql_migration.gograph/db/notifications.gorpcserver.goserver.golnrpc/devrpc/dev_server.goInspect captured patch +444 / −341
diff --git a/autopilot/prefattach_test.go b/autopilot/prefattach_test.go
index d9ec630..3ab0c4f 100644
--- a/autopilot/prefattach_test.go
+++ b/autopilot/prefattach_test.go
@@ -417,20 +417,20 @@ func (d *testDBGraph) addRandChannel(node1, node2 *btcec.PublicKey,
case errors.Is(err, graphdb.ErrGraphNodeNotFound):
fallthrough
case errors.Is(err, graphdb.ErrGraphNotFound):
- graphNode := &models.Node{
- Addresses: []net.Addr{&net.TCPAddr{
- IP: bytes.Repeat(
- []byte("a"), 16,
- ),
- }},
- Features: lnwire.NewFeatureVector(
- nil, lnwire.Features,
- ),
- AuthSigBytes: testSig.Serialize(),
- }
- copy(
- graphNode.PubKeyBytes[:],
- pub.SerializeCompressed(),
+ //nolint:ll
+ graphNode := models.NewV1Node(
+ route.NewVertex(pub),
+ &models.NodeV1Fields{
+ Addresses: []net.Addr{&net.TCPAddr{
+ IP: bytes.Repeat(
+ []byte("a"), 16,
+ ),
+ }},
+ Features: lnwire.NewFeatureVector(
+ nil, lnwire.Features,
+ ).RawFeatureVector,
+ AuthSigBytes: testSig.Serialize(),
+ },
)
err := d.db.AddNode(
context.Background(), graphNode,
@@ -449,18 +449,18 @@ func (d *testDBGraph) addRandChannel(node1, node2 *btcec.PublicKey,
if err != nil {
return nil, err
}
- dbNode := &models.Node{
- Addresses: []net.Addr{
- &net.TCPAddr{
+
+ dbNode := models.NewV1Node(
+ route.NewVertex(nodeKey), &models.NodeV1Fields{
+ Addresses: []net.Addr{&net.TCPAddr{
IP: bytes.Repeat([]byte("a"), 16),
- },
+ }},
+ Features: lnwire.NewFeatureVector(
+ nil, lnwire.Features,
+ ).RawFeatureVector,
+ AuthSigBytes: testSig.Serialize(),
},
- Features: lnwire.NewFeatureVector(
- nil, lnwire.Features,
- ),
- AuthSigBytes: testSig.Serialize(),
- }
- copy(dbNode.PubKeyBytes[:], nodeKey.SerializeCompressed())
+ )
if err := d.db.AddNode(
context.Background(), dbNode,
); err != nil {
@@ -549,18 +549,19 @@ func (d *testDBGraph) addRandNode() (*btcec.PublicKey, error) {
if err != nil {
return nil, err
}
- dbNode := &models.Node{
- Addresses: []net.Addr{
- &net.TCPAddr{
- IP: bytes.Repeat([]byte("a"), 16),
+ dbNode := models.NewV1Node(
+ route.NewVertex(nodeKey), &models.NodeV1Fields{
+ Addresses: []net.Addr{
+ &net.TCPAddr{
+ IP: bytes.Repeat([]byte("a"), 16),
+ },
},
+ Features: lnwire.NewFeatureVector(
+ nil, lnwire.Features,
+ ).RawFeatureVector,
+ AuthSigBytes: testSig.Serialize(),
},
- Features: lnwire.NewFeatureVector(
- nil, lnwire.Features,
- ),
- AuthSigBytes: testSig.Serialize(),
- }
- copy(dbNode.PubKeyBytes[:], nodeKey.SerializeCompressed())
+ )
err = d.db.AddNode(context.Background(), dbNode)
if err != nil {
return nil, err
diff --git a/channeldb/db_test.go b/channeldb/db_test.go
index 50a9457..e2e9a19 100644
--- a/channeldb/db_test.go
+++ b/channeldb/db_test.go
@@ -19,6 +19,7 @@ import (
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
+ "github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/shachain"
"github.com/stretchr/testify/require"
)
@@ -811,15 +812,17 @@ func createNode(priv *btcec.PrivateKey) *models.Node {
updateTime := rand.Int63()
pub := priv.PubKey().SerializeCompressed()
- n := &models.Node{
- AuthSigBytes: testSig.Serialize(),
- LastUpdate: time.Unix(updateTime, 0),
- Color: color.RGBA{1, 2, 3, 0},
- Alias: "kek" + string(pub),
- Features: testFeatures,
- Addresses: testAddrs,
- }
- copy(n.PubKeyBytes[:], priv.PubKey().SerializeCompressed())
+ n := models.NewV1Node(
+ route.NewVertex(priv.PubKey()),
+ &models.NodeV1Fields{
+ AuthSigBytes: testSig.Serialize(),
+ LastUpdate: time.Unix(updateTime, 0),
+ Color: color.RGBA{1, 2, 3, 0},
+ Alias: "kek" + string(pub),
+ Features: testFeatures.RawFeatureVector,
+ Addresses: testAddrs,
+ },
+ )
return n
}
diff --git a/graph/builder_test.go b/graph/builder_test.go
index 6e362fb..72f2719 100644
--- a/graph/builder_test.go
+++ b/graph/builder_test.go
@@ -97,15 +97,16 @@ func TestIgnoreNodeAnnouncement(t *testing.T) {
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
pub := priv1.PubKey()
- node := &models.Node{
- LastUpdate: time.Unix(123, 0),
- Addresses: testAddrs,
- Color: color.RGBA{1, 2, 3, 0},
- Alias: "node11",
- AuthSigBytes: testSig.Serialize(),
- Features: testFeatures,
- }
- copy(node.PubKeyBytes[:], pub.SerializeCompressed())
+ node := models.NewV1Node(
+ route.NewVertex(pub), &models.NodeV1Fields{
+ Addresses: testAddrs,
+ AuthSigBytes: testSig.Serialize(),
+ Features: testFeatures.RawFeatureVector,
+ LastUpdate: time.Unix(123, 0),
+ Color: color.RGBA{1, 2, 3, 0},
+ Alias: "node11",
+ },
+ )
err := ctx.builder.AddNode(t.Context(), node)
if !IsError(err, ErrIgnored) {
@@ -1083,15 +1084,16 @@ func TestIsStaleNode(t *testing.T) {
// With the node stub in the database, we'll add the fully node
// announcement to the database.
- n1 := &models.Node{
- LastUpdate: updateTimeStamp,
- Addresses: testAddrs,
- Color: color.RGBA{1, 2, 3, 0},
- Alias: "node11",
- AuthSigBytes: testSig.Serialize(),
- Features: testFeatures,
- }
- copy(n1.PubKeyBytes[:], priv1.PubKey().SerializeCompressed())
+ n1 := models.NewV1Node(
+ route.NewVertex(priv1.PubKey()), &models.NodeV1Fields{
+ LastUpdate: updateTimeStamp,
+ Addresses: testAddrs,
+ Color: color.RGBA{1, 2, 3, 0},
+ Alias: "node11",
+ AuthSigBytes: testSig.Serialize(),
+ Features: testFeatures.RawFeatureVector,
+ },
+ )
if err := ctx.builder.AddNode(t.Context(), n1); err != nil {
t.Fatalf("could not add node: %v", err)
}
@@ -1399,14 +1401,16 @@ func parseTestGraph(t *testing.T, useCache bool, path string) (
return nil, err
}
- dbNode := &models.Node{
+ pubKey, err := route.NewVertexFromBytes(pubBytes)
+ require.NoError(t, err)
+
+ dbNode := models.NewV1Node(pubKey, &models.NodeV1Fields{
AuthSigBytes: testSig.Serialize(),
LastUpdate: testTime,
Addresses: testAddrs,
Alias: node.Alias,
- Features: testFeatures,
- }
- copy(dbNode.PubKeyBytes[:], pubBytes)
+ Features: testFeatures.RawFeatureVector,
+ })
// We require all aliases within the graph to be unique for our
// tests.
@@ -1784,15 +1788,15 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
features = lnwire.EmptyFeatureVector()
}
- dbNode := &models.Node{
- AuthSigBytes: testSig.Serialize(),
- LastUpdate: testTime,
- Addresses: testAddrs,
- Alias: alias,
- Features: features,
- }
-
- copy(dbNode.PubKeyBytes[:], pubKey.SerializeCompressed())
+ dbNode := models.NewV1Node(
+ route.NewVertex(pubKey), &models.NodeV1Fields{
+ AuthSigBytes: testSig.Serialize(),
+ LastUpdate: testTime,
+ Addresses: testAddrs,
+ Alias: alias,
+ Features: features.RawFeatureVector,
+ },
+ )
privKeyMap[alias] = privKey
diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go
index 2e12351..e4a8c7f 100644
--- a/graph/db/graph_test.go
+++ b/graph/db/graph_test.go
@@ -70,18 +70,18 @@ var (
)
func createNode(priv *btcec.PrivateKey) *models.Node {
- pub := priv.PubKey().SerializeCompressed()
- n := &models.Node{
- AuthSigBytes: testSig.Serialize(),
- LastUpdate: nextUpdateTime(),
- Color: color.RGBA{1, 2, 3, 0},
- Alias: "kek" + hex.EncodeToString(pub),
- Features: testFeatures,
- Addresses: testAddrs,
- }
- copy(n.PubKeyBytes[:], priv.PubKey().SerializeCompressed())
-
- return n
+ pubKey := route.NewVertex(priv.PubKey())
+
+ return models.NewV1Node(
+ pubKey, &models.NodeV1Fields{
+ LastUpdate: nextUpdateTime(),
+ Color: color.RGBA{1, 2, 3, 0},
+ Alias: "kek" + hex.EncodeToString(pubKey[:]),
+ Addresses: testAddrs,
+ Features: testFeatures.RawFeatureVector,
+ AuthSigBytes: testSig.Serialize(),
+ },
+ )
}
func createTestVertex(t testing.TB) *models.Node {
@@ -105,16 +105,18 @@ func TestNodeInsertionAndDeletion(t *testing.T) {
timeStamp := int64(1232342)
nodeWithAddrs := func(addrs []net.Addr) *models.Node {
timeStamp++
- return &models.Node{
- AuthSigBytes: testSig.Serialize(),
- LastUpdate: time.Unix(timeStamp, 0),
- Color: color.RGBA{1, 2, 3, 0},
- Alias: "kek",
- Features: testFeatures,
- Addresses: addrs,
- ExtraOpaqueData: []byte{1, 1, 1, 2, 2, 2, 2},
- PubKeyBytes: testPub,
- }
+
+ return models.NewV1Node(
+ testPub, &models.NodeV1Fields{
+ AuthSigBytes: testSig.Serialize(),
+ LastUpdate: time.Unix(timeStamp, 0),
+ Color: color.RGBA{1, 2, 3, 0},
+ Alias: "kek",
+ Features: testFeatures.RawFeatureVector,
+ Addresses: addrs,
+ ExtraOpaqueData: []byte{1, 1, 1, 2, 2, 2, 2},
+ },
+ )
}
// First, insert the node into the graph DB. This should succeed
@@ -313,11 +315,7 @@ func TestPartialNode(t *testing.T) {
// The two nodes should match exactly! (with default values for
// LastUpdate and db set to satisfy compareNodes())
- expectedNode1 := &models.Node{
- LastUpdate: time.Unix(0, 0),
- PubKeyBytes: pubKey1,
- Features: lnwire.EmptyFeatureVector(),
- }
+ expectedNode1 := models.NewV1ShellNode(pubKey1)
compareNodes(t, expectedNode1, dbNode1)
_, exists, err = graph.HasNode(ctx, dbNode2.PubKeyBytes)
@@ -326,11 +324,7 @@ func TestPartialNode(t *testing.T) {
// The two nodes should match exactly! (with default values for
// LastUpdate and db set to satisfy compareNodes())
- expectedNode2 := &models.Node{
- LastUpdate: time.Unix(0, 0),
- PubKeyBytes: pubKey2,
- Features: lnwire.EmptyFeatureVector(),
- }
+ expectedNode2 := models.NewV1ShellNode(pubKey2)
compareNodes(t, expectedNode2, dbNode2)
// Next, delete the node from the graph, this should purge all data
@@ -365,7 +359,7 @@ func TestAliasLookup(t *testing.T) {
require.NoError(t, err, "unable to generate pubkey")
dbAlias, err := graph.LookupAlias(ctx, nodePub)
require.NoError(t, err, "unable to find alias")
- require.Equal(t, testNode.Alias, dbAlias)
+ require.Equal(t, testNode.Alias.UnwrapOr(""), dbAlias)
// Ensure that looking up a non-existent alias results in an error.
node := createTestVertex(t)
@@ -1600,7 +1594,7 @@ func fillTestGraph(t testing.TB, graph *ChannelGraph, numNodes,
node := createTestVertex(t)
nodes[i] = node
- nodeIndex[node.Alias] = struct{}{}
+ nodeIndex[node.Alias.UnwrapOr("")] = struct{}{}
}
// Add each of the nodes into the graph, they should be inserted
@@ -1612,7 +1606,7 @@ func fillTestGraph(t testing.TB, graph *ChannelGraph, numNodes,
// Iterate over each node as returned by the graph, if all nodes are
// reached, then the map created above should be empty.
err := graph.ForEachNode(ctx, func(n *models.Node) error {
- delete(nodeIndex, n.Alias)
+ delete(nodeIndex, n.Alias.UnwrapOr(""))
return nil
}, func() {})
require.NoError(t, err)
@@ -2289,7 +2283,7 @@ func TestNodeUpdatesInHorizon(t *testing.T) {
require.Len(t, resp, len(queryCase.resp))
for i := 0; i < len(resp); i++ {
- compareNodes(t, &queryCase.resp[i], &resp[i])
+ compareNodes(t, &queryCase.resp[i], resp[i])
}
}
}
@@ -2487,7 +2481,7 @@ func TestNodeUpdatesInHorizonEarlyTermination(t *testing.T) {
)
// Collect only up to stopAt nodes, breaking afterwards.
- var collected []models.Node
+ var collected []*models.Node
count := 0
for node := range iter {
if count >= stopAt {
@@ -3824,7 +3818,7 @@ func TestNodePruningUpdateIndexDeletion(t *testing.T) {
t.Fatalf("should have 1 nodes instead have: %v",
len(nodesInHorizon))
}
- compareNodes(t, node1, &nodesInHorizon[0])
+ compareNodes(t, node1, nodesInHorizon[0])
// We'll now delete the node from the graph, this should result in it
// being removed from the update index as well.
diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go
index 25eb6f5..2d4da91 100644
--- a/graph/db/interfaces.go
+++ b/graph/db/interfaces.go
@@ -111,7 +111,7 @@ type V1Store interface { //nolint:interfacebloat
// by two nodes to quickly determine if they have the same set of up to
// date node announcements.
NodeUpdatesInHorizon(startTime, endTime time.Time,
- opts ...IteratorOption) iter.Seq2[models.Node, error]
+ opts ...IteratorOption) iter.Seq2[*models.Node, error]
// FetchNode attempts to look up a target node by its identity
// public key. If the node isn't found in the database, then
diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go
index 99c9584..ae4136e 100644
--- a/graph/db/kv_store.go
+++ b/graph/db/kv_store.go
@@ -7,6 +7,7 @@ import (
"encoding/binary"
"errors"
"fmt"
+ "image/color"
"io"
"iter"
"math"
@@ -848,7 +849,7 @@ func forEachNode(db kvdb.Backend,
// Execute the callback, the transaction will abort if
// this returns an error.
- return cb(tx, &node)
+ return cb(tx, node)
})
}
@@ -945,12 +946,7 @@ func sourceNodeWithTx(nodes kvdb.RBucket) (*models.Node, error) {
// With the pubKey of the source node retrieved, we're able to
// fetch the full node information.
- node, err := fetchLightningNode(nodes, selfPub)
- if err != nil {
- return nil, err
- }
-
- return &node, nil
+ return fetchLightningNode(nodes, selfPub)
}
// SetSourceNode sets the source node within the graph database. The source
@@ -1205,10 +1201,9 @@ func (c *KVStore) addChannelEdge(tx kvdb.RwTx,
_, node1Err := fetchLightningNode(nodes, edge.NodeKey1Bytes[:])
switch {
case errors.Is(node1Err, ErrGraphNodeNotFound):
- node1Shell := models.Node{
- PubKeyBytes: edge.NodeKey1Bytes,
- }
- err := addLightningNode(tx, &node1Shell)
+ err := addLightningNode(
+ tx, models.NewV1ShellNode(edge.NodeKey1Bytes),
+ )
if err != nil {
return fmt.Errorf("unable to create shell node "+
"for: %x: %w", edge.NodeKey1Bytes, err)
@@ -1220,10 +1215,9 @@ func (c *KVStore) addChannelEdge(tx kvdb.RwTx,
_, node2Err := fetchLightningNode(nodes, edge.NodeKey2Bytes[:])
switch {
case errors.Is(node2Err, ErrGraphNodeNotFound):
- node2Shell := models.Node{
- PubKeyBytes: edge.NodeKey2Bytes,
- }
- err := addLightningNode(tx, &node2Shell)
+ err := addLightningNode(
+ tx, models.NewV1ShellNode(edge.NodeKey2Bytes),
+ )
if err != nil {
return fmt.Errorf("unable to create shell node "+
"for: %x: %w", edge.NodeKey2Bytes, err)
@@ -2247,8 +2241,8 @@ func (c *KVStore) fetchNextChanUpdateBatch(
Info: &edgeInfo,
Policy1: edge1,
Policy2: edge2,
- Node1: &node1,
- Node2: &node2,
+ Node1: node1,
+ Node2: node2,
}
state.edgesSeen[chanIDInt] = struct{}{}
@@ -2394,10 +2388,10 @@ func newNodeUpdatesIterator(batchSize int, startTime, endTime time.Time,
// fetchNextNodeBatch fetches the next batch of node announcements using the
// iterator state.
func (c *KVStore) fetchNextNodeBatch(
- state *nodeUpdatesIterator) ([]models.Node, bool, error) {
+ state *nodeUpdatesIterator) ([]*models.Node, bool, error) {
var (
- nodeBatch []models.Node
+ nodeBatch []*models.Node
hasMore bool
)
@@ -2536,14 +2530,14 @@ func (c *KVStore) fetchNextNodeBatch(
// update timestamp within the passed range.
func (c *KVStore) NodeUpdatesInHorizon(startTime,
endTime time.Time,
- opts ...IteratorOption) iter.Seq2[models.Node, error] {
+ opts ...IteratorOption) iter.Seq2[*models.Node, error] {
cfg := defaultIteratorConfig()
for _, opt := range opts {
opt(cfg)
}
- return func(yield func(models.Node, error) bool) {
+ return func(yield func(*models.Node, error) bool) {
// Initialize iterator state.
state := newNodeUpdatesIterator(
cfg.nodeUpdateIterBatchSize,
@@ -2557,7 +2551,7 @@ func (c *KVStore) NodeUpdatesInHorizon(startTime,
log.Errorf("unable to read node updates in "+
"horizon: %v", err)
- yield(models.Node{}, err)
+ yield(&models.Node{}, err)
return
}
@@ -2945,8 +2939,8 @@ func (c *KVStore) fetchChanInfos(tx kvdb.RTx, chanIDs []uint64) (
Info: &edgeInfo,
Policy1: edge1,
Policy2: edge2,
- Node1: &node1,
- Node2: &node2,
+ Node1: node1,
+ Node2: node2,
})
}
@@ -3411,7 +3405,7 @@ func (c *KVStore) fetchLightningNode(tx kvdb.RTx,
return err
}
- node = &n
+ node = n
return nil
}
@@ -3694,7 +3688,7 @@ func (c *KVStore) fetchOtherNode(tx kvdb.RTx,
return err
}
- targetNode = &node
+ targetNode = node
return nil
}
@@ -4383,17 +4377,20 @@ func putLightningNode(nodeBucket, aliasBucket, updateIndex kvdb.RwBucket,
return err
}
- if err := binary.Write(&b, byteOrder, node.Color.R); err != nil {
+ nodeColor := node.Color.UnwrapOr(color.RGBA{})
+
+ if err := binary.Write(&b, byteOrder, nodeColor.R); err != nil {
return err
}
- if err := binary.Write(&b, byteOrder, node.Color.G); err != nil {
+ if err := binary.Write(&b, byteOrder, nodeColor.G); err != nil {
return err
}
- if err := binary.Write(&b, byteOrder, node.Color.B); err != nil {
+ if err := binary.Write(&b, byteOrder, nodeColor.B); err != nil {
return err
}
- if err := wire.WriteVarString(&b, 0, node.Alias); err != nil {
+ err = wire.WriteVarString(&b, 0, node.Alias.UnwrapOr(""))
+ if err != nil {
return err
}
@@ -4432,7 +4429,8 @@ func putLightningNode(nodeBucket, aliasBucket, updateIndex kvdb.RwBucket,
return err
}
- if err := aliasBucket.Put(nodePub, []byte(node.Alias)); err != nil {
+ err = aliasBucket.Put(nodePub, []byte(node.Alias.UnwrapOr("")))
+ if err != nil {
return err
}
@@ -4466,11 +4464,11 @@ func putLightningNode(nodeBucket, aliasBucket, updateIndex kvdb.RwBucket,
}
func fetchLightningNode(nodeBucket kvdb.RBucket,
- nodePub []byte) (models.Node, error) {
+ nodePub []byte) (*models.Node, error) {
nodeBytes := nodeBucket.Get(nodePub)
if nodeBytes == nil {
- return models.Node{}, ErrGraphNodeNotFound
+ return nil, ErrGraphNodeNotFound
}
nodeReader := bytes.NewReader(nodeBytes)
@@ -4533,30 +4531,29 @@ func deserializeLightningNodeCacheable(r io.Reader) (route.Vertex,
return pubKey, features, nil
}
-func deserializeLightningNode(r io.Reader) (models.Node, error) {
+func deserializeLightningNode(r io.Reader) (*models.Node, error) {
var (
- node models.Node
scratch [8]byte
err error
+ pubKey [33]byte
)
- // Always populate a feature vector, even if we don't have a node
- // announcement and short circuit below.
- node.Features = lnwire.EmptyFeatureVector()
-
if _, err := r.Read(scratch[:]); err != nil {
- return models.Node{}, err
+ return nil, err
}
unix := int64(byteOrder.Uint64(scratch[:]))
- node.LastUpdate = time.Unix(unix, 0)
+ lastUpdate := time.Unix(unix, 0)
- if _, err := io.ReadFull(r, node.PubKeyBytes[:]); err != nil {
- return models.Node{}, err
+ if _, err := io.ReadFull(r, pubKey[:]); err != nil {
+ return nil, err
}
+ node := models.NewV1ShellNode(pubKey)
+ node.LastUpdate = lastUpdate
+
if _, err := r.Read(scratch[:2]); err != nil {
- return models.Node{}, err
+ return nil, err
}
hasNodeAnn := byteOrder.Uint16(scratch[:2])
@@ -4568,28 +4565,31 @@ func deserializeLightningNode(r io.Reader) (models.Node, error) {
// We did get a node announcement for this node, so we'll have the rest
// of the data available.
- if err := binary.Read(r, byteOrder, &node.Color.R); err != nil {
- return models.Node{}, err
+ var nodeColor color.RGBA
+ if err := binary.Read(r, byteOrder, &nodeColor.R); err != nil {
+ return nil, err
}
- if err := binary.Read(r, byteOrder, &node.Color.G); err != nil {
- return models.Node{}, err
+ if err := binary.Read(r, byteOrder, &nodeColor.G); err != nil {
+ return nil, err
}
- if err := binary.Read(r, byteOrder, &node.Color.B); err != nil {
- return models.Node{}, err
+ if err := binary.Read(r, byteOrder, &nodeColor.B); err != nil {
+ return nil, err
}
+ node.Color = fn.Some(nodeColor)
- node.Alias, err = wire.ReadVarString(r, 0)
+ alias, err := wire.ReadVarString(r, 0)
if err != nil {
- return models.Node{}, err
+ return nil, err
}
+ node.Alias = fn.Some(alias)
err = node.Features.Decode(r)
if err != nil {
- return models.Node{}, err
+ return nil, err
}
if _, err := r.Read(scratch[:2]); err != nil {
- return models.Node{}, err
+ return nil, err
}
numAddresses := int(byteOrder.Uint16(scratch[:2]))
@@ -4597,7 +4597,7 @@ func deserializeLightningNode(r io.Reader) (models.Node, error) {
for i := 0; i < numAddresses; i++ {
address, err := DeserializeAddr(r)
if err != nil {
- return models.Node{}, err
+ return nil, err
}
addresses = append(addresses, address)
}
@@ -4605,7 +4605,7 @@ func deserializeLightningNode(r io.Reader) (models.Node, error) {
node.AuthSigBytes, err = wire.ReadVarBytes(r, 0, 80, "sig")
if err != nil {
- return models.Node{}, err
+ return nil, err
}
// We'll try and see if there are any opaque bytes left, if not, then
@@ -4617,7 +4617,7 @@ func deserializeLightningNode(r io.Reader) (models.Node, error) {
case errors.Is(err, io.ErrUnexpectedEOF):
case errors.Is(err, io.EOF):
case err != nil:
- return models.Node{}, err
+ return nil, err
}
if len(extraBytes) > 0 {
diff --git a/graph/db/models/node.go b/graph/db/models/node.go
index c347db5..8bbd837 100644
--- a/graph/db/models/node.go
+++ b/graph/db/models/node.go
@@ -7,7 +7,9 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
+ "github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/lnwire"
+ "github.com/lightningnetwork/lnd/routing/route"
)
// Node represents an individual vertex/node within the channel graph.
@@ -15,6 +17,9 @@ import (
// from it. As the graph is directed, a node will also have an incoming edge
// attached to it for each outgoing edge.
type Node struct {
+ // Version is the gossip version that this node was advertised on.
+ Version lnwire.GossipVersion
+
// PubKeyBytes is the raw bytes of the public key of the target node.
PubKeyBytes [33]byte
pubKey *btcec.PublicKey
@@ -27,11 +32,11 @@ type Node struct {
Addresses []net.Addr
// Color is the selected color for the node.
- Color color.RGBA
+ Color fn.Option[color.RGBA]
// Alias is a nick-name for the node. The alias can be used to confirm
// a node's identity or to serve as a short ID for an address book.
- Alias string
+ Alias fn.Option[string]
// AuthSigBytes is the raw signature under the advertised public key
// which serves to authenticate the attributes announced by this node.
@@ -49,6 +54,72 @@ type Node struct {
ExtraOpaqueData []byte
}
+// NodeV1Fields houses the fields that are specific to a version 1 node
+// announcement.
+type NodeV1Fields struct {
+ // Address is the TCP address this node is reachable over.
+ Addresses []net.Addr
+
+ // AuthSigBytes is the raw signature under the advertised public key
+ // which serves to authenticate the attributes announced by this node.
+ AuthSigBytes []byte
+
+ // Features is the list of protocol features supported by this node.
+ Features *lnwire.RawFeatureVector
+
+ // Color is the selected color for the node.
+ Color color.RGBA
+
+ // Alias is a nick-name for the node. The alias can be used to confirm
+ // a node's identity or to serve as a short ID for an address book.
+ Alias string
+
+ // LastUpdate is the last time the vertex information for this node has
+ // been updated.
+ LastUpdate time.Time
+
+ // ExtraOpaqueData is the set of data that was appended to this
+ // message, some of which we may not actually know how to iterate or
+ // parse. By holding onto this data, we ensure that we're able to
+ // properly validate the set of signatures that cover these new fields,
+ // and ensure we're able to make upgrades to the network in a forwards
+ // compatible manner.
+ ExtraOpaqueData []byte
+}
+
+// NewV1Node creates a new version 1 node from the passed fields.
+func NewV1Node(pub route.Vertex, n *NodeV1Fields) *Node {
+ return &Node{
+ Version: lnwire.GossipVersion1,
+ PubKeyBytes: pub,
+ Addresses: n.Addresses,
+ AuthSigBytes: n.AuthSigBytes,
+ Features: lnwire.NewFeatureVector(
+ n.Features, lnwire.Features,
+ ),
+ Color: fn.Some(n.Color),
+ Alias: fn.Some(n.Alias),
+ LastUpdate: n.LastUpdate,
+ ExtraOpaqueData: n.ExtraOpaqueData,
+ }
+}
+
+// NewV1ShellNode creates a new shell version 1 node.
+func NewV1ShellNode(pubKey route.Vertex) *Node {
+ return NewShellNode(lnwire.GossipVersion1, pubKey)
+}
+
+// NewShellNode creates a new shell node with the given gossip version and
+// public key.
+func NewShellNode(v lnwire.GossipVersion, pubKey route.Vertex) *Node {
+ return &Node{
+ Version: v,
+ PubKeyBytes: pubKey,
+ Features: lnwire.EmptyFeatureVector(),
+ LastUpdate: time.Unix(0, 0),
+ }
+}
+
// HaveAnnouncement returns true if we have received a node announcement for
// this node. We determine this by checking if we have a signature for the
// announcement.
@@ -85,7 +156,7 @@ func (n *Node) NodeAnnouncement(signed bool) (*lnwire.NodeAnnouncement1,
return nil, fmt.Errorf("node does not have node announcement")
}
- alias, err := lnwire.NewNodeAlias(n.Alias)
+ alias, err := lnwire.NewNodeAlias(n.Alias.UnwrapOr(""))
if err != nil {
return nil, err
}
@@ -93,7 +164,7 @@ func (n *Node) NodeAnnouncement(signed bool) (*lnwire.NodeAnnouncement1,
nodeAnn := &lnwire.NodeAnnouncement1{
Features: n.Features.RawFeatureVector,
NodeID: n.PubKeyBytes,
- RGBColor: n.Color,
+ RGBColor: n.Color.UnwrapOr(color.RGBA{}),
Alias: alias,
Addresses: n.Addresses,
Timestamp: uint32(n.LastUpdate.Unix()),
@@ -118,16 +189,17 @@ func (n *Node) NodeAnnouncement(signed bool) (*lnwire.NodeAnnouncement1,
// lnwire.NodeAnnouncement1 message.
func NodeFromWireAnnouncement(msg *lnwire.NodeAnnouncement1) *Node {
timestamp := time.Unix(int64(msg.Timestamp), 0)
- features := lnwire.NewFeatureVector(msg.Features, lnwire.Features)
- return &Node{
- LastUpdate: timestamp,
- Addresses: msg.Addresses,
- PubKeyBytes: msg.NodeID,
- Alias: msg.Alias.String(),
- AuthSigBytes: msg.Signature.ToSignatureBytes(),
- Features: features,
- Color: msg.RGBColor,
- ExtraOpaqueData: msg.ExtraOpaqueData,
- }
+ return NewV1Node(
+ msg.NodeID,
+ &NodeV1Fields{
+ LastUpdate: timestamp,
+ Addresses: msg.Addresses,
+ Alias: msg.Alias.String(),
+ AuthSigBytes: msg.Signature.ToSignatureBytes(),
+ Features: msg.Features,
+ Color: msg.RGBColor,
+ ExtraOpaqueData: msg.ExtraOpaqueData,
+ },
+ )
}
diff --git a/graph/db/notifications.go b/graph/db/notifications.go
index eecc38c..54a748c 100644
--- a/graph/db/notifications.go
+++ b/graph/db/notifications.go
@@ -391,9 +391,11 @@ func (c *ChannelGraph) addToTopologyChange(update *TopologyChange,
nodeUpdate := &NetworkNodeUpdate{
Addresses: m.Addresses,
IdentityKey: pubKey,
- Alias: m.Alias,
- Color: EncodeHexColor(m.Color),
- Features: m.Features.Clone(),
+ Alias: m.Alias.UnwrapOr(""),
+ Color: EncodeHexColor(
+ m.Color.UnwrapOr(color.RGBA{}),
+ ),
+ Features: m.Features.Clone(),
}
update.NodeUpdates = append(update.NodeUpdates, nodeUpdate)
diff --git a/graph/db/sql_migration.go b/graph/db/sql_migration.go
index 71a5525..5b5f6bf 100644
--- a/graph/db/sql_migration.go
+++ b/graph/db/sql_migration.go
@@ -7,6 +7,7 @@ import (
"database/sql"
"errors"
"fmt"
+ "image/color"
"net"
"slices"
"time"
@@ -1451,8 +1452,10 @@ func insertNodeSQLMig(ctx context.Context, db SQLQueries,
if node.HaveAnnouncement() {
params.LastUpdate = sqldb.SQLInt64(node.LastUpdate.Unix())
- params.Color = sqldb.SQLStrValid(EncodeHexColor(node.Color))
- params.Alias = sqldb.SQLStrValid(node.Alias)
+ params.Color = sqldb.SQLStrValid(
+ EncodeHexColor(node.Color.UnwrapOr(color.RGBA{})),
+ )
+ params.Alias = sqldb.SQLStrValid(node.Alias.UnwrapOr(""))
params.Signature = node.AuthSigBytes
}
diff --git a/graph/db/sql_migration_test.go b/graph/db/sql_migration_test.go
index fd44e0f..9819cf7 100644
--- a/graph/db/sql_migration_test.go
+++ b/graph/db/sql_migration_test.go
@@ -384,9 +384,7 @@ func TestMigrateGraphToSQL(t *testing.T) {
// The PruneGraph call requires that the source
// node be set. So that is the first object
// we will write.
- &models.Node{
- PubKeyBytes: testPub,
- },
+ models.NewV1ShellNode(testPub),
// Now we add some block heights to prune
// the graph at.
uint32(1), uint32(2), uint32(20), uint32(3),
@@ -746,16 +744,15 @@ type testNodeOpt func(*models.Node)
// makeTestNode can be used to create a test models.Node. The
// functional options can be used to modify the node's attributes.
func makeTestNode(t *testing.T, opts ...testNodeOpt) *models.Node {
- n := &models.Node{
+ n := models.NewV1Node(genPubKey(t), &models.NodeV1Fields{
AuthSigBytes: testSigBytes,
LastUpdate: testTime,
Color: testColor,
Alias: "kek",
- Features: testFeatures,
+ Features: testFeatures.RawFeatureVector,
Addresses: testAddrs,
ExtraOpaqueData: testExtraData,
- PubKeyBytes: genPubKey(t),
- }
+ })
for _, opt := range opts {
opt(n)
@@ -774,11 +771,7 @@ func makeTestNode(t *testing.T, opts ...testNodeOpt) *models.Node {
func makeTestShellNode(t *testing.T,
opts ...testNodeOpt) *models.Node {
- n := &models.Node{
- PubKeyBytes: genPubKey(t),
- Features: testEmptyFeatures,
- LastUpdate: time.Unix(0, 0),
- }
+ n := models.NewV1ShellNode(genPubKey(t))
for _, opt := range opts {
opt(n)
@@ -1813,18 +1806,15 @@ func genRandomNode(t *rapid.T) *models.Node {
extraOpaqueData = nil
}
- node := &models.Node{
- AuthSigBytes: sigBytes,
- LastUpdate: randTime,
- Color: randColor,
- Alias: alias.String(),
- Features: lnwire.NewFeatureVector(
- features, lnwire.Features,
- ),
+ node := models.NewV1Node(pubKeyBytes, &models.NodeV1Fields{
+ AuthSigBytes: sigBytes,
+ LastUpdate: randTime,
+ Color: randColor,
+ Alias: alias.String(),
+ Features: features,
Addresses: addrs,
ExtraOpaqueData: extraOpaqueData,
- PubKeyBytes: pubKeyBytes,
- }
+ })
// We call this method so that the internal pubkey field is populated
// which then lets us to proper struct comparison later on.
diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go
index d63021a..3578fd9 100644
--- a/graph/db/sql_store.go
+++ b/graph/db/sql_store.go
@@ -7,6 +7,7 @@ import (
"encoding/hex"
"errors"
"fmt"
+ color "image/color"
"iter"
"maps"
"math"
@@ -555,14 +556,14 @@ func (s *SQLStore) SetSourceNode(ctx context.Context,
//
// NOTE: This is part of the V1Store interface.
func (s *SQLStore) NodeUpdatesInHorizon(startTime, endTime time.Time,
- opts ...IteratorOption) iter.Seq2[models.Node, error] {
+ opts ...IteratorOption) iter.Seq2[*models.Node, error] {
cfg := defaultIteratorConfig()
for _, opt := range opts {
opt(cfg)
}
- return func(yield func(models.Node, error) bool) {
+ return func(yield func(*models.Node, error) bool) {
var (
ctx = context.TODO()
lastUpdateTime sql.NullInt64
@@ -573,7 +574,7 @@ func (s *SQLStore) NodeUpdatesInHorizon(startTime, endTime time.Time,
// Each iteration, we'll read a batch amount of nodes, yield
// them, then decide is we have more or not.
for hasMore {
- var batch []models.Node
+ var batch []*models.Node
//nolint:ll
err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
@@ -607,7 +608,7 @@ func (s *SQLStore) NodeUpdatesInHorizon(startTime, endTime time.Time,
err = forEachNodeInBatch(
ctx, s.cfg.QueryCfg, db, rows,
func(_ int64, node *models.Node) error {
- batch = append(batch, *node)
+ batch = append(batch, node)
// Update pagination cursors
// based on the last processed
@@ -629,14 +630,14 @@ func (s *SQLStore) NodeUpdatesInHorizon(startTime, endTime time.Time,
return nil
}, func() {
- batch = []models.Node{}
+ batch = []*models.Node{}
})
if err != nil {
log.Errorf("NodeUpdatesInHorizon batch "+
"error: %v", err)
- yield(models.Node{}, err)
+ yield(&models.Node{}, err)
return
}
@@ -3485,27 +3486,30 @@ func buildNodeWithBatchData(dbNode sqlc.GraphNode,
var pub [33]byte
copy(pub[:], dbNode.PubKey)
- node := &models.Node{
- PubKeyBytes: pub,
- Features: lnwire.EmptyFeatureVector(),
- LastUpdate: time.Unix(0, 0),
- }
+ node := models.NewV1ShellNode(pub)
if len(dbNode.Signature) == 0 {
return node, nil
}
node.AuthSigBytes = dbNode.Signature
- node.Alias = dbNode.Alias.String
- node.LastUpdate = time.Unix(dbNode.LastUpdate.Int64, 0)
+
+ if dbNode.Alias.Valid {
+ node.Alias = fn.Some(dbNode.Alias.String)
+ }
+ if dbNode.LastUpdate.Valid {
+ node.LastUpdate = time.Unix(dbNode.LastUpdate.Int64, 0)
+ }
var err error
if dbNode.Color.Valid {
- node.Color, err = DecodeHexColor(dbNode.Color.String)
+ nodeColor, err := DecodeHexColor(dbNode.Color.String)
if err != nil {
return nil, fmt.Errorf("unable to decode color: %w",
err)
}
+
+ node.Color = fn.Some(nodeColor)
}
// Use preloaded features.
@@ -3608,9 +3612,26 @@ func upsertNode(ctx context.Context, db SQLQueries,
}
if node.HaveAnnouncement() {
- params.LastUpdate = sqldb.SQLInt64(node.LastUpdate.Unix())
- params.Color = sqldb.SQLStrValid(EncodeHexColor(node.Color))
- params.Alias = sqldb.SQLStrValid(node.Alias)
+ switch node.Version {
+ case lnwire.GossipVersion1:
+ params.LastUpdate = sqldb.SQLInt64(
+ node.LastUpdate.Unix(),
+ )
+
+ case lnwire.GossipVersion2:
+
+ default:
+ return 0, fmt.Errorf("unknown gossip version: %d",
+ node.Version)
+ }
+
+ node.Color.WhenSome(func(rgba color.RGBA) {
+ params.Color = sqldb.SQLStrValid(EncodeHexColor(rgba))
+ })
+ node.Alias.WhenSome(func(s string) {
+ params.Alias = sqldb.SQLStrValid(s)
+ })
+
params.Signature = node.AuthSigBytes
}
diff --git a/graph/notifications_test.go b/graph/notifications_test.go
index c3db240..3a30486 100644
--- a/graph/notifications_test.go
+++ b/graph/notifications_test.go
@@ -83,15 +83,16 @@ func createTestNode(t *testing.T) *models.Node {
require.NoError(t, err)
pub := priv.PubKey().SerializeCompressed()
- n := &models.Node{
- LastUpdate: time.Unix(updateTime, 0),
- Addresses: testAddrs,
- Color: color.RGBA{1, 2, 3, 0},
- Alias: "kek" + hex.EncodeToString(pub),
- AuthSigBytes: testSig.Serialize(),
- Features: testFeatures,
- }
- copy(n.PubKeyBytes[:], pub)
+ n := models.NewV1Node(
+ route.NewVertex(priv.PubKey()), &models.NodeV1Fields{
+ LastUpdate: time.Unix(updateTime, 0),
+ Addresses: testAddrs,
+ Color: color.RGBA{1, 2, 3, 0},
+ Alias: "kek" + hex.EncodeToString(pub),
+ AuthSigBytes: testSig.Serialize(),
+ Features: testFeatures.RawFeatureVector,
+ },
+ )
return n
}
@@ -699,15 +700,12 @@ func TestNodeUpdateNotification(t *testing.T) {
t, testFeaturesBuf.Bytes(), featuresBuf.Bytes(),
)
- if nodeUpdate.Alias != ann.Alias {
- t.Fatalf("node alias doesn't match: expected %v, got %v",
- ann.Alias, nodeUpdate.Alias)
- }
- if nodeUpdate.Color != graphdb.EncodeHexColor(ann.Color) {
- t.Fatalf("node color doesn't match: expected %v, "+
- "got %v", graphdb.EncodeHexColor(ann.Color),
- nodeUpdate.Color)
- }
+ require.Equal(t, nodeUpdate.Alias, ann.Alias.UnwrapOr(""))
+ require.Equal(
+ t, nodeUpdate.Color, graphdb.EncodeHexColor(
+ ann.Color.UnwrapOr(color.RGBA{}),
+ ),
+ )
}
// Create lookup map for notifications we are intending to receive. Entries
diff --git a/lnrpc/devrpc/dev_server.go b/lnrpc/devrpc/dev_server.go
index de142b9..39c089d 100644
--- a/lnrpc/devrpc/dev_server.go
+++ b/lnrpc/devrpc/dev_server.go
@@ -226,18 +226,7 @@ func (s *Server) ImportGraph(ctx context.Context,
var err error
for _, rpcNode := range graph.Nodes {
- node := &models.Node{
- LastUpdate: time.Unix(
- int64(rpcNode.LastUpdate), 0,
- ),
- Alias: rpcNode.Alias,
- // NOTE: this is a workaround to ensure that
- // HaveAnnouncement() returns true so that the other
- // fields are properly persisted. However,
- AuthSigBytes: []byte{0},
- }
-
- node.PubKeyBytes, err = parsePubKey(rpcNode.PubKey)
+ pubKeyBytes, err := parsePubKey(rpcNode.PubKey)
if err != nil {
return nil, err
}
@@ -254,15 +243,25 @@ func (s *Server) ImportGraph(ctx context.Context,
}
featureVector := lnwire.NewRawFeatureVector(featureBits...)
- node.Features = lnwire.NewFeatureVector(
- featureVector, featureNames,
- )
- node.Color, err = lncfg.ParseHexColor(rpcNode.Color)
+ nodeColor, err := lncfg.ParseHexColor(rpcNode.Color)
if err != nil {
return nil, err
}
+ node := models.NewV1Node(pubKeyBytes, &models.NodeV1Fields{
+ LastUpdate: time.Unix(
+ int64(rpcNode.LastUpdate), 0,
+ ),
+ Alias: rpcNode.Alias,
+ Features: featureVector,
+ Color: nodeColor,
+ // NOTE: this is a workaround to ensure that
+ // HaveAnnouncement() returns true so that the other
+ // fields are properly persisted. However,
+ AuthSigBytes: []byte{0},
+ })
+
if err := graphDB.AddNode(ctx, node); err != nil {
return nil, fmt.Errorf("unable to add node %v: %w",
rpcNode.PubKey, err)
diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go
index 664f5c0..473bd41 100644
--- a/routing/pathfind_test.go
+++ b/routing/pathfind_test.go
@@ -228,14 +228,16 @@ func parseTestGraph(t *testing.T, useCache bool, path string) (
return nil, err
}
- dbNode := &models.Node{
+ pubKey, err := route.NewVertexFromBytes(pubBytes)
+ require.NoError(t, err)
+
+ dbNode := models.NewV1Node(pubKey, &models.NodeV1Fields{
AuthSigBytes: testSig.Serialize(),
LastUpdate: testTime,
Addresses: testAddrs,
Alias: node.Alias,
- Features: testFeatures,
- }
- copy(dbNode.PubKeyBytes[:], pubBytes)
+ Features: testFeatures.RawFeatureVector,
+ })
// We require all aliases within the graph to be unique for our
// tests.
@@ -564,15 +566,15 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
features = lnwire.EmptyFeatureVector()
}
- dbNode := &models.Node{
- AuthSigBytes: testSig.Serialize(),
- LastUpdate: testTime,
- Addresses: testAddrs,
- Alias: alias,
- Features: features,
- }
-
- copy(dbNode.PubKeyBytes[:], pubKey.SerializeCompressed())
+ dbNode := models.NewV1Node(
+ route.NewVertex(pubKey), &models.NodeV1Fields{
+ AuthSigBytes: testSig.Serialize(),
+ LastUpdate: testTime,
+ Addresses: testAddrs,
+ Alias: alias,
+ Features: features.RawFeatureVector,
+ },
+ )
privKeyMap[alias] = privKey
@@ -1249,10 +1251,12 @@ func runPathFindingWithAdditionalEdges(t *testing.T, useCache bool) {
dogePubKeyBytes, err := hex.DecodeString(dogePubKeyHex)
require.NoError(t, err, "unable to decode public key")
- doge := &models.Node{}
- copy(doge.PubKeyBytes[:], dogePubKeyBytes[:])
- doge.Alias = "doge"
- copy(doge.PubKeyBytes[:], dogePubKeyBytes)
+ pubKey, err := route.NewVertexFromBytes(dogePubKeyBytes)
+ require.NoError(t, err)
+
+ doge := models.NewV1Node(pubKey, &models.NodeV1Fields{
+ Alias: "doge",
+ })
graph.aliasMap["doge"] = doge.PubKeyBytes
// Create the channel edge going from songoku to doge and include it in
diff --git a/routing/payment_session_test.go b/routing/payment_session_test.go
index 12d8608..547fe0e 100644
--- a/routing/payment_session_test.go
+++ b/routing/payment_session_test.go
@@ -89,8 +89,9 @@ func TestUpdateAdditionalEdge(t *testing.T) {
// Create a minimal test node using the private key priv1.
pub := priv1.PubKey().SerializeCompressed()
- testNode := &models.Node{}
- copy(testNode.PubKeyBytes[:], pub)
+ var pubKey [33]byte
+ copy(pubKey[:], pub)
+ testNode := models.NewV1ShellNode(pubKey)
nodeID, err := testNode.PubKey()
require.NoError(t, err, "failed to get node id")
diff --git a/routing/router_test.go b/routing/router_test.go
index e46bb1f..d087e2d 100644
--- a/routing/router_test.go
+++ b/routing/router_test.go
@@ -192,15 +192,16 @@ func createTestNode() (*models.Node, error) {
}
pub := priv.PubKey().SerializeCompressed()
- n := &models.Node{
- LastUpdate: time.Unix(updateTime, 0),
- Addresses: testAddrs,
- Color: color.RGBA{1, 2, 3, 0},
- Alias: "kek" + string(pub),
- AuthSigBytes: testSig.Serialize(),
- Features: testFeatures,
- }
- copy(n.PubKeyBytes[:], pub)
+ n := models.NewV1Node(
+ route.NewVertex(priv.PubKey()), &models.NodeV1Fields{
+ LastUpdate: time.Unix(updateTime, 0),
+ Addresses: testAddrs,
+ Color: color.RGBA{1, 2, 3, 0},
+ Alias: "kek" + string(pub),
+ AuthSigBytes: testSig.Serialize(),
+ Features: testFeatures.RawFeatureVector,
+ },
+ )
return n, nil
}
@@ -2870,27 +2871,29 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
// Now check that we can update the node info for the partial node
// without messing up the channel graph.
- n1 := &models.Node{
- LastUpdate: time.Unix(123, 0),
- Addresses: testAddrs,
- Color: color.RGBA{1, 2, 3, 0},
- Alias: "node11",
- AuthSigBytes: testSig.Serialize(),
- Features: testFeatures,
- }
- copy(n1.PubKeyBytes[:], priv1.PubKey().SerializeCompressed())
+ n1 := models.NewV1Node(
+ route.NewVertex(priv1.PubKey()), &models.NodeV1Fields{
+ LastUpdate: time.Unix(123, 0),
+ Addresses: testAddrs,
+ Color: color.RGBA{1, 2, 3, 0},
+ Alias: "node11",
+ AuthSigBytes: testSig.Serialize(),
+ Features: testFeatures.RawFeatureVector,
+ },
+ )
require.NoError(t, ctx.graph.AddNode(ctxb, n1))
- n2 := &models.Node{
- LastUpdate: time.Unix(123, 0),
- Addresses: testAddrs,
- Color: color.RGBA{1, 2, 3, 0},
- Alias: "node22",
- AuthSigBytes: testSig.Serialize(),
- Features: testFeatures,
- }
- copy(n2.PubKeyBytes[:], priv2.PubKey().SerializeCompressed())
+ n2 := models.NewV1Node(
+ route.NewVertex(priv2.PubKey()), &models.NodeV1Fields{
+ LastUpdate: time.Unix(123, 0),
+ Addresses: testAddrs,
+ Color: color.RGBA{1, 2, 3, 0},
+ Alias: "node22",
+ AuthSigBytes: testSig.Serialize(),
+ Features: testFeatures.RawFeatureVector,
+ },
+ )
require.NoError(t, ctx.graph.AddNode(ctxb, n2))
diff --git a/rpcserver.go b/rpcserver.go
index 6038563..ee810d1 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -6,6 +6,7 @@ import (
"encoding/hex"
"errors"
"fmt"
+ "image/color"
"io"
"maps"
"math"
@@ -7187,11 +7188,13 @@ func marshalNode(node *models.Node) *lnrpc.LightningNode {
customRecords := marshalExtraOpaqueData(node.ExtraOpaqueData)
return &lnrpc.LightningNode{
- LastUpdate: uint32(node.LastUpdate.Unix()),
- PubKey: hex.EncodeToString(node.PubKeyBytes[:]),
- Addresses: nodeAddrs,
- Alias: node.Alias,
- Color: graphdb.EncodeHexColor(node.Color),
+ LastUpdate: uint32(node.LastUpdate.Unix()),
+ PubKey: hex.EncodeToString(node.PubKeyBytes[:]),
+ Addresses: nodeAddrs,
+ Alias: node.Alias.UnwrapOr(""),
+ Color: graphdb.EncodeHexColor(
+ node.Color.UnwrapOr(color.RGBA{}),
+ ),
Features: features,
CustomRecords: customRecords,
}
@@ -8258,9 +8261,9 @@ func (r *rpcServer) ForwardingHistory(ctx context.Context,
}
// Cache the peer alias.
- chanToPeerAlias[chanID] = peer.Alias
+ chanToPeerAlias[chanID] = peer.Alias.UnwrapOr("")
- return peer.Alias, nil
+ return peer.Alias.UnwrapOr(""), nil
}
// TODO(roasbeef): add settlement latency?
diff --git a/server.go b/server.go
index 3594b7d..d0289de 100644
--- a/server.go
+++ b/server.go
@@ -7,6 +7,7 @@ import (
"encoding/hex"
"errors"
"fmt"
+ "image/color"
"math/big"
prand "math/rand"
"net"
@@ -3299,17 +3300,17 @@ func (s *server) createNewHiddenService(ctx context.Context) error {
// Finally, we'll update the on-disk version of our announcement so it
// will eventually propagate to nodes in the network.
- selfNode := &models.Node{
- LastUpdate: time.Unix(int64(newNodeAnn.Timestamp), 0),
- Addresses: newNodeAnn.Addresses,
- Alias: newNodeAnn.Alias.String(),
- Features: lnwire.NewFeatureVector(
- newNodeAnn.Features, lnwire.Features,
- ),
- Color: newNodeAnn.RGBColor,
- AuthSigBytes: newNodeAnn.Signature.ToSignatureBytes(),
- }
- copy(selfNode.PubKeyBytes[:], s.identityECDH.PubKey().SerializeCompressed())
+ selfNode := models.NewV1Node(
+ route.NewVertex(s.identityECDH.PubKey()), &models.NodeV1Fields{
+ Addresses: newNodeAnn.Addresses,
+ Features: newNodeAnn.Features,
+ AuthSigBytes: newNodeAnn.Signature.ToSignatureBytes(),
+ Color: newNodeAnn.RGBColor,
+ Alias: newNodeAnn.Alias.String(),
+ LastUpdate: time.Unix(int64(newNodeAnn.Timestamp), 0),
+ },
+ )
+
if err := s.graphDB.SetSourceNode(ctx, selfNode); err != nil {
return fmt.Errorf("can't set self node: %w", err)
}
@@ -3425,9 +3426,9 @@ func (s *server) updateAndBroadcastSelfNode(ctx context.Context,
selfNode.LastUpdate = time.Unix(int64(newNodeAnn.Timestamp), 0)
selfNode.Addresses = newNodeAnn.Addresses
- selfNode.Alias = newNodeAnn.Alias.String()
+ selfNode.Alias = fn.Some(newNodeAnn.Alias.String())
selfNode.Features = s.featureMgr.Get(feature.SetNodeAnn)
- selfNode.Color = newNodeAnn.RGBColor
+ selfNode.Color = fn.Some(newNodeAnn.RGBColor)
selfNode.AuthSigBytes = newNodeAnn.Signature.ToSignatureBytes()
copy(selfNode.PubKeyBytes[:], s.identityECDH.PubKey().SerializeCompressed())
@@ -5583,7 +5584,7 @@ func (s *server) setSelfNode(ctx context.Context, nodePub route.Vertex,
// Parse the color from config. We will update this later if the config
// color is not changed from default (#3399FF) and we have a value in
// the source node.
- color, err := lncfg.ParseHexColor(s.cfg.Color)
+ nodeColor, err := lncfg.ParseHexColor(s.cfg.Color)
if err != nil {
return fmt.Errorf("unable to parse color: %w", err)
}
@@ -5607,13 +5608,17 @@ func (s *server) setSelfNode(ctx context.Context, nodePub route.Vertex,
// didn't specify a different color in the config. We'll use the
// source node's color.
if s.cfg.Color == defaultColor {
- color = srcNode.Color
+ srcNode.Color.WhenSome(func(rgba color.RGBA) {
+ nodeColor = rgba
+ })
}
// If an alias is not specified in the config, we'll use the
// source node's alias.
if alias == "" {
- alias = srcNode.Alias
+ srcNode.Alias.WhenSome(func(s string) {
+ alias = s
+ })
}
// If the `externalip` is not specified in the config, it means
@@ -5643,15 +5648,15 @@ func (s *server) setSelfNode(ctx context.Context, nodePub route.Vertex,
// TODO(abdulkbk): potentially find a way to use the source node's
// features in the self node.
- selfNode := &models.Node{
- LastUpdate: nodeLastUpdate,
- Addresses: addrs,
- Alias: nodeAlias.String(),
- Color: color,
- Features: s.featureMgr.Get(feature.SetNodeAnn),
- }
-
- copy(selfNode.PubKeyBytes[:], nodePub[:])
+ selfNode := models.NewV1Node(
+ nodePub, &models.NodeV1Fields{
+ Alias: nodeAlias.String(),
+ Color: nodeColor,
+ LastUpdate: nodeLastUpdate,
+ Addresses: addrs,
+ Features: s.featureMgr.GetRaw(feature.SetNodeAnn),
+ },
+ )
// Based on the disk representation of the node announcement generated
// above, we'll generate a node announcement that can go out on the
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.