From 3b7bef072693a46ba37563d4f8ebc728839eefae Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 16:17:42 +0200 Subject: [PATCH 01/14] graph/db: add v2 fields to ChannelEdgeInfo --- graph/db/models/channel_edge_info.go | 151 +++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/graph/db/models/channel_edge_info.go b/graph/db/models/channel_edge_info.go index 05573fb1d1..5d223836ca 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. @@ -165,6 +188,53 @@ 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] + + // 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, + 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 +318,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) From f354cda2f32e432b8cd5a5d46c71921df0cba3f7 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 16:44:15 +0200 Subject: [PATCH 02/14] graph/db: udpate SQLStore to read&write V2 Channels --- graph/db/models/channel_edge_info.go | 24 ++++ graph/db/sql_store.go | 191 ++++++++++++++++++--------- 2 files changed, 154 insertions(+), 61 deletions(-) diff --git a/graph/db/models/channel_edge_info.go b/graph/db/models/channel_edge_info.go index 5d223836ca..54deb644e2 100644 --- a/graph/db/models/channel_edge_info.go +++ b/graph/db/models/channel_edge_info.go @@ -133,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 { @@ -197,6 +211,14 @@ type ChannelV2Fields struct { // 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. // @@ -215,6 +237,8 @@ func NewV2Channel(chanID uint64, chainHash chainhash.Hash, node1, NodeKey2Bytes: node2, BitcoinKey1Bytes: v2Fields.BitcoinKey1Bytes, BitcoinKey2Bytes: v2Fields.BitcoinKey2Bytes, + FundingScript: v2Fields.FundingScript, + MerkleRootHash: v2Fields.MerkleRootHash, ChannelID: chanID, ChainHash: chainHash, Features: lnwire.EmptyFeatureVector(), diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 1e5be9f17b..22ca6f2086 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -701,6 +701,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 +723,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 { @@ -4141,26 +4146,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 +4179,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 +4193,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 +4217,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 +4335,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 +4361,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 +4412,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 From f937673019b7d622a422615d54ef7bf063f85c99 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 16:45:27 +0200 Subject: [PATCH 03/14] graph/db: update TestNodeIsPublic to use require --- graph/db/graph_test.go | 43 +++++++++++++----------------------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 72b200d554..5ed84b5793 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -4040,21 +4040,18 @@ func TestNodeIsPublic(t *testing.T) { // 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) - } + 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) - } + 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) - } + 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) @@ -4088,19 +4085,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,9 +4104,7 @@ 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}, @@ -4135,18 +4121,15 @@ 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 From ff56ed63627fcd0cf76d08ce33fa4a7d44d92bcd Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 16:57:04 +0200 Subject: [PATCH 04/14] graph/db: let createEdge test helper take version so that we can also use it to create V2 channels. --- graph/db/graph_test.go | 164 +++++++++++++++++++++++++++-------------- 1 file changed, 107 insertions(+), 57 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 5ed84b5793..8d6735ba10 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -378,7 +378,7 @@ func TestPartialNode(t *testing.T) { 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(lnwire.GossipVersion1, 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) @@ -587,9 +587,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 +610,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 +721,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 +2191,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 +2213,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 +2267,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 +2735,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 +2904,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 +2921,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 +3080,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 +3396,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 +3578,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 +3621,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 +3673,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 +3779,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 +3928,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 +3977,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 @@ -4053,8 +4102,8 @@ func TestNodeIsPublic(t *testing.T) { 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(lnwire.GossipVersion1, 10, 0, 0, 0, aliceNode, bobNode) + bobCarolEdge, _ := createEdge(lnwire.GossipVersion1, 10, 1, 0, 1, bobNode, carolNode) // After creating all of our nodes and edges, we'll add them to each // participant's graph. @@ -4594,19 +4643,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)) From b8dde50540dcd3f886dfe056dcd20d7e7a7d7aa4 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 17:12:51 +0200 Subject: [PATCH 05/14] graph/db: surface version in DeleteChannelEdges --- graph/db/graph.go | 3 ++- graph/db/interfaces.go | 5 +++-- graph/db/kv_store.go | 9 +++++++-- graph/db/sql_store.go | 24 ++++++++++++++++-------- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index 3fbb00e936..f2a5ffb0e4 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 diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index b6a8c10ead..24cfae9ab4 100644 --- a/graph/db/interfaces.go +++ b/graph/db/interfaces.go @@ -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. diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index b801d6780b..92594e22b9 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 diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 22ca6f2086..7e012f6323 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -1890,8 +1890,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() @@ -1924,7 +1925,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 @@ -1952,7 +1953,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 { @@ -2370,7 +2371,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 @@ -2418,7 +2419,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 { @@ -2428,7 +2429,7 @@ func (s *SQLStore) forEachChanWithPoliciesInSCIDList(ctx context.Context, return db.GetChannelsBySCIDWithPolicies( ctx, sqlc.GetChannelsBySCIDWithPoliciesParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), Scids: scids, }, ) @@ -5909,12 +5910,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) @@ -5933,7 +5941,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[:], From 2f84bcfa97bd517c28ba5ec2bed49c961bec9b02 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 17:19:53 +0200 Subject: [PATCH 06/14] sqldb/sqlc: add AddV2ChannelProof query --- sqldb/sqlc/graph.sql.go | 16 ++++++++++++++++ sqldb/sqlc/querier.go | 1 + sqldb/sqlc/queries/graph.sql | 6 ++++++ 3 files changed, 23 insertions(+) diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index a3abf88767..622385b092 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 diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index 0087559be8..76c8aafc91 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) diff --git a/sqldb/sqlc/queries/graph.sql b/sqldb/sqlc/queries/graph.sql index 38a25d5cc7..31cec8575b 100644 --- a/sqldb/sqlc/queries/graph.sql +++ b/sqldb/sqlc/queries/graph.sql @@ -273,6 +273,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, From 4200dac9c4dd6a996daa0fe8fb7f3d783edf69d8 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 17:23:32 +0200 Subject: [PATCH 07/14] graph/db: update AddEdgeProof for v2 proof --- graph/db/sql_store.go | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 7e012f6323..928c290487 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -80,6 +80,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) @@ -2975,9 +2976,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) } @@ -2987,15 +2987,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) } From 431be8af482a200fae3261e113d7cff90defe9ff Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 17:34:22 +0200 Subject: [PATCH 08/14] graph/db: let TestPartialNode be run for v1 and v2 nodes --- graph/db/graph_test.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 8d6735ba10..5cdd90476e 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -138,6 +138,10 @@ var versionedTests = []versionedTest{ name: "alias lookup", test: testAliasLookup, }, + { + name: "partial node", + test: testPartialNode, + }, } // TestVersionedDBs runs various tests against both v1 and v2 versioned @@ -363,22 +367,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(lnwire.GossipVersion1, 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 +390,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 +403,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 +412,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 From 45064ed830b597e5e01c16d4ba42d31c232036b4 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 17:39:58 +0200 Subject: [PATCH 09/14] multi: add DeleteChannelEdge to VersionedGraph --- graph/builder.go | 2 +- graph/db/graph.go | 30 ++++++++++++++++++++++++++++++ graph/db/graph_test.go | 1 + rpcserver.go | 5 +++-- server.go | 2 +- 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/graph/builder.go b/graph/builder.go index 3e15713965..96a7582eb3 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 { diff --git a/graph/db/graph.go b/graph/db/graph.go index f2a5ffb0e4..302813f49d 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -831,6 +831,36 @@ 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 +} + // 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 5cdd90476e..c15bea6ba5 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -149,6 +149,7 @@ var versionedTests = []versionedTest{ func TestVersionedDBs(t *testing.T) { t.Parallel() + // Run all v1 tests. for _, vt := range versionedTests { vt := vt diff --git a/rpcserver.go b/rpcserver.go index ca9343bb6e..fa558615ce 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 0a676c30ac..0041cc7a27 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 From 5398f570d967bcb61f3cc03223b73783fc4ce6e8 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 18:01:27 +0200 Subject: [PATCH 10/14] sqldb/sqlc: add IsPublicV2Node query --- sqldb/sqlc/graph.sql.go | 21 +++++++++++++++++++++ sqldb/sqlc/querier.go | 1 + sqldb/sqlc/queries/graph.sql | 13 +++++++++++++ 3 files changed, 35 insertions(+) diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index 622385b092..b9bf85e623 100644 --- a/sqldb/sqlc/graph.sql.go +++ b/sqldb/sqlc/graph.sql.go @@ -2744,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 76c8aafc91..bc8e295c6f 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -117,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 31cec8575b..bfa91b38eb 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 From 261f17ea2b2aee4af246dc105de30bdbbe74a0a8 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 18:03:42 +0200 Subject: [PATCH 11/14] graph/db: version IsPublicNode --- graph/builder.go | 2 +- graph/db/graph.go | 7 ++++++- graph/db/graph_test.go | 32 ++++++++++++++++++-------------- graph/db/interfaces.go | 2 +- graph/db/kv_store.go | 8 +++++++- graph/db/sql_store.go | 15 +++++++++++++-- 6 files changed, 46 insertions(+), 20 deletions(-) diff --git a/graph/builder.go b/graph/builder.go index 96a7582eb3..1f3309c048 100644 --- a/graph/builder.go +++ b/graph/builder.go @@ -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 302813f49d..6c4b0a150d 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -637,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. @@ -861,6 +861,11 @@ func (c *VersionedGraph) DeleteChannelEdges(strictZombiePruning, 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 c15bea6ba5..ca95a8d251 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -142,6 +142,10 @@ var versionedTests = []versionedTest{ name: "partial node", test: testPartialNode, }, + { + name: "node is public", + test: testNodeIsPublic, + }, } // TestVersionedDBs runs various tests against both v1 and v2 versioned @@ -4078,9 +4082,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() @@ -4092,29 +4096,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) + 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) + 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) + 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(lnwire.GossipVersion1, 10, 0, 0, 0, aliceNode, bobNode) - bobCarolEdge, _ := createEdge(lnwire.GossipVersion1, 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() @@ -4130,7 +4134,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() @@ -4162,7 +4166,7 @@ func TestNodeIsPublic(t *testing.T) { } checkNodes( []*models.Node{aliceNode}, - []*ChannelGraph{bobGraph, carolGraph}, + []*VersionedGraph{bobGraph, carolGraph}, false, ) @@ -4190,7 +4194,7 @@ func TestNodeIsPublic(t *testing.T) { // node from both Alice's and Carol's perspective. checkNodes( []*models.Node{bobNode}, - []*ChannelGraph{aliceGraph, carolGraph}, + []*VersionedGraph{aliceGraph, carolGraph}, false, ) } diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index 24cfae9ab4..5658fa5c7d 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 diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 92594e22b9..b004429dd0 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -4006,7 +4006,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) diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 928c290487..43e0e5a2d1 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 @@ -2331,13 +2332,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) From eb672ecf8e32a550f6a6220311a161a022aa5926 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 18:28:20 +0200 Subject: [PATCH 12/14] graph/db: version FetchChannelEdgesByID/Outpoint --- graph/db/graph.go | 25 +++++++++++++++++++++++-- graph/db/interfaces.go | 4 ++-- graph/db/kv_store.go | 16 ++++++++++++---- graph/db/sql_store.go | 41 ++++++++++++++++++++++++++++++++++------- 4 files changed, 71 insertions(+), 15 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index 6c4b0a150d..0f4dda6616 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -711,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. @@ -719,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. @@ -785,6 +789,23 @@ 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) +} + // 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) { diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index 5658fa5c7d..c6848132e4 100644 --- a/graph/db/interfaces.go +++ b/graph/db/interfaces.go @@ -276,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) @@ -291,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) diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index b004429dd0..42d9294a47 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -3809,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 ( @@ -3819,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. @@ -3895,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 diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 43e0e5a2d1..4f5783919c 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -1998,8 +1998,8 @@ func (s *SQLStore) DeleteChannelEdges(v lnwire.GossipVersion, // 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 ( @@ -2008,11 +2008,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) { @@ -2021,7 +2027,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) { @@ -2111,8 +2117,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 ( @@ -2120,11 +2126,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) { @@ -4544,6 +4556,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) From 9da10f24ce4aef29f160152477735e09d90ee124 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 18:36:54 +0200 Subject: [PATCH 13/14] graph/db: version IsZombie and update tests Update testEdgeInsertionDeletion to be run for both protocol versions. --- graph/db/graph.go | 9 ++++- graph/db/graph_test.go | 84 +++++++++++++----------------------------- graph/db/interfaces.go | 3 +- graph/db/kv_store.go | 8 +++- graph/db/sql_store.go | 11 ++++-- 5 files changed, 49 insertions(+), 66 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index 0f4dda6616..41396aceb3 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -735,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. @@ -806,6 +806,13 @@ func (c *VersionedGraph) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( 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) { diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index ca95a8d251..b912d1c833 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -138,6 +138,10 @@ var versionedTests = []versionedTest{ name: "alias lookup", test: testAliasLookup, }, + { + name: "edge insertion deletion", + test: testEdgeInsertionDeletion, + }, { name: "partial node", test: testPartialNode, @@ -488,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) - - btcKey1, err := route.NewVertexFromBytes( - node1Pub.SerializeCompressed(), - ) - require.NoError(t, err) - btcKey2, err := route.NewVertexFromBytes( - node2Pub.SerializeCompressed(), - ) - require.NoError(t, err) + node1 := createTestVertex(t, v) + node2 := createTestVertex(t, v) - proof := models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), + // Create a fake channel and add it to the graph. + const ( + blockHeight = 1234 + txIndex = 1 + txPosition = 0 + outPointIndex = 9 ) - 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), + edgeInfo, shortChanID := createEdge( + v, blockHeight, txIndex, txPosition, outPointIndex, node1, node2, ) - 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). @@ -572,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. diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index c6848132e4..24a48e4c73 100644 --- a/graph/db/interfaces.go +++ b/graph/db/interfaces.go @@ -314,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 42d9294a47..bf033f0fca 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -4258,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/sql_store.go b/graph/db/sql_store.go index 4f5783919c..cbe2179af2 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -1814,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() @@ -1824,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) { From 3f6e9d37378f251e21a714858d20bb502826b8b0 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 19:06:08 +0200 Subject: [PATCH 14/14] docs: update release notes --- docs/release-notes/release-notes-0.21.0.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index 9d24b36dc1..23ffb686aa 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