diff --git a/channeldb/graph.go b/channeldb/graph.go index c945238a..cae4790f 100644 --- a/channeldb/graph.go +++ b/channeldb/graph.go @@ -328,9 +328,13 @@ func (c *ChannelGraph) SetSourceNode(node *LightningNode) error { }) } -// AddLightningNode adds a new (unconnected) vertex/node to the graph database. -// When adding an edge, each node must be added before the edge can be -// inserted. Afterwards the edge information can then be updated. +// AddLightningNode adds a vertex/node to the graph database. If the node is not +// in the database from before, this will add a new, unconnected one to the +// graph. If it is present from before, this will update that node's +// information. Note that this method is expected to only be called to update +// an already present node from a node annoucement, or to insert a node found +// in a channel update. +// // TODO(roasbeef): also need sig of announcement func (c *ChannelGraph) AddLightningNode(node *LightningNode) error { return c.db.Update(func(tx *bolt.Tx) error { @@ -850,6 +854,15 @@ func (c *ChannelGraph) UpdateEdgePolicy(edge *ChannelEdgePolicy) error { // from it. As the graph is directed, a node will also have an incoming edge // attached to it for each outgoing edge. type LightningNode struct { + // PubKey is the node's long-term identity public key. This key will be + // used to authenticated any advertisements/updates sent by the node. + PubKey *btcec.PublicKey + + // HaveNodeAnnouncement indicates whether we received a node annoucement + // for this particular node. If true, the remaining fields will be set, + // if false only the PubKey is known for this node. + HaveNodeAnnouncement bool + // LastUpdate is the last time the vertex information for this node has // been updated. LastUpdate time.Time @@ -857,10 +870,6 @@ type LightningNode struct { // Address is the TCP address this node is reachable over. Addresses []net.Addr - // PubKey is the node's long-term identity public key. This key will be - // used to authenticated any advertisements/updates sent by the node. - PubKey *btcec.PublicKey - // Color is the selected color for the node. Color color.RGBA @@ -1384,11 +1393,12 @@ func putLightningNode(nodeBucket *bolt.Bucket, aliasBucket *bolt.Bucket, node *L nodePub := node.PubKey.SerializeCompressed() - if err := aliasBucket.Put(nodePub, []byte(node.Alias)); err != nil { - return err + // If the node has the update time set, write it, else write 0. + updateUnix := uint64(0) + if node.LastUpdate.Unix() > 0 { + updateUnix = uint64(node.LastUpdate.Unix()) } - updateUnix := uint64(node.LastUpdate.Unix()) byteOrder.PutUint64(scratch[:8], updateUnix) if _, err := b.Write(scratch[:8]); err != nil { return err @@ -1398,6 +1408,24 @@ func putLightningNode(nodeBucket *bolt.Bucket, aliasBucket *bolt.Bucket, node *L return err } + // If we got a node announcement for this node, we will have the rest of + // the data available. If not we don't have more data to write. + if !node.HaveNodeAnnouncement { + // Write HaveNodeAnnouncement=0. + byteOrder.PutUint16(scratch[:2], 0) + if _, err := b.Write(scratch[:2]); err != nil { + return err + } + + return nodeBucket.Put(nodePub, b.Bytes()) + } + + // Write HaveNodeAnnouncement=1. + byteOrder.PutUint16(scratch[:2], 1) + if _, err := b.Write(scratch[:2]); err != nil { + return err + } + if err := binary.Write(&b, byteOrder, node.Color.R); err != nil { return err } @@ -1456,7 +1484,12 @@ func putLightningNode(nodeBucket *bolt.Bucket, aliasBucket *bolt.Bucket, node *L return err } + if err := aliasBucket.Put(nodePub, []byte(node.Alias)); err != nil { + return err + } + return nodeBucket.Put(nodePub, b.Bytes()) + } func fetchLightningNode(nodeBucket *bolt.Bucket, @@ -1492,6 +1525,25 @@ func deserializeLightningNode(r io.Reader) (*LightningNode, error) { return nil, err } + if _, err := r.Read(scratch[:2]); err != nil { + return nil, err + } + + hasNodeAnn := byteOrder.Uint16(scratch[:2]) + if hasNodeAnn == 1 { + node.HaveNodeAnnouncement = true + } else { + node.HaveNodeAnnouncement = false + } + + // The rest of the data is optional, and will only be there if we got a node + // announcement for this node. + if !node.HaveNodeAnnouncement { + return node, nil + } + + // 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 nil, err } diff --git a/channeldb/graph_test.go b/channeldb/graph_test.go index 5233266d..11ee5523 100644 --- a/channeldb/graph_test.go +++ b/channeldb/graph_test.go @@ -51,14 +51,15 @@ func createTestVertex(db *DB) (*LightningNode, error) { pub := priv.PubKey().SerializeCompressed() return &LightningNode{ - AuthSig: testSig, - LastUpdate: time.Unix(updateTime, 0), - PubKey: priv.PubKey(), - Color: color.RGBA{1, 2, 3, 0}, - Alias: "kek" + string(pub[:]), - Features: testFeatures, - Addresses: testAddrs, - db: db, + HaveNodeAnnouncement: true, + AuthSig: testSig, + LastUpdate: time.Unix(updateTime, 0), + PubKey: priv.PubKey(), + Color: color.RGBA{1, 2, 3, 0}, + Alias: "kek" + string(pub[:]), + Features: testFeatures, + Addresses: testAddrs, + db: db, }, nil } @@ -77,14 +78,15 @@ func TestNodeInsertionAndDeletion(t *testing.T) { // graph, so we'll create a test vertex to start with. _, testPub := btcec.PrivKeyFromBytes(btcec.S256(), key[:]) node := &LightningNode{ - AuthSig: testSig, - LastUpdate: time.Unix(1232342, 0), - PubKey: testPub, - Color: color.RGBA{1, 2, 3, 0}, - Alias: "kek", - Features: testFeatures, - Addresses: testAddrs, - db: db, + HaveNodeAnnouncement: true, + AuthSig: testSig, + LastUpdate: time.Unix(1232342, 0), + PubKey: testPub, + Color: color.RGBA{1, 2, 3, 0}, + Alias: "kek", + Features: testFeatures, + Addresses: testAddrs, + db: db, } // First, insert the node into the graph DB. This should succeed @@ -125,6 +127,71 @@ func TestNodeInsertionAndDeletion(t *testing.T) { } } +// TestPartialNode checks that we can add and retrieve a LightningNode where +// where only the pubkey is known to the database. +func TestPartialNode(t *testing.T) { + t.Parallel() + + db, cleanUp, err := makeTestDB() + defer cleanUp() + if err != nil { + t.Fatalf("unable to make test database: %v", err) + } + + graph := db.ChannelGraph() + + // We want to be able to insert nodes into the graph that only has the + // PubKey set. + _, testPub := btcec.PrivKeyFromBytes(btcec.S256(), key[:]) + node := &LightningNode{ + PubKey: testPub, + HaveNodeAnnouncement: false, + } + + if err := graph.AddLightningNode(node); err != nil { + t.Fatalf("unable to add node: %v", err) + } + + // Next, fetch the node from the database to ensure everything was + // serialized properly. + dbNode, err := graph.FetchLightningNode(testPub) + if err != nil { + t.Fatalf("unable to locate node: %v", err) + } + + if _, exists, err := graph.HasLightningNode(testPub); err != nil { + t.Fatalf("unable to query for node: %v", err) + } else if !exists { + t.Fatalf("node should be found but wasn't") + } + + // The two nodes should match exactly! (with default values for + // LastUpdate and db set to satisfy compareNodes()) + node = &LightningNode{ + PubKey: testPub, + HaveNodeAnnouncement: false, + LastUpdate: time.Unix(0, 0), + db: db, + } + + if err := compareNodes(node, dbNode); err != nil { + t.Fatalf("nodes don't match: %v", err) + } + + // Next, delete the node from the graph, this should purge all data + // related to the node. + if err := graph.DeleteLightningNode(testPub); err != nil { + t.Fatalf("unable to delete node; %v", err) + } + + // Finally, attempt to fetch the node again. This should fail as the + // node should've been deleted from the database. + _, err = graph.FetchLightningNode(testPub) + if err != ErrGraphNodeNotFound { + t.Fatalf("fetch after delete should fail!") + } +} + func TestAliasLookup(t *testing.T) { t.Parallel() @@ -929,6 +996,10 @@ func compareNodes(a, b *LightningNode) error { return fmt.Errorf("db doesn't match: expected %#v, \n "+ "got %#v", a.db, b.db) } + if !reflect.DeepEqual(a.HaveNodeAnnouncement, b.HaveNodeAnnouncement) { + return fmt.Errorf("HaveNodeAnnouncement doesn't match: expected %#v, \n "+ + "got %#v", a.HaveNodeAnnouncement, b.HaveNodeAnnouncement) + } return nil }