diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index 9d24b36dc1e..23ffb686aad 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -70,7 +70,8 @@ need for maintenance as the sqlc code evolves. * Prepare the graph DB for handling gossip V2 nodes and channels [1](https://github.com/lightningnetwork/lnd/pull/10339) - [2](https://github.com/lightningnetwork/lnd/pull/10379). + [2](https://github.com/lightningnetwork/lnd/pull/10379) + [3](). ## Code Health diff --git a/graph/builder.go b/graph/builder.go index 3e157139651..1f3309c0483 100644 --- a/graph/builder.go +++ b/graph/builder.go @@ -625,7 +625,7 @@ func (b *Builder) pruneZombieChans() error { toPrune = append(toPrune, chanID) log.Tracef("Pruning zombie channel with ChannelID(%v)", chanID) } - err := b.cfg.Graph.DeleteChannelEdges( + err := b.v1Graph.DeleteChannelEdges( b.cfg.StrictZombiePruning, true, toPrune..., ) if err != nil { @@ -1330,7 +1330,7 @@ func (b *Builder) IsStaleNode(ctx context.Context, node route.Vertex, // // NOTE: This method is part of the ChannelGraphSource interface. func (b *Builder) IsPublicNode(node route.Vertex) (bool, error) { - return b.cfg.Graph.IsPublicNode(node) + return b.v1Graph.IsPublicNode(node) } // IsKnownEdge returns true if the graph source already knows of the passed diff --git a/graph/db/graph.go b/graph/db/graph.go index 3fbb00e9363..41396aceb38 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -371,7 +371,8 @@ func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning, markZombie bool, chanIDs ...uint64) error { infos, err := c.db.DeleteChannelEdges( - strictZombiePruning, markZombie, chanIDs..., + lnwire.GossipVersion1, strictZombiePruning, markZombie, + chanIDs..., ) if err != nil { return err @@ -636,7 +637,7 @@ func (c *ChannelGraph) HasV1Node(ctx context.Context, // IsPublicNode determines whether the node is seen as public in the graph. func (c *ChannelGraph) IsPublicNode(pubKey [33]byte) (bool, error) { - return c.db.IsPublicNode(pubKey) + return c.db.IsPublicNode(lnwire.GossipVersion1, pubKey) } // ForEachChannel iterates through all channel edges stored within the graph. @@ -710,7 +711,9 @@ func (c *ChannelGraph) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { - return c.db.FetchChannelEdgesByOutpoint(op) + return c.db.FetchChannelEdgesByOutpoint( + lnwire.GossipVersion1, op, + ) } // FetchChannelEdgesByID attempts to lookup directed edges by channel ID. @@ -718,7 +721,9 @@ func (c *ChannelGraph) FetchChannelEdgesByID(chanID uint64) ( *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { - return c.db.FetchChannelEdgesByID(chanID) + return c.db.FetchChannelEdgesByID( + lnwire.GossipVersion1, chanID, + ) } // ChannelView returns the verifiable edge information for each active channel. @@ -730,7 +735,7 @@ func (c *ChannelGraph) ChannelView() ([]EdgePoint, error) { func (c *ChannelGraph) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte, error) { - return c.db.IsZombieEdge(chanID) + return c.db.IsZombieEdge(lnwire.GossipVersion1, chanID) } // NumZombies returns the current number of zombie channels in the graph. @@ -784,6 +789,30 @@ func (c *VersionedGraph) FetchNode(ctx context.Context, return c.db.FetchNode(ctx, c.v, nodePub) } +// FetchChannelEdgesByID attempts to lookup directed edges by channel ID. +func (c *VersionedGraph) FetchChannelEdgesByID(chanID uint64) ( + *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy, error) { + + return c.db.FetchChannelEdgesByID(c.v, chanID) +} + +// FetchChannelEdgesByOutpoint attempts to lookup directed edges by funding +// outpoint. +func (c *VersionedGraph) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( + *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy, error) { + + return c.db.FetchChannelEdgesByOutpoint(c.v, op) +} + +// IsZombieEdge returns whether the edge is considered zombie for this version. +func (c *VersionedGraph) IsZombieEdge(chanID uint64) (bool, [33]byte, + [33]byte, error) { + + return c.db.IsZombieEdge(c.v, chanID) +} + // AddrsForNode returns all known addresses for the target node public key. func (c *VersionedGraph) AddrsForNode(ctx context.Context, nodePub *btcec.PublicKey) (bool, []net.Addr, error) { @@ -830,6 +859,41 @@ func (c *VersionedGraph) SourceNode(ctx context.Context) (*models.Node, return c.db.SourceNode(ctx, c.v) } +// 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 *VersionedGraph) DeleteChannelEdges(strictZombiePruning, + markZombie bool, chanIDs ...uint64) error { + + infos, err := c.db.DeleteChannelEdges( + c.v, 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 +} + +// IsPublicNode determines whether the node is seen as public in the graph. +func (c *VersionedGraph) IsPublicNode(pubKey [33]byte) (bool, error) { + return c.db.IsPublicNode(c.v, pubKey) +} + // MakeTestGraph creates a new instance of the ChannelGraph for testing // purposes. The backing Store implementation depends on the version of // NewTestDB included in the current build. diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 72b200d554c..b912d1c8333 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -138,6 +138,18 @@ var versionedTests = []versionedTest{ name: "alias lookup", test: testAliasLookup, }, + { + name: "edge insertion deletion", + test: testEdgeInsertionDeletion, + }, + { + name: "partial node", + test: testPartialNode, + }, + { + name: "node is public", + test: testNodeIsPublic, + }, } // TestVersionedDBs runs various tests against both v1 and v2 versioned @@ -145,6 +157,7 @@ var versionedTests = []versionedTest{ func TestVersionedDBs(t *testing.T) { t.Parallel() + // Run all v1 tests. for _, vt := range versionedTests { vt := vt @@ -363,22 +376,22 @@ func testNodeInsertionAndDeletion(t *testing.T, v lnwire.GossipVersion) { require.NoError(t, err) } -// TestPartialNode checks that we can add and retrieve a Node where -// only the pubkey is known to the database. -func TestPartialNode(t *testing.T) { +// testPartialNode tests that partial/shell nodes are correctly created when +// a channel edge is added referencing nodes we are not yet aware of. +func testPartialNode(t *testing.T, v lnwire.GossipVersion) { t.Parallel() ctx := t.Context() - graph := NewVersionedGraph(MakeTestGraph(t), lnwire.GossipVersion1) + graph := NewVersionedGraph(MakeTestGraph(t), v) // To insert a partial node, we need to add a channel edge that has - // node keys for nodes we are not yet aware + // node keys for nodes we are not yet aware of. var node1, node2 models.Node copy(node1.PubKeyBytes[:], pubKey1Bytes) copy(node2.PubKeyBytes[:], pubKey2Bytes) // Create an edge attached to these nodes and add it to the graph. - edgeInfo, _ := createEdge(140, 0, 0, 0, &node1, &node2) + edgeInfo, _ := createEdge(v, 140, 0, 0, 0, &node1, &node2) require.NoError(t, graph.AddChannelEdge(ctx, edgeInfo)) // Both of the nodes should now be in both the graph (as partial/shell) @@ -386,7 +399,7 @@ func TestPartialNode(t *testing.T) { assertNodeInCache(t, graph.ChannelGraph, &node1, nil) assertNodeInCache(t, graph.ChannelGraph, &node2, nil) - // Next, fetch the node2 from the database to ensure everything was + // Next, fetch the nodes from the database to ensure everything was // serialized properly. dbNode1, err := graph.FetchNode(ctx, pubKey1) require.NoError(t, err) @@ -399,7 +412,7 @@ func TestPartialNode(t *testing.T) { // The two nodes should match exactly! (with default values for // LastUpdate and db set to satisfy compareNodes()) - expectedNode1 := models.NewV1ShellNode(pubKey1) + expectedNode1 := models.NewShellNode(v, pubKey1) compareNodes(t, expectedNode1, dbNode1) exists, err = graph.HasNode(ctx, dbNode2.PubKeyBytes) @@ -408,7 +421,7 @@ func TestPartialNode(t *testing.T) { // The two nodes should match exactly! (with default values for // LastUpdate and db set to satisfy compareNodes()) - expectedNode2 := models.NewV1ShellNode(pubKey2) + expectedNode2 := models.NewShellNode(v, pubKey2) compareNodes(t, expectedNode2, dbNode2) // Next, delete the node from the graph, this should purge all data @@ -479,79 +492,41 @@ func testSourceNode(t *testing.T, v lnwire.GossipVersion) { compareNodes(t, testNode, sourceNode) } -// TestEdgeInsertionDeletion tests the basic CRUD operations for channel edges. -func TestEdgeInsertionDeletion(t *testing.T) { +// testEdgeInsertionDeletion tests the basic CRUD operations for channel edges. +func testEdgeInsertionDeletion(t *testing.T, v lnwire.GossipVersion) { t.Parallel() ctx := t.Context() - graph := MakeTestGraph(t) + graph := NewVersionedGraph(MakeTestGraph(t), v) // We'd like to test the insertion/deletion of edges, so we create two // vertexes to connect. - node1 := createTestVertex(t, lnwire.GossipVersion1) - node2 := createTestVertex(t, lnwire.GossipVersion1) - - // In addition to the fake vertexes we create some fake channel - // identifiers. - chanID := uint64(prand.Int63()) - outpoint := wire.OutPoint{ - Hash: rev, - Index: 9, - } - - // Add the new edge to the database, this should proceed without any - // errors. - node1Pub, err := node1.PubKey() - require.NoError(t, err, "unable to generate node key") - node2Pub, err := node2.PubKey() - require.NoError(t, err, "unable to generate node key") - - node1Vertex, err := route.NewVertexFromBytes( - node1Pub.SerializeCompressed(), - ) - require.NoError(t, err) - node2Vertex, err := route.NewVertexFromBytes( - node2Pub.SerializeCompressed(), - ) - require.NoError(t, err) + node1 := createTestVertex(t, v) + node2 := createTestVertex(t, v) - btcKey1, err := route.NewVertexFromBytes( - node1Pub.SerializeCompressed(), - ) - require.NoError(t, err) - btcKey2, err := route.NewVertexFromBytes( - node2Pub.SerializeCompressed(), + // Create a fake channel and add it to the graph. + const ( + blockHeight = 1234 + txIndex = 1 + txPosition = 0 + outPointIndex = 9 ) - require.NoError(t, err) - proof := models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), + edgeInfo, shortChanID := createEdge( + v, blockHeight, txIndex, txPosition, outPointIndex, node1, node2, ) - - edgeInfo, err := models.NewV1Channel( - chanID, - *chaincfg.MainNetParams.GenesisHash, - node1Vertex, - node2Vertex, - &models.ChannelV1Fields{ - BitcoinKey1Bytes: btcKey1, - BitcoinKey2Bytes: btcKey2, - }, - models.WithChanProof(proof), - models.WithChannelPoint(outpoint), - models.WithCapacity(9000), - ) - require.NoError(t, err) + chanID := shortChanID.ToUint64() + outpoint := wire.OutPoint{ + Hash: rev, + Index: outPointIndex, + } require.NoError(t, graph.AddChannelEdge(ctx, edgeInfo)) - assertEdgeWithNoPoliciesInCache(t, graph, edgeInfo) + assertEdgeWithNoPoliciesInCache(t, graph.ChannelGraph, edgeInfo) // Show that trying to insert the same channel again will return the // expected error. - err = graph.AddChannelEdge(ctx, edgeInfo) + err := graph.AddChannelEdge(ctx, edgeInfo) require.ErrorIs(t, err, ErrEdgeAlreadyExist) // Ensure that both policies are returned as unknown (nil). @@ -563,7 +538,7 @@ func TestEdgeInsertionDeletion(t *testing.T) { // Next, attempt to delete the edge from the database, again this // should proceed without any issues. require.NoError(t, graph.DeleteChannelEdges(false, true, chanID)) - assertNoEdge(t, graph, chanID) + assertNoEdge(t, graph.ChannelGraph, chanID) // Ensure that any query attempts to lookup the delete channel edge are // properly deleted. @@ -587,9 +562,9 @@ func TestEdgeInsertionDeletion(t *testing.T) { require.ErrorIs(t, err, ErrEdgeNotFound) } -func createEdge(height, txIndex uint32, txPosition uint16, outPointIndex uint32, - node1, node2 *models.Node) (*models.ChannelEdgeInfo, - lnwire.ShortChannelID) { +func createEdge(version lnwire.GossipVersion, height, txIndex uint32, + txPosition uint16, outPointIndex uint32, node1, node2 *models.Node) ( + *models.ChannelEdgeInfo, lnwire.ShortChannelID) { shortChanID := lnwire.ShortChannelID{ BlockHeight: height, @@ -610,34 +585,74 @@ func createEdge(height, txIndex uint32, txPosition uint16, outPointIndex uint32, node2Vertex, _ := route.NewVertexFromBytes( node2Pub.SerializeCompressed(), ) - btcKey1, _ := route.NewVertexFromBytes( - node1Pub.SerializeCompressed(), - ) - btcKey2, _ := route.NewVertexFromBytes( - node2Pub.SerializeCompressed(), - ) - proof := models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ) + var edgeInfo *models.ChannelEdgeInfo + switch version { + case lnwire.GossipVersion1: + btcKey1, _ := route.NewVertexFromBytes( + node1Pub.SerializeCompressed(), + ) + btcKey2, _ := route.NewVertexFromBytes( + node2Pub.SerializeCompressed(), + ) - edgeInfo, _ := models.NewV1Channel( - shortChanID.ToUint64(), - *chaincfg.MainNetParams.GenesisHash, - node1Vertex, - node2Vertex, - &models.ChannelV1Fields{ - BitcoinKey1Bytes: btcKey1, - BitcoinKey2Bytes: btcKey2, - ExtraOpaqueData: make([]byte, 0), - }, - models.WithChanProof(proof), - models.WithChannelPoint(outpoint), - models.WithCapacity(9000), - ) + proof := models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) + + edgeInfo, _ = models.NewV1Channel( + shortChanID.ToUint64(), + *chaincfg.MainNetParams.GenesisHash, + node1Vertex, + node2Vertex, + &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + ExtraOpaqueData: make([]byte, 0), + }, + models.WithChanProof(proof), + models.WithChannelPoint(outpoint), + models.WithCapacity(9000), + ) + + case lnwire.GossipVersion2: + btcKey1, _ := route.NewVertexFromBytes( + node1Pub.SerializeCompressed(), + ) + btcKey2, _ := route.NewVertexFromBytes( + node2Pub.SerializeCompressed(), + ) + + // Create a test merkle root hash. + var merkleRoot chainhash.Hash + copy(merkleRoot[:], bytes.Repeat([]byte{0xaa}, 32)) + + // Create a test funding script. + fundingScript := []byte{0x00, 0x20} + fundingScript = append(fundingScript, bytes.Repeat([]byte{0xbb}, 32)...) + + proof := models.NewV2ChannelAuthProof(testSig.Serialize()) + + edgeInfo, _ = models.NewV2Channel( + shortChanID.ToUint64(), + *chaincfg.MainNetParams.GenesisHash, + node1Vertex, + node2Vertex, + &models.ChannelV2Fields{ + BitcoinKey1Bytes: fn.Some(btcKey1), + BitcoinKey2Bytes: fn.Some(btcKey2), + MerkleRootHash: fn.Some(merkleRoot), + FundingScript: fn.Some(fundingScript), + ExtraSignedFields: make(map[uint64][]byte), + }, + models.WithChanProof(proof), + models.WithChannelPoint(outpoint), + models.WithCapacity(9000), + ) + } return edgeInfo, shortChanID } @@ -681,19 +696,20 @@ func TestDisconnectBlockAtHeight(t *testing.T) { // Create an edge which has its block height at 156. height := uint32(156) - edgeInfo, _ := createEdge(height, 0, 0, 0, node1, node2) + edgeInfo, _ := createEdge(lnwire.GossipVersion1, height, 0, 0, 0, node1, node2) // Create an edge with block height 157. We give it // maximum values for tx index and position, to make // sure our database range scan get edges from the // entire range. edgeInfo2, _ := createEdge( - height+1, math.MaxUint32&0x00ffffff, math.MaxUint16, 1, - node1, node2, + lnwire.GossipVersion1, height+1, + math.MaxUint32&0x00ffffff, math.MaxUint16, 1, node1, + node2, ) // Create a third edge, this with a block height of 155. - edgeInfo3, _ := createEdge(height-1, 0, 0, 2, node1, node2) + edgeInfo3, _ := createEdge(lnwire.GossipVersion1, height-1, 0, 0, 2, node1, node2) // Now add all these new edges to the database. if err := graph.AddChannelEdge(ctx, edgeInfo); err != nil { @@ -2150,8 +2166,8 @@ func TestHighestChanID(t *testing.T) { // The first channel with be at height 10, while the other will be at // height 100. - edge1, _ := createEdge(10, 0, 0, 0, node1, node2) - edge2, chanID2 := createEdge(100, 0, 0, 0, node1, node2) + edge1, _ := createEdge(lnwire.GossipVersion1, 10, 0, 0, 0, node1, node2) + edge2, chanID2 := createEdge(lnwire.GossipVersion1, 100, 0, 0, 0, node1, node2) if err := graph.AddChannelEdge(ctx, edge1); err != nil { t.Fatalf("unable to create channel edge: %v", err) @@ -2172,7 +2188,7 @@ func TestHighestChanID(t *testing.T) { // If we add another edge, then the current best chan ID should be // updated as well. - edge3, chanID3 := createEdge(1000, 0, 0, 0, node1, node2) + edge3, chanID3 := createEdge(lnwire.GossipVersion1, 1000, 0, 0, 0, node1, node2) if err := graph.AddChannelEdge(ctx, edge3); err != nil { t.Fatalf("unable to create channel edge: %v", err) } @@ -2226,7 +2242,8 @@ func TestChanUpdatesInHorizon(t *testing.T) { edges := make([]ChannelEdge, 0, numChans) for i := 0; i < numChans; i++ { channel, chanID := createEdge( - uint32(i*10), 0, 0, 0, node1, node2, + lnwire.GossipVersion1, uint32(i*10), 0, 0, 0, + node1, node2, ) if err := graph.AddChannelEdge(ctx, channel); err != nil { @@ -2693,7 +2710,8 @@ func TestChanUpdatesInHorizonBoundaryConditions(t *testing.T) { ) channel, chanID := createEdge( - uint32(i*10), 0, 0, 0, node1, node2, + lnwire.GossipVersion1, uint32(i*10), 0, + 0, 0, node1, node2, ) require.NoError( t, graph.AddChannelEdge(ctx, channel), @@ -2861,7 +2879,8 @@ func TestFilterKnownChanIDs(t *testing.T) { chanIDs := make([]ChannelUpdateInfo, 0, numChans) for i := 0; i < numChans; i++ { channel, chanID := createEdge( - uint32(i*10), 0, 0, 0, node1, node2, + lnwire.GossipVersion1, uint32(i*10), 0, 0, 0, + node1, node2, ) if err := graph.AddChannelEdge(ctx, channel); err != nil { @@ -2877,7 +2896,8 @@ func TestFilterKnownChanIDs(t *testing.T) { zombieIDs := make([]ChannelUpdateInfo, 0, numZombies) for i := 0; i < numZombies; i++ { channel, chanID := createEdge( - uint32(i*10+1), 0, 0, 0, node1, node2, + lnwire.GossipVersion1, uint32(i*10+1), 0, 0, 0, + node1, node2, ) if err := graph.AddChannelEdge(ctx, channel); err != nil { t.Fatalf("unable to create channel edge: %v", err) @@ -3035,8 +3055,9 @@ func TestStressTestChannelGraphAPI(t *testing.T) { defer mu.Unlock() channel, chanID := createEdge( - newBlockHeight(), rand.Uint32(), uint16(rand.Int()), - rand.Uint32(), node1, node2, + lnwire.GossipVersion1, newBlockHeight(), + rand.Uint32(), uint16(rand.Int()), rand.Uint32(), + node1, node2, ) newChan := &chanInfo{ @@ -3350,12 +3371,14 @@ func TestFilterChannelRange(t *testing.T) { for i := 0; i < numChans/2; i++ { chanHeight := endHeight channel1, chanID1 := createEdge( - chanHeight, uint32(i+1), 0, 0, node1, node2, + lnwire.GossipVersion1, chanHeight, uint32(i+1), 0, + 0, node1, node2, ) require.NoError(t, graph.AddChannelEdge(ctx, channel1)) channel2, chanID2 := createEdge( - chanHeight, uint32(i+2), 0, 0, node1, node2, + lnwire.GossipVersion1, chanHeight, uint32(i+2), 0, + 0, node1, node2, ) require.NoError(t, graph.AddChannelEdge(ctx, channel2)) @@ -3530,7 +3553,8 @@ func TestFetchChanInfos(t *testing.T) { edgeQuery := make([]uint64, 0, numChans) for i := 0; i < numChans; i++ { channel, chanID := createEdge( - uint32(i*10), 0, 0, 0, node1, node2, + lnwire.GossipVersion1, uint32(i*10), 0, 0, 0, + node1, node2, ) if err := graph.AddChannelEdge(ctx, channel); err != nil { @@ -3572,7 +3596,7 @@ func TestFetchChanInfos(t *testing.T) { // Add an another edge to the query that has been marked as a zombie // edge. The query should also skip this channel. zombieChan, zombieChanID := createEdge( - 666, 0, 0, 0, node1, node2, + lnwire.GossipVersion1, 666, 0, 0, 0, node1, node2, ) if err := graph.AddChannelEdge(ctx, zombieChan); err != nil { t.Fatalf("unable to create channel edge: %v", err) @@ -3624,7 +3648,7 @@ func TestIncompleteChannelPolicies(t *testing.T) { } channel, chanID := createEdge( - uint32(0), 0, 0, 0, node1, node2, + lnwire.GossipVersion1, uint32(0), 0, 0, 0, node1, node2, ) if err := graph.AddChannelEdge(ctx, channel); err != nil { @@ -3730,7 +3754,7 @@ func TestChannelEdgePruningUpdateIndexDeletion(t *testing.T) { // With the two nodes created, we'll now create a random channel, as // well as two edges in the database with distinct update times. - edgeInfo, chanID := createEdge(100, 0, 0, 0, node1, node2) + edgeInfo, chanID := createEdge(lnwire.GossipVersion1, 100, 0, 0, 0, node1, node2) if err := graph.AddChannelEdge(ctx, edgeInfo); err != nil { t.Fatalf("unable to add edge: %v", err) } @@ -3879,7 +3903,7 @@ func TestPruneGraphNodes(t *testing.T) { // We'll now add a new edge to the graph, but only actually advertise // the edge of *one* of the nodes. - edgeInfo, chanID := createEdge(100, 0, 0, 0, node1, node2) + edgeInfo, chanID := createEdge(lnwire.GossipVersion1, 100, 0, 0, 0, node1, node2) if err := graph.AddChannelEdge(ctx, edgeInfo); err != nil { t.Fatalf("unable to add edge: %v", err) } @@ -3928,7 +3952,7 @@ func TestAddChannelEdgeShellNodes(t *testing.T) { // We'll now create an edge between the two nodes, as a result, node2 // should be inserted into the database as a shell node. - edgeInfo, _ := createEdge(100, 0, 0, 0, node1, node2) + edgeInfo, _ := createEdge(lnwire.GossipVersion1, 100, 0, 0, 0, node1, node2) require.NoError(t, graph.AddChannelEdge(ctx, edgeInfo)) // Ensure that node1 was inserted as a full node, while node2 only has @@ -4024,9 +4048,9 @@ func nextBlockHeight() uint32 { return updateBlock } -// TestNodeIsPublic ensures that we properly detect nodes that are seen as +// testNodeIsPublic ensures that we properly detect nodes that are seen as // public within the network graph. -func TestNodeIsPublic(t *testing.T) { +func testNodeIsPublic(t *testing.T, v lnwire.GossipVersion) { t.Parallel() ctx := t.Context() @@ -4038,32 +4062,29 @@ func TestNodeIsPublic(t *testing.T) { // We'll need to create a separate database and channel graph for each // participant to replicate real-world scenarios (private edges being in // some graphs but not others, etc.). - aliceGraph := MakeTestGraph(t) - aliceNode := createTestVertex(t, lnwire.GossipVersion1) - if err := aliceGraph.SetSourceNode(ctx, aliceNode); err != nil { - t.Fatalf("unable to set source node: %v", err) - } + aliceGraph := NewVersionedGraph(MakeTestGraph(t), v) + aliceNode := createTestVertex(t, v) + err := aliceGraph.SetSourceNode(ctx, aliceNode) + require.NoError(t, err, "unable to set source node") - bobGraph := MakeTestGraph(t) - bobNode := createTestVertex(t, lnwire.GossipVersion1) - if err := bobGraph.SetSourceNode(ctx, bobNode); err != nil { - t.Fatalf("unable to set source node: %v", err) - } + bobGraph := NewVersionedGraph(MakeTestGraph(t), v) + bobNode := createTestVertex(t, v) + err = bobGraph.SetSourceNode(ctx, bobNode) + require.NoError(t, err, "unable to set source node") - carolGraph := MakeTestGraph(t) - carolNode := createTestVertex(t, lnwire.GossipVersion1) - if err := carolGraph.SetSourceNode(ctx, carolNode); err != nil { - t.Fatalf("unable to set source node: %v", err) - } + carolGraph := NewVersionedGraph(MakeTestGraph(t), v) + carolNode := createTestVertex(t, v) + err = carolGraph.SetSourceNode(ctx, carolNode) + require.NoError(t, err, "unable to set source node") - aliceBobEdge, _ := createEdge(10, 0, 0, 0, aliceNode, bobNode) - bobCarolEdge, _ := createEdge(10, 1, 0, 1, bobNode, carolNode) + aliceBobEdge, _ := createEdge(v, 10, 0, 0, 0, aliceNode, bobNode) + bobCarolEdge, _ := createEdge(v, 10, 1, 0, 1, bobNode, carolNode) // After creating all of our nodes and edges, we'll add them to each // participant's graph. nodes := []*models.Node{aliceNode, bobNode, carolNode} edges := []*models.ChannelEdgeInfo{aliceBobEdge, bobCarolEdge} - graphs := []*ChannelGraph{aliceGraph, bobGraph, carolGraph} + graphs := []*VersionedGraph{aliceGraph, bobGraph, carolGraph} for _, graph := range graphs { for _, node := range nodes { node.LastUpdate = nextUpdateTime() @@ -4079,7 +4100,7 @@ func TestNodeIsPublic(t *testing.T) { // checkNodes is a helper closure that will be used to assert that the // given nodes are seen as public/private within the given graphs. checkNodes := func(nodes []*models.Node, - graphs []*ChannelGraph, public bool) { + graphs []*VersionedGraph, public bool) { t.Helper() @@ -4088,19 +4109,10 @@ func TestNodeIsPublic(t *testing.T) { isPublic, err := graph.IsPublicNode( node.PubKeyBytes, ) - if err != nil { - t.Fatalf("unable to determine if "+ - "pivot is public: %v", err) - } + require.NoError(t, err, + "unable to determine if pivot is public") - switch { - case isPublic && !public: - t.Fatalf("expected %x to be private", - node.PubKeyBytes) - case !isPublic && public: - t.Fatalf("expected %x to be public", - node.PubKeyBytes) - } + require.Equal(t, public, isPublic) } } } @@ -4116,13 +4128,11 @@ func TestNodeIsPublic(t *testing.T) { err := graph.DeleteChannelEdges( false, true, aliceBobEdge.ChannelID, ) - if err != nil { - t.Fatalf("unable to remove edge: %v", err) - } + require.NoError(t, err, "unable to remove edge") } checkNodes( []*models.Node{aliceNode}, - []*ChannelGraph{bobGraph, carolGraph}, + []*VersionedGraph{bobGraph, carolGraph}, false, ) @@ -4135,25 +4145,22 @@ func TestNodeIsPublic(t *testing.T) { err := graph.DeleteChannelEdges( false, true, bobCarolEdge.ChannelID, ) - if err != nil { - t.Fatalf("unable to remove edge: %v", err) - } + require.NoError(t, err, "unable to remove edge") if graph == aliceGraph { continue } bobCarolEdge.AuthProof = nil - if err := graph.AddChannelEdge(ctx, bobCarolEdge); err != nil { - t.Fatalf("unable to add edge: %v", err) - } + err = graph.AddChannelEdge(ctx, bobCarolEdge) + require.NoError(t, err, "unable to add edge") } // With the modifications above, Bob should now be seen as a private // node from both Alice's and Carol's perspective. checkNodes( []*models.Node{bobNode}, - []*ChannelGraph{aliceGraph, carolGraph}, + []*VersionedGraph{aliceGraph, carolGraph}, false, ) } @@ -4611,19 +4618,20 @@ func TestBatchedAddChannelEdge(t *testing.T) { // Create an edge which has its block height at 156. height := uint32(156) - edgeInfo, _ := createEdge(height, 0, 0, 0, node1, node2) + edgeInfo, _ := createEdge(lnwire.GossipVersion1, height, 0, 0, 0, node1, node2) // Create an edge with block height 157. We give it // maximum values for tx index and position, to make // sure our database range scan get edges from the // entire range. edgeInfo2, _ := createEdge( - height+1, math.MaxUint32&0x00ffffff, math.MaxUint16, 1, - node1, node2, + lnwire.GossipVersion1, height+1, + math.MaxUint32&0x00ffffff, math.MaxUint16, 1, node1, + node2, ) // Create a third edge, this with a block height of 155. - edgeInfo3, _ := createEdge(height-1, 0, 0, 2, node1, node2) + edgeInfo3, _ := createEdge(lnwire.GossipVersion1, height-1, 0, 0, 2, node1, node2) edges := []models.ChannelEdgeInfo{*edgeInfo, *edgeInfo2, *edgeInfo3} errChan := make(chan error, len(edges)) diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index b6a8c10eadf..24a48e4c734 100644 --- a/graph/db/interfaces.go +++ b/graph/db/interfaces.go @@ -146,7 +146,7 @@ type Store interface { //nolint:interfacebloat // 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) + IsPublicNode(v lnwire.GossipVersion, 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 @@ -215,8 +215,9 @@ type Store interface { //nolint:interfacebloat // 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) + DeleteChannelEdges(v lnwire.GossipVersion, strictZombiePruning, + markZombie bool, chanIDs ...uint64) ( + []*models.ChannelEdgeInfo, error) // AddEdgeProof sets the proof of an existing edge in the graph // database. @@ -275,7 +276,7 @@ type Store interface { //nolint:interfacebloat // 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) ( + FetchChannelEdgesByOutpoint(v lnwire.GossipVersion, op *wire.OutPoint) ( *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) @@ -290,7 +291,7 @@ type Store interface { //nolint:interfacebloat // 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) ( + FetchChannelEdgesByID(v lnwire.GossipVersion, chanID uint64) ( *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) @@ -313,7 +314,8 @@ type Store interface { //nolint:interfacebloat // 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, error) + IsZombieEdge(v lnwire.GossipVersion, chanID uint64) (bool, [33]byte, + [33]byte, error) // NumZombies returns the current number of zombie channels in the // graph. diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index b801d6780b1..bf033f0fca1 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -1889,8 +1889,13 @@ func (c *KVStore) 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 *KVStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, - chanIDs ...uint64) ([]*models.ChannelEdgeInfo, error) { +func (c *KVStore) DeleteChannelEdges(v lnwire.GossipVersion, + strictZombiePruning, markZombie bool, chanIDs ...uint64) ( + []*models.ChannelEdgeInfo, error) { + + if v != lnwire.GossipVersion1 { + return nil, ErrVersionNotSupportedForKVDB + } // TODO(roasbeef): possibly delete from node bucket if node has no more // channels @@ -3804,8 +3809,8 @@ 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 *KVStore) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( - *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, +func (c *KVStore) FetchChannelEdgesByOutpoint(v lnwire.GossipVersion, + op *wire.OutPoint) (*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { var ( @@ -3814,6 +3819,10 @@ func (c *KVStore) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( policy2 *models.ChannelEdgePolicy ) + if v != lnwire.GossipVersion1 { + return nil, nil, nil, ErrVersionNotSupportedForKVDB + } + err := kvdb.View(c.db, func(tx kvdb.RTx) error { // First, grab the node bucket. This will be used to populate // the Node pointers in each edge read from disk. @@ -3890,10 +3899,14 @@ func (c *KVStore) 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 *KVStore) FetchChannelEdgesByID(chanID uint64) ( - *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, +func (c *KVStore) FetchChannelEdgesByID(v lnwire.GossipVersion, + chanID uint64) (*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { + if v != lnwire.GossipVersion1 { + return nil, nil, nil, ErrVersionNotSupportedForKVDB + } + var ( edgeInfo *models.ChannelEdgeInfo policy1 *models.ChannelEdgePolicy @@ -4001,7 +4014,13 @@ func (c *KVStore) 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 *KVStore) IsPublicNode(pubKey [33]byte) (bool, error) { +func (c *KVStore) IsPublicNode(v lnwire.GossipVersion, pubKey [33]byte) (bool, + error) { + + if v != lnwire.GossipVersion1 { + return false, ErrVersionNotSupportedForKVDB + } + var nodeIsPublic bool err := kvdb.View(c.db, func(tx kvdb.RTx) error { nodes := tx.ReadBucket(nodeBucket) @@ -4239,14 +4258,18 @@ func (c *KVStore) 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 *KVStore) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte, - error) { +func (c *KVStore) IsZombieEdge(v lnwire.GossipVersion, + chanID uint64) (bool, [33]byte, [33]byte, error) { var ( isZombie bool pubKey1, pubKey2 [33]byte ) + if v != lnwire.GossipVersion1 { + return false, [33]byte{}, [33]byte{}, ErrVersionNotSupportedForKVDB + } + err := kvdb.View(c.db, func(tx kvdb.RTx) error { edges := tx.ReadBucket(edgeBucket) if edges == nil { diff --git a/graph/db/models/channel_edge_info.go b/graph/db/models/channel_edge_info.go index 05573fb1d1a..54deb644e2d 100644 --- a/graph/db/models/channel_edge_info.go +++ b/graph/db/models/channel_edge_info.go @@ -5,6 +5,8 @@ import ( "fmt" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -42,9 +44,15 @@ type ChannelEdgeInfo struct { nodeKey2 *btcec.PublicKey // BitcoinKey1Bytes is the raw public key of the first node. + // + // NOTE: this must be set for v1 channels but is optional for v2 and + // beyond. BitcoinKey1Bytes fn.Option[route.Vertex] // BitcoinKey2Bytes is the raw public key of the first node. + // + // NOTE: this must be set for v1 channels but is optional for v2 and + // beyond. BitcoinKey2Bytes fn.Option[route.Vertex] // Features is the list of protocol features supported by this channel @@ -70,13 +78,28 @@ type ChannelEdgeInfo struct { // the edge object is loaded from the database. FundingScript fn.Option[[]byte] + // MerkleRootHash is an optional root hash of a Merkle tree that the + // funding output is committed to. This is then used to compute the + // final funding output script. + // + // NOTE: only used for version 2 channels and beyond. + MerkleRootHash fn.Option[chainhash.Hash] + // ExtraOpaqueData is the set of data that was appended to this // message, some of which we may not actually know how to iterate or // parse. By holding onto this data, we ensure that we're able to // properly validate the set of signatures that cover these new fields, // and ensure we're able to make upgrades to the network in a forwards // compatible manner. + // + // NOTE: only used for version 1 channels. ExtraOpaqueData []byte + + // ExtraSignedFields is a map of extra fields that are covered by the + // node announcement's signature that we have not explicitly parsed. + // + // NOTE: This is only used for version 2 node announcements and beyond. + ExtraSignedFields map[uint64][]byte } // EdgeModifier is a functional option that modifies a ChannelEdgeInfo. @@ -110,6 +133,20 @@ func WithChanProof(proof *ChannelAuthProof) EdgeModifier { } } +// WithFundingScript sets the funding script on the edge. +func WithFundingScript(script []byte) EdgeModifier { + return func(e *ChannelEdgeInfo) { + e.FundingScript = fn.Some(script) + } +} + +// WithMerkleRootHash sets the merkle root hash on the edge. +func WithMerkleRootHash(hash chainhash.Hash) EdgeModifier { + return func(e *ChannelEdgeInfo) { + e.MerkleRootHash = fn.Some(hash) + } +} + // ChannelV1Fields contains the fields that are specific to v1 channel // announcements. type ChannelV1Fields struct { @@ -165,6 +202,63 @@ func NewV1Channel(chanID uint64, chainHash chainhash.Hash, node1, return edge, nil } +// ChannelV2Fields contains the fields that are specific to v2 channel +// announcements. +type ChannelV2Fields struct { + // BitcoinKey1Bytes is the raw public key of the first node. + BitcoinKey1Bytes fn.Option[route.Vertex] + + // BitcoinKey2Bytes is the raw public key of the first node. + BitcoinKey2Bytes fn.Option[route.Vertex] + + // FundingScript is the funding output's pkScript. This is required for + // v2 channels when the bitcoin keys are not provided. + FundingScript fn.Option[[]byte] + + // MerkleRootHash is an optional root hash of a Merkle tree that the + // funding output is committed to. + MerkleRootHash fn.Option[chainhash.Hash] + + // ExtraSignedFields is a map of extra fields that are covered by the + // node announcement's signature that we have not explicitly parsed. + // + // NOTE: This is only used for version 2 node announcements and beyond. + ExtraSignedFields map[uint64][]byte +} + +// NewV2Channel creates a new ChannelEdgeInfo for a v2 channel announcement. +func NewV2Channel(chanID uint64, chainHash chainhash.Hash, node1, + node2 route.Vertex, v2Fields *ChannelV2Fields, + opts ...EdgeModifier) (*ChannelEdgeInfo, error) { + + edge := &ChannelEdgeInfo{ + Version: lnwire.GossipVersion2, + NodeKey1Bytes: node1, + NodeKey2Bytes: node2, + BitcoinKey1Bytes: v2Fields.BitcoinKey1Bytes, + BitcoinKey2Bytes: v2Fields.BitcoinKey2Bytes, + FundingScript: v2Fields.FundingScript, + MerkleRootHash: v2Fields.MerkleRootHash, + ChannelID: chanID, + ChainHash: chainHash, + Features: lnwire.EmptyFeatureVector(), + ExtraSignedFields: v2Fields.ExtraSignedFields, + } + + for _, opt := range opts { + opt(edge) + } + + // Validate some fields after the options have been applied. + if edge.AuthProof != nil && edge.AuthProof.Version != edge.Version { + return nil, fmt.Errorf("channel auth proof version %d does "+ + "not match channel version %d", edge.AuthProof.Version, + edge.Version) + } + + return edge, nil +} + // NodeKey1 is the identity public key of the "first" node that was involved in // the creation of this channel. A node is considered "first" if the // lexicographical ordering the its serialized public key is "smaller" than @@ -248,6 +342,87 @@ func (c *ChannelEdgeInfo) FundingPKScript() ([]byte, error) { return input.WitnessScriptHash(witnessScript) + case lnwire.GossipVersion2: + var ( + pubKey1 *btcec.PublicKey + pubKey2 *btcec.PublicKey + err error + ) + c.BitcoinKey1Bytes.WhenSome(func(key route.Vertex) { + pubKey1, err = btcec.ParsePubKey(key[:]) + }) + if err != nil { + return nil, err + } + + c.BitcoinKey2Bytes.WhenSome(func(key route.Vertex) { + pubKey2, err = btcec.ParsePubKey(key[:]) + }) + if err != nil { + return nil, err + } + + // If both bitcoin keys are not present in the announcement, + // then we should previously have stored the funding script + // found on-chain. + if pubKey1 == nil || pubKey2 == nil { + return c.FundingScript.UnwrapOrErr(fmt.Errorf( + "expected a funding pk script since no " + + "bitcoin keys were provided", + )) + } + + // Initially we set the tweak to an empty byte array. If a + // merkle root hash is provided in the announcement then we use + // that to set the tweak but otherwise, the empty tweak will + // have the same effect as a BIP86 tweak. + var tweak []byte + c.MerkleRootHash.WhenSome(func(hash chainhash.Hash) { + tweak = hash[:] + }) + + // Calculate the internal key by computing the MuSig2 + // combination of the two public keys. + internalKey, _, _, err := musig2.AggregateKeys( + []*btcec.PublicKey{pubKey1, pubKey2}, true, + ) + if err != nil { + return nil, err + } + + // Now, determine the tweak to be added to the internal key. If + // the tweak is empty, then this will effectively be a BIP86 + // tweak. + tapTweakHash := chainhash.TaggedHash( + chainhash.TagTapTweak, schnorr.SerializePubKey( + internalKey.FinalKey, + ), tweak, + ) + + // Compute the final output key. + combinedKey, _, _, err := musig2.AggregateKeys( + []*btcec.PublicKey{pubKey1, pubKey2}, true, + musig2.WithKeyTweaks(musig2.KeyTweakDesc{ + Tweak: *tapTweakHash, + IsXOnly: true, + }), + ) + if err != nil { + return nil, err + } + + // Now that we have the combined key, we can create a taproot + // pkScript from this, and then make the txout given the amount. + fundingScript, err := input.PayToTaprootScript( + combinedKey.FinalKey, + ) + if err != nil { + return nil, fmt.Errorf("unable to make taproot "+ + "pkscript: %w", err) + } + + return fundingScript, nil + default: return nil, fmt.Errorf("unsupported channel version: %d", c.Version) diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 1e5be9f17bd..cbe2179af24 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -49,6 +49,7 @@ type SQLQueries interface { ListNodesPaginated(ctx context.Context, arg sqlc.ListNodesPaginatedParams) ([]sqlc.GraphNode, error) ListNodeIDsAndPubKeys(ctx context.Context, arg sqlc.ListNodeIDsAndPubKeysParams) ([]sqlc.ListNodeIDsAndPubKeysRow, error) IsPublicV1Node(ctx context.Context, pubKey []byte) (bool, error) + IsPublicV2Node(ctx context.Context, pubKey []byte) (bool, error) DeleteUnconnectedNodes(ctx context.Context) ([][]byte, error) DeleteNodeByPubKey(ctx context.Context, arg sqlc.DeleteNodeByPubKeyParams) (sql.Result, error) DeleteNode(ctx context.Context, id int64) error @@ -80,6 +81,7 @@ type SQLQueries interface { */ CreateChannel(ctx context.Context, arg sqlc.CreateChannelParams) (int64, error) AddV1ChannelProof(ctx context.Context, arg sqlc.AddV1ChannelProofParams) (sql.Result, error) + AddV2ChannelProof(ctx context.Context, arg sqlc.AddV2ChannelProofParams) (sql.Result, error) GetChannelBySCID(ctx context.Context, arg sqlc.GetChannelBySCIDParams) (sqlc.GraphChannel, error) GetChannelsBySCIDs(ctx context.Context, arg sqlc.GetChannelsBySCIDsParams) ([]sqlc.GraphChannel, error) GetChannelsByOutpoints(ctx context.Context, outpoints []string) ([]sqlc.GetChannelsByOutpointsRow, error) @@ -701,6 +703,11 @@ func (s *SQLStore) NodeUpdatesInHorizon(startTime, endTime time.Time, func (s *SQLStore) AddChannelEdge(ctx context.Context, edge *models.ChannelEdgeInfo, opts ...batch.SchedulerOption) error { + if !isKnownGossipVersion(edge.Version) { + return fmt.Errorf("unsupported gossip version: %d", + edge.Version) + } + var alreadyExists bool r := &batch.Request[SQLQueries]{ Opts: batch.NewSchedulerOptions(opts...), @@ -718,7 +725,7 @@ func (s *SQLStore) AddChannelEdge(ctx context.Context, _, err := tx.GetChannelBySCID( ctx, sqlc.GetChannelBySCIDParams{ Scid: chanIDB, - Version: int16(lnwire.GossipVersion1), + Version: int16(edge.Version), }, ) if err == nil { @@ -1807,8 +1814,8 @@ func (s *SQLStore) MarkEdgeLive(chanID uint64) error { // returned. // // NOTE: part of the Store interface. -func (s *SQLStore) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte, - error) { +func (s *SQLStore) IsZombieEdge(v lnwire.GossipVersion, + chanID uint64) (bool, [33]byte, [33]byte, error) { var ( ctx = context.TODO() @@ -1817,11 +1824,16 @@ func (s *SQLStore) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte, chanIDB = channelIDToBytes(chanID) ) + if !isKnownGossipVersion(v) { + return false, [33]byte{}, [33]byte{}, + fmt.Errorf("unsupported gossip version: %d", v) + } + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { zombie, err := db.GetZombieChannel( ctx, sqlc.GetZombieChannelParams{ Scid: chanIDB, - Version: int16(lnwire.GossipVersion1), + Version: int16(v), }, ) if errors.Is(err, sql.ErrNoRows) { @@ -1885,8 +1897,9 @@ func (s *SQLStore) NumZombies() (uint64, error) { // denotes whether to mark the channel as a zombie. // // NOTE: part of the Store interface. -func (s *SQLStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, - chanIDs ...uint64) ([]*models.ChannelEdgeInfo, error) { +func (s *SQLStore) DeleteChannelEdges(v lnwire.GossipVersion, + strictZombiePruning, markZombie bool, chanIDs ...uint64) ( + []*models.ChannelEdgeInfo, error) { s.cacheMu.Lock() defer s.cacheMu.Unlock() @@ -1919,7 +1932,7 @@ func (s *SQLStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, } err := s.forEachChanWithPoliciesInSCIDList( - ctx, db, chanCallBack, chanIDs, + ctx, db, v, chanCallBack, chanIDs, ) if err != nil { return err @@ -1947,7 +1960,7 @@ func (s *SQLStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, scid := byteOrder.Uint64(row.GraphChannel.Scid) err := handleZombieMarking( - ctx, db, row, edges[i], + ctx, db, v, row, edges[i], strictZombiePruning, scid, ) if err != nil { @@ -1990,8 +2003,8 @@ func (s *SQLStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, // the ChannelEdgeInfo will only include the public keys of each node. // // NOTE: part of the Store interface. -func (s *SQLStore) FetchChannelEdgesByID(chanID uint64) ( - *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, +func (s *SQLStore) FetchChannelEdgesByID(v lnwire.GossipVersion, + chanID uint64) (*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { var ( @@ -2000,11 +2013,17 @@ func (s *SQLStore) FetchChannelEdgesByID(chanID uint64) ( policy1, policy2 *models.ChannelEdgePolicy chanIDB = channelIDToBytes(chanID) ) + + if !isKnownGossipVersion(v) { + return nil, nil, nil, fmt.Errorf("unsupported gossip version: %d", + v) + } + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { row, err := db.GetChannelBySCIDWithPolicies( ctx, sqlc.GetChannelBySCIDWithPoliciesParams{ Scid: chanIDB, - Version: int16(lnwire.GossipVersion1), + Version: int16(v), }, ) if errors.Is(err, sql.ErrNoRows) { @@ -2013,7 +2032,7 @@ func (s *SQLStore) FetchChannelEdgesByID(chanID uint64) ( zombie, err := db.GetZombieChannel( ctx, sqlc.GetZombieChannelParams{ Scid: chanIDB, - Version: int16(lnwire.GossipVersion1), + Version: int16(v), }, ) if errors.Is(err, sql.ErrNoRows) { @@ -2103,8 +2122,8 @@ func (s *SQLStore) FetchChannelEdgesByID(chanID uint64) ( // contain the routing policies for the channel in either direction. // // NOTE: part of the Store interface. -func (s *SQLStore) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( - *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, +func (s *SQLStore) FetchChannelEdgesByOutpoint(v lnwire.GossipVersion, + op *wire.OutPoint) (*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { var ( @@ -2112,11 +2131,17 @@ func (s *SQLStore) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( edge *models.ChannelEdgeInfo policy1, policy2 *models.ChannelEdgePolicy ) + + if !isKnownGossipVersion(v) { + return nil, nil, nil, fmt.Errorf("unsupported gossip version: %d", + v) + } + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { row, err := db.GetChannelByOutpointWithPolicies( ctx, sqlc.GetChannelByOutpointWithPoliciesParams{ Outpoint: op.String(), - Version: int16(lnwire.GossipVersion1), + Version: int16(v), }, ) if errors.Is(err, sql.ErrNoRows) { @@ -2324,13 +2349,23 @@ func (s *SQLStore) ChannelID(chanPoint *wire.OutPoint) (uint64, error) { // source node's point of view. // // NOTE: part of the Store interface. -func (s *SQLStore) IsPublicNode(pubKey [33]byte) (bool, error) { +func (s *SQLStore) IsPublicNode(v lnwire.GossipVersion, pubKey [33]byte) (bool, + error) { + ctx := context.TODO() + if !isKnownGossipVersion(v) { + return false, fmt.Errorf("unsupported gossip version: %d", v) + } var isPublic bool err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { var err error - isPublic, err = db.IsPublicV1Node(ctx, pubKey[:]) + switch v { + case lnwire.GossipVersion1: + isPublic, err = db.IsPublicV1Node(ctx, pubKey[:]) + case lnwire.GossipVersion2: + isPublic, err = db.IsPublicV2Node(ctx, pubKey[:]) + } return err }, sqldb.NoOpReset) @@ -2365,7 +2400,7 @@ func (s *SQLStore) FetchChanInfos(chanIDs []uint64) ([]ChannelEdge, error) { } err := s.forEachChanWithPoliciesInSCIDList( - ctx, db, chanCallBack, chanIDs, + ctx, db, lnwire.GossipVersion1, chanCallBack, chanIDs, ) if err != nil { return err @@ -2413,7 +2448,7 @@ func (s *SQLStore) FetchChanInfos(chanIDs []uint64) ([]ChannelEdge, error) { // GetChannelsBySCIDWithPolicies query that allows us to iterate through // channels in a paginated manner. func (s *SQLStore) forEachChanWithPoliciesInSCIDList(ctx context.Context, - db SQLQueries, cb func(ctx context.Context, + db SQLQueries, v lnwire.GossipVersion, cb func(ctx context.Context, row sqlc.GetChannelsBySCIDWithPoliciesRow) error, chanIDs []uint64) error { @@ -2423,7 +2458,7 @@ func (s *SQLStore) forEachChanWithPoliciesInSCIDList(ctx context.Context, return db.GetChannelsBySCIDWithPolicies( ctx, sqlc.GetChannelsBySCIDWithPoliciesParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), Scids: scids, }, ) @@ -2969,9 +3004,8 @@ func (s *SQLStore) DisconnectBlockAtHeight(height uint32) ( func (s *SQLStore) AddEdgeProof(scid lnwire.ShortChannelID, proof *models.ChannelAuthProof) error { - // For now, we only support v1 channel proofs. - if proof.Version != lnwire.GossipVersion1 { - return fmt.Errorf("only v1 channel proofs supported, got v%d", + if !isKnownGossipVersion(proof.Version) { + return fmt.Errorf("unsupported gossip version: %d", proof.Version) } @@ -2981,15 +3015,34 @@ func (s *SQLStore) AddEdgeProof(scid lnwire.ShortChannelID, ) err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { - res, err := db.AddV1ChannelProof( - ctx, sqlc.AddV1ChannelProofParams{ - Scid: scidBytes, - Node1Signature: proof.NodeSig1(), - Node2Signature: proof.NodeSig2(), - Bitcoin1Signature: proof.BitcoinSig1(), - Bitcoin2Signature: proof.BitcoinSig2(), - }, + var ( + res sql.Result + err error ) + switch proof.Version { + case lnwire.GossipVersion1: + res, err = db.AddV1ChannelProof( + ctx, sqlc.AddV1ChannelProofParams{ + Scid: scidBytes, + Node1Signature: proof.NodeSig1(), + Node2Signature: proof.NodeSig2(), + Bitcoin1Signature: proof.BitcoinSig1(), + Bitcoin2Signature: proof.BitcoinSig2(), + }, + ) + + case lnwire.GossipVersion2: + res, err = db.AddV2ChannelProof( + ctx, sqlc.AddV2ChannelProofParams{ + Scid: scidBytes, + Signature: proof.Sig(), + }, + ) + + default: + return fmt.Errorf("unsupported gossip version: %d", + proof.Version) + } if err != nil { return fmt.Errorf("unable to add edge proof: %w", err) } @@ -4141,26 +4194,16 @@ func marshalExtraOpaqueData(data []byte) (map[uint64][]byte, error) { func insertChannel(ctx context.Context, db SQLQueries, edge *models.ChannelEdgeInfo) error { - v := lnwire.GossipVersion1 - - // For now, we only support V1 channel edges in the SQL store. - if edge.Version != v { - return fmt.Errorf("only V1 channel edges supported, got V%d", - edge.Version) - } + v := edge.Version // Make sure that at least a "shell" entry for each node is present in // the nodes table. - node1DBID, err := maybeCreateShellNode( - ctx, db, v, edge.NodeKey1Bytes, - ) + node1DBID, err := maybeCreateShellNode(ctx, db, v, edge.NodeKey1Bytes) if err != nil { return fmt.Errorf("unable to create shell node: %w", err) } - node2DBID, err := maybeCreateShellNode( - ctx, db, v, edge.NodeKey2Bytes, - ) + node2DBID, err := maybeCreateShellNode(ctx, db, v, edge.NodeKey2Bytes) if err != nil { return fmt.Errorf("unable to create shell node: %w", err) } @@ -4184,6 +4227,12 @@ func insertChannel(ctx context.Context, db SQLQueries, edge.BitcoinKey2Bytes.WhenSome(func(vertex route.Vertex) { createParams.BitcoinKey2 = vertex[:] }) + edge.FundingScript.WhenSome(func(script []byte) { + createParams.FundingPkScript = script + }) + edge.MerkleRootHash.WhenSome(func(hash chainhash.Hash) { + createParams.MerkleRootHash = hash[:] + }) if edge.AuthProof != nil { proof := edge.AuthProof @@ -4192,6 +4241,7 @@ func insertChannel(ctx context.Context, db SQLQueries, createParams.Node2Signature = proof.NodeSig2() createParams.Bitcoin1Signature = proof.BitcoinSig1() createParams.Bitcoin2Signature = proof.BitcoinSig2() + createParams.Signature = proof.Sig() } // Insert the new channel record. @@ -4215,10 +4265,13 @@ func insertChannel(ctx context.Context, db SQLQueries, } // Finally, insert any extra TLV fields in the channel announcement. - extra, err := marshalExtraOpaqueData(edge.ExtraOpaqueData) - if err != nil { - return fmt.Errorf("unable to marshal extra opaque data: %w", - err) + extra := edge.ExtraSignedFields + if v == lnwire.GossipVersion1 { + extra, err = marshalExtraOpaqueData(edge.ExtraOpaqueData) + if err != nil { + return fmt.Errorf("unable to marshal extra opaque "+ + "data: %w", err) + } } for tlvType, value := range extra { @@ -4330,9 +4383,9 @@ func buildEdgeInfoWithBatchData(chain chainhash.Hash, dbChan sqlc.GraphChannel, node1, node2 route.Vertex, batchData *batchChannelData) (*models.ChannelEdgeInfo, error) { - if dbChan.Version != int16(lnwire.GossipVersion1) { - return nil, fmt.Errorf("unsupported channel version: %d", - dbChan.Version) + v := lnwire.GossipVersion(dbChan.Version) + if !isKnownGossipVersion(v) { + return nil, fmt.Errorf("unknown channel version: %d", v) } // Use pre-loaded features and extras types. @@ -4356,48 +4409,50 @@ func buildEdgeInfoWithBatchData(chain chainhash.Hash, return nil, err } - recs, err := lnwire.CustomRecords(extras).Serialize() - if err != nil { - return nil, fmt.Errorf("unable to serialize extra signed "+ - "fields: %w", err) - } - if recs == nil { - recs = make([]byte, 0) - } + // Build the appropriate channel based on version. + var channel *models.ChannelEdgeInfo + switch v { + case lnwire.GossipVersion1: + // For v1, serialize extras into ExtraOpaqueData. + recs, err := lnwire.CustomRecords(extras).Serialize() + if err != nil { + return nil, fmt.Errorf("unable to serialize extra "+ + "signed fields: %w", err) + } + if recs == nil { + recs = make([]byte, 0) + } - btcKey1, err := route.NewVertexFromBytes(dbChan.BitcoinKey1) - if err != nil { - return nil, err - } - btcKey2, err := route.NewVertexFromBytes(dbChan.BitcoinKey2) - if err != nil { - return nil, err - } + // Bitcoin keys are required for v1. + btcKey1, err := route.NewVertexFromBytes(dbChan.BitcoinKey1) + if err != nil { + return nil, err + } + btcKey2, err := route.NewVertexFromBytes(dbChan.BitcoinKey2) + if err != nil { + return nil, err + } - channel, err := models.NewV1Channel( - byteOrder.Uint64(dbChan.Scid), - chain, - node1, - node2, - &models.ChannelV1Fields{ - BitcoinKey1Bytes: btcKey1, - BitcoinKey2Bytes: btcKey2, - ExtraOpaqueData: recs, - }, - models.WithChannelPoint(*op), - models.WithCapacity(btcutil.Amount(dbChan.Capacity.Int64)), - models.WithFeatures(fv.RawFeatureVector), - ) - if err != nil { - return nil, err - } + channel, err = models.NewV1Channel( + byteOrder.Uint64(dbChan.Scid), chain, node1, node2, + &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + ExtraOpaqueData: recs, + }, + models.WithChannelPoint(*op), + models.WithCapacity( + btcutil.Amount(dbChan.Capacity.Int64), + ), + models.WithFeatures(fv.RawFeatureVector), + ) + if err != nil { + return nil, err + } - // We always set all the signatures at the same time, so we can - // safely check if one signature is present to determine if we have the - // rest of the signatures for the auth proof. - if len(dbChan.Bitcoin1Signature) > 0 { - // For v1 channels, we have four signatures. - if dbChan.Version == int16(lnwire.GossipVersion1) { + // For v1 channels, attach the auth proof if all four + // signatures are present. + if len(dbChan.Bitcoin1Signature) > 0 { channel.AuthProof = models.NewV1ChannelAuthProof( dbChan.Node1Signature, dbChan.Node2Signature, @@ -4405,7 +4460,69 @@ func buildEdgeInfoWithBatchData(chain chainhash.Hash, dbChan.Bitcoin2Signature, ) } - // TODO(elle): Add v2 support when needed. + + case lnwire.GossipVersion2: + v2Fields := &models.ChannelV2Fields{ + ExtraSignedFields: extras, + } + + // For v2, bitcoin keys are optional. + if len(dbChan.BitcoinKey1) > 0 { + btcKey1, err := route.NewVertexFromBytes( + dbChan.BitcoinKey1, + ) + if err != nil { + return nil, err + } + v2Fields.BitcoinKey1Bytes = fn.Some(btcKey1) + } + if len(dbChan.BitcoinKey2) > 0 { + btcKey2, err := route.NewVertexFromBytes( + dbChan.BitcoinKey2, + ) + if err != nil { + return nil, err + } + v2Fields.BitcoinKey2Bytes = fn.Some(btcKey2) + } + + // Parse funding script if present. + if len(dbChan.FundingPkScript) > 0 { + v2Fields.FundingScript = fn.Some(dbChan.FundingPkScript) + } + + // Parse merkle root hash if present. + if len(dbChan.MerkleRootHash) > 0 { + var hash chainhash.Hash + copy(hash[:], dbChan.MerkleRootHash) + v2Fields.MerkleRootHash = fn.Some(hash) + } + + opts := []models.EdgeModifier{ + models.WithChannelPoint(*op), + models.WithCapacity(btcutil.Amount( + dbChan.Capacity.Int64, + )), + models.WithFeatures(fv.RawFeatureVector), + } + + // For v2 channels, attach the auth proof if the signature is + // present. + if len(dbChan.Signature) > 0 { + proof := models.NewV2ChannelAuthProof(dbChan.Signature) + opts = append(opts, models.WithChanProof(proof)) + } + + channel, err = models.NewV2Channel( + byteOrder.Uint64(dbChan.Scid), chain, node1, node2, + v2Fields, opts..., + ) + if err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unsupported channel version: %d", v) } return channel, nil @@ -4444,6 +4561,21 @@ func getAndBuildChanPolicies(ctx context.Context, cfg *sqldb.QueryConfig, return nil, nil, nil } + // TODO(elle): update to support v2 policies. + if dbPol1 != nil && + lnwire.GossipVersion(dbPol1.Version) != lnwire.GossipVersion1 { + + return nil, nil, fmt.Errorf("unsupported policy1 version: %d", + dbPol1.Version) + } + + if dbPol2 != nil && + lnwire.GossipVersion(dbPol2.Version) != lnwire.GossipVersion1 { + + return nil, nil, fmt.Errorf("unsupported policy2 version: %d", + dbPol2.Version) + } + var policyIDs = make([]int64, 0, 2) if dbPol1 != nil { policyIDs = append(policyIDs, dbPol1.ID) @@ -5840,12 +5972,19 @@ func batchBuildChannelInfo[T sqlc.ChannelAndNodeIDs](ctx context.Context, // we are in strict zombie pruning mode, and adjusts the node public keys // accordingly based on the last update timestamps of the channel policies. func handleZombieMarking(ctx context.Context, db SQLQueries, + v lnwire.GossipVersion, row sqlc.GetChannelsBySCIDWithPoliciesRow, info *models.ChannelEdgeInfo, strictZombiePruning bool, scid uint64) error { nodeKey1, nodeKey2 := info.NodeKey1Bytes, info.NodeKey2Bytes if strictZombiePruning { + // TODO(elle): update for V2 last update times. + if v != lnwire.GossipVersion1 { + return fmt.Errorf("strict zombie pruning only "+ + "supported for gossip v1, got %v", v) + } + var e1UpdateTime, e2UpdateTime *time.Time if row.Policy1LastUpdate.Valid { e1Time := time.Unix(row.Policy1LastUpdate.Int64, 0) @@ -5864,7 +6003,7 @@ func handleZombieMarking(ctx context.Context, db SQLQueries, return db.UpsertZombieChannel( ctx, sqlc.UpsertZombieChannelParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), Scid: channelIDToBytes(scid), NodeKey1: nodeKey1[:], NodeKey2: nodeKey2[:], diff --git a/rpcserver.go b/rpcserver.go index ca9343bb6e8..fa558615ce9 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -3154,7 +3154,7 @@ func createRPCCloseUpdate( // abandonChanFromGraph attempts to remove a channel from the channel graph. If // we can't find the chanID in the graph, then we assume it has already been // removed, and will return a nop. -func abandonChanFromGraph(chanGraph *graphdb.ChannelGraph, +func abandonChanFromGraph(chanGraph *graphdb.VersionedGraph, chanPoint *wire.OutPoint) error { // First, we'll obtain the channel ID. If we can't locate this, then @@ -3195,7 +3195,8 @@ func (r *rpcServer) abandonChan(chanPoint *wire.OutPoint, if err != nil { return err } - err = abandonChanFromGraph(r.server.graphDB, chanPoint) + // TODO: update to support deletions for v2 channels. + err = abandonChanFromGraph(r.server.v1Graph, chanPoint) if err != nil { return err } diff --git a/server.go b/server.go index 0a676c30acd..0041cc7a275 100644 --- a/server.go +++ b/server.go @@ -1422,7 +1422,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr, return nil, fmt.Errorf("we don't have an edge") } - err = s.graphDB.DeleteChannelEdges( + err = s.v1Graph.DeleteChannelEdges( false, false, scid.ToUint64(), ) return ourPolicy, err diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index a3abf88767b..b9bf85e623b 100644 --- a/sqldb/sqlc/graph.sql.go +++ b/sqldb/sqlc/graph.sql.go @@ -55,6 +55,22 @@ func (q *Queries) AddV1ChannelProof(ctx context.Context, arg AddV1ChannelProofPa ) } +const addV2ChannelProof = `-- name: AddV2ChannelProof :execresult +UPDATE graph_channels +SET signature = $2 +WHERE scid = $1 + AND version = 2 +` + +type AddV2ChannelProofParams struct { + Scid []byte + Signature []byte +} + +func (q *Queries) AddV2ChannelProof(ctx context.Context, arg AddV2ChannelProofParams) (sql.Result, error) { + return q.db.ExecContext(ctx, addV2ChannelProof, arg.Scid, arg.Signature) +} + const countZombieChannels = `-- name: CountZombieChannels :one SELECT COUNT(*) FROM graph_zombie_channels @@ -2728,6 +2744,27 @@ func (q *Queries) IsPublicV1Node(ctx context.Context, pubKey []byte) (bool, erro return exists, err } +const isPublicV2Node = `-- name: IsPublicV2Node :one +SELECT EXISTS ( + SELECT 1 + FROM graph_channels c + JOIN graph_nodes n ON n.id = c.node_id_1 OR n.id = c.node_id_2 + -- NOTE: we hard-code the version here since the clauses + -- here that determine if a node is public is specific + -- to the V2 gossip protocol. + WHERE c.version = 2 + AND c.signature IS NOT NULL + AND n.pub_key = $1 +) +` + +func (q *Queries) IsPublicV2Node(ctx context.Context, pubKey []byte) (bool, error) { + row := q.db.QueryRowContext(ctx, isPublicV2Node, pubKey) + var exists bool + err := row.Scan(&exists) + return exists, err +} + const isZombieChannel = `-- name: IsZombieChannel :one SELECT EXISTS ( SELECT 1 diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index 0087559be8f..bc8e295c6f5 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -13,6 +13,7 @@ import ( type Querier interface { AddSourceNode(ctx context.Context, nodeID int64) error AddV1ChannelProof(ctx context.Context, arg AddV1ChannelProofParams) (sql.Result, error) + AddV2ChannelProof(ctx context.Context, arg AddV2ChannelProofParams) (sql.Result, error) ClearKVInvoiceHashIndex(ctx context.Context) error CountZombieChannels(ctx context.Context, version int16) (int64, error) CreateChannel(ctx context.Context, arg CreateChannelParams) (int64, error) @@ -116,6 +117,7 @@ type Querier interface { InsertNodeMig(ctx context.Context, arg InsertNodeMigParams) (int64, error) IsClosedChannel(ctx context.Context, scid []byte) (bool, error) IsPublicV1Node(ctx context.Context, pubKey []byte) (bool, error) + IsPublicV2Node(ctx context.Context, pubKey []byte) (bool, error) IsZombieChannel(ctx context.Context, arg IsZombieChannelParams) (bool, error) ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNodeIDParams) ([]ListChannelsByNodeIDRow, error) ListChannelsForNodeIDs(ctx context.Context, arg ListChannelsForNodeIDsParams) ([]ListChannelsForNodeIDsRow, error) diff --git a/sqldb/sqlc/queries/graph.sql b/sqldb/sqlc/queries/graph.sql index 38a25d5cc77..bfa91b38ebd 100644 --- a/sqldb/sqlc/queries/graph.sql +++ b/sqldb/sqlc/queries/graph.sql @@ -73,6 +73,19 @@ SELECT EXISTS ( AND n.pub_key = $1 ); +-- name: IsPublicV2Node :one +SELECT EXISTS ( + SELECT 1 + FROM graph_channels c + JOIN graph_nodes n ON n.id = c.node_id_1 OR n.id = c.node_id_2 + -- NOTE: we hard-code the version here since the clauses + -- here that determine if a node is public is specific + -- to the V2 gossip protocol. + WHERE c.version = 2 + AND c.signature IS NOT NULL + AND n.pub_key = $1 +); + -- name: DeleteUnconnectedNodes :many DELETE FROM graph_nodes WHERE @@ -273,6 +286,12 @@ SET node_1_signature = $2, WHERE scid = $1 AND version = 1; +-- name: AddV2ChannelProof :execresult +UPDATE graph_channels +SET signature = $2 +WHERE scid = $1 + AND version = 2; + -- name: GetChannelsBySCIDRange :many SELECT sqlc.embed(c), n1.pub_key AS node1_pub_key,