diff --git a/docs/release-notes/release-notes-0.20.0.md b/docs/release-notes/release-notes-0.20.0.md index 1ee2e05fa..94bcd18a6 100644 --- a/docs/release-notes/release-notes-0.20.0.md +++ b/docs/release-notes/release-notes-0.20.0.md @@ -31,6 +31,11 @@ # Improvements ## Functional Updates +* Graph Store SQL implementation and migration project: + * Introduce an [abstract graph + store](https://github.com/lightningnetwork/lnd/pull/9791) interface. + + ## RPC Updates ## lncli Updates diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index f5dce71ca..25d7257be 100644 --- a/graph/db/interfaces.go +++ b/graph/db/interfaces.go @@ -1,6 +1,13 @@ package graphdb import ( + "net" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "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/lnwire" "github.com/lightningnetwork/lnd/routing/route" @@ -37,3 +44,323 @@ type NodeTraverser interface { // FetchNodeFeatures returns the features of the given node. FetchNodeFeatures(nodePub route.Vertex) (*lnwire.FeatureVector, error) } + +// V1Store represents the main interface for the channel graph database for all +// channels and nodes gossiped via the V1 gossip protocol as defined in BOLT 7. +type V1Store interface { //nolint:interfacebloat + NodeTraverser + + // 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. + AddLightningNode(node *models.LightningNode, + op ...batch.SchedulerOption) error + + // AddrsForNode returns all known addresses for the target node public + // key that the graph DB is aware of. The returned boolean indicates if + // the given node is unknown to the graph DB or not. + AddrsForNode(nodePub *btcec.PublicKey) (bool, []net.Addr, error) + + // ForEachSourceNodeChannel iterates through all channels of the source + // node, executing the passed callback on each. The call-back is + // provided with the channel's outpoint, whether we have a policy for + // the channel and the channel peer's node information. + ForEachSourceNodeChannel(cb func(chanPoint wire.OutPoint, + havePolicy bool, otherNode *models.LightningNode) error) error + + // ForEachNodeChannel iterates through all channels of the given node, + // executing the passed callback with an edge info structure and the + // policies of each end of the channel. The first edge policy is the + // outgoing edge *to* the connecting node, while the second is the + // incoming edge *from* the connecting node. If the callback returns an + // error, then the iteration is halted with the error propagated back up + // to the caller. + // + // Unknown policies are passed into the callback as nil values. + ForEachNodeChannel(nodePub route.Vertex, + cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy) error) error + + // ForEachNodeCached is similar to forEachNode, but it returns + // DirectedChannel data to the call-back. + // + // NOTE: The callback contents MUST not be modified. + ForEachNodeCached(cb func(node route.Vertex, + chans map[uint64]*DirectedChannel) error) error + + // ForEachNode iterates through all the stored vertices/nodes in the + // 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. 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. + ForEachNode(cb func(tx NodeRTx) error) error + + // ForEachNodeCacheable iterates through all the stored vertices/nodes + // in the 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. + ForEachNodeCacheable(cb func(route.Vertex, + *lnwire.FeatureVector) error) error + + // LookupAlias attempts to return the alias as advertised by the target + // node. + 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. + DeleteLightningNode(nodePub route.Vertex) error + + // NodeUpdatesInHorizon returns all the known lightning node which have + // an 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. + NodeUpdatesInHorizon(startTime, + endTime time.Time) ([]models.LightningNode, error) + + // 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. + FetchLightningNode(nodePub route.Vertex) ( + *models.LightningNode, error) + + // HasLightningNode determines if the graph has a vertex identified by + // the target node identity public key. If the node exists in the + // database, a 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. + HasLightningNode(nodePub [33]byte) (time.Time, bool, + error) + + // 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. + IsPublicNode(pubKey [33]byte) (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. + GraphSession(cb func(graph NodeTraverser) error) error + + // ForEachChannel iterates through all the channel edges stored within + // the graph and invokes the passed callback for each edge. The callback + // takes two edges as since this is a directed graph, both the in/out + // edges are visited. If the callback returns an error, then the + // transaction is aborted and the iteration stops early. + // + // 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. + ForEachChannel(cb func(*models.ChannelEdgeInfo, + *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy) error) error + + // DisabledChannelIDs returns the channel ids of disabled channels. + // A channel is disabled when two of the associated ChanelEdgePolicies + // have their disabled bit on. + DisabledChannelIDs() ([]uint64, error) + + // 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. + AddChannelEdge(edge *models.ChannelEdgeInfo, + op ...batch.SchedulerOption) error + + // HasChannelEdge returns true if the database knows of a channel edge + // with the passed channel ID, and false otherwise. If an edge with that + // ID is found within the graph, then two time stamps representing the + // last time the edge 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. + HasChannelEdge(chanID uint64) (time.Time, time.Time, bool, bool, + error) + + // 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. + DeleteChannelEdges(strictZombiePruning, markZombie bool, + chanIDs ...uint64) ([]*models.ChannelEdgeInfo, error) + + // AddEdgeProof sets the proof of an existing edge in the graph + // database. + AddEdgeProof(chanID lnwire.ShortChannelID, + proof *models.ChannelAuthProof) error + + // 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. + ChannelID(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. + HighestChanID() (uint64, error) + + // ChanUpdatesInHorizon returns all the known channel edges which have + // at least one edge that has an update timestamp within the specified + // horizon. + ChanUpdatesInHorizon(startTime, endTime time.Time) ([]ChannelEdge, + error) + + // 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. The ChannelUpdateInfos for the known zombies is also + // returned. + FilterKnownChanIDs(chansInfo []ChannelUpdateInfo) ([]uint64, + []ChannelUpdateInfo, error) + + // FilterChannelRange returns the channel ID's of all known channels + // which were mined in a block height within the passed range. The + // channel IDs are grouped by their common block height. This method can + // be used to quickly share with a peer the set of channels we know of + // within a particular range to catch them 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. + FilterChannelRange(startHeight, endHeight uint32, withTimestamps bool) ( + []BlockChannelRange, error) + + // FetchChanInfos returns the set of channel edges that correspond to + // the passed channel ID's. If an edge is the query is unknown to the + // database, it will 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. + FetchChanInfos(chanIDs []uint64) ([]ChannelEdge, error) + + // FetchChannelEdgesByOutpoint attempts to lookup the two directed edges + // for the channel identified by the funding outpoint. If the channel + // can't be 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. + FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( + *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy, error) + + // FetchChannelEdgesByID attempts to lookup the two directed edges for + // the channel identified by the channel ID. If the channel can't be + // 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. + // + // ErrZombieEdge can 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. + FetchChannelEdgesByID(chanID uint64) ( + *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy, error) + + // ChannelView returns the verifiable edge information for each active + // channel 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. + 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. + MarkEdgeZombie(chanID uint64, + pubKey1, pubKey2 [33]byte) error + + // MarkEdgeLive clears an edge from our zombie index, deeming it as + // live. + MarkEdgeLive(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. + IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte) + + // NumZombies returns the current number of zombie channels in the + // graph. + 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. + 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. + IsClosedScid(scid lnwire.ShortChannelID) (bool, error) + + // 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. + UpdateEdgePolicy(edge *models.ChannelEdgePolicy, + op ...batch.SchedulerOption) (route.Vertex, route.Vertex, error) + + // SourceNode returns the source node 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. + SourceNode() (*models.LightningNode, error) + + // 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. + SetSourceNode(node *models.LightningNode) error + + // PruneTip returns the block height and hash of the latest block that + // has been 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. + PruneTip() (*chainhash.Hash, uint32, error) + + // PruneGraphNodes is a garbage collection method which attempts to + // prune out any nodes from the channel graph that are currently + // unconnected. This ensures 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. + PruneGraphNodes() ([]route.Vertex, error) + + // 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 along + // with any pruned nodes are returned if the function succeeds without + // error. + PruneGraph(spentOutputs []*wire.OutPoint, + blockHash *chainhash.Hash, blockHeight uint32) ( + []*models.ChannelEdgeInfo, []route.Vertex, error) + + // 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. + DisconnectBlockAtHeight(height uint32) ([]*models.ChannelEdgeInfo, + error) +} diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 34be8cfc0..6c283cb81 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -196,6 +196,10 @@ type KVStore struct { nodeScheduler batch.Scheduler } +// A compile-time assertion to ensure that the KVStore struct implements the +// V1Store interface. +var _ V1Store = (*KVStore)(nil) + // 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 ...KVStoreOptionModifier) (*KVStore,