From c0389f2cdb84c2ba301ea423a2afeec0452ad655 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 a058774965a667ecd77f5f23d3f03fed60c77f69 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 0000000000..217302ca98 --- /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 0a643144e0..dfe8564197 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 311a14a0b66e9a8ac93c8033d01485bc1f680c2e 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 118c1fde5d..1c8cefeafa 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -262,6 +262,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 dfe8564197..6f327d73b9 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 cc3ded883838bf1c9551606520fc61c6fbf01816 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 43c9e4a68f..aa464ece5f 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 217302ca98..ac52a03067 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 6f327d73b9..8ddafc1790 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 a512ec4bce..a6cf2e9096 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 c95e753909bb334302908f847b12caa11013dd88 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 784d1a0f8c..f553482bb8 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 aa464ece5f..f4d109d8ba 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 ac52a03067..b8140a9d6b 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 62b9cd4e14..e3681f4b0b 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 8ddafc1790..96babbec9f 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 4049c9f81b..6846be15eb 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 34c42e2f7c..3508ce909f 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 fd92ed9343..6aba44a4f2 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 ac107ab3658392d9e9a51475ae3066698b09cb76 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 f4d109d8ba..f0b65f7130 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 1c8cefeafa..b46e710962 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -264,6 +264,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 b8140a9d6b..c8f660bbbb 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 e3681f4b0b..8a6e3e82c6 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 96babbec9f..c54d08ca3a 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 a6cf2e9096..7bff8637ab 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 6846be15eb..aab1d4137e 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 6aba44a4f2..029383e0be 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 138e846879a5f781fb8c61b80d17cff05a219961 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 c8f660bbbb..93c563122a 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 8a6e3e82c6..cf5452eb7e 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 c54d08ca3a..37f1ec8b1d 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 54208bcdc749b1492b06fc61b566e5e8717d7d1c 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 b46e710962..7f60c81ea0 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -265,6 +265,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 93c563122a..742c094786 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 37f1ec8b1d..ed80fbc9f3 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 1788668889154869069e62a1dbc194db10b1e874 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 ed80fbc9f3..be41570220 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 28dd4e0f67a660e28bf13692225ce87909370096 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 742c094786..c3f195bc05 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 be41570220..86df5ab343 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 39964ce6e120dc5c8d2ad434d675e27e7304d873 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 86df5ab343..9d784bf0d1 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 b3f75fb4e03c0c272dd509c58718fb496a3cc779 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 7f60c81ea0..4959bd4de4 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -266,6 +266,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 c3f195bc05..6185de410b 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 9d784bf0d1..486971e03e 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 c4294b266257c892aac6a1d8019ae6eeb2107b23 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 6185de410b..1d53ad3c26 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 cf5452eb7e..f336294a18 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 486971e03e..04e0d75aee 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 44c4c19fb83214e90c90e6a1da3211ccbdd43e04 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 1d53ad3c26..5d9ea819e2 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 04e0d75aee..3dd3365789 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 7454c1549122c4be3cb497209c9a862b23e51072 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 4959bd4de4..916a8dd241 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -267,6 +267,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 5d9ea819e2..ac80c259aa 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 3dd3365789..d9786c2c6c 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 7d17e5f3c0a8daaf64f30740aeefae522ca11da5 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 ac80c259aa..6e2132a5f2 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 f336294a18..bc130c5998 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 d9786c2c6c..0ea61a76ce 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 fb4ec92fc4ae3f6311b838c887fc3d08604d3114 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 f553482bb8..f20c3a480b 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 6e2132a5f2..f926aaea3b 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 bc130c5998..dc41f02a20 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 0ea61a76ce..aa120a39a7 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 aab1d4137e..807b3fea7e 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 3508ce909f..0fe88e2674 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 029383e0be..8a0280686b 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 ecb3eceae3..dba560bb2d 100644 --- a/server.go +++ b/server.go @@ -2287,6 +2287,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 @@ -2588,6 +2594,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 c9e57c2539b77eb76017c05f761ede3aa4e8d89b 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 916a8dd241..cb4b3fdf38 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -268,6 +268,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 f926aaea3b..b9891ffc20 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 b97e322aec5cc62ba69461457adb0e51f5fd0aeb 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 18c2328e97..7a333f0730 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 50e07029e39a8d763560e0863a74d32d46b7482c 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 cb4b3fdf38..c4efc2d99b 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -262,13 +262,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 ce4cd5dfc61565c1db01d3eaab357d43da3e5ee3 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 dc41f02a20..87214fc778 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 fa4cfc82d8aa2a5e3531505fd3f57c50aed5f8b2 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 dba4cc6cc5..0463f98d99 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 3e11155535..f92b523b00 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 b9891ffc20..fa8a6c29c5 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 + } - var isUpdate1 bool - if edge.ChannelFlags&lnwire.ChanUpdateDirection == 0 { - isUpdate1 = true + c.graphCache.UpdatePolicy(edge, from, to, isUpdate1) } - c.graphCache.UpdatePolicy(edge, from, to, isUpdate1) + select { + case c.topologyUpdate <- edge: + case <-c.quit: + return ErrChanGraphShuttingDown + } return nil } diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 87214fc778..754da5eef3 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 76eabdb02f..7d54a74319 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 807b3fea7e..0e2ec7afba 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 11333a0722..8cbf23cc65 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 7236a5dad5..692ba74d2b 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" @@ -3294,7 +3293,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{ @@ -6886,7 +6885,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, } @@ -7084,7 +7083,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 } @@ -7137,7 +7136,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 dba560bb2d..deae1f5915 100644 --- a/server.go +++ b/server.go @@ -368,7 +368,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 2614110684aad4013a093ec35bd3a5afc95343f0 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 fa8a6c29c5..9e35e58dd5 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 7d54a74319..2ed2be16f3 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 220eac2f0f39cdfbf006ba0fdd101cceb496b064 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 5 Mar 2025 08:01:47 +0200 Subject: [PATCH 24/24] docs: update release notes --- docs/release-notes/release-notes-0.19.0.md | 2 -- docs/release-notes/release-notes-0.20.0.md | 9 +++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index c4efc2d99b..118c1fde5d 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -262,8 +262,6 @@ 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](https://github.com/lightningnetwork/lnd/pull/9544) layer. * [Golang was updated to `v1.22.11`](https://github.com/lightningnetwork/lnd/pull/9462). diff --git a/docs/release-notes/release-notes-0.20.0.md b/docs/release-notes/release-notes-0.20.0.md index 1ee2e05fab..1e528bc8da 100644 --- a/docs/release-notes/release-notes-0.20.0.md +++ b/docs/release-notes/release-notes-0.20.0.md @@ -69,6 +69,15 @@ ## Code Health +* Graph abstraction and refactoring work: + - 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. + ## Tooling and Documentation # Contributors (Alphabetical Order) + +* Elle Mouton