From ed8e10e4b90638c038807e1736992fbeadd70b8f Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 18 Feb 2025 13:10:07 -0300 Subject: [PATCH 01/24] graph/db: rename graph.go file Rename it to kv_store.go so that we can re-use the graph.go file name later on. We will use it to house the _new_ ChannelGraph when the existing ChannelGraph is renamed to more clearly reflect its responsibilities as the CRUD layer. --- graph/db/{graph.go => kv_store.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename graph/db/{graph.go => kv_store.go} (100%) diff --git a/graph/db/graph.go b/graph/db/kv_store.go similarity index 100% rename from graph/db/graph.go rename to graph/db/kv_store.go From 1ee4bb8c519a69edb721a480edb81b8223a4a147 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 18 Feb 2025 13:19:50 -0300 Subject: [PATCH 02/24] graph/db: rename ChannelGraph and introduce the new ChannelGraph layer In this commit, we rename the existing ChannelGraph struct to KVStore to better reflect its responsibilities as the CRUD layer. We then introduce a new ChannelGraph struct which will eventually be the layer above the CRUD layer in which we will handle cacheing and topology subscriptions. For now, however, it houses only the KVStore. This means that all calls to the KVStore will now go through this layer of indirection first. This will allow us to slowly move the graph Cache management out of the KVStore and into the new ChannelGraph layer. We introduce the new ChannelGraph and rename the old one in the same commit so that all existing call-sites don't need to change at all :) --- graph/db/graph.go | 26 ++++++++ graph/db/kv_store.go | 150 +++++++++++++++++++++---------------------- 2 files changed, 101 insertions(+), 75 deletions(-) create mode 100644 graph/db/graph.go diff --git a/graph/db/graph.go b/graph/db/graph.go new file mode 100644 index 000000000..217302ca9 --- /dev/null +++ b/graph/db/graph.go @@ -0,0 +1,26 @@ +package graphdb + +import "github.com/lightningnetwork/lnd/kvdb" + +// ChannelGraph is a layer above the graph's CRUD layer. +// +// NOTE: currently, this is purely a pass-through layer directly to the backing +// KVStore. Upcoming commits will move the graph cache out of the KVStore and +// into this layer so that the KVStore is only responsible for CRUD operations. +type ChannelGraph struct { + *KVStore +} + +// NewChannelGraph creates a new ChannelGraph instance with the given backend. +func NewChannelGraph(db kvdb.Backend, options ...OptionModifier) (*ChannelGraph, + error) { + + store, err := NewKVStore(db, options...) + if err != nil { + return nil, err + } + + return &ChannelGraph{ + KVStore: store, + }, nil +} diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 0a643144e..dfe856419 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -171,7 +171,7 @@ const ( MaxAllowedExtraOpaqueBytes = 10000 ) -// ChannelGraph is a persistent, on-disk graph representation of the Lightning +// KVStore is a persistent, on-disk graph representation of the Lightning // Network. This struct can be used to implement path finding algorithms on top // of, and also to update a node's view based on information received from the // p2p network. Internally, the graph is stored using a modified adjacency list @@ -181,7 +181,7 @@ const ( // Nodes, edges, and edge information can all be added to the graph // independently. Edge removal results in the deletion of all edge information // for that edge. -type ChannelGraph struct { +type KVStore struct { db kvdb.Backend // cacheMu guards all caches (rejectCache, chanCache, graphCache). If @@ -196,9 +196,9 @@ type ChannelGraph struct { nodeScheduler batch.Scheduler } -// NewChannelGraph allocates a new ChannelGraph backed by a DB instance. The +// NewKVStore allocates a new KVStore backed by a DB instance. The // returned instance has its own unique reject cache and channel cache. -func NewChannelGraph(db kvdb.Backend, options ...OptionModifier) (*ChannelGraph, +func NewKVStore(db kvdb.Backend, options ...OptionModifier) (*KVStore, error) { opts := DefaultOptions() @@ -207,12 +207,12 @@ func NewChannelGraph(db kvdb.Backend, options ...OptionModifier) (*ChannelGraph, } if !opts.NoMigration { - if err := initChannelGraph(db); err != nil { + if err := initKVStore(db); err != nil { return nil, err } } - g := &ChannelGraph{ + g := &KVStore{ db: db, rejectCache: newRejectCache(opts.RejectCacheSize), chanCache: newChannelCache(opts.ChannelCacheSize), @@ -269,7 +269,7 @@ type channelMapKey struct { // getChannelMap loads all channel edge policies from the database and stores // them in a map. -func (c *ChannelGraph) getChannelMap(edges kvdb.RBucket) ( +func (c *KVStore) getChannelMap(edges kvdb.RBucket) ( map[channelMapKey]*models.ChannelEdgePolicy, error) { // Create a map to store all channel edge policies. @@ -336,7 +336,7 @@ var graphTopLevelBuckets = [][]byte{ // Wipe completely deletes all saved state within all used buckets within the // database. The deletion is done in a single transaction, therefore this // operation is fully atomic. -func (c *ChannelGraph) Wipe() error { +func (c *KVStore) Wipe() error { err := kvdb.Update(c.db, func(tx kvdb.RwTx) error { for _, tlb := range graphTopLevelBuckets { err := tx.DeleteTopLevelBucket(tlb) @@ -350,14 +350,14 @@ func (c *ChannelGraph) Wipe() error { return err } - return initChannelGraph(c.db) + return initKVStore(c.db) } // createChannelDB creates and initializes a fresh version of In // the case that the target path has not yet been created or doesn't yet exist, // then the path is created. Additionally, all required top-level buckets used // within the database are created. -func initChannelGraph(db kvdb.Backend) error { +func initKVStore(db kvdb.Backend) error { err := kvdb.Update(db, func(tx kvdb.RwTx) error { for _, tlb := range graphTopLevelBuckets { if _, err := tx.CreateTopLevelBucket(tlb); err != nil { @@ -409,7 +409,7 @@ func initChannelGraph(db kvdb.Backend) error { // unknown to the graph DB or not. // // NOTE: this is part of the channeldb.AddrSource interface. -func (c *ChannelGraph) AddrsForNode(nodePub *btcec.PublicKey) (bool, []net.Addr, +func (c *KVStore) AddrsForNode(nodePub *btcec.PublicKey) (bool, []net.Addr, error) { pubKey, err := route.NewVertexFromBytes(nodePub.SerializeCompressed()) @@ -439,7 +439,7 @@ func (c *ChannelGraph) AddrsForNode(nodePub *btcec.PublicKey) (bool, []net.Addr, // NOTE: If an edge can't be found, or wasn't advertised, then a nil pointer // for that particular channel edge routing policy will be passed into the // callback. -func (c *ChannelGraph) ForEachChannel(cb func(*models.ChannelEdgeInfo, +func (c *KVStore) ForEachChannel(cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error) error { return c.db.View(func(tx kvdb.RTx) error { @@ -498,7 +498,7 @@ func (c *ChannelGraph) ForEachChannel(cb func(*models.ChannelEdgeInfo, // transaction may be provided. If none is provided, a new one will be created. // // Unknown policies are passed into the callback as nil values. -func (c *ChannelGraph) forEachNodeDirectedChannel(tx kvdb.RTx, +func (c *KVStore) forEachNodeDirectedChannel(tx kvdb.RTx, node route.Vertex, cb func(channel *DirectedChannel) error) error { if c.graphCache != nil { @@ -556,7 +556,7 @@ func (c *ChannelGraph) forEachNodeDirectedChannel(tx kvdb.RTx, // fetchNodeFeatures returns the features of a given node. If no features are // known for the node, an empty feature vector is returned. An optional read // transaction may be provided. If none is provided, a new one will be created. -func (c *ChannelGraph) fetchNodeFeatures(tx kvdb.RTx, +func (c *KVStore) fetchNodeFeatures(tx kvdb.RTx, node route.Vertex) (*lnwire.FeatureVector, error) { if c.graphCache != nil { @@ -591,7 +591,7 @@ func (c *ChannelGraph) fetchNodeFeatures(tx kvdb.RTx, // Unknown policies are passed into the callback as nil values. // // NOTE: this is part of the graphdb.NodeTraverser interface. -func (c *ChannelGraph) ForEachNodeDirectedChannel(nodePub route.Vertex, +func (c *KVStore) ForEachNodeDirectedChannel(nodePub route.Vertex, cb func(channel *DirectedChannel) error) error { return c.forEachNodeDirectedChannel(nil, nodePub, cb) @@ -603,7 +603,7 @@ func (c *ChannelGraph) ForEachNodeDirectedChannel(nodePub route.Vertex, // features instead of the database. // // NOTE: this is part of the graphdb.NodeTraverser interface. -func (c *ChannelGraph) FetchNodeFeatures(nodePub route.Vertex) ( +func (c *KVStore) FetchNodeFeatures(nodePub route.Vertex) ( *lnwire.FeatureVector, error) { return c.fetchNodeFeatures(nil, nodePub) @@ -614,7 +614,7 @@ func (c *ChannelGraph) FetchNodeFeatures(nodePub route.Vertex) ( // regular forEachNode method does. // // NOTE: The callback contents MUST not be modified. -func (c *ChannelGraph) ForEachNodeCached(cb func(node route.Vertex, +func (c *KVStore) ForEachNodeCached(cb func(node route.Vertex, chans map[uint64]*DirectedChannel) error) error { if c.graphCache != nil { @@ -685,7 +685,7 @@ func (c *ChannelGraph) ForEachNodeCached(cb func(node route.Vertex, // DisabledChannelIDs returns the channel ids of disabled channels. // A channel is disabled when two of the associated ChanelEdgePolicies // have their disabled bit on. -func (c *ChannelGraph) DisabledChannelIDs() ([]uint64, error) { +func (c *KVStore) DisabledChannelIDs() ([]uint64, error) { var disabledChanIDs []uint64 var chanEdgeFound map[uint64]struct{} @@ -740,7 +740,7 @@ func (c *ChannelGraph) DisabledChannelIDs() ([]uint64, error) { // early. Any operations performed on the NodeTx passed to the call-back are // executed under the same read transaction and so, methods on the NodeTx object // _MUST_ only be called from within the call-back. -func (c *ChannelGraph) ForEachNode(cb func(tx NodeRTx) error) error { +func (c *KVStore) ForEachNode(cb func(tx NodeRTx) error) error { return c.forEachNode(func(tx kvdb.RTx, node *models.LightningNode) error { @@ -755,7 +755,7 @@ func (c *ChannelGraph) ForEachNode(cb func(tx NodeRTx) error) error { // // TODO(roasbeef): add iterator interface to allow for memory efficient graph // traversal when graph gets mega. -func (c *ChannelGraph) forEachNode( +func (c *KVStore) forEachNode( cb func(kvdb.RTx, *models.LightningNode) error) error { traversal := func(tx kvdb.RTx) error { @@ -793,7 +793,7 @@ func (c *ChannelGraph) forEachNode( // graph, executing the passed callback with each node encountered. If the // callback returns an error, then the transaction is aborted and the iteration // stops early. -func (c *ChannelGraph) ForEachNodeCacheable(cb func(route.Vertex, +func (c *KVStore) ForEachNodeCacheable(cb func(route.Vertex, *lnwire.FeatureVector) error) error { traversal := func(tx kvdb.RTx) error { @@ -833,7 +833,7 @@ func (c *ChannelGraph) ForEachNodeCacheable(cb func(route.Vertex, // as the center node within a star-graph. This method may be used to kick off // a path finding algorithm in order to explore the reachability of another // node based off the source node. -func (c *ChannelGraph) SourceNode() (*models.LightningNode, error) { +func (c *KVStore) SourceNode() (*models.LightningNode, error) { var source *models.LightningNode err := kvdb.View(c.db, func(tx kvdb.RTx) error { // First grab the nodes bucket which stores the mapping from @@ -864,7 +864,7 @@ func (c *ChannelGraph) SourceNode() (*models.LightningNode, error) { // of the graph. The source node is treated as the center node within a // star-graph. This method may be used to kick off a path finding algorithm in // order to explore the reachability of another node based off the source node. -func (c *ChannelGraph) sourceNode(nodes kvdb.RBucket) (*models.LightningNode, +func (c *KVStore) sourceNode(nodes kvdb.RBucket) (*models.LightningNode, error) { selfPub := nodes.Get(sourceKey) @@ -885,7 +885,7 @@ func (c *ChannelGraph) sourceNode(nodes kvdb.RBucket) (*models.LightningNode, // SetSourceNode sets the source node within the graph database. The source // node is to be used as the center of a star-graph within path finding // algorithms. -func (c *ChannelGraph) SetSourceNode(node *models.LightningNode) error { +func (c *KVStore) SetSourceNode(node *models.LightningNode) error { nodePubBytes := node.PubKeyBytes[:] return kvdb.Update(c.db, func(tx kvdb.RwTx) error { @@ -916,7 +916,7 @@ func (c *ChannelGraph) SetSourceNode(node *models.LightningNode) error { // channel update. // // TODO(roasbeef): also need sig of announcement -func (c *ChannelGraph) AddLightningNode(node *models.LightningNode, +func (c *KVStore) AddLightningNode(node *models.LightningNode, op ...batch.SchedulerOption) error { r := &batch.Request{ @@ -961,7 +961,7 @@ func addLightningNode(tx kvdb.RwTx, node *models.LightningNode) error { // LookupAlias attempts to return the alias as advertised by the target node. // TODO(roasbeef): currently assumes that aliases are unique... -func (c *ChannelGraph) LookupAlias(pub *btcec.PublicKey) (string, error) { +func (c *KVStore) LookupAlias(pub *btcec.PublicKey) (string, error) { var alias string err := kvdb.View(c.db, func(tx kvdb.RTx) error { @@ -997,7 +997,7 @@ func (c *ChannelGraph) LookupAlias(pub *btcec.PublicKey) (string, error) { // DeleteLightningNode starts a new database transaction to remove a vertex/node // from the database according to the node's public key. -func (c *ChannelGraph) DeleteLightningNode(nodePub route.Vertex) error { +func (c *KVStore) DeleteLightningNode(nodePub route.Vertex) error { // TODO(roasbeef): ensure dangling edges are removed... return kvdb.Update(c.db, func(tx kvdb.RwTx) error { nodes := tx.ReadWriteBucket(nodeBucket) @@ -1015,7 +1015,7 @@ func (c *ChannelGraph) DeleteLightningNode(nodePub route.Vertex) error { // deleteLightningNode uses an existing database transaction to remove a // vertex/node from the database according to the node's public key. -func (c *ChannelGraph) deleteLightningNode(nodes kvdb.RwBucket, +func (c *KVStore) deleteLightningNode(nodes kvdb.RwBucket, compressedPubKey []byte) error { aliases := nodes.NestedReadWriteBucket(aliasIndexBucket) @@ -1063,7 +1063,7 @@ func (c *ChannelGraph) deleteLightningNode(nodes kvdb.RwBucket, // involved in creation of the channel, and the set of features that the channel // supports. The chanPoint and chanID are used to uniquely identify the edge // globally within the database. -func (c *ChannelGraph) AddChannelEdge(edge *models.ChannelEdgeInfo, +func (c *KVStore) AddChannelEdge(edge *models.ChannelEdgeInfo, op ...batch.SchedulerOption) error { var alreadyExists bool @@ -1110,7 +1110,7 @@ func (c *ChannelGraph) AddChannelEdge(edge *models.ChannelEdgeInfo, // addChannelEdge is the private form of AddChannelEdge that allows callers to // utilize an existing db transaction. -func (c *ChannelGraph) addChannelEdge(tx kvdb.RwTx, +func (c *KVStore) addChannelEdge(tx kvdb.RwTx, edge *models.ChannelEdgeInfo) error { // Construct the channel's primary key which is the 8-byte channel ID. @@ -1215,7 +1215,7 @@ func (c *ChannelGraph) addChannelEdge(tx kvdb.RwTx, // was updated for both directed edges are returned along with the boolean. If // it is not found, then the zombie index is checked and its result is returned // as the second boolean. -func (c *ChannelGraph) HasChannelEdge( +func (c *KVStore) HasChannelEdge( chanID uint64) (time.Time, time.Time, bool, bool, error) { var ( @@ -1319,7 +1319,7 @@ func (c *ChannelGraph) HasChannelEdge( } // AddEdgeProof sets the proof of an existing edge in the graph database. -func (c *ChannelGraph) AddEdgeProof(chanID lnwire.ShortChannelID, +func (c *KVStore) AddEdgeProof(chanID lnwire.ShortChannelID, proof *models.ChannelAuthProof) error { // Construct the channel's primary key which is the 8-byte channel ID. @@ -1364,7 +1364,7 @@ const ( // prune the graph is stored so callers can ensure the graph is fully in sync // with the current UTXO state. A slice of channels that have been closed by // the target block are returned if the function succeeds without error. -func (c *ChannelGraph) PruneGraph(spentOutputs []*wire.OutPoint, +func (c *KVStore) PruneGraph(spentOutputs []*wire.OutPoint, blockHash *chainhash.Hash, blockHeight uint32) ( []*models.ChannelEdgeInfo, error) { @@ -1499,7 +1499,7 @@ func (c *ChannelGraph) PruneGraph(spentOutputs []*wire.OutPoint, // any nodes from the channel graph that are currently unconnected. This ensure // that we only maintain a graph of reachable nodes. In the event that a pruned // node gains more channels, it will be re-added back to the graph. -func (c *ChannelGraph) PruneGraphNodes() error { +func (c *KVStore) PruneGraphNodes() error { return kvdb.Update(c.db, func(tx kvdb.RwTx) error { nodes := tx.ReadWriteBucket(nodeBucket) if nodes == nil { @@ -1521,7 +1521,7 @@ func (c *ChannelGraph) PruneGraphNodes() error { // pruneGraphNodes attempts to remove any nodes from the graph who have had a // channel closed within the current block. If the node still has existing // channels in the graph, this will act as a no-op. -func (c *ChannelGraph) pruneGraphNodes(nodes kvdb.RwBucket, +func (c *KVStore) pruneGraphNodes(nodes kvdb.RwBucket, edgeIndex kvdb.RwBucket) error { log.Trace("Pruning nodes from graph with no open channels") @@ -1632,7 +1632,7 @@ func (c *ChannelGraph) pruneGraphNodes(nodes kvdb.RwBucket, // set to the last prune height valid for the remaining chain. // Channels that were removed from the graph resulting from the // disconnected block are returned. -func (c *ChannelGraph) DisconnectBlockAtHeight(height uint32) ( +func (c *KVStore) DisconnectBlockAtHeight(height uint32) ( []*models.ChannelEdgeInfo, error) { // Every channel having a ShortChannelID starting at 'height' @@ -1764,7 +1764,7 @@ func (c *ChannelGraph) DisconnectBlockAtHeight(height uint32) ( // used to prune channels in the graph. Knowing the "prune tip" allows callers // to tell if the graph is currently in sync with the current best known UTXO // state. -func (c *ChannelGraph) PruneTip() (*chainhash.Hash, uint32, error) { +func (c *KVStore) PruneTip() (*chainhash.Hash, uint32, error) { var ( tipHash chainhash.Hash tipHeight uint32 @@ -1811,7 +1811,7 @@ func (c *ChannelGraph) PruneTip() (*chainhash.Hash, uint32, error) { // that we require the node that failed to send the fresh update to be the one // that resurrects the channel from its zombie state. The markZombie bool // denotes whether or not to mark the channel as a zombie. -func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning, markZombie bool, +func (c *KVStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, chanIDs ...uint64) error { // TODO(roasbeef): possibly delete from node bucket if node has no more @@ -1872,7 +1872,7 @@ func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning, markZombie bool, // ChannelID attempt to lookup the 8-byte compact channel ID which maps to the // passed channel point (outpoint). If the passed channel doesn't exist within // the database, then ErrEdgeNotFound is returned. -func (c *ChannelGraph) ChannelID(chanPoint *wire.OutPoint) (uint64, error) { +func (c *KVStore) ChannelID(chanPoint *wire.OutPoint) (uint64, error) { var chanID uint64 if err := kvdb.View(c.db, func(tx kvdb.RTx) error { var err error @@ -1918,7 +1918,7 @@ func getChanID(tx kvdb.RTx, chanPoint *wire.OutPoint) (uint64, error) { // HighestChanID returns the "highest" known channel ID in the channel graph. // This represents the "newest" channel from the PoV of the chain. This method // can be used by peers to quickly determine if they're graphs are in sync. -func (c *ChannelGraph) HighestChanID() (uint64, error) { +func (c *KVStore) HighestChanID() (uint64, error) { var cid uint64 err := kvdb.View(c.db, func(tx kvdb.RTx) error { @@ -1983,7 +1983,7 @@ type ChannelEdge struct { // ChanUpdatesInHorizon returns all the known channel edges which have at least // one edge that has an update timestamp within the specified horizon. -func (c *ChannelGraph) ChanUpdatesInHorizon(startTime, +func (c *KVStore) ChanUpdatesInHorizon(startTime, endTime time.Time) ([]ChannelEdge, error) { // To ensure we don't return duplicate ChannelEdges, we'll use an @@ -2135,7 +2135,7 @@ func (c *ChannelGraph) ChanUpdatesInHorizon(startTime, // update timestamp within the passed range. This method can be used by two // nodes to quickly determine if they have the same set of up to date node // announcements. -func (c *ChannelGraph) NodeUpdatesInHorizon(startTime, +func (c *KVStore) NodeUpdatesInHorizon(startTime, endTime time.Time) ([]models.LightningNode, error) { var nodesInHorizon []models.LightningNode @@ -2202,7 +2202,7 @@ func (c *ChannelGraph) NodeUpdatesInHorizon(startTime, // words, we perform a set difference of our set of chan ID's and the ones // passed in. This method can be used by callers to determine the set of // channels another peer knows of that we don't. -func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, +func (c *KVStore) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, isZombieChan func(time.Time, time.Time) bool) ([]uint64, error) { var newChanIDs []uint64 @@ -2369,7 +2369,7 @@ type BlockChannelRange struct { // up after a period of time offline. If withTimestamps is true then the // timestamp info of the latest received channel update messages of the channel // will be included in the response. -func (c *ChannelGraph) FilterChannelRange(startHeight, +func (c *KVStore) FilterChannelRange(startHeight, endHeight uint32, withTimestamps bool) ([]BlockChannelRange, error) { startChanID := &lnwire.ShortChannelID{ @@ -2514,7 +2514,7 @@ func (c *ChannelGraph) FilterChannelRange(startHeight, // skipped and the result will contain only those edges that exist at the time // of the query. This can be used to respond to peer queries that are seeking to // fill in gaps in their view of the channel graph. -func (c *ChannelGraph) FetchChanInfos(chanIDs []uint64) ([]ChannelEdge, error) { +func (c *KVStore) FetchChanInfos(chanIDs []uint64) ([]ChannelEdge, error) { return c.fetchChanInfos(nil, chanIDs) } @@ -2526,7 +2526,7 @@ func (c *ChannelGraph) FetchChanInfos(chanIDs []uint64) ([]ChannelEdge, error) { // // NOTE: An optional transaction may be provided. If none is provided, then a // new one will be created. -func (c *ChannelGraph) fetchChanInfos(tx kvdb.RTx, chanIDs []uint64) ( +func (c *KVStore) fetchChanInfos(tx kvdb.RTx, chanIDs []uint64) ( []ChannelEdge, error) { // TODO(roasbeef): sort cids? @@ -2667,7 +2667,7 @@ func delEdgeUpdateIndexEntry(edgesBucket kvdb.RwBucket, chanID uint64, // // NOTE: this method MUST only be called if the cacheMu has already been // acquired. -func (c *ChannelGraph) delChannelEdgeUnsafe(edges, edgeIndex, chanIndex, +func (c *KVStore) delChannelEdgeUnsafe(edges, edgeIndex, chanIndex, zombieIndex kvdb.RwBucket, chanID []byte, isZombie, strictZombie bool) error { @@ -2806,7 +2806,7 @@ func makeZombiePubkeys(info *models.ChannelEdgeInfo, // updated, otherwise it's the second node's information. The node ordering is // determined by the lexicographical ordering of the identity public keys of the // nodes on either side of the channel. -func (c *ChannelGraph) UpdateEdgePolicy(edge *models.ChannelEdgePolicy, +func (c *KVStore) UpdateEdgePolicy(edge *models.ChannelEdgePolicy, op ...batch.SchedulerOption) error { var ( @@ -2858,7 +2858,7 @@ func (c *ChannelGraph) UpdateEdgePolicy(edge *models.ChannelEdgePolicy, return c.chanScheduler.Execute(r) } -func (c *ChannelGraph) updateEdgeCache(e *models.ChannelEdgePolicy, +func (c *KVStore) updateEdgeCache(e *models.ChannelEdgePolicy, isUpdate1 bool) { // If an entry for this channel is found in reject cache, we'll modify @@ -2956,7 +2956,7 @@ func updateEdgePolicy(tx kvdb.RwTx, edge *models.ChannelEdgePolicy, // isPublic determines whether the node is seen as public within the graph from // the source node's point of view. An existing database transaction can also be // specified. -func (c *ChannelGraph) isPublic(tx kvdb.RTx, nodePub route.Vertex, +func (c *KVStore) isPublic(tx kvdb.RTx, nodePub route.Vertex, sourcePubKey []byte) (bool, error) { // In order to determine whether this node is publicly advertised within @@ -3001,7 +3001,7 @@ func (c *ChannelGraph) isPublic(tx kvdb.RTx, nodePub route.Vertex, // public key. If the node isn't found in the database, then // ErrGraphNodeNotFound is returned. An optional transaction may be provided. // If none is provided, then a new one will be created. -func (c *ChannelGraph) FetchLightningNodeTx(tx kvdb.RTx, nodePub route.Vertex) ( +func (c *KVStore) FetchLightningNodeTx(tx kvdb.RTx, nodePub route.Vertex) ( *models.LightningNode, error) { return c.fetchLightningNode(tx, nodePub) @@ -3010,7 +3010,7 @@ func (c *ChannelGraph) FetchLightningNodeTx(tx kvdb.RTx, nodePub route.Vertex) ( // FetchLightningNode attempts to look up a target node by its identity public // key. If the node isn't found in the database, then ErrGraphNodeNotFound is // returned. -func (c *ChannelGraph) FetchLightningNode(nodePub route.Vertex) ( +func (c *KVStore) FetchLightningNode(nodePub route.Vertex) ( *models.LightningNode, error) { return c.fetchLightningNode(nil, nodePub) @@ -3020,7 +3020,7 @@ func (c *ChannelGraph) FetchLightningNode(nodePub route.Vertex) ( // key. If the node isn't found in the database, then ErrGraphNodeNotFound is // returned. An optional transaction may be provided. If none is provided, then // a new one will be created. -func (c *ChannelGraph) fetchLightningNode(tx kvdb.RTx, +func (c *KVStore) fetchLightningNode(tx kvdb.RTx, nodePub route.Vertex) (*models.LightningNode, error) { var node *models.LightningNode @@ -3078,7 +3078,7 @@ func (c *ChannelGraph) fetchLightningNode(tx kvdb.RTx, // timestamp of when the data for the node was lasted updated is returned along // with a true boolean. Otherwise, an empty time.Time is returned with a false // boolean. -func (c *ChannelGraph) HasLightningNode(nodePub [33]byte) (time.Time, bool, +func (c *KVStore) HasLightningNode(nodePub [33]byte) (time.Time, bool, error) { var ( @@ -3216,7 +3216,7 @@ func nodeTraversal(tx kvdb.RTx, nodePub []byte, db kvdb.Backend, // halted with the error propagated back up to the caller. // // Unknown policies are passed into the callback as nil values. -func (c *ChannelGraph) ForEachNodeChannel(nodePub route.Vertex, +func (c *KVStore) ForEachNodeChannel(nodePub route.Vertex, cb func(kvdb.RTx, *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error) error { @@ -3236,7 +3236,7 @@ func (c *ChannelGraph) ForEachNodeChannel(nodePub route.Vertex, // should be passed as the first argument. Otherwise, the first argument should // be nil and a fresh transaction will be created to execute the graph // traversal. -func (c *ChannelGraph) ForEachNodeChannelTx(tx kvdb.RTx, +func (c *KVStore) ForEachNodeChannelTx(tx kvdb.RTx, nodePub route.Vertex, cb func(kvdb.RTx, *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error) error { @@ -3248,7 +3248,7 @@ func (c *ChannelGraph) ForEachNodeChannelTx(tx kvdb.RTx, // the target node in the channel. This is useful when one knows the pubkey of // one of the nodes, and wishes to obtain the full LightningNode for the other // end of the channel. -func (c *ChannelGraph) FetchOtherNode(tx kvdb.RTx, +func (c *KVStore) FetchOtherNode(tx kvdb.RTx, channel *models.ChannelEdgeInfo, thisNodeKey []byte) ( *models.LightningNode, error) { @@ -3319,7 +3319,7 @@ func computeEdgePolicyKeys(info *models.ChannelEdgeInfo) ([]byte, []byte) { // found, then ErrEdgeNotFound is returned. A struct which houses the general // information for the channel itself is returned as well as two structs that // contain the routing policies for the channel in either direction. -func (c *ChannelGraph) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( +func (c *KVStore) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { @@ -3404,7 +3404,7 @@ func (c *ChannelGraph) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( // ErrZombieEdge an be returned if the edge is currently marked as a zombie // within the database. In this case, the ChannelEdgePolicy's will be nil, and // the ChannelEdgeInfo will only include the public keys of each node. -func (c *ChannelGraph) FetchChannelEdgesByID(chanID uint64) ( +func (c *KVStore) FetchChannelEdgesByID(chanID uint64) ( *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { @@ -3506,7 +3506,7 @@ func (c *ChannelGraph) FetchChannelEdgesByID(chanID uint64) ( // IsPublicNode is a helper method that determines whether the node with the // given public key is seen as a public node in the graph from the graph's // source node's point of view. -func (c *ChannelGraph) IsPublicNode(pubKey [33]byte) (bool, error) { +func (c *KVStore) IsPublicNode(pubKey [33]byte) (bool, error) { var nodeIsPublic bool err := kvdb.View(c.db, func(tx kvdb.RTx) error { nodes := tx.ReadBucket(nodeBucket) @@ -3576,7 +3576,7 @@ func (e *EdgePoint) String() string { // within the known channel graph. The set of UTXO's (along with their scripts) // returned are the ones that need to be watched on chain to detect channel // closes on the resident blockchain. -func (c *ChannelGraph) ChannelView() ([]EdgePoint, error) { +func (c *KVStore) ChannelView() ([]EdgePoint, error) { var edgePoints []EdgePoint if err := kvdb.View(c.db, func(tx kvdb.RTx) error { // We're going to iterate over the entire channel index, so @@ -3645,7 +3645,7 @@ func (c *ChannelGraph) ChannelView() ([]EdgePoint, error) { // MarkEdgeZombie attempts to mark a channel identified by its channel ID as a // zombie. This method is used on an ad-hoc basis, when channels need to be // marked as zombies outside the normal pruning cycle. -func (c *ChannelGraph) MarkEdgeZombie(chanID uint64, +func (c *KVStore) MarkEdgeZombie(chanID uint64, pubKey1, pubKey2 [33]byte) error { c.cacheMu.Lock() @@ -3695,7 +3695,7 @@ func markEdgeZombie(zombieIndex kvdb.RwBucket, chanID uint64, pubKey1, } // MarkEdgeLive clears an edge from our zombie index, deeming it as live. -func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error { +func (c *KVStore) MarkEdgeLive(chanID uint64) error { c.cacheMu.Lock() defer c.cacheMu.Unlock() @@ -3708,7 +3708,7 @@ func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error { // // NOTE: this method MUST only be called if the cacheMu has already been // acquired. -func (c *ChannelGraph) markEdgeLiveUnsafe(tx kvdb.RwTx, chanID uint64) error { +func (c *KVStore) markEdgeLiveUnsafe(tx kvdb.RwTx, chanID uint64) error { dbFn := func(tx kvdb.RwTx) error { edges := tx.ReadWriteBucket(edgeBucket) if edges == nil { @@ -3766,7 +3766,7 @@ func (c *ChannelGraph) markEdgeLiveUnsafe(tx kvdb.RwTx, chanID uint64) error { // IsZombieEdge returns whether the edge is considered zombie. If it is a // zombie, then the two node public keys corresponding to this edge are also // returned. -func (c *ChannelGraph) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte) { +func (c *KVStore) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte) { var ( isZombie bool pubKey1, pubKey2 [33]byte @@ -3818,7 +3818,7 @@ func isZombieEdge(zombieIndex kvdb.RBucket, } // NumZombies returns the current number of zombie channels in the graph. -func (c *ChannelGraph) NumZombies() (uint64, error) { +func (c *KVStore) NumZombies() (uint64, error) { var numZombies uint64 err := kvdb.View(c.db, func(tx kvdb.RTx) error { edges := tx.ReadBucket(edgeBucket) @@ -3847,7 +3847,7 @@ func (c *ChannelGraph) NumZombies() (uint64, error) { // PutClosedScid stores a SCID for a closed channel in the database. This is so // that we can ignore channel announcements that we know to be closed without // having to validate them and fetch a block. -func (c *ChannelGraph) PutClosedScid(scid lnwire.ShortChannelID) error { +func (c *KVStore) PutClosedScid(scid lnwire.ShortChannelID) error { return kvdb.Update(c.db, func(tx kvdb.RwTx) error { closedScids, err := tx.CreateTopLevelBucket(closedScidBucket) if err != nil { @@ -3864,7 +3864,7 @@ func (c *ChannelGraph) PutClosedScid(scid lnwire.ShortChannelID) error { // IsClosedScid checks whether a channel identified by the passed in scid is // closed. This helps avoid having to perform expensive validation checks. // TODO: Add an LRU cache to cut down on disc reads. -func (c *ChannelGraph) IsClosedScid(scid lnwire.ShortChannelID) (bool, error) { +func (c *KVStore) IsClosedScid(scid lnwire.ShortChannelID) (bool, error) { var isClosed bool err := kvdb.View(c.db, func(tx kvdb.RTx) error { closedScids := tx.ReadBucket(closedScidBucket) @@ -3895,7 +3895,7 @@ func (c *ChannelGraph) IsClosedScid(scid lnwire.ShortChannelID) (bool, error) { // instance which can be used to perform queries against the channel graph. If // the graph cache is not enabled, then the call-back will be provided with // access to the graph via a consistent read-only transaction. -func (c *ChannelGraph) GraphSession(cb func(graph NodeTraverser) error) error { +func (c *KVStore) GraphSession(cb func(graph NodeTraverser) error) error { if c.graphCache != nil { return cb(&nodeTraverserSession{db: c}) } @@ -3913,7 +3913,7 @@ func (c *ChannelGraph) GraphSession(cb func(graph NodeTraverser) error) error { // where the graph Cache has not been enabled. type nodeTraverserSession struct { tx kvdb.RTx - db *ChannelGraph + db *KVStore } // ForEachNodeDirectedChannel calls the callback for every channel of the given @@ -4746,10 +4746,10 @@ func deserializeChanEdgePolicyRaw(r io.Reader) (*models.ChannelEdgePolicy, } // chanGraphNodeTx is an implementation of the NodeRTx interface backed by the -// ChannelGraph and a kvdb.RTx. +// KVStore and a kvdb.RTx. type chanGraphNodeTx struct { tx kvdb.RTx - db *ChannelGraph + db *KVStore node *models.LightningNode } @@ -4757,7 +4757,7 @@ type chanGraphNodeTx struct { // interface. var _ NodeRTx = (*chanGraphNodeTx)(nil) -func newChanGraphNodeTx(tx kvdb.RTx, db *ChannelGraph, +func newChanGraphNodeTx(tx kvdb.RTx, db *KVStore, node *models.LightningNode) *chanGraphNodeTx { return &chanGraphNodeTx{ @@ -4804,7 +4804,7 @@ func (c *chanGraphNodeTx) ForEachChannel(f func(*models.ChannelEdgeInfo, ) } -// MakeTestGraph creates a new instance of the ChannelGraph for testing +// MakeTestGraph creates a new instance of the KVStore for testing // purposes. func MakeTestGraph(t testing.TB, modifiers ...OptionModifier) (*ChannelGraph, error) { @@ -4814,7 +4814,7 @@ func MakeTestGraph(t testing.TB, modifiers ...OptionModifier) (*ChannelGraph, modifier(opts) } - // Next, create channelgraph for the first time. + // Next, create KVStore for the first time. backend, backendCleanup, err := kvdb.GetTestBackend(t.TempDir(), "cgr") if err != nil { backendCleanup() From ae3961b47f0ac5d7a6c8fbe7bf23c95e9c22d665 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Feb 2025 07:03:20 -0300 Subject: [PATCH 03/24] graph/db: fix linter issues of old code Since we have renamed a file housing some very old code, the linter has now run on all this code for the first time. So we gotta do some clean-up work here to make it happy. --- docs/release-notes/release-notes-0.19.0.md | 2 + graph/db/kv_store.go | 101 +++++++++++++-------- 2 files changed, 64 insertions(+), 39 deletions(-) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index e4ce83116..ff8752810 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -321,6 +321,8 @@ The underlying functionality between those two options remain the same. - [Abstract autopilot access](https://github.com/lightningnetwork/lnd/pull/9480) - [Abstract invoicerpc server access](https://github.com/lightningnetwork/lnd/pull/9516) - [Refactor to hide DB transactions](https://github.com/lightningnetwork/lnd/pull/9513) + - Move the graph cache out of the graph CRUD layer: + - [1](https://github.com/lightningnetwork/lnd/pull/9533) * [Golang was updated to `v1.22.11`](https://github.com/lightningnetwork/lnd/pull/9462). diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index dfe856419..6f327d73b 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -132,8 +132,8 @@ var ( // // The chanID represents the channel ID of the edge and the direction is // one byte representing the direction of the edge. The main purpose of - // this index is to allow pruning disabled channels in a fast way without - // the need to iterate all over the graph. + // this index is to allow pruning disabled channels in a fast way + // without the need to iterate all over the graph. disabledEdgePolicyBucket = []byte("disabled-edge-policy-index") // graphMetaBucket is a top-level bucket which stores various meta-deta @@ -308,7 +308,7 @@ func (c *KVStore) getChannelMap(edges kvdb.RBucket) ( switch { // If the db policy was missing an expected optional field, we // return nil as if the policy was unknown. - case err == ErrEdgePolicyOptionalFieldNotFound: + case errors.Is(err, ErrEdgePolicyOptionalFieldNotFound): return nil case err != nil: @@ -340,10 +340,13 @@ func (c *KVStore) Wipe() error { err := kvdb.Update(c.db, func(tx kvdb.RwTx) error { for _, tlb := range graphTopLevelBuckets { err := tx.DeleteTopLevelBucket(tlb) - if err != nil && err != kvdb.ErrBucketNotFound { + if err != nil && + !errors.Is(err, kvdb.ErrBucketNotFound) { + return err } } + return nil }, func() {}) if err != nil { @@ -395,6 +398,7 @@ func initKVStore(db kvdb.Backend) error { graphMeta := tx.ReadWriteBucket(graphMetaBucket) _, err = graphMeta.CreateBucketIfNotExists(pruneLogBucket) + return err }, func() {}) if err != nil { @@ -550,6 +554,7 @@ func (c *KVStore) forEachNodeDirectedChannel(tx kvdb.RTx, return cb(directedChannel) } + return nodeTraversal(tx, node[:], c.db, dbCallback) } @@ -565,14 +570,14 @@ func (c *KVStore) fetchNodeFeatures(tx kvdb.RTx, // Fallback that uses the database. targetNode, err := c.FetchLightningNodeTx(tx, node) - switch err { + switch { // If the node exists and has features, return them directly. - case nil: + case err == nil: return targetNode.Features, nil // If we couldn't find a node announcement, populate a blank feature // vector. - case ErrGraphNodeNotFound: + case errors.Is(err, ErrGraphNodeNotFound): return lnwire.EmptyFeatureVector(), nil // Otherwise, bubble the error up. @@ -915,7 +920,7 @@ func (c *KVStore) SetSourceNode(node *models.LightningNode) error { // already present node from a node announcement, or to insert a node found in a // channel update. // -// TODO(roasbeef): also need sig of announcement +// TODO(roasbeef): also need sig of announcement. func (c *KVStore) AddLightningNode(node *models.LightningNode, op ...batch.SchedulerOption) error { @@ -984,6 +989,7 @@ func (c *KVStore) LookupAlias(pub *btcec.PublicKey) (string, error) { // TODO(roasbeef): should actually be using the utf-8 // package... alias = string(a) + return nil }, func() { alias = "" @@ -1076,7 +1082,7 @@ func (c *KVStore) AddChannelEdge(edge *models.ChannelEdgeInfo, // Silence ErrEdgeAlreadyExist so that the batch can // succeed, but propagate the error via local state. - if err == ErrEdgeAlreadyExist { + if errors.Is(err, ErrEdgeAlreadyExist) { alreadyExists = true return nil } @@ -1150,7 +1156,7 @@ func (c *KVStore) addChannelEdge(tx kvdb.RwTx, // public key, so subsequent validation and queries can work properly. _, node1Err := fetchLightningNode(nodes, edge.NodeKey1Bytes[:]) switch { - case node1Err == ErrGraphNodeNotFound: + case errors.Is(node1Err, ErrGraphNodeNotFound): node1Shell := models.LightningNode{ PubKeyBytes: edge.NodeKey1Bytes, HaveNodeAnnouncement: false, @@ -1166,7 +1172,7 @@ func (c *KVStore) addChannelEdge(tx kvdb.RwTx, _, node2Err := fetchLightningNode(nodes, edge.NodeKey2Bytes[:]) switch { - case node2Err == ErrGraphNodeNotFound: + case errors.Is(node2Err, ErrGraphNodeNotFound): node2Shell := models.LightningNode{ PubKeyBytes: edge.NodeKey2Bytes, HaveNodeAnnouncement: false, @@ -1206,6 +1212,7 @@ func (c *KVStore) addChannelEdge(tx kvdb.RwTx, if err := WriteOutpoint(&b, &edge.ChannelPoint); err != nil { return err } + return chanIndex.Put(b.Bytes(), chanKey[:]) } @@ -1233,6 +1240,7 @@ func (c *KVStore) HasChannelEdge( upd1Time = time.Unix(entry.upd1Time, 0) upd2Time = time.Unix(entry.upd2Time, 0) exists, isZombie = entry.flags.unpack() + return upd1Time, upd2Time, exists, isZombie, nil } c.cacheMu.RUnlock() @@ -1247,6 +1255,7 @@ func (c *KVStore) HasChannelEdge( upd1Time = time.Unix(entry.upd1Time, 0) upd2Time = time.Unix(entry.upd2Time, 0) exists, isZombie = entry.flags.unpack() + return upd1Time, upd2Time, exists, isZombie, nil } @@ -1791,8 +1800,8 @@ func (c *KVStore) PruneTip() (*chainhash.Hash, uint32, error) { // Once we have the prune tip, the value will be the block hash, // and the key the block height. - copy(tipHash[:], v[:]) - tipHeight = byteOrder.Uint32(k[:]) + copy(tipHash[:], v) + tipHeight = byteOrder.Uint32(k) return nil }, func() {}) @@ -1946,11 +1955,12 @@ func (c *KVStore) HighestChanID() (uint64, error) { // Otherwise, we'll de serialize the channel ID and return it // to the caller. cid = byteOrder.Uint64(lastChanID) + return nil }, func() { cid = 0 }) - if err != nil && err != ErrGraphNoEdgesFound { + if err != nil && !errors.Is(err, ErrGraphNoEdgesFound) { return 0, err } @@ -2035,7 +2045,6 @@ func (c *KVStore) ChanUpdatesInHorizon(startTime, //nolint:ll for indexKey, _ := updateCursor.Seek(startTimeBytes[:]); indexKey != nil && bytes.Compare(indexKey, endTimeBytes[:]) <= 0; indexKey, _ = updateCursor.Next() { - // We have a new eligible entry, so we'll slice of the // chan ID so we can query it in the DB. chanID := indexKey[8:] @@ -2052,6 +2061,7 @@ func (c *KVStore) ChanUpdatesInHorizon(startTime, hits++ edgesSeen[chanIDInt] = struct{}{} edgesInHorizon = append(edgesInHorizon, channel) + continue } @@ -2110,9 +2120,9 @@ func (c *KVStore) ChanUpdatesInHorizon(startTime, edgesInHorizon = nil }) switch { - case err == ErrGraphNoEdgesFound: + case errors.Is(err, ErrGraphNoEdgesFound): fallthrough - case err == ErrGraphNodesNotFound: + case errors.Is(err, ErrGraphNodesNotFound): break case err != nil: @@ -2170,7 +2180,6 @@ func (c *KVStore) NodeUpdatesInHorizon(startTime, //nolint:ll for indexKey, _ := updateCursor.Seek(startTimeBytes[:]); indexKey != nil && bytes.Compare(indexKey, endTimeBytes[:]) <= 0; indexKey, _ = updateCursor.Next() { - nodePub := indexKey[8:] node, err := fetchLightningNode(nodes, nodePub) if err != nil { @@ -2185,9 +2194,9 @@ func (c *KVStore) NodeUpdatesInHorizon(startTime, nodesInHorizon = nil }) switch { - case err == ErrGraphNoEdgesFound: + case errors.Is(err, ErrGraphNoEdgesFound): fallthrough - case err == ErrGraphNodesNotFound: + case errors.Is(err, ErrGraphNodesNotFound): break case err != nil: @@ -2294,7 +2303,7 @@ func (c *KVStore) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, switch { // If we don't know of any edges yet, then we'll return the entire set // of chan IDs specified. - case err == ErrGraphNoEdgesFound: + case errors.Is(err, ErrGraphNoEdgesFound): ogChanIDs := make([]uint64, len(chansInfo)) for i, info := range chansInfo { ogChanIDs[i] = info.ShortChannelID.ToUint64() @@ -2482,7 +2491,7 @@ func (c *KVStore) FilterChannelRange(startHeight, switch { // If we don't know of any channels yet, then there's nothing to // filter, so we'll return an empty slice. - case err == ErrGraphNoEdgesFound || len(channelsPerBlock) == 0: + case errors.Is(err, ErrGraphNoEdgesFound) || len(channelsPerBlock) == 0: return nil, nil case err != nil: @@ -2596,6 +2605,7 @@ func (c *KVStore) fetchChanInfos(tx kvdb.RTx, chanIDs []uint64) ( Node2: &node2, }) } + return nil } @@ -2990,7 +3000,7 @@ func (c *KVStore) isPublic(tx kvdb.RTx, nodePub route.Vertex, // Otherwise, we'll continue our search. return nil }) - if err != nil && err != errDone { + if err != nil && !errors.Is(err, errDone) { return false, err } @@ -3113,6 +3123,7 @@ func (c *KVStore) HasLightningNode(nodePub [33]byte) (time.Time, bool, exists = true updateTime = node.LastUpdate + return nil }, func() { updateTime = time.Time{} @@ -3382,6 +3393,7 @@ func (c *KVStore) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( policy1 = e1 policy2 = e2 + return nil }, func() { edgeInfo = nil @@ -3466,6 +3478,7 @@ func (c *KVStore) FetchChannelEdgesByID(chanID uint64) ( NodeKey1Bytes: pubKey1, NodeKey2Bytes: pubKey2, } + return ErrZombieEdge } @@ -3487,13 +3500,14 @@ func (c *KVStore) FetchChannelEdgesByID(chanID uint64) ( policy1 = e1 policy2 = e2 + return nil }, func() { edgeInfo = nil policy1 = nil policy2 = nil }) - if err == ErrZombieEdge { + if errors.Is(err, ErrZombieEdge) { return edgeInfo, nil, nil, err } if err != nil { @@ -3523,6 +3537,7 @@ func (c *KVStore) IsPublicNode(pubKey [33]byte) (bool, error) { } nodeIsPublic, err = c.isPublic(tx, node.PubKeyBytes, ourPubKey) + return err }, func() { nodeIsPublic = false @@ -3783,6 +3798,7 @@ func (c *KVStore) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte) { } isZombie, pubKey1, pubKey2 = isZombieEdge(zombieIndex, chanID) + return nil }, func() { isZombie = false @@ -3936,8 +3952,8 @@ func (c *nodeTraverserSession) FetchNodeFeatures(nodePub route.Vertex) ( return c.db.fetchNodeFeatures(c.tx, nodePub) } -func putLightningNode(nodeBucket kvdb.RwBucket, aliasBucket kvdb.RwBucket, // nolint:dupl - updateIndex kvdb.RwBucket, node *models.LightningNode) error { +func putLightningNode(nodeBucket, aliasBucket, updateIndex kvdb.RwBucket, + node *models.LightningNode) error { var ( scratch [16]byte @@ -4074,6 +4090,7 @@ func fetchLightningNode(nodeBucket kvdb.RBucket, } nodeReader := bytes.NewReader(nodeBytes) + return deserializeLightningNode(nodeReader) } @@ -4219,8 +4236,8 @@ func deserializeLightningNode(r io.Reader) (models.LightningNode, error) { r, 0, MaxAllowedExtraOpaqueBytes, "blob", ) switch { - case err == io.ErrUnexpectedEOF: - case err == io.EOF: + case errors.Is(err, io.ErrUnexpectedEOF): + case errors.Is(err, io.EOF): case err != nil: return models.LightningNode{}, err } @@ -4306,6 +4323,7 @@ func fetchChanEdgeInfo(edgeIndex kvdb.RBucket, } edgeInfoReader := bytes.NewReader(edgeInfoBytes) + return deserializeChanEdgeInfo(edgeInfoReader) } @@ -4377,8 +4395,8 @@ func deserializeChanEdgeInfo(r io.Reader) (models.ChannelEdgeInfo, error) { r, 0, MaxAllowedExtraOpaqueBytes, "blob", ) switch { - case err == io.ErrUnexpectedEOF: - case err == io.EOF: + case errors.Is(err, io.ErrUnexpectedEOF): + case errors.Is(err, io.EOF): case err != nil: return models.ChannelEdgeInfo{}, err } @@ -4415,7 +4433,7 @@ func putChanEdgePolicy(edges kvdb.RwBucket, edge *models.ChannelEdgePolicy, // An unknown policy value does not have a update time recorded, so // it also does not need to be removed. if edgeBytes := edges.Get(edgeKey[:]); edgeBytes != nil && - !bytes.Equal(edgeBytes[:], unknownPolicy) { + !bytes.Equal(edgeBytes, unknownPolicy) { // In order to delete the old entry, we'll need to obtain the // *prior* update time in order to delete it. To do this, we'll @@ -4429,7 +4447,9 @@ func putChanEdgePolicy(edges kvdb.RwBucket, edge *models.ChannelEdgePolicy, oldEdgePolicy, err := deserializeChanEdgePolicy( bytes.NewReader(edgeBytes), ) - if err != nil && err != ErrEdgePolicyOptionalFieldNotFound { + if err != nil && + !errors.Is(err, ErrEdgePolicyOptionalFieldNotFound) { + return err } @@ -4457,7 +4477,7 @@ func putChanEdgePolicy(edges kvdb.RwBucket, edge *models.ChannelEdgePolicy, return err } - return edges.Put(edgeKey[:], b.Bytes()[:]) + return edges.Put(edgeKey[:], b.Bytes()) } // updateEdgePolicyDisabledIndex is used to update the disabledEdgePolicyIndex @@ -4514,7 +4534,7 @@ func fetchChanEdgePolicy(edges kvdb.RBucket, chanID []byte, var edgeKey [33 + 8]byte copy(edgeKey[:], nodePub) - copy(edgeKey[33:], chanID[:]) + copy(edgeKey[33:], chanID) edgeBytes := edges.Get(edgeKey[:]) if edgeBytes == nil { @@ -4522,7 +4542,7 @@ func fetchChanEdgePolicy(edges kvdb.RBucket, chanID []byte, } // No need to deserialize unknown policy. - if bytes.Equal(edgeBytes[:], unknownPolicy) { + if bytes.Equal(edgeBytes, unknownPolicy) { return nil, nil } @@ -4532,7 +4552,7 @@ func fetchChanEdgePolicy(edges kvdb.RBucket, chanID []byte, switch { // If the db policy was missing an expected optional field, we return // nil as if the policy was unknown. - case err == ErrEdgePolicyOptionalFieldNotFound: + case errors.Is(err, ErrEdgePolicyOptionalFieldNotFound): return nil, nil case err != nil: @@ -4642,6 +4662,7 @@ func serializeChanEdgePolicy(w io.Writer, edge *models.ChannelEdgePolicy, if err := wire.WriteVarBytes(w, 0, opaqueBuf.Bytes()); err != nil { return err } + return nil } @@ -4650,7 +4671,7 @@ func deserializeChanEdgePolicy(r io.Reader) (*models.ChannelEdgePolicy, error) { // found, both an error and a populated policy object are returned. edge, deserializeErr := deserializeChanEdgePolicyRaw(r) if deserializeErr != nil && - deserializeErr != ErrEdgePolicyOptionalFieldNotFound { + !errors.Is(deserializeErr, ErrEdgePolicyOptionalFieldNotFound) { return nil, deserializeErr } @@ -4716,8 +4737,8 @@ func deserializeChanEdgePolicyRaw(r io.Reader) (*models.ChannelEdgePolicy, r, 0, MaxAllowedExtraOpaqueBytes, "blob", ) switch { - case err == io.ErrUnexpectedEOF: - case err == io.EOF: + case errors.Is(err, io.ErrUnexpectedEOF): + case errors.Is(err, io.EOF): case err != nil: return nil, err } @@ -4818,12 +4839,14 @@ func MakeTestGraph(t testing.TB, modifiers ...OptionModifier) (*ChannelGraph, backend, backendCleanup, err := kvdb.GetTestBackend(t.TempDir(), "cgr") if err != nil { backendCleanup() + return nil, err } graph, err := NewChannelGraph(backend) if err != nil { backendCleanup() + return nil, err } From 81e0608c10f72606e3dae17372f8a5fca9712a1d Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 18 Feb 2025 13:39:43 -0300 Subject: [PATCH 04/24] graph/db: rename Options to KVStoreOptions Namespace these options so that we can introduce separate options for the new ChannelGraph. --- config_builder.go | 2 +- graph/db/graph.go | 4 ++-- graph/db/kv_store.go | 6 +++--- graph/db/options.go | 35 ++++++++++++++++++----------------- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/config_builder.go b/config_builder.go index 26e1e8a40..aef188b53 100644 --- a/config_builder.go +++ b/config_builder.go @@ -1026,7 +1026,7 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( "instances") } - graphDBOptions := []graphdb.OptionModifier{ + graphDBOptions := []graphdb.KVStoreOptionModifier{ graphdb.WithRejectCacheSize(cfg.Caches.RejectCacheSize), graphdb.WithChannelCacheSize(cfg.Caches.ChannelCacheSize), graphdb.WithBatchCommitInterval(cfg.DB.BatchCommitInterval), diff --git a/graph/db/graph.go b/graph/db/graph.go index 217302ca9..ac52a0306 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -12,8 +12,8 @@ type ChannelGraph struct { } // NewChannelGraph creates a new ChannelGraph instance with the given backend. -func NewChannelGraph(db kvdb.Backend, options ...OptionModifier) (*ChannelGraph, - error) { +func NewChannelGraph(db kvdb.Backend, options ...KVStoreOptionModifier) ( + *ChannelGraph, error) { store, err := NewKVStore(db, options...) if err != nil { diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 6f327d73b..8ddafc179 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -198,7 +198,7 @@ type KVStore struct { // NewKVStore allocates a new KVStore backed by a DB instance. The // returned instance has its own unique reject cache and channel cache. -func NewKVStore(db kvdb.Backend, options ...OptionModifier) (*KVStore, +func NewKVStore(db kvdb.Backend, options ...KVStoreOptionModifier) (*KVStore, error) { opts := DefaultOptions() @@ -4827,8 +4827,8 @@ func (c *chanGraphNodeTx) ForEachChannel(f func(*models.ChannelEdgeInfo, // MakeTestGraph creates a new instance of the KVStore for testing // purposes. -func MakeTestGraph(t testing.TB, modifiers ...OptionModifier) (*ChannelGraph, - error) { +func MakeTestGraph(t testing.TB, modifiers ...KVStoreOptionModifier) ( + *ChannelGraph, error) { opts := DefaultOptions() for _, modifier := range modifiers { diff --git a/graph/db/options.go b/graph/db/options.go index a512ec4bc..a6cf2e909 100644 --- a/graph/db/options.go +++ b/graph/db/options.go @@ -20,8 +20,8 @@ const ( DefaultPreAllocCacheNumNodes = 15000 ) -// Options holds parameters for tuning and customizing a graph.DB. -type Options struct { +// KVStoreOptions holds parameters for tuning and customizing a graph.DB. +type KVStoreOptions struct { // RejectCacheSize is the maximum number of rejectCacheEntries to hold // in the rejection cache. RejectCacheSize int @@ -49,9 +49,9 @@ type Options struct { NoMigration bool } -// DefaultOptions returns an Options populated with default values. -func DefaultOptions() *Options { - return &Options{ +// DefaultOptions returns a KVStoreOptions populated with default values. +func DefaultOptions() *KVStoreOptions { + return &KVStoreOptions{ RejectCacheSize: DefaultRejectCacheSize, ChannelCacheSize: DefaultChannelCacheSize, PreAllocCacheNumNodes: DefaultPreAllocCacheNumNodes, @@ -60,41 +60,42 @@ func DefaultOptions() *Options { } } -// OptionModifier is a function signature for modifying the default Options. -type OptionModifier func(*Options) +// KVStoreOptionModifier is a function signature for modifying the default +// KVStoreOptions. +type KVStoreOptionModifier func(*KVStoreOptions) // WithRejectCacheSize sets the RejectCacheSize to n. -func WithRejectCacheSize(n int) OptionModifier { - return func(o *Options) { +func WithRejectCacheSize(n int) KVStoreOptionModifier { + return func(o *KVStoreOptions) { o.RejectCacheSize = n } } // WithChannelCacheSize sets the ChannelCacheSize to n. -func WithChannelCacheSize(n int) OptionModifier { - return func(o *Options) { +func WithChannelCacheSize(n int) KVStoreOptionModifier { + return func(o *KVStoreOptions) { o.ChannelCacheSize = n } } // WithPreAllocCacheNumNodes sets the PreAllocCacheNumNodes to n. -func WithPreAllocCacheNumNodes(n int) OptionModifier { - return func(o *Options) { +func WithPreAllocCacheNumNodes(n int) KVStoreOptionModifier { + return func(o *KVStoreOptions) { o.PreAllocCacheNumNodes = n } } // WithBatchCommitInterval sets the batch commit interval for the interval batch // schedulers. -func WithBatchCommitInterval(interval time.Duration) OptionModifier { - return func(o *Options) { +func WithBatchCommitInterval(interval time.Duration) KVStoreOptionModifier { + return func(o *KVStoreOptions) { o.BatchCommitInterval = interval } } // WithUseGraphCache sets the UseGraphCache option to the given value. -func WithUseGraphCache(use bool) OptionModifier { - return func(o *Options) { +func WithUseGraphCache(use bool) KVStoreOptionModifier { + return func(o *KVStoreOptions) { o.UseGraphCache = use } } From 00432e46f312d942341150679fb34bc81d910bf2 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 18 Feb 2025 13:48:11 -0300 Subject: [PATCH 05/24] multi: add ChannelGraph Config struct And use this struct to pass NewChannelGraph anything it needs to be able to init the KVStore that it houses. This will allow us to add ChannelGraph specific options. --- autopilot/prefattach_test.go | 2 +- config_builder.go | 7 ++++--- graph/db/graph.go | 18 ++++++++++++++---- graph/db/graph_test.go | 4 ++-- graph/db/kv_store.go | 5 ++++- graph/notifications_test.go | 9 ++++++--- peer/test_utils.go | 4 +++- routing/pathfind_test.go | 9 ++++++--- 8 files changed, 40 insertions(+), 18 deletions(-) diff --git a/autopilot/prefattach_test.go b/autopilot/prefattach_test.go index 784d1a0f8..f553482bb 100644 --- a/autopilot/prefattach_test.go +++ b/autopilot/prefattach_test.go @@ -46,7 +46,7 @@ func newDiskChanGraph(t *testing.T) (testGraph, error) { }) require.NoError(t, err) - graphDB, err := graphdb.NewChannelGraph(backend) + graphDB, err := graphdb.NewChannelGraph(&graphdb.Config{KVDB: backend}) require.NoError(t, err) return &testDBGraph{ diff --git a/config_builder.go b/config_builder.go index aef188b53..9b33520fd 100644 --- a/config_builder.go +++ b/config_builder.go @@ -1043,9 +1043,10 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( ) } - dbs.GraphDB, err = graphdb.NewChannelGraph( - databaseBackends.GraphDB, graphDBOptions..., - ) + dbs.GraphDB, err = graphdb.NewChannelGraph(&graphdb.Config{ + KVDB: databaseBackends.GraphDB, + KVStoreOpts: graphDBOptions, + }) if err != nil { cleanUp() diff --git a/graph/db/graph.go b/graph/db/graph.go index ac52a0306..b8140a9d6 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -2,6 +2,18 @@ package graphdb import "github.com/lightningnetwork/lnd/kvdb" +// Config is a struct that holds all the necessary dependencies for a +// ChannelGraph. +type Config struct { + // KVDB is the kvdb.Backend that will be used for initializing the + // KVStore CRUD layer. + KVDB kvdb.Backend + + // KVStoreOpts is a list of functional options that will be used when + // initializing the KVStore. + KVStoreOpts []KVStoreOptionModifier +} + // ChannelGraph is a layer above the graph's CRUD layer. // // NOTE: currently, this is purely a pass-through layer directly to the backing @@ -12,10 +24,8 @@ type ChannelGraph struct { } // NewChannelGraph creates a new ChannelGraph instance with the given backend. -func NewChannelGraph(db kvdb.Backend, options ...KVStoreOptionModifier) ( - *ChannelGraph, error) { - - store, err := NewKVStore(db, options...) +func NewChannelGraph(cfg *Config) (*ChannelGraph, error) { + store, err := NewKVStore(cfg.KVDB, cfg.KVStoreOpts...) if err != nil { return nil, err } diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 62b9cd4e1..e3681f4b0 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -4005,7 +4005,7 @@ func TestGraphLoading(t *testing.T) { defer backend.Close() defer backendCleanup() - graph, err := NewChannelGraph(backend) + graph, err := NewChannelGraph(&Config{KVDB: backend}) require.NoError(t, err) // Populate the graph with test data. @@ -4015,7 +4015,7 @@ func TestGraphLoading(t *testing.T) { // Recreate the graph. This should cause the graph cache to be // populated. - graphReloaded, err := NewChannelGraph(backend) + graphReloaded, err := NewChannelGraph(&Config{KVDB: backend}) require.NoError(t, err) // Assert that the cache content is identical. diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 8ddafc179..96babbec9 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -4843,7 +4843,10 @@ func MakeTestGraph(t testing.TB, modifiers ...KVStoreOptionModifier) ( return nil, err } - graph, err := NewChannelGraph(backend) + graph, err := NewChannelGraph(&Config{ + KVDB: backend, + KVStoreOpts: modifiers, + }) if err != nil { backendCleanup() diff --git a/graph/notifications_test.go b/graph/notifications_test.go index 4049c9f81..6846be15e 100644 --- a/graph/notifications_test.go +++ b/graph/notifications_test.go @@ -1093,9 +1093,12 @@ func makeTestGraph(t *testing.T, useCache bool) (*graphdb.ChannelGraph, t.Cleanup(backendCleanup) - graph, err := graphdb.NewChannelGraph( - backend, graphdb.WithUseGraphCache(useCache), - ) + graph, err := graphdb.NewChannelGraph(&graphdb.Config{ + KVDB: backend, + KVStoreOpts: []graphdb.KVStoreOptionModifier{ + graphdb.WithUseGraphCache(useCache), + }, + }) if err != nil { return nil, nil, err } diff --git a/peer/test_utils.go b/peer/test_utils.go index bcde83bf9..7d3c5ca37 100644 --- a/peer/test_utils.go +++ b/peer/test_utils.go @@ -615,7 +615,9 @@ func createTestPeer(t *testing.T) *peerTestCtx { }) require.NoError(t, err) - dbAliceGraph, err := graphdb.NewChannelGraph(graphBackend) + dbAliceGraph, err := graphdb.NewChannelGraph(&graphdb.Config{ + KVDB: graphBackend, + }) require.NoError(t, err) dbAliceChannel := channeldb.OpenForTesting(t, dbPath) diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index fd92ed934..6aba44a4f 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -166,9 +166,12 @@ func makeTestGraph(t *testing.T, useCache bool) (*graphdb.ChannelGraph, t.Cleanup(backendCleanup) - graph, err := graphdb.NewChannelGraph( - backend, graphdb.WithUseGraphCache(useCache), - ) + graph, err := graphdb.NewChannelGraph(&graphdb.Config{ + KVDB: backend, + KVStoreOpts: []graphdb.KVStoreOptionModifier{ + graphdb.WithUseGraphCache(useCache), + }, + }) if err != nil { return nil, nil, err } From 88398e3dd9de738db38904e19152fff74a965295 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 18 Feb 2025 14:23:03 -0300 Subject: [PATCH 06/24] graph/db: let ChannelGraph init the graphCache In this commit, we let the ChannelGraph be responsible for populating the graphCache and then passing it to the KVStore. This is a first step in moving the graphCache completely out of the KVStore layer. --- config_builder.go | 9 ++- docs/release-notes/release-notes-0.19.0.md | 1 + graph/db/graph.go | 63 ++++++++++++++++++- graph/db/graph_test.go | 1 + graph/db/kv_store.go | 43 +++---------- graph/db/options.go | 72 +++++++++++++--------- graph/notifications_test.go | 10 ++- routing/pathfind_test.go | 10 ++- 8 files changed, 129 insertions(+), 80 deletions(-) diff --git a/config_builder.go b/config_builder.go index 9b33520fd..e18c0b2ff 100644 --- a/config_builder.go +++ b/config_builder.go @@ -1030,14 +1030,17 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( graphdb.WithRejectCacheSize(cfg.Caches.RejectCacheSize), graphdb.WithChannelCacheSize(cfg.Caches.ChannelCacheSize), graphdb.WithBatchCommitInterval(cfg.DB.BatchCommitInterval), + } + + chanGraphOpts := []graphdb.ChanGraphOption{ graphdb.WithUseGraphCache(!cfg.DB.NoGraphCache), } // We want to pre-allocate the channel graph cache according to what we // expect for mainnet to speed up memory allocation. if cfg.ActiveNetParams.Name == chaincfg.MainNetParams.Name { - graphDBOptions = append( - graphDBOptions, graphdb.WithPreAllocCacheNumNodes( + chanGraphOpts = append( + chanGraphOpts, graphdb.WithPreAllocCacheNumNodes( graphdb.DefaultPreAllocCacheNumNodes, ), ) @@ -1046,7 +1049,7 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( dbs.GraphDB, err = graphdb.NewChannelGraph(&graphdb.Config{ KVDB: databaseBackends.GraphDB, KVStoreOpts: graphDBOptions, - }) + }, chanGraphOpts...) if err != nil { cleanUp() diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index ff8752810..0ba2bb48d 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -323,6 +323,7 @@ The underlying functionality between those two options remain the same. - [Refactor to hide DB transactions](https://github.com/lightningnetwork/lnd/pull/9513) - Move the graph cache out of the graph CRUD layer: - [1](https://github.com/lightningnetwork/lnd/pull/9533) + - [2](https://github.com/lightningnetwork/lnd/pull/9545) * [Golang was updated to `v1.22.11`](https://github.com/lightningnetwork/lnd/pull/9462). diff --git a/graph/db/graph.go b/graph/db/graph.go index b8140a9d6..c8f660bbb 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -1,6 +1,13 @@ package graphdb -import "github.com/lightningnetwork/lnd/kvdb" +import ( + "time" + + "github.com/lightningnetwork/lnd/graph/db/models" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) // Config is a struct that holds all the necessary dependencies for a // ChannelGraph. @@ -20,17 +27,67 @@ type Config struct { // KVStore. Upcoming commits will move the graph cache out of the KVStore and // into this layer so that the KVStore is only responsible for CRUD operations. type ChannelGraph struct { + graphCache *GraphCache + *KVStore } // NewChannelGraph creates a new ChannelGraph instance with the given backend. -func NewChannelGraph(cfg *Config) (*ChannelGraph, error) { +func NewChannelGraph(cfg *Config, options ...ChanGraphOption) (*ChannelGraph, + error) { + + opts := defaultChanGraphOptions() + for _, o := range options { + o(opts) + } + store, err := NewKVStore(cfg.KVDB, cfg.KVStoreOpts...) if err != nil { return nil, err } + if !opts.useGraphCache { + return &ChannelGraph{ + KVStore: store, + }, nil + } + + // The graph cache can be turned off (e.g. for mobile users) for a + // speed/memory usage tradeoff. + graphCache := NewGraphCache(opts.preAllocCacheNumNodes) + startTime := time.Now() + log.Debugf("Populating in-memory channel graph, this might take a " + + "while...") + + err = store.ForEachNodeCacheable(func(node route.Vertex, + features *lnwire.FeatureVector) error { + + graphCache.AddNodeFeatures(node, features) + + return nil + }) + if err != nil { + return nil, err + } + + err = store.ForEachChannel(func(info *models.ChannelEdgeInfo, + policy1, policy2 *models.ChannelEdgePolicy) error { + + graphCache.AddChannel(info, policy1, policy2) + + return nil + }) + if err != nil { + return nil, err + } + + log.Debugf("Finished populating in-memory channel graph (took %v, %s)", + time.Since(startTime), graphCache.Stats()) + + store.setGraphCache(graphCache) + return &ChannelGraph{ - KVStore: store, + KVStore: store, + graphCache: graphCache, }, nil } diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index e3681f4b0..8a6e3e82c 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -3924,6 +3924,7 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) { // Unset the channel graph cache to simulate the user running with the // option turned off. graph.graphCache = nil + graph.KVStore.graphCache = nil node1, err := createTestVertex(graph.db) require.Nil(t, err) diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 96babbec9..c54d08ca3 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -224,43 +224,18 @@ func NewKVStore(db kvdb.Backend, options ...KVStoreOptionModifier) (*KVStore, db, nil, opts.BatchCommitInterval, ) - // The graph cache can be turned off (e.g. for mobile users) for a - // speed/memory usage tradeoff. - if opts.UseGraphCache { - g.graphCache = NewGraphCache(opts.PreAllocCacheNumNodes) - startTime := time.Now() - log.Debugf("Populating in-memory channel graph, this might " + - "take a while...") - - err := g.ForEachNodeCacheable(func(node route.Vertex, - features *lnwire.FeatureVector) error { - - g.graphCache.AddNodeFeatures(node, features) - - return nil - }) - if err != nil { - return nil, err - } - - err = g.ForEachChannel(func(info *models.ChannelEdgeInfo, - policy1, policy2 *models.ChannelEdgePolicy) error { - - g.graphCache.AddChannel(info, policy1, policy2) - - return nil - }) - if err != nil { - return nil, err - } - - log.Debugf("Finished populating in-memory channel graph (took "+ - "%v, %s)", time.Since(startTime), g.graphCache.Stats()) - } - return g, nil } +// setGraphCache sets the KVStore's graphCache. +// +// NOTE: this is temporary and will only be called from the ChannelGraph's +// constructor before the KVStore methods are available to be called. This will +// be removed once the graph cache is fully owned by the ChannelGraph. +func (c *KVStore) setGraphCache(cache *GraphCache) { + c.graphCache = cache +} + // channelMapKey is the key structure used for storing channel edge policies. type channelMapKey struct { nodeKey route.Vertex diff --git a/graph/db/options.go b/graph/db/options.go index a6cf2e909..7bff8637a 100644 --- a/graph/db/options.go +++ b/graph/db/options.go @@ -20,6 +20,47 @@ const ( DefaultPreAllocCacheNumNodes = 15000 ) +// chanGraphOptions holds parameters for tuning and customizing the +// ChannelGraph. +type chanGraphOptions struct { + // useGraphCache denotes whether the in-memory graph cache should be + // used or a fallback version that uses the underlying database for + // path finding. + useGraphCache bool + + // preAllocCacheNumNodes is the number of nodes we expect to be in the + // graph cache, so we can pre-allocate the map accordingly. + preAllocCacheNumNodes int +} + +// defaultChanGraphOptions returns a new chanGraphOptions instance populated +// with default values. +func defaultChanGraphOptions() *chanGraphOptions { + return &chanGraphOptions{ + useGraphCache: true, + preAllocCacheNumNodes: DefaultPreAllocCacheNumNodes, + } +} + +// ChanGraphOption describes the signature of a functional option that can be +// used to customize a ChannelGraph instance. +type ChanGraphOption func(*chanGraphOptions) + +// WithUseGraphCache sets whether the in-memory graph cache should be used. +func WithUseGraphCache(use bool) ChanGraphOption { + return func(o *chanGraphOptions) { + o.useGraphCache = use + } +} + +// WithPreAllocCacheNumNodes sets the number of nodes we expect to be in the +// graph cache, so we can pre-allocate the map accordingly. +func WithPreAllocCacheNumNodes(n int) ChanGraphOption { + return func(o *chanGraphOptions) { + o.preAllocCacheNumNodes = n + } +} + // KVStoreOptions holds parameters for tuning and customizing a graph.DB. type KVStoreOptions struct { // RejectCacheSize is the maximum number of rejectCacheEntries to hold @@ -34,15 +75,6 @@ type KVStoreOptions struct { // wait before attempting to commit a pending set of updates. BatchCommitInterval time.Duration - // PreAllocCacheNumNodes is the number of nodes we expect to be in the - // graph cache, so we can pre-allocate the map accordingly. - PreAllocCacheNumNodes int - - // UseGraphCache denotes whether the in-memory graph cache should be - // used or a fallback version that uses the underlying database for - // path finding. - UseGraphCache bool - // NoMigration specifies that underlying backend was opened in read-only // mode and migrations shouldn't be performed. This can be useful for // applications that use the channeldb package as a library. @@ -52,11 +84,9 @@ type KVStoreOptions struct { // DefaultOptions returns a KVStoreOptions populated with default values. func DefaultOptions() *KVStoreOptions { return &KVStoreOptions{ - RejectCacheSize: DefaultRejectCacheSize, - ChannelCacheSize: DefaultChannelCacheSize, - PreAllocCacheNumNodes: DefaultPreAllocCacheNumNodes, - UseGraphCache: true, - NoMigration: false, + RejectCacheSize: DefaultRejectCacheSize, + ChannelCacheSize: DefaultChannelCacheSize, + NoMigration: false, } } @@ -78,13 +108,6 @@ func WithChannelCacheSize(n int) KVStoreOptionModifier { } } -// WithPreAllocCacheNumNodes sets the PreAllocCacheNumNodes to n. -func WithPreAllocCacheNumNodes(n int) KVStoreOptionModifier { - return func(o *KVStoreOptions) { - o.PreAllocCacheNumNodes = n - } -} - // WithBatchCommitInterval sets the batch commit interval for the interval batch // schedulers. func WithBatchCommitInterval(interval time.Duration) KVStoreOptionModifier { @@ -92,10 +115,3 @@ func WithBatchCommitInterval(interval time.Duration) KVStoreOptionModifier { o.BatchCommitInterval = interval } } - -// WithUseGraphCache sets the UseGraphCache option to the given value. -func WithUseGraphCache(use bool) KVStoreOptionModifier { - return func(o *KVStoreOptions) { - o.UseGraphCache = use - } -} diff --git a/graph/notifications_test.go b/graph/notifications_test.go index 6846be15e..aab1d4137 100644 --- a/graph/notifications_test.go +++ b/graph/notifications_test.go @@ -1093,12 +1093,10 @@ func makeTestGraph(t *testing.T, useCache bool) (*graphdb.ChannelGraph, t.Cleanup(backendCleanup) - graph, err := graphdb.NewChannelGraph(&graphdb.Config{ - KVDB: backend, - KVStoreOpts: []graphdb.KVStoreOptionModifier{ - graphdb.WithUseGraphCache(useCache), - }, - }) + graph, err := graphdb.NewChannelGraph( + &graphdb.Config{KVDB: backend}, + graphdb.WithUseGraphCache(useCache), + ) if err != nil { return nil, nil, err } diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 6aba44a4f..029383e0b 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -166,12 +166,10 @@ func makeTestGraph(t *testing.T, useCache bool) (*graphdb.ChannelGraph, t.Cleanup(backendCleanup) - graph, err := graphdb.NewChannelGraph(&graphdb.Config{ - KVDB: backend, - KVStoreOpts: []graphdb.KVStoreOptionModifier{ - graphdb.WithUseGraphCache(useCache), - }, - }) + graph, err := graphdb.NewChannelGraph( + &graphdb.Config{KVDB: backend}, + graphdb.WithUseGraphCache(useCache), + ) if err != nil { return nil, nil, err } From 9fe9e32c6e20fa2dc454ee0208581bfbe1a8eba8 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 18 Feb 2025 14:27:00 -0300 Subject: [PATCH 07/24] graph/db: move cache read checks to ChannelGraph. This commit moves the graph cache checks for FetchNodeFeatures, ForEachNodeDirectedChannel, GraphSession and ForEachNodeCached from the KVStore to the ChannelGraph. Since the ChannelGraph is currently just a pass-through for any of the KVStore methods, all that needs to be done for calls to go via the ChannelGraph instead directly to the KVStore is for the ChannelGraph to go and implement those methods. --- graph/db/graph.go | 62 ++++++++++++++++++++++++++++++++++++++++++ graph/db/graph_test.go | 2 +- graph/db/kv_store.go | 34 ++++------------------- 3 files changed, 68 insertions(+), 30 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index c8f660bbb..93c563122 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -91,3 +91,65 @@ func NewChannelGraph(cfg *Config, options ...ChanGraphOption) (*ChannelGraph, graphCache: graphCache, }, nil } + +// ForEachNodeDirectedChannel iterates through all channels of a given node, +// executing the passed callback on the directed edge representing the channel +// and its incoming policy. If the callback returns an error, then the iteration +// is halted with the error propagated back up to the caller. If the graphCache +// is available, then it will be used to retrieve the node's channels instead +// of the database. +// +// Unknown policies are passed into the callback as nil values. +// +// NOTE: this is part of the graphdb.NodeTraverser interface. +func (c *ChannelGraph) ForEachNodeDirectedChannel(node route.Vertex, + cb func(channel *DirectedChannel) error) error { + + if c.graphCache != nil { + return c.graphCache.ForEachChannel(node, cb) + } + + return c.KVStore.ForEachNodeDirectedChannel(node, cb) +} + +// FetchNodeFeatures returns the features of the given node. If no features are +// known for the node, an empty feature vector is returned. +// If the graphCache is available, then it will be used to retrieve the node's +// features instead of the database. +// +// NOTE: this is part of the graphdb.NodeTraverser interface. +func (c *ChannelGraph) FetchNodeFeatures(node route.Vertex) ( + *lnwire.FeatureVector, error) { + + if c.graphCache != nil { + return c.graphCache.GetFeatures(node), nil + } + + return c.KVStore.FetchNodeFeatures(node) +} + +// GraphSession will provide the call-back with access to a NodeTraverser +// instance which can be used to perform queries against the channel graph. If +// the graph cache is not enabled, then the call-back will be provided with +// access to the graph via a consistent read-only transaction. +func (c *ChannelGraph) GraphSession(cb func(graph NodeTraverser) error) error { + if c.graphCache != nil { + return cb(c) + } + + return c.KVStore.GraphSession(cb) +} + +// ForEachNodeCached iterates through all the stored vertices/nodes in the +// graph, executing the passed callback with each node encountered. +// +// NOTE: The callback contents MUST not be modified. +func (c *ChannelGraph) ForEachNodeCached(cb func(node route.Vertex, + chans map[uint64]*DirectedChannel) error) error { + + if c.graphCache != nil { + return c.graphCache.ForEachNode(cb) + } + + return c.KVStore.ForEachNodeCached(cb) +} diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 8a6e3e82c..cf5452eb7 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -3953,7 +3953,7 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) { getSingleChannel := func() *DirectedChannel { var ch *DirectedChannel - err = graph.forEachNodeDirectedChannel(nil, node1.PubKeyBytes, + err = graph.ForEachNodeDirectedChannel(node1.PubKeyBytes, func(c *DirectedChannel) error { require.Nil(t, ch) ch = c diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index c54d08ca3..37f1ec8b1 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -480,10 +480,6 @@ func (c *KVStore) ForEachChannel(cb func(*models.ChannelEdgeInfo, func (c *KVStore) forEachNodeDirectedChannel(tx kvdb.RTx, node route.Vertex, cb func(channel *DirectedChannel) error) error { - if c.graphCache != nil { - return c.graphCache.ForEachChannel(node, cb) - } - // Fallback that uses the database. toNodeCallback := func() route.Vertex { return node @@ -539,10 +535,6 @@ func (c *KVStore) forEachNodeDirectedChannel(tx kvdb.RTx, func (c *KVStore) fetchNodeFeatures(tx kvdb.RTx, node route.Vertex) (*lnwire.FeatureVector, error) { - if c.graphCache != nil { - return c.graphCache.GetFeatures(node), nil - } - // Fallback that uses the database. targetNode, err := c.FetchLightningNodeTx(tx, node) switch { @@ -564,9 +556,7 @@ func (c *KVStore) fetchNodeFeatures(tx kvdb.RTx, // ForEachNodeDirectedChannel iterates through all channels of a given node, // executing the passed callback on the directed edge representing the channel // and its incoming policy. If the callback returns an error, then the iteration -// is halted with the error propagated back up to the caller. If the graphCache -// is available, then it will be used to retrieve the node's channels instead -// of the database. +// is halted with the error propagated back up to the caller. // // Unknown policies are passed into the callback as nil values. // @@ -579,8 +569,6 @@ func (c *KVStore) ForEachNodeDirectedChannel(nodePub route.Vertex, // FetchNodeFeatures returns the features of the given node. If no features are // known for the node, an empty feature vector is returned. -// If the graphCache is available, then it will be used to retrieve the node's -// features instead of the database. // // NOTE: this is part of the graphdb.NodeTraverser interface. func (c *KVStore) FetchNodeFeatures(nodePub route.Vertex) ( @@ -589,18 +577,13 @@ func (c *KVStore) FetchNodeFeatures(nodePub route.Vertex) ( return c.fetchNodeFeatures(nil, nodePub) } -// ForEachNodeCached is similar to forEachNode, but it utilizes the channel -// graph cache instead. Note that this doesn't return all the information the -// regular forEachNode method does. +// ForEachNodeCached is similar to forEachNode, but it returns DirectedChannel +// data to the call-back. // // NOTE: The callback contents MUST not be modified. func (c *KVStore) ForEachNodeCached(cb func(node route.Vertex, chans map[uint64]*DirectedChannel) error) error { - if c.graphCache != nil { - return c.graphCache.ForEachNode(cb) - } - // Otherwise call back to a version that uses the database directly. // We'll iterate over each node, then the set of channels for each // node, and construct a similar callback functiopn signature as the @@ -3883,14 +3866,8 @@ func (c *KVStore) IsClosedScid(scid lnwire.ShortChannelID) (bool, error) { } // GraphSession will provide the call-back with access to a NodeTraverser -// instance which can be used to perform queries against the channel graph. If -// the graph cache is not enabled, then the call-back will be provided with -// access to the graph via a consistent read-only transaction. +// instance which can be used to perform queries against the channel graph. func (c *KVStore) GraphSession(cb func(graph NodeTraverser) error) error { - if c.graphCache != nil { - return cb(&nodeTraverserSession{db: c}) - } - return c.db.View(func(tx walletdb.ReadTx) error { return cb(&nodeTraverserSession{ db: c, @@ -3900,8 +3877,7 @@ func (c *KVStore) GraphSession(cb func(graph NodeTraverser) error) error { } // nodeTraverserSession implements the NodeTraverser interface but with a -// backing read only transaction for a consistent view of the graph in the case -// where the graph Cache has not been enabled. +// backing read only transaction for a consistent view of the graph. type nodeTraverserSession struct { tx kvdb.RTx db *KVStore From f75e6a1c1093406a2c2e24cebdd0eaa3575e4ef9 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 18 Feb 2025 14:48:46 -0300 Subject: [PATCH 08/24] graph/db: move various cache write calls to ChannelGraph Here, we move the graph cache writes for AddLightningNode, DeleteLightningNode, AddChannelEdge and MarkEdgeLive to the ChannelGraph. Since these are writes, the cache is only updated if the DB write is successful. --- docs/release-notes/release-notes-0.19.0.md | 1 + graph/db/graph.go | 106 +++++++++++++++++++++ graph/db/kv_store.go | 30 ------ 3 files changed, 107 insertions(+), 30 deletions(-) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index 0ba2bb48d..d6adeede4 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -324,6 +324,7 @@ The underlying functionality between those two options remain the same. - Move the graph cache out of the graph CRUD layer: - [1](https://github.com/lightningnetwork/lnd/pull/9533) - [2](https://github.com/lightningnetwork/lnd/pull/9545) + - [3](https://github.com/lightningnetwork/lnd/pull/9550) * [Golang was updated to `v1.22.11`](https://github.com/lightningnetwork/lnd/pull/9462). diff --git a/graph/db/graph.go b/graph/db/graph.go index 93c563122..742c09478 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -1,8 +1,10 @@ package graphdb import ( + "sync" "time" + "github.com/lightningnetwork/lnd/batch" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" @@ -27,6 +29,10 @@ type Config struct { // KVStore. Upcoming commits will move the graph cache out of the KVStore and // into this layer so that the KVStore is only responsible for CRUD operations. type ChannelGraph struct { + // cacheMu guards any writes to the graphCache. It should be held + // across the DB write call and the graphCache update to make the + // two updates as atomic as possible. + cacheMu sync.Mutex graphCache *GraphCache *KVStore @@ -153,3 +159,103 @@ func (c *ChannelGraph) ForEachNodeCached(cb func(node route.Vertex, return c.KVStore.ForEachNodeCached(cb) } + +// 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 announcement, or to insert a node found in a +// channel update. +func (c *ChannelGraph) AddLightningNode(node *models.LightningNode, + op ...batch.SchedulerOption) error { + + c.cacheMu.Lock() + defer c.cacheMu.Unlock() + + err := c.KVStore.AddLightningNode(node, op...) + if err != nil { + return err + } + + if c.graphCache != nil { + c.graphCache.AddNodeFeatures( + node.PubKeyBytes, node.Features, + ) + } + + return nil +} + +// DeleteLightningNode starts a new database transaction to remove a vertex/node +// from the database according to the node's public key. +func (c *ChannelGraph) DeleteLightningNode(nodePub route.Vertex) error { + c.cacheMu.Lock() + defer c.cacheMu.Unlock() + + err := c.KVStore.DeleteLightningNode(nodePub) + if err != nil { + return err + } + + if c.graphCache != nil { + c.graphCache.RemoveNode(nodePub) + } + + return nil +} + +// AddChannelEdge adds a new (undirected, blank) edge to the graph database. An +// undirected edge from the two target nodes are created. The information stored +// denotes the static attributes of the channel, such as the channelID, the keys +// involved in creation of the channel, and the set of features that the channel +// supports. The chanPoint and chanID are used to uniquely identify the edge +// globally within the database. +func (c *ChannelGraph) AddChannelEdge(edge *models.ChannelEdgeInfo, + op ...batch.SchedulerOption) error { + + c.cacheMu.Lock() + defer c.cacheMu.Unlock() + + err := c.KVStore.AddChannelEdge(edge, op...) + if err != nil { + return err + } + + if c.graphCache != nil { + c.graphCache.AddChannel(edge, nil, nil) + } + + return nil +} + +// MarkEdgeLive clears an edge from our zombie index, deeming it as live. +// If the cache is enabled, the edge will be added back to the graph cache if +// we still have a record of this channel in the DB. +func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error { + c.cacheMu.Lock() + defer c.cacheMu.Unlock() + + err := c.KVStore.MarkEdgeLive(chanID) + if err != nil { + return err + } + + if c.graphCache != nil { + // We need to add the channel back into our graph cache, + // otherwise we won't use it for path finding. + infos, err := c.KVStore.FetchChanInfos([]uint64{chanID}) + if err != nil { + return err + } + + if len(infos) == 0 { + return nil + } + + info := infos[0] + + c.graphCache.AddChannel(info.Info, info.Policy1, info.Policy2) + } + + return nil +} diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 37f1ec8b1..ed80fbc9f 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -884,12 +884,6 @@ func (c *KVStore) AddLightningNode(node *models.LightningNode, r := &batch.Request{ Update: func(tx kvdb.RwTx) error { - if c.graphCache != nil { - c.graphCache.AddNodeFeatures( - node.PubKeyBytes, node.Features, - ) - } - return addLightningNode(tx, node) }, } @@ -969,10 +963,6 @@ func (c *KVStore) DeleteLightningNode(nodePub route.Vertex) error { return ErrGraphNodeNotFound } - if c.graphCache != nil { - c.graphCache.RemoveNode(nodePub) - } - return c.deleteLightningNode(nodes, nodePub[:]) }, func() {}) } @@ -1104,10 +1094,6 @@ func (c *KVStore) addChannelEdge(tx kvdb.RwTx, return ErrEdgeAlreadyExist } - if c.graphCache != nil { - c.graphCache.AddChannel(edge, nil, nil) - } - // Before we insert the channel into the database, we'll ensure that // both nodes already exist in the channel graph. If either node // doesn't, then we'll insert a "shell" node that just includes its @@ -3717,22 +3703,6 @@ func (c *KVStore) markEdgeLiveUnsafe(tx kvdb.RwTx, chanID uint64) error { c.rejectCache.remove(chanID) c.chanCache.remove(chanID) - // We need to add the channel back into our graph cache, otherwise we - // won't use it for path finding. - if c.graphCache != nil { - edgeInfos, err := c.fetchChanInfos(tx, []uint64{chanID}) - if err != nil { - return err - } - - for _, edgeInfo := range edgeInfos { - c.graphCache.AddChannel( - edgeInfo.Info, edgeInfo.Policy1, - edgeInfo.Policy2, - ) - } - } - return nil } From 081c9dc0825fed2a42658ecea33eb6e69520c5ee Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 17 Feb 2025 12:31:30 -0300 Subject: [PATCH 09/24] graph/db: refactor delChannelEdgeUnsafe to return edge info And update cache outside the method rather. This will make it easier to completely move the cache write out to the ChannelGraph layer. --- graph/db/kv_store.go | 86 +++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index ed80fbc9f..be4157022 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -1375,19 +1375,11 @@ func (c *KVStore) PruneGraph(spentOutputs []*wire.OutPoint, continue } - // However, if it does, then we'll read out the full - // version so we can add it to the set of deleted - // channels. - edgeInfo, err := fetchChanEdgeInfo(edgeIndex, chanID) - if err != nil { - return err - } - // Attempt to delete the channel, an ErrEdgeNotFound // will be returned if that outpoint isn't known to be // a channel. If no error is returned, then a channel // was successfully pruned. - err = c.delChannelEdgeUnsafe( + edgeInfo, err := c.delChannelEdgeUnsafe( edges, edgeIndex, chanIndex, zombieIndex, chanID, false, false, ) @@ -1395,7 +1387,15 @@ func (c *KVStore) PruneGraph(spentOutputs []*wire.OutPoint, return err } - chansClosed = append(chansClosed, &edgeInfo) + if c.graphCache != nil { + c.graphCache.RemoveChannel( + edgeInfo.NodeKey1Bytes, + edgeInfo.NodeKey2Bytes, + edgeInfo.ChannelID, + ) + } + + chansClosed = append(chansClosed, edgeInfo) } metaBucket, err := tx.CreateTopLevelBucket(graphMetaBucket) @@ -1640,26 +1640,29 @@ func (c *KVStore) DisconnectBlockAtHeight(height uint32) ( cursor := edgeIndex.ReadWriteCursor() //nolint:ll - for k, v := cursor.Seek(chanIDStart[:]); k != nil && - bytes.Compare(k, chanIDEnd[:]) < 0; k, v = cursor.Next() { - edgeInfoReader := bytes.NewReader(v) - edgeInfo, err := deserializeChanEdgeInfo(edgeInfoReader) - if err != nil { - return err - } - + for k, _ := cursor.Seek(chanIDStart[:]); k != nil && + bytes.Compare(k, chanIDEnd[:]) < 0; k, _ = cursor.Next() { keys = append(keys, k) - removedChans = append(removedChans, &edgeInfo) } for _, k := range keys { - err = c.delChannelEdgeUnsafe( + edgeInfo, err := c.delChannelEdgeUnsafe( edges, edgeIndex, chanIndex, zombieIndex, k, false, false, ) if err != nil && !errors.Is(err, ErrEdgeNotFound) { return err } + + if c.graphCache != nil { + c.graphCache.RemoveChannel( + edgeInfo.NodeKey1Bytes, + edgeInfo.NodeKey2Bytes, + edgeInfo.ChannelID, + ) + } + + removedChans = append(removedChans, edgeInfo) } // Delete all the entries in the prune log having a height @@ -1799,13 +1802,21 @@ func (c *KVStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, var rawChanID [8]byte for _, chanID := range chanIDs { byteOrder.PutUint64(rawChanID[:], chanID) - err := c.delChannelEdgeUnsafe( + edgeInfo, err := c.delChannelEdgeUnsafe( edges, edgeIndex, chanIndex, zombieIndex, rawChanID[:], markZombie, strictZombiePruning, ) if err != nil { return err } + + if c.graphCache != nil { + c.graphCache.RemoveChannel( + edgeInfo.NodeKey1Bytes, + edgeInfo.NodeKey2Bytes, + edgeInfo.ChannelID, + ) + } } return nil @@ -2623,18 +2634,11 @@ func delEdgeUpdateIndexEntry(edgesBucket kvdb.RwBucket, chanID uint64, // acquired. func (c *KVStore) delChannelEdgeUnsafe(edges, edgeIndex, chanIndex, zombieIndex kvdb.RwBucket, chanID []byte, isZombie, - strictZombie bool) error { + strictZombie bool) (*models.ChannelEdgeInfo, error) { edgeInfo, err := fetchChanEdgeInfo(edgeIndex, chanID) if err != nil { - return err - } - - if c.graphCache != nil { - c.graphCache.RemoveChannel( - edgeInfo.NodeKey1Bytes, edgeInfo.NodeKey2Bytes, - edgeInfo.ChannelID, - ) + return nil, err } // We'll also remove the entry in the edge update index bucket before @@ -2643,11 +2647,11 @@ func (c *KVStore) delChannelEdgeUnsafe(edges, edgeIndex, chanIndex, cid := byteOrder.Uint64(chanID) edge1, edge2, err := fetchChanEdgePolicies(edgeIndex, edges, chanID) if err != nil { - return err + return nil, err } err = delEdgeUpdateIndexEntry(edges, cid, edge1, edge2) if err != nil { - return err + return nil, err } // The edge key is of the format pubKey || chanID. First we construct @@ -2661,13 +2665,13 @@ func (c *KVStore) delChannelEdgeUnsafe(edges, edgeIndex, chanIndex, copy(edgeKey[:33], edgeInfo.NodeKey1Bytes[:]) if edges.Get(edgeKey[:]) != nil { if err := edges.Delete(edgeKey[:]); err != nil { - return err + return nil, err } } copy(edgeKey[:33], edgeInfo.NodeKey2Bytes[:]) if edges.Get(edgeKey[:]) != nil { if err := edges.Delete(edgeKey[:]); err != nil { - return err + return nil, err } } @@ -2676,31 +2680,31 @@ func (c *KVStore) delChannelEdgeUnsafe(edges, edgeIndex, chanIndex, // directions. err = updateEdgePolicyDisabledIndex(edges, cid, false, false) if err != nil { - return err + return nil, err } err = updateEdgePolicyDisabledIndex(edges, cid, true, false) if err != nil { - return err + return nil, err } // With the edge data deleted, we can purge the information from the two // edge indexes. if err := edgeIndex.Delete(chanID); err != nil { - return err + return nil, err } var b bytes.Buffer if err := WriteOutpoint(&b, &edgeInfo.ChannelPoint); err != nil { - return err + return nil, err } if err := chanIndex.Delete(b.Bytes()); err != nil { - return err + return nil, err } // Finally, we'll mark the edge as a zombie within our index if it's // being removed due to the channel becoming a zombie. We do this to // ensure we don't store unnecessary data for spent channels. if !isZombie { - return nil + return &edgeInfo, nil } nodeKey1, nodeKey2 := edgeInfo.NodeKey1Bytes, edgeInfo.NodeKey2Bytes @@ -2708,7 +2712,7 @@ func (c *KVStore) delChannelEdgeUnsafe(edges, edgeIndex, chanIndex, nodeKey1, nodeKey2 = makeZombiePubkeys(&edgeInfo, edge1, edge2) } - return markEdgeZombie( + return &edgeInfo, markEdgeZombie( zombieIndex, byteOrder.Uint64(chanID), nodeKey1, nodeKey2, ) } From 71e5ab6200d36cfc8f85860ab91e0cb633acb900 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 17 Feb 2025 12:34:53 -0300 Subject: [PATCH 10/24] graph/db: move some cache writes to ChannelGraph. Here we move the cache writes for DisconnectBlockAtHeight and DeleteChannelEdges to the ChannelGraph. --- graph/db/graph.go | 63 ++++++++++++++++++++++++++++++++++++++++++++ graph/db/kv_store.go | 27 ++++++------------- 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index 742c09478..c3f195bc0 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -259,3 +259,66 @@ func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error { return nil } + +// DeleteChannelEdges removes edges with the given channel IDs from the +// database and marks them as zombies. This ensures that we're unable to re-add +// it to our database once again. If an edge does not exist within the +// database, then ErrEdgeNotFound will be returned. If strictZombiePruning is +// true, then when we mark these edges as zombies, we'll set up the keys such +// that we require the node that failed to send the fresh update to be the one +// that resurrects the channel from its zombie state. The markZombie bool +// denotes whether to mark the channel as a zombie. +func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning, markZombie bool, + chanIDs ...uint64) error { + + c.cacheMu.Lock() + defer c.cacheMu.Unlock() + + infos, err := c.KVStore.DeleteChannelEdges( + strictZombiePruning, markZombie, chanIDs..., + ) + if err != nil { + return err + } + + if c.graphCache != nil { + for _, info := range infos { + c.graphCache.RemoveChannel( + info.NodeKey1Bytes, info.NodeKey2Bytes, + info.ChannelID, + ) + } + } + + return err +} + +// DisconnectBlockAtHeight is used to indicate that the block specified +// by the passed height has been disconnected from the main chain. This +// will "rewind" the graph back to the height below, deleting channels +// that are no longer confirmed from the graph. The prune log will be +// set to the last prune height valid for the remaining chain. +// Channels that were removed from the graph resulting from the +// disconnected block are returned. +func (c *ChannelGraph) DisconnectBlockAtHeight(height uint32) ( + []*models.ChannelEdgeInfo, error) { + + c.cacheMu.Lock() + defer c.cacheMu.Unlock() + + edges, err := c.KVStore.DisconnectBlockAtHeight(height) + if err != nil { + return nil, err + } + + if c.graphCache != nil { + for _, edge := range edges { + c.graphCache.RemoveChannel( + edge.NodeKey1Bytes, edge.NodeKey2Bytes, + edge.ChannelID, + ) + } + } + + return edges, nil +} diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index be4157022..86df5ab34 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -1654,14 +1654,6 @@ func (c *KVStore) DisconnectBlockAtHeight(height uint32) ( return err } - if c.graphCache != nil { - c.graphCache.RemoveChannel( - edgeInfo.NodeKey1Bytes, - edgeInfo.NodeKey2Bytes, - edgeInfo.ChannelID, - ) - } - removedChans = append(removedChans, edgeInfo) } @@ -1768,7 +1760,7 @@ func (c *KVStore) PruneTip() (*chainhash.Hash, uint32, error) { // that resurrects the channel from its zombie state. The markZombie bool // denotes whether or not to mark the channel as a zombie. func (c *KVStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, - chanIDs ...uint64) error { + chanIDs ...uint64) ([]*models.ChannelEdgeInfo, error) { // TODO(roasbeef): possibly delete from node bucket if node has no more // channels @@ -1777,6 +1769,7 @@ func (c *KVStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, c.cacheMu.Lock() defer c.cacheMu.Unlock() + var infos []*models.ChannelEdgeInfo err := kvdb.Update(c.db, func(tx kvdb.RwTx) error { edges := tx.ReadWriteBucket(edgeBucket) if edges == nil { @@ -1810,19 +1803,15 @@ func (c *KVStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, return err } - if c.graphCache != nil { - c.graphCache.RemoveChannel( - edgeInfo.NodeKey1Bytes, - edgeInfo.NodeKey2Bytes, - edgeInfo.ChannelID, - ) - } + infos = append(infos, edgeInfo) } return nil - }, func() {}) + }, func() { + infos = nil + }) if err != nil { - return err + return nil, err } for _, chanID := range chanIDs { @@ -1830,7 +1819,7 @@ func (c *KVStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, c.chanCache.remove(chanID) } - return nil + return infos, nil } // ChannelID attempt to lookup the 8-byte compact channel ID which maps to the From 941e7bf6b386da49763ab084a4dc831b08438019 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 17 Feb 2025 12:41:57 -0300 Subject: [PATCH 11/24] graph/db: move cache update out of pruneGraphNodes In preparation for moving the cache write completely out of KVStore, we move the cache write up one layer. --- graph/db/kv_store.go | 50 ++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 86df5ab34..9d784bf0d 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -1427,7 +1427,18 @@ func (c *KVStore) PruneGraph(spentOutputs []*wire.OutPoint, // Now that the graph has been pruned, we'll also attempt to // prune any nodes that have had a channel closed within the // latest block. - return c.pruneGraphNodes(nodes, edgeIndex) + prunedNodes, err := c.pruneGraphNodes(nodes, edgeIndex) + if err != nil { + return err + } + + if c.graphCache != nil { + for _, nodePubKey := range prunedNodes { + c.graphCache.RemoveNode(nodePubKey) + } + } + + return nil }, func() { chansClosed = nil }) @@ -1467,7 +1478,18 @@ func (c *KVStore) PruneGraphNodes() error { return ErrGraphNoEdgesFound } - return c.pruneGraphNodes(nodes, edgeIndex) + prunedNodes, err := c.pruneGraphNodes(nodes, edgeIndex) + if err != nil { + return err + } + + if c.graphCache != nil { + for _, nodePubKey := range prunedNodes { + c.graphCache.RemoveNode(nodePubKey) + } + } + + return nil }, func() {}) } @@ -1475,7 +1497,7 @@ func (c *KVStore) PruneGraphNodes() error { // channel closed within the current block. If the node still has existing // channels in the graph, this will act as a no-op. func (c *KVStore) pruneGraphNodes(nodes kvdb.RwBucket, - edgeIndex kvdb.RwBucket) error { + edgeIndex kvdb.RwBucket) ([]route.Vertex, error) { log.Trace("Pruning nodes from graph with no open channels") @@ -1483,7 +1505,7 @@ func (c *KVStore) pruneGraphNodes(nodes kvdb.RwBucket, // even if it no longer has any open channels. sourceNode, err := c.sourceNode(nodes) if err != nil { - return err + return nil, err } // We'll use this map to keep count the number of references to a node @@ -1505,7 +1527,7 @@ func (c *KVStore) pruneGraphNodes(nodes kvdb.RwBucket, return nil }) if err != nil { - return err + return nil, err } // To ensure we never delete the source node, we'll start off by @@ -1531,12 +1553,12 @@ func (c *KVStore) pruneGraphNodes(nodes kvdb.RwBucket, return nil }) if err != nil { - return err + return nil, err } // Finally, we'll make a second pass over the set of nodes, and delete // any nodes that have a ref count of zero. - var numNodesPruned int + var pruned []route.Vertex for nodePubKey, refCount := range nodeRefCounts { // If the ref count of the node isn't zero, then we can safely // skip it as it still has edges to or from it within the @@ -1545,10 +1567,6 @@ func (c *KVStore) pruneGraphNodes(nodes kvdb.RwBucket, continue } - if c.graphCache != nil { - c.graphCache.RemoveNode(nodePubKey) - } - // If we reach this point, then there are no longer any edges // that connect this node, so we can delete it. err := c.deleteLightningNode(nodes, nodePubKey[:]) @@ -1561,21 +1579,21 @@ func (c *KVStore) pruneGraphNodes(nodes kvdb.RwBucket, continue } - return err + return nil, err } log.Infof("Pruned unconnected node %x from channel graph", nodePubKey[:]) - numNodesPruned++ + pruned = append(pruned, nodePubKey) } - if numNodesPruned > 0 { + if len(pruned) > 0 { log.Infof("Pruned %v unconnected nodes from the channel graph", - numNodesPruned) + len(pruned)) } - return nil + return pruned, err } // DisconnectBlockAtHeight is used to indicate that the block specified From cc4fcbf83882c7afb24c07580ef735319adef8d4 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 17 Feb 2025 12:58:51 -0300 Subject: [PATCH 12/24] graph/db: move cache writes for Prune methods This commit moves the cache writes for PruneGraphNodes and PruneGraph from the KVStore to the ChannelGraph. --- docs/release-notes/release-notes-0.19.0.md | 1 + graph/db/graph.go | 64 ++++++++++++++++++++++ graph/db/kv_store.go | 61 ++++++++------------- 3 files changed, 87 insertions(+), 39 deletions(-) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index d6adeede4..54941074c 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -325,6 +325,7 @@ The underlying functionality between those two options remain the same. - [1](https://github.com/lightningnetwork/lnd/pull/9533) - [2](https://github.com/lightningnetwork/lnd/pull/9545) - [3](https://github.com/lightningnetwork/lnd/pull/9550) + - [4](https://github.com/lightningnetwork/lnd/pull/9551) * [Golang was updated to `v1.22.11`](https://github.com/lightningnetwork/lnd/pull/9462). diff --git a/graph/db/graph.go b/graph/db/graph.go index c3f195bc0..6185de410 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -4,6 +4,8 @@ import ( "sync" "time" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/batch" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/kvdb" @@ -322,3 +324,65 @@ func (c *ChannelGraph) DisconnectBlockAtHeight(height uint32) ( return edges, nil } + +// PruneGraph prunes newly closed channels from the channel graph in response +// to a new block being solved on the network. Any transactions which spend the +// funding output of any known channels within he graph will be deleted. +// Additionally, the "prune tip", or the last block which has been used to +// prune the graph is stored so callers can ensure the graph is fully in sync +// with the current UTXO state. A slice of channels that have been closed by +// the target block are returned if the function succeeds without error. +func (c *ChannelGraph) PruneGraph(spentOutputs []*wire.OutPoint, + blockHash *chainhash.Hash, blockHeight uint32) ( + []*models.ChannelEdgeInfo, error) { + + c.cacheMu.Lock() + defer c.cacheMu.Unlock() + + edges, nodes, err := c.KVStore.PruneGraph( + spentOutputs, blockHash, blockHeight, + ) + if err != nil { + return nil, err + } + + if c.graphCache != nil { + for _, edge := range edges { + c.graphCache.RemoveChannel( + edge.NodeKey1Bytes, edge.NodeKey2Bytes, + edge.ChannelID, + ) + } + + for _, node := range nodes { + c.graphCache.RemoveNode(node) + } + + log.Debugf("Pruned graph, cache now has %s", + c.graphCache.Stats()) + } + + return edges, nil +} + +// PruneGraphNodes is a garbage collection method which attempts to prune out +// any nodes from the channel graph that are currently unconnected. This ensure +// that we only maintain a graph of reachable nodes. In the event that a pruned +// node gains more channels, it will be re-added back to the graph. +func (c *ChannelGraph) PruneGraphNodes() error { + c.cacheMu.Lock() + defer c.cacheMu.Unlock() + + nodes, err := c.KVStore.PruneGraphNodes() + if err != nil { + return err + } + + if c.graphCache != nil { + for _, node := range nodes { + c.graphCache.RemoveNode(node) + } + } + + return nil +} diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 9d784bf0d..486971e03 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -1316,15 +1316,19 @@ const ( // Additionally, the "prune tip", or the last block which has been used to // prune the graph is stored so callers can ensure the graph is fully in sync // with the current UTXO state. A slice of channels that have been closed by -// the target block are returned if the function succeeds without error. +// the target block along with any pruned nodes are returned if the function +// succeeds without error. func (c *KVStore) PruneGraph(spentOutputs []*wire.OutPoint, blockHash *chainhash.Hash, blockHeight uint32) ( - []*models.ChannelEdgeInfo, error) { + []*models.ChannelEdgeInfo, []route.Vertex, error) { c.cacheMu.Lock() defer c.cacheMu.Unlock() - var chansClosed []*models.ChannelEdgeInfo + var ( + chansClosed []*models.ChannelEdgeInfo + prunedNodes []route.Vertex + ) err := kvdb.Update(c.db, func(tx kvdb.RwTx) error { // First grab the edges bucket which houses the information @@ -1387,14 +1391,6 @@ func (c *KVStore) PruneGraph(spentOutputs []*wire.OutPoint, return err } - if c.graphCache != nil { - c.graphCache.RemoveChannel( - edgeInfo.NodeKey1Bytes, - edgeInfo.NodeKey2Bytes, - edgeInfo.ChannelID, - ) - } - chansClosed = append(chansClosed, edgeInfo) } @@ -1427,23 +1423,15 @@ func (c *KVStore) PruneGraph(spentOutputs []*wire.OutPoint, // Now that the graph has been pruned, we'll also attempt to // prune any nodes that have had a channel closed within the // latest block. - prunedNodes, err := c.pruneGraphNodes(nodes, edgeIndex) - if err != nil { - return err - } + prunedNodes, err = c.pruneGraphNodes(nodes, edgeIndex) - if c.graphCache != nil { - for _, nodePubKey := range prunedNodes { - c.graphCache.RemoveNode(nodePubKey) - } - } - - return nil + return err }, func() { chansClosed = nil + prunedNodes = nil }) if err != nil { - return nil, err + return nil, nil, err } for _, channel := range chansClosed { @@ -1451,20 +1439,16 @@ func (c *KVStore) PruneGraph(spentOutputs []*wire.OutPoint, c.chanCache.remove(channel.ChannelID) } - if c.graphCache != nil { - log.Debugf("Pruned graph, cache now has %s", - c.graphCache.Stats()) - } - - return chansClosed, nil + return chansClosed, prunedNodes, nil } // PruneGraphNodes is a garbage collection method which attempts to prune out // any nodes from the channel graph that are currently unconnected. This ensure // that we only maintain a graph of reachable nodes. In the event that a pruned // node gains more channels, it will be re-added back to the graph. -func (c *KVStore) PruneGraphNodes() error { - return kvdb.Update(c.db, func(tx kvdb.RwTx) error { +func (c *KVStore) PruneGraphNodes() ([]route.Vertex, error) { + var prunedNodes []route.Vertex + err := kvdb.Update(c.db, func(tx kvdb.RwTx) error { nodes := tx.ReadWriteBucket(nodeBucket) if nodes == nil { return ErrGraphNodesNotFound @@ -1478,19 +1462,18 @@ func (c *KVStore) PruneGraphNodes() error { return ErrGraphNoEdgesFound } - prunedNodes, err := c.pruneGraphNodes(nodes, edgeIndex) + var err error + prunedNodes, err = c.pruneGraphNodes(nodes, edgeIndex) if err != nil { return err } - if c.graphCache != nil { - for _, nodePubKey := range prunedNodes { - c.graphCache.RemoveNode(nodePubKey) - } - } - return nil - }, func() {}) + }, func() { + prunedNodes = nil + }) + + return prunedNodes, err } // pruneGraphNodes attempts to remove any nodes from the graph who have had a From 4d00eb2aa43669b3367e6cb7a42bfb83ed8e0f18 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Feb 2025 06:24:17 -0300 Subject: [PATCH 13/24] graph/db: move FilterKnownChanIDs zombie logic up one layer Here, we move the business logic in FilterKnownChanIDs from the CRUD layer to the ChannelGraph layer. We also add a test for the logic. --- graph/db/graph.go | 53 ++++++++++++++++++++++++++++++++ graph/db/graph_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++ graph/db/kv_store.go | 61 ++++++++++-------------------------- 3 files changed, 140 insertions(+), 44 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index 6185de410..1d53ad3c2 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -1,6 +1,7 @@ package graphdb import ( + "errors" "sync" "time" @@ -386,3 +387,55 @@ func (c *ChannelGraph) PruneGraphNodes() error { return nil } + +// FilterKnownChanIDs takes a set of channel IDs and return the subset of chan +// ID's that we don't know and are not known zombies of the passed set. In other +// words, we perform a set difference of our set of chan ID's and the ones +// passed in. This method can be used by callers to determine the set of +// channels another peer knows of that we don't. +func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, + isZombieChan func(time.Time, time.Time) bool) ([]uint64, error) { + + unknown, knownZombies, err := c.KVStore.FilterKnownChanIDs(chansInfo) + if err != nil { + return nil, err + } + + for _, info := range knownZombies { + // TODO(ziggie): Make sure that for the strict pruning case we + // compare the pubkeys and whether the right timestamp is not + // older than the `ChannelPruneExpiry`. + // + // NOTE: The timestamp data has no verification attached to it + // in the `ReplyChannelRange` msg so we are trusting this data + // at this point. However it is not critical because we are just + // removing the channel from the db when the timestamps are more + // recent. During the querying of the gossip msg verification + // happens as usual. However we should start punishing peers + // when they don't provide us honest data ? + isStillZombie := isZombieChan( + info.Node1UpdateTimestamp, info.Node2UpdateTimestamp, + ) + + if isStillZombie { + continue + } + + // If we have marked it as a zombie but the latest update + // timestamps could bring it back from the dead, then we mark it + // alive, and we let it be added to the set of IDs to query our + // peer for. + err := c.KVStore.MarkEdgeLive( + info.ShortChannelID.ToUint64(), + ) + // Since there is a chance that the edge could have been marked + // as "live" between the FilterKnownChanIDs call and the + // MarkEdgeLive call, we ignore the error if the edge is already + // marked as live. + if err != nil && !errors.Is(err, ErrZombieEdgeNotFound) { + return nil, err + } + } + + return unknown, nil +} diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index cf5452eb7..f336294a1 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -1919,6 +1919,76 @@ func TestNodeUpdatesInHorizon(t *testing.T) { } } +// TestFilterKnownChanIDsZombieRevival tests that if a ChannelUpdateInfo is +// passed to FilterKnownChanIDs that contains a channel that we have marked as +// a zombie, then we will mark it as live again if the new ChannelUpdate has +// timestamps that would make the channel be considered live again. +// +// NOTE: this tests focuses on zombie revival. The main logic of +// FilterKnownChanIDs is tested in TestFilterKnownChanIDs. +func TestFilterKnownChanIDsZombieRevival(t *testing.T) { + t.Parallel() + + graph, err := MakeTestGraph(t) + require.NoError(t, err) + + var ( + scid1 = lnwire.ShortChannelID{BlockHeight: 1} + scid2 = lnwire.ShortChannelID{BlockHeight: 2} + scid3 = lnwire.ShortChannelID{BlockHeight: 3} + ) + + isZombie := func(scid lnwire.ShortChannelID) bool { + zombie, _, _ := graph.IsZombieEdge(scid.ToUint64()) + return zombie + } + + // Mark channel 1 and 2 as zombies. + err = graph.MarkEdgeZombie(scid1.ToUint64(), [33]byte{}, [33]byte{}) + require.NoError(t, err) + err = graph.MarkEdgeZombie(scid2.ToUint64(), [33]byte{}, [33]byte{}) + require.NoError(t, err) + + require.True(t, isZombie(scid1)) + require.True(t, isZombie(scid2)) + require.False(t, isZombie(scid3)) + + // Call FilterKnownChanIDs with an isStillZombie call-back that would + // result in the current zombies still be considered as zombies. + _, err = graph.FilterKnownChanIDs([]ChannelUpdateInfo{ + {ShortChannelID: scid1}, + {ShortChannelID: scid2}, + {ShortChannelID: scid3}, + }, func(_ time.Time, _ time.Time) bool { + return true + }) + require.NoError(t, err) + + require.True(t, isZombie(scid1)) + require.True(t, isZombie(scid2)) + require.False(t, isZombie(scid3)) + + // Now call it again but this time with a isStillZombie call-back that + // would result in channel with SCID 2 no longer being considered a + // zombie. + _, err = graph.FilterKnownChanIDs([]ChannelUpdateInfo{ + {ShortChannelID: scid1}, + { + ShortChannelID: scid2, + Node1UpdateTimestamp: time.Unix(1000, 0), + }, + {ShortChannelID: scid3}, + }, func(t1 time.Time, _ time.Time) bool { + return !t1.Equal(time.Unix(1000, 0)) + }) + require.NoError(t, err) + + // Show that SCID 2 has been marked as live. + require.True(t, isZombie(scid1)) + require.False(t, isZombie(scid2)) + require.False(t, isZombie(scid3)) +} + // TestFilterKnownChanIDs tests that we're able to properly perform the set // differences of an incoming set of channel ID's, and those that we already // know of on disk. diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 486971e03..04e0d75ae 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -2155,16 +2155,20 @@ func (c *KVStore) NodeUpdatesInHorizon(startTime, // ID's that we don't know and are not known zombies of the passed set. In other // words, we perform a set difference of our set of chan ID's and the ones // passed in. This method can be used by callers to determine the set of -// channels another peer knows of that we don't. -func (c *KVStore) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, - isZombieChan func(time.Time, time.Time) bool) ([]uint64, error) { +// channels another peer knows of that we don't. The ChannelUpdateInfos for the +// known zombies is also returned. +func (c *KVStore) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo) ([]uint64, + []ChannelUpdateInfo, error) { - var newChanIDs []uint64 + var ( + newChanIDs []uint64 + knownZombies []ChannelUpdateInfo + ) c.cacheMu.Lock() defer c.cacheMu.Unlock() - err := kvdb.Update(c.db, func(tx kvdb.RwTx) error { + err := kvdb.View(c.db, func(tx kvdb.RTx) error { edges := tx.ReadBucket(edgeBucket) if edges == nil { return ErrGraphNoEdgesFound @@ -2197,44 +2201,12 @@ func (c *KVStore) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, zombieIndex, scid, ) - // TODO(ziggie): Make sure that for the strict - // pruning case we compare the pubkeys and - // whether the right timestamp is not older than - // the `ChannelPruneExpiry`. - // - // NOTE: The timestamp data has no verification - // attached to it in the `ReplyChannelRange` msg - // so we are trusting this data at this point. - // However it is not critical because we are - // just removing the channel from the db when - // the timestamps are more recent. During the - // querying of the gossip msg verification - // happens as usual. - // However we should start punishing peers when - // they don't provide us honest data ? - isStillZombie := isZombieChan( - info.Node1UpdateTimestamp, - info.Node2UpdateTimestamp, - ) + if isZombie { + knownZombies = append( + knownZombies, info, + ) - switch { - // If the edge is a known zombie and if we - // would still consider it a zombie given the - // latest update timestamps, then we skip this - // channel. - case isZombie && isStillZombie: continue - - // Otherwise, if we have marked it as a zombie - // but the latest update timestamps could bring - // it back from the dead, then we mark it alive, - // and we let it be added to the set of IDs to - // query our peer for. - case isZombie && !isStillZombie: - err := c.markEdgeLiveUnsafe(tx, scid) - if err != nil { - return err - } } } @@ -2244,6 +2216,7 @@ func (c *KVStore) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, return nil }, func() { newChanIDs = nil + knownZombies = nil }) switch { // If we don't know of any edges yet, then we'll return the entire set @@ -2254,13 +2227,13 @@ func (c *KVStore) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, ogChanIDs[i] = info.ShortChannelID.ToUint64() } - return ogChanIDs, nil + return ogChanIDs, nil, nil case err != nil: - return nil, err + return nil, nil, err } - return newChanIDs, nil + return newChanIDs, knownZombies, nil } // ChannelUpdateInfo couples the SCID of a channel with the timestamps of the From 9d0b9f9acea6e25c834befc0f644c1cac40384a7 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 25 Feb 2025 11:31:03 +0200 Subject: [PATCH 14/24] graph/db: move cache write for MarkEdgeZombie From the KVStore to the ChannelGraph. --- graph/db/graph.go | 21 +++++++++++++++++++++ graph/db/kv_store.go | 4 ---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index 1d53ad3c2..5d9ea819e 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -439,3 +439,24 @@ func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, return unknown, nil } + +// MarkEdgeZombie attempts to mark a channel identified by its channel ID as a +// zombie. This method is used on an ad-hoc basis, when channels need to be +// marked as zombies outside the normal pruning cycle. +func (c *ChannelGraph) MarkEdgeZombie(chanID uint64, + pubKey1, pubKey2 [33]byte) error { + + c.cacheMu.Lock() + defer c.cacheMu.Unlock() + + err := c.KVStore.MarkEdgeZombie(chanID, pubKey1, pubKey2) + if err != nil { + return err + } + + if c.graphCache != nil { + c.graphCache.RemoveChannel(pubKey1, pubKey2, chanID) + } + + return nil +} diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 04e0d75ae..3dd336578 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -3588,10 +3588,6 @@ func (c *KVStore) MarkEdgeZombie(chanID uint64, "bucket: %w", err) } - if c.graphCache != nil { - c.graphCache.RemoveChannel(pubKey1, pubKey2, chanID) - } - return markEdgeZombie(zombieIndex, chanID, pubKey1, pubKey2) }) if err != nil { From ba1d21d5c730b5f928a57f49884ba3ec7be76957 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 17 Feb 2025 13:15:49 -0300 Subject: [PATCH 15/24] graph/db: move cache write for UpdateEdgePolicy To the ChannelGraph. --- docs/release-notes/release-notes-0.19.0.md | 1 + graph/db/graph.go | 32 ++++++++++++++++++++ graph/db/kv_store.go | 34 ++++++++++------------ 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index 54941074c..156b71b5d 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -326,6 +326,7 @@ The underlying functionality between those two options remain the same. - [2](https://github.com/lightningnetwork/lnd/pull/9545) - [3](https://github.com/lightningnetwork/lnd/pull/9550) - [4](https://github.com/lightningnetwork/lnd/pull/9551) + - [5](https://github.com/lightningnetwork/lnd/pull/9552) * [Golang was updated to `v1.22.11`](https://github.com/lightningnetwork/lnd/pull/9462). diff --git a/graph/db/graph.go b/graph/db/graph.go index 5d9ea819e..ac80c259a 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -460,3 +460,35 @@ func (c *ChannelGraph) MarkEdgeZombie(chanID uint64, return nil } + +// UpdateEdgePolicy updates the edge routing policy for a single directed edge +// within the database for the referenced channel. The `flags` attribute within +// the ChannelEdgePolicy determines which of the directed edges are being +// updated. If the flag is 1, then the first node's information is being +// updated, otherwise it's the second node's information. The node ordering is +// determined by the lexicographical ordering of the identity public keys of the +// nodes on either side of the channel. +func (c *ChannelGraph) UpdateEdgePolicy(edge *models.ChannelEdgePolicy, + op ...batch.SchedulerOption) error { + + c.cacheMu.Lock() + defer c.cacheMu.Unlock() + + from, to, err := c.KVStore.UpdateEdgePolicy(edge, op...) + if err != nil { + return err + } + + if c.graphCache == nil { + return nil + } + + var isUpdate1 bool + if edge.ChannelFlags&lnwire.ChanUpdateDirection == 0 { + isUpdate1 = true + } + + c.graphCache.UpdatePolicy(edge, from, to, isUpdate1) + + return nil +} diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 3dd336578..d9786c2c6 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -2728,11 +2728,12 @@ func makeZombiePubkeys(info *models.ChannelEdgeInfo, // determined by the lexicographical ordering of the identity public keys of the // nodes on either side of the channel. func (c *KVStore) UpdateEdgePolicy(edge *models.ChannelEdgePolicy, - op ...batch.SchedulerOption) error { + op ...batch.SchedulerOption) (route.Vertex, route.Vertex, error) { var ( isUpdate1 bool edgeNotFound bool + from, to route.Vertex ) r := &batch.Request{ @@ -2742,10 +2743,7 @@ func (c *KVStore) UpdateEdgePolicy(edge *models.ChannelEdgePolicy, }, Update: func(tx kvdb.RwTx) error { var err error - isUpdate1, err = updateEdgePolicy( - tx, edge, c.graphCache, - ) - + from, to, isUpdate1, err = updateEdgePolicy(tx, edge) if err != nil { log.Errorf("UpdateEdgePolicy faild: %v", err) } @@ -2776,7 +2774,9 @@ func (c *KVStore) UpdateEdgePolicy(edge *models.ChannelEdgePolicy, f(r) } - return c.chanScheduler.Execute(r) + err := c.chanScheduler.Execute(r) + + return from, to, err } func (c *KVStore) updateEdgeCache(e *models.ChannelEdgePolicy, @@ -2813,16 +2813,18 @@ func (c *KVStore) updateEdgeCache(e *models.ChannelEdgePolicy, // buckets using an existing database transaction. The returned boolean will be // true if the updated policy belongs to node1, and false if the policy belonged // to node2. -func updateEdgePolicy(tx kvdb.RwTx, edge *models.ChannelEdgePolicy, - graphCache *GraphCache) (bool, error) { +func updateEdgePolicy(tx kvdb.RwTx, edge *models.ChannelEdgePolicy) ( + route.Vertex, route.Vertex, bool, error) { + + var noVertex route.Vertex edges := tx.ReadWriteBucket(edgeBucket) if edges == nil { - return false, ErrEdgeNotFound + return noVertex, noVertex, false, ErrEdgeNotFound } edgeIndex := edges.NestedReadWriteBucket(edgeIndexBucket) if edgeIndex == nil { - return false, ErrEdgeNotFound + return noVertex, noVertex, false, ErrEdgeNotFound } // Create the channelID key be converting the channel ID @@ -2834,7 +2836,7 @@ func updateEdgePolicy(tx kvdb.RwTx, edge *models.ChannelEdgePolicy, // nodes which connect this channel edge. nodeInfo := edgeIndex.Get(chanID[:]) if nodeInfo == nil { - return false, ErrEdgeNotFound + return noVertex, noVertex, false, ErrEdgeNotFound } // Depending on the flags value passed above, either the first @@ -2855,7 +2857,7 @@ func updateEdgePolicy(tx kvdb.RwTx, edge *models.ChannelEdgePolicy, // identified, we update the on-disk edge representation. err := putChanEdgePolicy(edges, edge, fromNode, toNode) if err != nil { - return false, err + return noVertex, noVertex, false, err } var ( @@ -2865,13 +2867,7 @@ func updateEdgePolicy(tx kvdb.RwTx, edge *models.ChannelEdgePolicy, copy(fromNodePubKey[:], fromNode) copy(toNodePubKey[:], toNode) - if graphCache != nil { - graphCache.UpdatePolicy( - edge, fromNodePubKey, toNodePubKey, isUpdate1, - ) - } - - return isUpdate1, nil + return fromNodePubKey, toNodePubKey, isUpdate1, nil } // isPublic determines whether the node is seen as public within the graph from From bb3839e422a43dde3f5736fb63593b5877dc296e Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 18 Feb 2025 15:06:33 -0300 Subject: [PATCH 16/24] graph/db: completely remove cache from KVStore --- graph/db/graph.go | 2 -- graph/db/graph_test.go | 1 - graph/db/kv_store.go | 12 +----------- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index ac80c259a..6e2132a5f 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -93,8 +93,6 @@ func NewChannelGraph(cfg *Config, options ...ChanGraphOption) (*ChannelGraph, log.Debugf("Finished populating in-memory channel graph (took %v, %s)", time.Since(startTime), graphCache.Stats()) - store.setGraphCache(graphCache) - return &ChannelGraph{ KVStore: store, graphCache: graphCache, diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index f336294a1..bc130c599 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -3994,7 +3994,6 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) { // Unset the channel graph cache to simulate the user running with the // option turned off. graph.graphCache = nil - graph.KVStore.graphCache = nil node1, err := createTestVertex(graph.db) require.Nil(t, err) diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index d9786c2c6..0ea61a76c 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -184,13 +184,12 @@ const ( type KVStore struct { db kvdb.Backend - // cacheMu guards all caches (rejectCache, chanCache, graphCache). If + // cacheMu guards all caches (rejectCache and chanCache). If // this mutex will be acquired at the same time as the DB mutex then // the cacheMu MUST be acquired first to prevent deadlock. cacheMu sync.RWMutex rejectCache *rejectCache chanCache *channelCache - graphCache *GraphCache chanScheduler batch.Scheduler nodeScheduler batch.Scheduler @@ -227,15 +226,6 @@ func NewKVStore(db kvdb.Backend, options ...KVStoreOptionModifier) (*KVStore, return g, nil } -// setGraphCache sets the KVStore's graphCache. -// -// NOTE: this is temporary and will only be called from the ChannelGraph's -// constructor before the KVStore methods are available to be called. This will -// be removed once the graph cache is fully owned by the ChannelGraph. -func (c *KVStore) setGraphCache(cache *GraphCache) { - c.graphCache = cache -} - // channelMapKey is the key structure used for storing channel edge policies. type channelMapKey struct { nodeKey route.Vertex From b497c4694e5ddfe8908d97cb788b67f86d739054 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Feb 2025 07:51:54 -0300 Subject: [PATCH 17/24] multi: add Start and Stop methods for ChannelGraph We do this in preparation for moving channel cache population logic out of the constructor and into the Start method. We also will later on (when topology subscription is moved to the ChannelGraph), have a goroutine that will need to be kicked off and stopped. --- autopilot/prefattach_test.go | 5 +++++ graph/db/graph.go | 40 +++++++++++++++++++++++++++++++++++- graph/db/graph_test.go | 8 ++++++++ graph/db/kv_store.go | 3 +++ graph/notifications_test.go | 4 ++++ peer/test_utils.go | 4 ++++ routing/pathfind_test.go | 4 ++++ server.go | 9 ++++++++ 8 files changed, 76 insertions(+), 1 deletion(-) diff --git a/autopilot/prefattach_test.go b/autopilot/prefattach_test.go index f553482bb..f20c3a480 100644 --- a/autopilot/prefattach_test.go +++ b/autopilot/prefattach_test.go @@ -49,6 +49,11 @@ func newDiskChanGraph(t *testing.T) (testGraph, error) { graphDB, err := graphdb.NewChannelGraph(&graphdb.Config{KVDB: backend}) require.NoError(t, err) + require.NoError(t, graphDB.Start()) + t.Cleanup(func() { + require.NoError(t, graphDB.Stop()) + }) + return &testDBGraph{ db: graphDB, databaseChannelGraph: databaseChannelGraph{ diff --git a/graph/db/graph.go b/graph/db/graph.go index 6e2132a5f..f926aaea3 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -3,6 +3,7 @@ package graphdb import ( "errors" "sync" + "sync/atomic" "time" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -32,13 +33,20 @@ type Config struct { // KVStore. Upcoming commits will move the graph cache out of the KVStore and // into this layer so that the KVStore is only responsible for CRUD operations. type ChannelGraph struct { + started atomic.Bool + stopped atomic.Bool + // cacheMu guards any writes to the graphCache. It should be held // across the DB write call and the graphCache update to make the // two updates as atomic as possible. - cacheMu sync.Mutex + cacheMu sync.Mutex + graphCache *GraphCache *KVStore + + quit chan struct{} + wg sync.WaitGroup } // NewChannelGraph creates a new ChannelGraph instance with the given backend. @@ -58,6 +66,7 @@ func NewChannelGraph(cfg *Config, options ...ChanGraphOption) (*ChannelGraph, if !opts.useGraphCache { return &ChannelGraph{ KVStore: store, + quit: make(chan struct{}), }, nil } @@ -96,9 +105,38 @@ func NewChannelGraph(cfg *Config, options ...ChanGraphOption) (*ChannelGraph, return &ChannelGraph{ KVStore: store, graphCache: graphCache, + quit: make(chan struct{}), }, nil } +// Start kicks off any goroutines required for the ChannelGraph to function. +// If the graph cache is enabled, then it will be populated with the contents of +// the database. +func (c *ChannelGraph) Start() error { + if !c.started.CompareAndSwap(false, true) { + return nil + } + log.Debugf("ChannelGraph starting") + defer log.Debug("ChannelGraph started") + + return nil +} + +// Stop signals any active goroutines for a graceful closure. +func (c *ChannelGraph) Stop() error { + if !c.stopped.CompareAndSwap(false, true) { + return nil + } + + log.Debugf("ChannelGraph shutting down...") + defer log.Debug("ChannelGraph shutdown complete") + + close(c.quit) + c.wg.Wait() + + return nil +} + // ForEachNodeDirectedChannel iterates through all channels of a given node, // executing the passed callback on the directed edge representing the channel // and its incoming policy. If the callback returns an error, then the iteration diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index bc130c599..dc41f02a2 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -4077,6 +4077,10 @@ func TestGraphLoading(t *testing.T) { graph, err := NewChannelGraph(&Config{KVDB: backend}) require.NoError(t, err) + require.NoError(t, graph.Start()) + t.Cleanup(func() { + require.NoError(t, graph.Stop()) + }) // Populate the graph with test data. const numNodes = 100 @@ -4087,6 +4091,10 @@ func TestGraphLoading(t *testing.T) { // populated. graphReloaded, err := NewChannelGraph(&Config{KVDB: backend}) require.NoError(t, err) + require.NoError(t, graphReloaded.Start()) + t.Cleanup(func() { + require.NoError(t, graphReloaded.Stop()) + }) // Assert that the cache content is identical. require.Equal( diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 0ea61a76c..aa120a39a 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -26,6 +26,7 @@ import ( "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" ) var ( @@ -4722,10 +4723,12 @@ func MakeTestGraph(t testing.TB, modifiers ...KVStoreOptionModifier) ( return nil, err } + require.NoError(t, graph.Start()) t.Cleanup(func() { _ = backend.Close() backendCleanup() + require.NoError(t, graph.Stop()) }) return graph, nil diff --git a/graph/notifications_test.go b/graph/notifications_test.go index aab1d4137..807b3fea7 100644 --- a/graph/notifications_test.go +++ b/graph/notifications_test.go @@ -1100,6 +1100,10 @@ func makeTestGraph(t *testing.T, useCache bool) (*graphdb.ChannelGraph, if err != nil { return nil, nil, err } + require.NoError(t, graph.Start()) + t.Cleanup(func() { + require.NoError(t, graph.Stop()) + }) return graph, backend, nil } diff --git a/peer/test_utils.go b/peer/test_utils.go index 7d3c5ca37..87a80712f 100644 --- a/peer/test_utils.go +++ b/peer/test_utils.go @@ -619,6 +619,10 @@ func createTestPeer(t *testing.T) *peerTestCtx { KVDB: graphBackend, }) require.NoError(t, err) + require.NoError(t, dbAliceGraph.Start()) + t.Cleanup(func() { + require.NoError(t, dbAliceGraph.Stop()) + }) dbAliceChannel := channeldb.OpenForTesting(t, dbPath) diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 029383e0b..8a0280686 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -173,6 +173,10 @@ func makeTestGraph(t *testing.T, useCache bool) (*graphdb.ChannelGraph, if err != nil { return nil, nil, err } + require.NoError(t, graph.Start()) + t.Cleanup(func() { + require.NoError(t, graph.Stop()) + }) return graph, backend, nil } diff --git a/server.go b/server.go index ad521cd67..731d69797 100644 --- a/server.go +++ b/server.go @@ -2377,6 +2377,12 @@ func (s *server) Start() error { return } + cleanup = cleanup.add(s.graphDB.Stop) + if err := s.graphDB.Start(); err != nil { + startErr = err + return + } + cleanup = cleanup.add(s.graphBuilder.Stop) if err := s.graphBuilder.Start(); err != nil { startErr = err @@ -2683,6 +2689,9 @@ func (s *server) Stop() error { if err := s.graphBuilder.Stop(); err != nil { srvrLog.Warnf("failed to stop graphBuilder %v", err) } + if err := s.graphDB.Stop(); err != nil { + srvrLog.Warnf("failed to stop graphDB %v", err) + } if err := s.chainArb.Stop(); err != nil { srvrLog.Warnf("failed to stop chainArb: %v", err) } From 45450886d74c81215d30265d2e49f338ac7e9a57 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Feb 2025 07:58:22 -0300 Subject: [PATCH 18/24] graph/db: populate the graph cache in Start instead of during construction In this commit, we move the graph cache population logic out of the ChannelGraph constructor and into its Start method instead. --- docs/release-notes/release-notes-0.19.0.md | 1 + graph/db/graph.go | 88 ++++++++++++---------- 2 files changed, 51 insertions(+), 38 deletions(-) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index 156b71b5d..bd4b5d2c9 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -327,6 +327,7 @@ The underlying functionality between those two options remain the same. - [3](https://github.com/lightningnetwork/lnd/pull/9550) - [4](https://github.com/lightningnetwork/lnd/pull/9551) - [5](https://github.com/lightningnetwork/lnd/pull/9552) + - [6](https://github.com/lightningnetwork/lnd/pull/9555) * [Golang was updated to `v1.22.11`](https://github.com/lightningnetwork/lnd/pull/9462). diff --git a/graph/db/graph.go b/graph/db/graph.go index f926aaea3..b9891ffc2 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -2,6 +2,7 @@ package graphdb import ( "errors" + "fmt" "sync" "sync/atomic" "time" @@ -63,50 +64,18 @@ func NewChannelGraph(cfg *Config, options ...ChanGraphOption) (*ChannelGraph, return nil, err } - if !opts.useGraphCache { - return &ChannelGraph{ - KVStore: store, - quit: make(chan struct{}), - }, nil + g := &ChannelGraph{ + KVStore: store, + quit: make(chan struct{}), } // The graph cache can be turned off (e.g. for mobile users) for a // speed/memory usage tradeoff. - graphCache := NewGraphCache(opts.preAllocCacheNumNodes) - startTime := time.Now() - log.Debugf("Populating in-memory channel graph, this might take a " + - "while...") - - err = store.ForEachNodeCacheable(func(node route.Vertex, - features *lnwire.FeatureVector) error { - - graphCache.AddNodeFeatures(node, features) - - return nil - }) - if err != nil { - return nil, err + if opts.useGraphCache { + g.graphCache = NewGraphCache(opts.preAllocCacheNumNodes) } - err = store.ForEachChannel(func(info *models.ChannelEdgeInfo, - policy1, policy2 *models.ChannelEdgePolicy) error { - - graphCache.AddChannel(info, policy1, policy2) - - return nil - }) - if err != nil { - return nil, err - } - - log.Debugf("Finished populating in-memory channel graph (took %v, %s)", - time.Since(startTime), graphCache.Stats()) - - return &ChannelGraph{ - KVStore: store, - graphCache: graphCache, - quit: make(chan struct{}), - }, nil + return g, nil } // Start kicks off any goroutines required for the ChannelGraph to function. @@ -119,6 +88,13 @@ func (c *ChannelGraph) Start() error { log.Debugf("ChannelGraph starting") defer log.Debug("ChannelGraph started") + if c.graphCache != nil { + if err := c.populateCache(); err != nil { + return fmt.Errorf("could not populate the graph "+ + "cache: %w", err) + } + } + return nil } @@ -137,6 +113,42 @@ func (c *ChannelGraph) Stop() error { return nil } +// populateCache loads the entire channel graph into the in-memory graph cache. +// +// NOTE: This should only be called if the graphCache has been constructed. +func (c *ChannelGraph) populateCache() error { + startTime := time.Now() + log.Info("Populating in-memory channel graph, this might take a " + + "while...") + + err := c.KVStore.ForEachNodeCacheable(func(node route.Vertex, + features *lnwire.FeatureVector) error { + + c.graphCache.AddNodeFeatures(node, features) + + return nil + }) + if err != nil { + return err + } + + err = c.KVStore.ForEachChannel(func(info *models.ChannelEdgeInfo, + policy1, policy2 *models.ChannelEdgePolicy) error { + + c.graphCache.AddChannel(info, policy1, policy2) + + return nil + }) + if err != nil { + return err + } + + log.Infof("Finished populating in-memory channel graph (took %v, %s)", + time.Since(startTime), c.graphCache.Stats()) + + return nil +} + // ForEachNodeDirectedChannel iterates through all channels of a given node, // executing the passed callback on the directed edge representing the channel // and its incoming policy. If the callback returns an error, then the iteration From 8c11ca97e1f6f135fdcbdc16ec90d2efd3f824ee Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 3 Mar 2025 08:46:34 +0200 Subject: [PATCH 19/24] itest: rename closure for clarity --- itest/lnd_channel_policy_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/itest/lnd_channel_policy_test.go b/itest/lnd_channel_policy_test.go index 18c2328e9..7a333f073 100644 --- a/itest/lnd_channel_policy_test.go +++ b/itest/lnd_channel_policy_test.go @@ -341,10 +341,10 @@ func testUpdateChannelPolicy(ht *lntest.HarnessTest) { // but not the second, as she only allows two updates per day and a day // has yet to elapse from the previous update. - // assertAliceAndBob is a helper closure which updates Alice's policy - // and asserts that both Alice and Bob have heard and updated the + // updateAndAssertAliceAndBob is a helper closure which updates Alice's + // policy and asserts that both Alice and Bob have heard and updated the // policy in their graph. - assertAliceAndBob := func(req *lnrpc.PolicyUpdateRequest, + updateAndAssertAliceAndBob := func(req *lnrpc.PolicyUpdateRequest, expectedPolicy *lnrpc.RoutingPolicy) { alice.RPC.UpdateChannelPolicy(req) @@ -384,7 +384,7 @@ func testUpdateChannelPolicy(ht *lntest.HarnessTest) { expectedPolicy.FeeBaseMsat = baseFee1 req.BaseFeeMsat = baseFee1 req.InboundFee = nil - assertAliceAndBob(req, expectedPolicy) + updateAndAssertAliceAndBob(req, expectedPolicy) // Check that Carol has both heard the policy and updated it in her // graph. @@ -407,7 +407,7 @@ func testUpdateChannelPolicy(ht *lntest.HarnessTest) { baseFee2 := baseFee1 * 2 expectedPolicy.FeeBaseMsat = baseFee2 req.BaseFeeMsat = baseFee2 - assertAliceAndBob(req, expectedPolicy) + updateAndAssertAliceAndBob(req, expectedPolicy) // Since Carol didn't receive the last update, she still has Alice's // old policy. We validate this by checking the base fee is the older From caf69cc4f94caa401ca22d195a2de58e9ca98d50 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 3 Mar 2025 08:49:01 +0200 Subject: [PATCH 20/24] docs: update release notes --- docs/release-notes/release-notes-0.19.0.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index bd4b5d2c9..a7172fda2 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -321,13 +321,8 @@ The underlying functionality between those two options remain the same. - [Abstract autopilot access](https://github.com/lightningnetwork/lnd/pull/9480) - [Abstract invoicerpc server access](https://github.com/lightningnetwork/lnd/pull/9516) - [Refactor to hide DB transactions](https://github.com/lightningnetwork/lnd/pull/9513) - - Move the graph cache out of the graph CRUD layer: - - [1](https://github.com/lightningnetwork/lnd/pull/9533) - - [2](https://github.com/lightningnetwork/lnd/pull/9545) - - [3](https://github.com/lightningnetwork/lnd/pull/9550) - - [4](https://github.com/lightningnetwork/lnd/pull/9551) - - [5](https://github.com/lightningnetwork/lnd/pull/9552) - - [6](https://github.com/lightningnetwork/lnd/pull/9555) + - Move the [graph cache out of the graph + CRUD](https://github.com/lightningnetwork/lnd/pull/9544) layer. * [Golang was updated to `v1.22.11`](https://github.com/lightningnetwork/lnd/pull/9462). From 4131b3fc7eb924d2ce3fe7274659ad340e4b21d2 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 6 Mar 2025 12:23:46 +0200 Subject: [PATCH 21/24] graph/db: adjust TestPartialNode The test as it stands today does not make sense as it adds a Partial/Shell node to the graph via AddLightningNode which will never happen since this is only ever triggered by the gossiper which only calls the method with a full node announcement. Shell/Partial nodes are only ever added via AddChannelEdge which will insert a partial node if we are adding a channel edge which has node pub keys that we dont have a node entry for. So we adjust the test to use this more accurate flow. --- graph/db/graph_test.go | 68 +++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index dc41f02a2..87214fc77 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -155,62 +155,70 @@ 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. +// only the pubkey is known to the database. func TestPartialNode(t *testing.T) { t.Parallel() graph, err := MakeTestGraph(t) require.NoError(t, err, "unable to make test database") - // We want to be able to insert nodes into the graph that only has the - // PubKey set. - node := &models.LightningNode{ - HaveNodeAnnouncement: false, - PubKeyBytes: testPub, - } + // To insert a partial node, we need to add a channel edge that has + // node keys for nodes we are not yet aware + var node1, node2 models.LightningNode + copy(node1.PubKeyBytes[:], pubKey1Bytes) + copy(node2.PubKeyBytes[:], pubKey2Bytes) - if err := graph.AddLightningNode(node); err != nil { - t.Fatalf("unable to add node: %v", err) - } - assertNodeInCache(t, graph, node, nil) + // Create an edge attached to these nodes and add it to the graph. + edgeInfo, _ := createEdge(140, 0, 0, 0, &node1, &node2) + require.NoError(t, graph.AddChannelEdge(&edgeInfo)) - // Next, fetch the node from the database to ensure everything was + // Both of the nodes should now be in both the graph (as partial/shell) + // nodes _and_ the cache should also have an awareness of both nodes. + assertNodeInCache(t, graph, &node1, nil) + assertNodeInCache(t, graph, &node2, nil) + + // Next, fetch the node2 from the database to ensure everything was // serialized properly. - dbNode, err := graph.FetchLightningNode(testPub) - require.NoError(t, err, "unable to locate node") + dbNode1, err := graph.FetchLightningNode(pubKey1) + require.NoError(t, err) + dbNode2, err := graph.FetchLightningNode(pubKey2) + require.NoError(t, err) - _, exists, err := graph.HasLightningNode(dbNode.PubKeyBytes) - if err != nil { - t.Fatalf("unable to query for node: %v", err) - } else if !exists { - t.Fatalf("node should be found but wasn't") - } + _, exists, err := graph.HasLightningNode(dbNode1.PubKeyBytes) + require.NoError(t, err) + require.True(t, exists) // The two nodes should match exactly! (with default values for // LastUpdate and db set to satisfy compareNodes()) - node = &models.LightningNode{ + expectedNode1 := &models.LightningNode{ HaveNodeAnnouncement: false, LastUpdate: time.Unix(0, 0), - PubKeyBytes: testPub, + PubKeyBytes: pubKey1, } + require.NoError(t, compareNodes(dbNode1, expectedNode1)) - if err := compareNodes(node, dbNode); err != nil { - t.Fatalf("nodes don't match: %v", err) + _, exists, err = graph.HasLightningNode(dbNode2.PubKeyBytes) + require.NoError(t, err) + require.True(t, exists) + + // The two nodes should match exactly! (with default values for + // LastUpdate and db set to satisfy compareNodes()) + expectedNode2 := &models.LightningNode{ + HaveNodeAnnouncement: false, + LastUpdate: time.Unix(0, 0), + PubKeyBytes: pubKey2, } + require.NoError(t, compareNodes(dbNode2, expectedNode2)) // 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) - } + require.NoError(t, graph.DeleteLightningNode(pubKey1)) assertNodeNotInCache(t, graph, testPub) // Finally, attempt to fetch the node again. This should fail as the // node should have been deleted from the database. _, err = graph.FetchLightningNode(testPub) - if err != ErrGraphNodeNotFound { - t.Fatalf("fetch after delete should fail!") - } + require.ErrorIs(t, err, ErrGraphNodeNotFound) } func TestAliasLookup(t *testing.T) { From 2221aaa8894d546e288a3aaf61f0e42bbd833afe Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Feb 2025 08:48:30 -0300 Subject: [PATCH 22/24] graph/db: move Topology client management to ChannelGraph We plan to later on add an option for a remote graph source which will be managed from the ChannelGraph. In such a set-up, a node would rely on the remote graph source for graph updates instead of from gossip sync. In this scenario, however, our topology subscription logic should still notify clients of all updates and so it makes more sense to have the logic as part of the ChannelGraph so that we can send updates we receive from the remote graph. --- autopilot/manager.go | 4 +- graph/builder.go | 117 ++-------------------------- graph/db/graph.go | 130 +++++++++++++++++++++++++++++--- graph/db/graph_test.go | 20 +++++ graph/{ => db}/notifications.go | 59 ++++++++++----- graph/notifications_test.go | 24 +++--- pilot.go | 2 +- rpcserver.go | 9 +-- server.go | 2 +- 9 files changed, 209 insertions(+), 158 deletions(-) rename graph/{ => db}/notifications.go (91%) diff --git a/autopilot/manager.go b/autopilot/manager.go index dba4cc6cc..0463f98d9 100644 --- a/autopilot/manager.go +++ b/autopilot/manager.go @@ -6,7 +6,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" - "github.com/lightningnetwork/lnd/graph" + graphdb "github.com/lightningnetwork/lnd/graph/db" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" ) @@ -36,7 +36,7 @@ type ManagerCfg struct { // SubscribeTopology is used to get a subscription for topology changes // on the network. - SubscribeTopology func() (*graph.TopologyClient, error) + SubscribeTopology func() (*graphdb.TopologyClient, error) } // Manager is struct that manages an autopilot agent, making it possible to diff --git a/graph/builder.go b/graph/builder.go index 3e1115553..f92b523b0 100644 --- a/graph/builder.go +++ b/graph/builder.go @@ -109,8 +109,7 @@ type Builder struct { started atomic.Bool stopped atomic.Bool - ntfnClientCounter atomic.Uint64 - bestHeight atomic.Uint32 + bestHeight atomic.Uint32 cfg *Config @@ -123,22 +122,6 @@ type Builder struct { // of our currently known best chain are sent over. staleBlocks <-chan *chainview.FilteredBlock - // topologyUpdates is a channel that carries new topology updates - // messages from outside the Builder to be processed by the - // networkHandler. - topologyUpdates chan any - - // topologyClients maps a client's unique notification ID to a - // topologyClient client that contains its notification dispatch - // channel. - topologyClients *lnutils.SyncMap[uint64, *topologyClient] - - // ntfnClientUpdates is a channel that's used to send new updates to - // topology notification clients to the Builder. Updates either - // add a new notification client, or cancel notifications for an - // existing client. - ntfnClientUpdates chan *topologyClientUpdate - // channelEdgeMtx is a mutex we use to make sure we process only one // ChannelEdgePolicy at a time for a given channelID, to ensure // consistency between the various database accesses. @@ -163,14 +146,11 @@ var _ ChannelGraphSource = (*Builder)(nil) // NewBuilder constructs a new Builder. func NewBuilder(cfg *Config) (*Builder, error) { return &Builder{ - cfg: cfg, - topologyUpdates: make(chan any), - topologyClients: &lnutils.SyncMap[uint64, *topologyClient]{}, - ntfnClientUpdates: make(chan *topologyClientUpdate), - channelEdgeMtx: multimutex.NewMutex[uint64](), - statTicker: ticker.New(defaultStatInterval), - stats: new(builderStats), - quit: make(chan struct{}), + cfg: cfg, + channelEdgeMtx: multimutex.NewMutex[uint64](), + statTicker: ticker.New(defaultStatInterval), + stats: new(builderStats), + quit: make(chan struct{}), }, nil } @@ -656,28 +636,6 @@ func (b *Builder) pruneZombieChans() error { return nil } -// handleTopologyUpdate is responsible for sending any topology changes -// notifications to registered clients. -// -// NOTE: must be run inside goroutine. -func (b *Builder) handleTopologyUpdate(update any) { - defer b.wg.Done() - - topChange := &TopologyChange{} - err := addToTopologyChange(b.cfg.Graph, topChange, update) - if err != nil { - log.Errorf("unable to update topology change notification: %v", - err) - return - } - - if topChange.isEmpty() { - return - } - - b.notifyTopologyChange(topChange) -} - // networkHandler is the primary goroutine for the Builder. The roles of // this goroutine include answering queries related to the state of the // network, pruning the graph on new block notification, applying network @@ -701,16 +659,6 @@ func (b *Builder) networkHandler() { } select { - // A new fully validated topology update has just arrived. - // We'll notify any registered clients. - case update := <-b.topologyUpdates: - b.wg.Add(1) - go b.handleTopologyUpdate(update) - - // TODO(roasbeef): remove all unconnected vertexes - // after N blocks pass with no corresponding - // announcements. - case chainUpdate, ok := <-b.staleBlocks: // If the channel has been closed, then this indicates // the daemon is shutting down, so we exit ourselves. @@ -783,31 +731,6 @@ func (b *Builder) networkHandler() { " processed.", chainUpdate.Height) } - // A new notification client update has arrived. We're either - // gaining a new client, or cancelling notifications for an - // existing client. - case ntfnUpdate := <-b.ntfnClientUpdates: - clientID := ntfnUpdate.clientID - - if ntfnUpdate.cancel { - client, ok := b.topologyClients.LoadAndDelete( - clientID, - ) - if ok { - close(client.exit) - client.wg.Wait() - - close(client.ntfnChan) - } - - continue - } - - b.topologyClients.Store(clientID, &topologyClient{ - ntfnChan: ntfnUpdate.ntfnChan, - exit: make(chan struct{}), - }) - // The graph prune ticker has ticked, so we'll examine the // state of the known graph to filter out any zombie channels // for pruning. @@ -934,16 +857,6 @@ func (b *Builder) updateGraphWithClosedChannels( log.Infof("Block %v (height=%v) closed %v channels", chainUpdate.Hash, blockHeight, len(chansClosed)) - if len(chansClosed) == 0 { - return err - } - - // Notify all currently registered clients of the newly closed channels. - closeSummaries := createCloseSummaries(blockHeight, chansClosed...) - b.notifyTopologyChange(&TopologyChange{ - ClosedChannels: closeSummaries, - }) - return nil } @@ -1067,12 +980,6 @@ func (b *Builder) AddNode(node *models.LightningNode, return err } - select { - case b.topologyUpdates <- node: - case <-b.quit: - return ErrGraphBuilderShuttingDown - } - return nil } @@ -1117,12 +1024,6 @@ func (b *Builder) AddEdge(edge *models.ChannelEdgeInfo, return err } - select { - case b.topologyUpdates <- edge: - case <-b.quit: - return ErrGraphBuilderShuttingDown - } - return nil } @@ -1224,12 +1125,6 @@ func (b *Builder) UpdateEdge(update *models.ChannelEdgePolicy, return err } - select { - case b.topologyUpdates <- update: - case <-b.quit: - return ErrGraphBuilderShuttingDown - } - return nil } diff --git a/graph/db/graph.go b/graph/db/graph.go index b9891ffc2..fa8a6c29c 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -12,10 +12,15 @@ import ( "github.com/lightningnetwork/lnd/batch" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" ) +// ErrChanGraphShuttingDown indicates that the ChannelGraph has shutdown or is +// busy shutting down. +var ErrChanGraphShuttingDown = fmt.Errorf("ChannelGraph shutting down") + // Config is a struct that holds all the necessary dependencies for a // ChannelGraph. type Config struct { @@ -46,6 +51,26 @@ type ChannelGraph struct { *KVStore + // ntfnClientCounter is an atomic counter that's used to assign unique + // notification client IDs to new clients. + ntfnClientCounter atomic.Uint64 + + // topologyUpdate is a channel that carries new topology updates + // messages from outside the ChannelGraph to be processed by the + // networkHandler. + topologyUpdate chan any + + // topologyClients maps a client's unique notification ID to a + // topologyClient client that contains its notification dispatch + // channel. + topologyClients *lnutils.SyncMap[uint64, *topologyClient] + + // ntfnClientUpdates is a channel that's used to send new updates to + // topology notification clients to the ChannelGraph. Updates either + // add a new notification client, or cancel notifications for an + // existing client. + ntfnClientUpdates chan *topologyClientUpdate + quit chan struct{} wg sync.WaitGroup } @@ -65,8 +90,11 @@ func NewChannelGraph(cfg *Config, options ...ChanGraphOption) (*ChannelGraph, } g := &ChannelGraph{ - KVStore: store, - quit: make(chan struct{}), + KVStore: store, + topologyUpdate: make(chan any), + topologyClients: &lnutils.SyncMap[uint64, *topologyClient]{}, + ntfnClientUpdates: make(chan *topologyClientUpdate), + quit: make(chan struct{}), } // The graph cache can be turned off (e.g. for mobile users) for a @@ -95,6 +123,9 @@ func (c *ChannelGraph) Start() error { } } + c.wg.Add(1) + go c.handleTopologySubscriptions() + return nil } @@ -113,6 +144,60 @@ func (c *ChannelGraph) Stop() error { return nil } +// handleTopologySubscriptions ensures that topology client subscriptions, +// subscription cancellations and topology notifications are handled +// synchronously. +// +// NOTE: this MUST be run in a goroutine. +func (c *ChannelGraph) handleTopologySubscriptions() { + defer c.wg.Done() + + for { + select { + // A new fully validated topology update has just arrived. + // We'll notify any registered clients. + case update := <-c.topologyUpdate: + // TODO(elle): change topology handling to be handled + // synchronously so that we can guarantee the order of + // notification delivery. + c.wg.Add(1) + go c.handleTopologyUpdate(update) + + // TODO(roasbeef): remove all unconnected vertexes + // after N blocks pass with no corresponding + // announcements. + + // A new notification client update has arrived. We're either + // gaining a new client, or cancelling notifications for an + // existing client. + case ntfnUpdate := <-c.ntfnClientUpdates: + clientID := ntfnUpdate.clientID + + if ntfnUpdate.cancel { + client, ok := c.topologyClients.LoadAndDelete( + clientID, + ) + if ok { + close(client.exit) + client.wg.Wait() + + close(client.ntfnChan) + } + + continue + } + + c.topologyClients.Store(clientID, &topologyClient{ + ntfnChan: ntfnUpdate.ntfnChan, + exit: make(chan struct{}), + }) + + case <-c.quit: + return + } + } +} + // populateCache loads the entire channel graph into the in-memory graph cache. // // NOTE: This should only be called if the graphCache has been constructed. @@ -234,6 +319,12 @@ func (c *ChannelGraph) AddLightningNode(node *models.LightningNode, ) } + select { + case c.topologyUpdate <- node: + case <-c.quit: + return ErrChanGraphShuttingDown + } + return nil } @@ -276,6 +367,12 @@ func (c *ChannelGraph) AddChannelEdge(edge *models.ChannelEdgeInfo, c.graphCache.AddChannel(edge, nil, nil) } + select { + case c.topologyUpdate <- edge: + case <-c.quit: + return ErrChanGraphShuttingDown + } + return nil } @@ -411,6 +508,17 @@ func (c *ChannelGraph) PruneGraph(spentOutputs []*wire.OutPoint, c.graphCache.Stats()) } + if len(edges) != 0 { + // Notify all currently registered clients of the newly closed + // channels. + closeSummaries := createCloseSummaries( + blockHeight, edges..., + ) + c.notifyTopologyChange(&TopologyChange{ + ClosedChannels: closeSummaries, + }) + } + return edges, nil } @@ -527,16 +635,20 @@ func (c *ChannelGraph) UpdateEdgePolicy(edge *models.ChannelEdgePolicy, return err } - if c.graphCache == nil { - return nil + if c.graphCache != nil { + var isUpdate1 bool + if edge.ChannelFlags&lnwire.ChanUpdateDirection == 0 { + isUpdate1 = true + } + + c.graphCache.UpdatePolicy(edge, from, to, isUpdate1) } - var isUpdate1 bool - if edge.ChannelFlags&lnwire.ChanUpdateDirection == 0 { - isUpdate1 = true + select { + case c.topologyUpdate <- edge: + case <-c.quit: + return ErrChanGraphShuttingDown } - c.graphCache.UpdatePolicy(edge, from, to, isUpdate1) - return nil } diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 87214fc77..754da5eef 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -972,6 +972,23 @@ func randEdgePolicy(chanID uint64, db kvdb.Backend) *models.ChannelEdgePolicy { return newEdgePolicy(chanID, db, update) } +func copyEdgePolicy(p *models.ChannelEdgePolicy) *models.ChannelEdgePolicy { + return &models.ChannelEdgePolicy{ + SigBytes: p.SigBytes, + ChannelID: p.ChannelID, + LastUpdate: p.LastUpdate, + MessageFlags: p.MessageFlags, + ChannelFlags: p.ChannelFlags, + TimeLockDelta: p.TimeLockDelta, + MinHTLC: p.MinHTLC, + MaxHTLC: p.MaxHTLC, + FeeBaseMSat: p.FeeBaseMSat, + FeeProportionalMillionths: p.FeeProportionalMillionths, + ToNode: p.ToNode, + ExtraOpaqueData: p.ExtraOpaqueData, + } +} + func newEdgePolicy(chanID uint64, db kvdb.Backend, updateTime int64) *models.ChannelEdgePolicy { @@ -2937,6 +2954,7 @@ func TestChannelEdgePruningUpdateIndexDeletion(t *testing.T) { if err := graph.UpdateEdgePolicy(edge1); err != nil { t.Fatalf("unable to update edge: %v", err) } + edge1 = copyEdgePolicy(edge1) // Avoid read/write race conditions. edge2 := randEdgePolicy(chanID.ToUint64(), graph.db) edge2.ChannelFlags = 1 @@ -2945,6 +2963,7 @@ func TestChannelEdgePruningUpdateIndexDeletion(t *testing.T) { if err := graph.UpdateEdgePolicy(edge2); err != nil { t.Fatalf("unable to update edge: %v", err) } + edge2 = copyEdgePolicy(edge2) // Avoid read/write race conditions. // checkIndexTimestamps is a helper function that checks the edge update // index only includes the given timestamps. @@ -4052,6 +4071,7 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) { 253, 217, 3, 8, 0, 0, 0, 10, 0, 0, 0, 20, } require.NoError(t, graph.UpdateEdgePolicy(edge1)) + edge1 = copyEdgePolicy(edge1) // Avoid read/write race conditions. directedChan := getSingleChannel() require.NotNil(t, directedChan) diff --git a/graph/notifications.go b/graph/db/notifications.go similarity index 91% rename from graph/notifications.go rename to graph/db/notifications.go index 76eabdb02..7d54a7431 100644 --- a/graph/notifications.go +++ b/graph/db/notifications.go @@ -1,4 +1,4 @@ -package graph +package graphdb import ( "fmt" @@ -54,16 +54,16 @@ type topologyClientUpdate struct { // topology occurs. Changes that will be sent at notifications include: new // nodes appearing, node updating their attributes, new channels, channels // closing, and updates in the routing policies of a channel's directed edges. -func (b *Builder) SubscribeTopology() (*TopologyClient, error) { +func (c *ChannelGraph) SubscribeTopology() (*TopologyClient, error) { // If the router is not yet started, return an error to avoid a // deadlock waiting for it to handle the subscription request. - if !b.started.Load() { + if !c.started.Load() { return nil, fmt.Errorf("router not started") } // We'll first atomically obtain the next ID for this client from the // incrementing client ID counter. - clientID := b.ntfnClientCounter.Add(1) + clientID := c.ntfnClientCounter.Add(1) log.Debugf("New graph topology client subscription, client %v", clientID) @@ -71,12 +71,12 @@ func (b *Builder) SubscribeTopology() (*TopologyClient, error) { ntfnChan := make(chan *TopologyChange, 10) select { - case b.ntfnClientUpdates <- &topologyClientUpdate{ + case c.ntfnClientUpdates <- &topologyClientUpdate{ cancel: false, clientID: clientID, ntfnChan: ntfnChan, }: - case <-b.quit: + case <-c.quit: return nil, errors.New("ChannelRouter shutting down") } @@ -84,11 +84,11 @@ func (b *Builder) SubscribeTopology() (*TopologyClient, error) { TopologyChanges: ntfnChan, Cancel: func() { select { - case b.ntfnClientUpdates <- &topologyClientUpdate{ + case c.ntfnClientUpdates <- &topologyClientUpdate{ cancel: true, clientID: clientID, }: - case <-b.quit: + case <-c.quit: return } }, @@ -114,7 +114,7 @@ type topologyClient struct { // notifyTopologyChange notifies all registered clients of a new change in // graph topology in a non-blocking. -func (b *Builder) notifyTopologyChange(topologyDiff *TopologyChange) { +func (c *ChannelGraph) notifyTopologyChange(topologyDiff *TopologyChange) { // notifyClient is a helper closure that will send topology updates to // the given client. notifyClient := func(clientID uint64, client *topologyClient) bool { @@ -127,23 +127,22 @@ func (b *Builder) notifyTopologyChange(topologyDiff *TopologyChange) { len(topologyDiff.ChannelEdgeUpdates), len(topologyDiff.ClosedChannels)) - go func(c *topologyClient) { - defer c.wg.Done() + go func(t *topologyClient) { + defer t.wg.Done() select { // In this case we'll try to send the notification // directly to the upstream client consumer. - case c.ntfnChan <- topologyDiff: + case t.ntfnChan <- topologyDiff: // If the client cancels the notifications, then we'll // exit early. - case <-c.exit: + case <-t.exit: // Similarly, if the ChannelRouter itself exists early, // then we'll also exit ourselves. - case <-b.quit: - + case <-c.quit: } }(client) @@ -154,7 +153,29 @@ func (b *Builder) notifyTopologyChange(topologyDiff *TopologyChange) { // Range over the set of active clients, and attempt to send the // topology updates. - b.topologyClients.Range(notifyClient) + c.topologyClients.Range(notifyClient) +} + +// handleTopologyUpdate is responsible for sending any topology changes +// notifications to registered clients. +// +// NOTE: must be run inside goroutine. +func (c *ChannelGraph) handleTopologyUpdate(update any) { + defer c.wg.Done() + + topChange := &TopologyChange{} + err := c.addToTopologyChange(topChange, update) + if err != nil { + log.Errorf("unable to update topology change notification: %v", + err) + return + } + + if topChange.isEmpty() { + return + } + + c.notifyTopologyChange(topChange) } // TopologyChange represents a new set of modifications to the channel graph. @@ -310,8 +331,8 @@ type ChannelEdgeUpdate struct { // constitutes. This function will also fetch any required auxiliary // information required to create the topology change update from the graph // database. -func addToTopologyChange(graph DB, update *TopologyChange, - msg interface{}) error { +func (c *ChannelGraph) addToTopologyChange(update *TopologyChange, + msg any) error { switch m := msg.(type) { @@ -345,7 +366,7 @@ func addToTopologyChange(graph DB, update *TopologyChange, // We'll need to fetch the edge's information from the database // in order to get the information concerning which nodes are // being connected. - edgeInfo, _, _, err := graph.FetchChannelEdgesByID(m.ChannelID) + edgeInfo, _, _, err := c.FetchChannelEdgesByID(m.ChannelID) if err != nil { return errors.Errorf("unable fetch channel edge: %v", err) diff --git a/graph/notifications_test.go b/graph/notifications_test.go index 807b3fea7..0e2ec7afb 100644 --- a/graph/notifications_test.go +++ b/graph/notifications_test.go @@ -469,7 +469,7 @@ func TestEdgeUpdateNotification(t *testing.T) { // With the channel edge now in place, we'll subscribe for topology // notifications. - ntfnClient, err := ctx.builder.SubscribeTopology() + ntfnClient, err := ctx.graph.SubscribeTopology() require.NoError(t, err, "unable to subscribe for channel notifications") // Create random policy edges that are stemmed to the channel id @@ -489,7 +489,8 @@ func TestEdgeUpdateNotification(t *testing.T) { t.Fatalf("unable to add edge update: %v", err) } - assertEdgeCorrect := func(t *testing.T, edgeUpdate *ChannelEdgeUpdate, + assertEdgeCorrect := func(t *testing.T, + edgeUpdate *graphdb.ChannelEdgeUpdate, edgeAnn *models.ChannelEdgePolicy) { if edgeUpdate.ChanID != edgeAnn.ChannelID { @@ -659,7 +660,7 @@ func TestNodeUpdateNotification(t *testing.T) { } // Create a new client to receive notifications. - ntfnClient, err := ctx.builder.SubscribeTopology() + ntfnClient, err := ctx.graph.SubscribeTopology() require.NoError(t, err, "unable to subscribe for channel notifications") // Change network topology by adding the updated info for the two nodes @@ -672,7 +673,7 @@ func TestNodeUpdateNotification(t *testing.T) { } assertNodeNtfnCorrect := func(t *testing.T, ann *models.LightningNode, - nodeUpdate *NetworkNodeUpdate) { + nodeUpdate *graphdb.NetworkNodeUpdate) { nodeKey, _ := ann.PubKey() @@ -699,9 +700,10 @@ func TestNodeUpdateNotification(t *testing.T) { t.Fatalf("node alias doesn't match: expected %v, got %v", ann.Alias, nodeUpdate.Alias) } - if nodeUpdate.Color != EncodeHexColor(ann.Color) { - t.Fatalf("node color doesn't match: expected %v, got %v", - EncodeHexColor(ann.Color), nodeUpdate.Color) + if nodeUpdate.Color != graphdb.EncodeHexColor(ann.Color) { + t.Fatalf("node color doesn't match: expected %v, "+ + "got %v", graphdb.EncodeHexColor(ann.Color), + nodeUpdate.Color) } } @@ -793,7 +795,7 @@ func TestNotificationCancellation(t *testing.T) { ctx := createTestCtxSingleNode(t, startingBlockHeight) // Create a new client to receive notifications. - ntfnClient, err := ctx.builder.SubscribeTopology() + ntfnClient, err := ctx.graph.SubscribeTopology() require.NoError(t, err, "unable to subscribe for channel notifications") // We'll create the utxo for a new channel. @@ -919,7 +921,7 @@ func TestChannelCloseNotification(t *testing.T) { // With the channel edge now in place, we'll subscribe for topology // notifications. - ntfnClient, err := ctx.builder.SubscribeTopology() + ntfnClient, err := ctx.graph.SubscribeTopology() require.NoError(t, err, "unable to subscribe for channel notifications") // Next, we'll simulate the closure of our channel by generating a new @@ -1002,7 +1004,9 @@ func TestEncodeHexColor(t *testing.T) { } for _, tc := range colorTestCases { - encoded := EncodeHexColor(color.RGBA{tc.R, tc.G, tc.B, 0}) + encoded := graphdb.EncodeHexColor( + color.RGBA{tc.R, tc.G, tc.B, 0}, + ) if (encoded == tc.encoded) != tc.isValid { t.Fatalf("incorrect color encoding, "+ "want: %v, got: %v", tc.encoded, encoded) diff --git a/pilot.go b/pilot.go index 11333a072..8cbf23cc6 100644 --- a/pilot.go +++ b/pilot.go @@ -295,6 +295,6 @@ func initAutoPilot(svr *server, cfg *lncfg.AutoPilot, }, nil }, SubscribeTransactions: svr.cc.Wallet.SubscribeTransactions, - SubscribeTopology: svr.graphBuilder.SubscribeTopology, + SubscribeTopology: svr.graphDB.SubscribeTopology, }, nil } diff --git a/rpcserver.go b/rpcserver.go index fea7ac30e..c686d911a 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -48,7 +48,6 @@ import ( "github.com/lightningnetwork/lnd/feature" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/funding" - "github.com/lightningnetwork/lnd/graph" graphdb "github.com/lightningnetwork/lnd/graph/db" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/htlcswitch" @@ -3347,7 +3346,7 @@ func (r *rpcServer) GetInfo(_ context.Context, // TODO(roasbeef): add synced height n stuff isTestNet := chainreg.IsTestnet(&r.cfg.ActiveNetParams) - nodeColor := graph.EncodeHexColor(nodeAnn.RGBColor) + nodeColor := graphdb.EncodeHexColor(nodeAnn.RGBColor) version := build.Version() + " commit=" + build.Commit return &lnrpc.GetInfoResponse{ @@ -6960,7 +6959,7 @@ func marshalNode(node *models.LightningNode) *lnrpc.LightningNode { PubKey: hex.EncodeToString(node.PubKeyBytes[:]), Addresses: nodeAddrs, Alias: node.Alias, - Color: graph.EncodeHexColor(node.Color), + Color: graphdb.EncodeHexColor(node.Color), Features: features, CustomRecords: customRecords, } @@ -7158,7 +7157,7 @@ func (r *rpcServer) SubscribeChannelGraph(req *lnrpc.GraphTopologySubscription, // First, we start by subscribing to a new intent to receive // notifications from the channel router. - client, err := r.server.graphBuilder.SubscribeTopology() + client, err := r.server.graphDB.SubscribeTopology() if err != nil { return err } @@ -7211,7 +7210,7 @@ func (r *rpcServer) SubscribeChannelGraph(req *lnrpc.GraphTopologySubscription, // returned by the router to the form of notifications expected by the current // gRPC service. func marshallTopologyChange( - topChange *graph.TopologyChange) *lnrpc.GraphTopologyUpdate { + topChange *graphdb.TopologyChange) *lnrpc.GraphTopologyUpdate { // encodeKey is a simple helper function that converts a live public // key into a hex-encoded version of the compressed serialization for diff --git a/server.go b/server.go index 731d69797..daea1550b 100644 --- a/server.go +++ b/server.go @@ -417,7 +417,7 @@ type server struct { // updatePersistentPeerAddrs subscribes to topology changes and stores // advertised addresses for any NodeAnnouncements from our persisted peers. func (s *server) updatePersistentPeerAddrs() error { - graphSub, err := s.graphBuilder.SubscribeTopology() + graphSub, err := s.graphDB.SubscribeTopology() if err != nil { return err } From 878746c9c9dc6115a59ffcef9f9b1e1748f372ac Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 5 Mar 2025 07:58:14 +0200 Subject: [PATCH 23/24] graph/db: refactor to group all topology notification fields A clean-up commit just to separate out all topology related fields in ChannelGraph into a dedicated struct that then gets mounted to the ChannelGraph. --- graph/db/graph.go | 30 ++++-------------------------- graph/db/notifications.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index fa8a6c29c..9e35e58dd 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -12,7 +12,6 @@ import ( "github.com/lightningnetwork/lnd/batch" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/kvdb" - "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" ) @@ -50,26 +49,7 @@ type ChannelGraph struct { graphCache *GraphCache *KVStore - - // ntfnClientCounter is an atomic counter that's used to assign unique - // notification client IDs to new clients. - ntfnClientCounter atomic.Uint64 - - // topologyUpdate is a channel that carries new topology updates - // messages from outside the ChannelGraph to be processed by the - // networkHandler. - topologyUpdate chan any - - // topologyClients maps a client's unique notification ID to a - // topologyClient client that contains its notification dispatch - // channel. - topologyClients *lnutils.SyncMap[uint64, *topologyClient] - - // ntfnClientUpdates is a channel that's used to send new updates to - // topology notification clients to the ChannelGraph. Updates either - // add a new notification client, or cancel notifications for an - // existing client. - ntfnClientUpdates chan *topologyClientUpdate + *topologyManager quit chan struct{} wg sync.WaitGroup @@ -90,11 +70,9 @@ func NewChannelGraph(cfg *Config, options ...ChanGraphOption) (*ChannelGraph, } g := &ChannelGraph{ - KVStore: store, - topologyUpdate: make(chan any), - topologyClients: &lnutils.SyncMap[uint64, *topologyClient]{}, - ntfnClientUpdates: make(chan *topologyClientUpdate), - quit: make(chan struct{}), + KVStore: store, + topologyManager: newTopologyManager(), + quit: make(chan struct{}), } // The graph cache can be turned off (e.g. for mobile users) for a diff --git a/graph/db/notifications.go b/graph/db/notifications.go index 7d54a7431..2ed2be16f 100644 --- a/graph/db/notifications.go +++ b/graph/db/notifications.go @@ -5,15 +5,50 @@ import ( "image/color" "net" "sync" + "sync/atomic" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/graph/db/models" + "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwire" ) +// topologyManager holds all the fields required to manage the network topology +// subscriptions and notifications. +type topologyManager struct { + // ntfnClientCounter is an atomic counter that's used to assign unique + // notification client IDs to new clients. + ntfnClientCounter atomic.Uint64 + + // topologyUpdate is a channel that carries new topology updates + // messages from outside the ChannelGraph to be processed by the + // networkHandler. + topologyUpdate chan any + + // topologyClients maps a client's unique notification ID to a + // topologyClient client that contains its notification dispatch + // channel. + topologyClients *lnutils.SyncMap[uint64, *topologyClient] + + // ntfnClientUpdates is a channel that's used to send new updates to + // topology notification clients to the ChannelGraph. Updates either + // add a new notification client, or cancel notifications for an + // existing client. + ntfnClientUpdates chan *topologyClientUpdate +} + +// newTopologyManager creates a new instance of the topologyManager. +func newTopologyManager() *topologyManager { + return &topologyManager{ + topologyUpdate: make(chan any), + topologyClients: &lnutils.SyncMap[uint64, *topologyClient]{}, + ntfnClientUpdates: make(chan *topologyClientUpdate), + } +} + // TopologyClient represents an intent to receive notifications from the // channel router regarding changes to the topology of the channel graph. The // TopologyChanges channel will be sent upon with new updates to the channel From 947ca937c701b92c2666b72b956670c7ae8d3b3d Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 25 Mar 2025 08:04:42 +0200 Subject: [PATCH 24/24] docs: update release notes --- docs/release-notes/release-notes-0.19.0.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index a7172fda2..10b6b7089 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -321,8 +321,11 @@ The underlying functionality between those two options remain the same. - [Abstract autopilot access](https://github.com/lightningnetwork/lnd/pull/9480) - [Abstract invoicerpc server access](https://github.com/lightningnetwork/lnd/pull/9516) - [Refactor to hide DB transactions](https://github.com/lightningnetwork/lnd/pull/9513) - - Move the [graph cache out of the graph + - Move the [graph cache out of the graph CRUD](https://github.com/lightningnetwork/lnd/pull/9544) layer. + - Move [topology + subscription](https://github.com/lightningnetwork/lnd/pull/9577) and + notification handling from the graph.Builder to the ChannelGraph. * [Golang was updated to `v1.22.11`](https://github.com/lightningnetwork/lnd/pull/9462).