From 2e58c07570b84f6c125e6dc15c263c3388c6b1a3 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 29 Oct 2025 17:42:38 +0200 Subject: [PATCH 01/55] devrpc: fix comment --- lnrpc/devrpc/dev_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnrpc/devrpc/dev_server.go b/lnrpc/devrpc/dev_server.go index 39c089dff..08f01e147 100644 --- a/lnrpc/devrpc/dev_server.go +++ b/lnrpc/devrpc/dev_server.go @@ -258,7 +258,7 @@ func (s *Server) ImportGraph(ctx context.Context, Color: nodeColor, // NOTE: this is a workaround to ensure that // HaveAnnouncement() returns true so that the other - // fields are properly persisted. However, + // fields are properly persisted. AuthSigBytes: []byte{0}, }) From cb2fc6621e3e5f1fc3d391c14b159d75bdb0200a Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 3 Nov 2025 09:45:13 +0200 Subject: [PATCH 02/55] sqldb: add new gossip v2 columns to graph tables Add a new migration that updates the graph tables (nodes, channels and policies) in preparation for the new columns required for V2 announcements. This migration has to be added to the set of "live" migrations instead of "dev only" since it edits the columns of existing tables and so changes the existing sql models. We are going to prep the SQLStore code to handle the V2 types in the coming commits, so we need this migration to be in place. In this commit we also remove the TestSchemaMigrationIdempotency test since this test fails with the new "ALTER TABLE" migrations which dont have "IF NOT EXISTS" options like tables and indexes do. Migrations should be idempotent anyways due to the migration tracker file and/or the sqlc migration tracker. --- sqldb/migrations.go | 5 + sqldb/migrations_test.go | 126 ------------------ sqldb/sqlc/graph.sql.go | 107 +++++++++++---- .../sqlc/migrations/000009_graph_v2.down.sql | 17 +++ sqldb/sqlc/migrations/000009_graph_v2.up.sql | 23 ++++ sqldb/sqlc/models.go | 20 ++- 6 files changed, 140 insertions(+), 158 deletions(-) create mode 100644 sqldb/sqlc/migrations/000009_graph_v2.down.sql create mode 100644 sqldb/sqlc/migrations/000009_graph_v2.up.sql diff --git a/sqldb/migrations.go b/sqldb/migrations.go index 38535d8b9..28d45f37f 100644 --- a/sqldb/migrations.go +++ b/sqldb/migrations.go @@ -92,6 +92,11 @@ var ( // schema. This is optional and can be disabled by the // user if necessary. }, + { + Name: "000009_graph_v2_columns", + Version: 11, + SchemaVersion: 9, + }, }, migrationAdditions...) // ErrMigrationMismatch is returned when a migrated record does not diff --git a/sqldb/migrations_test.go b/sqldb/migrations_test.go index 063f810b0..536ba8971 100644 --- a/sqldb/migrations_test.go +++ b/sqldb/migrations_test.go @@ -452,132 +452,6 @@ func TestCustomMigration(t *testing.T) { } } -// TestSchemaMigrationIdempotency tests that the our schema migrations are -// idempotent. This means that we can apply the migrations multiple times and -// the schema version will always be the same. -func TestSchemaMigrationIdempotency(t *testing.T) { - dropMigrationTrackerEntries := func(t *testing.T, db *BaseDB) { - _, err := db.Exec("DELETE FROM migration_tracker;") - require.NoError(t, err) - } - - lastMigration := migrationConfig[len(migrationConfig)-1] - - t.Run("SQLite", func(t *testing.T) { - // First instantiate the database and run the migrations - // including the custom migrations. - t.Logf("Creating new SQLite DB for testing migrations") - - dbFileName := filepath.Join(t.TempDir(), "tmp.db") - var ( - db *SqliteStore - err error - ) - - // Run the migration 3 times to test that the migrations - // are idempotent. - for i := 0; i < 3; i++ { - db, err = NewSqliteStore(&SqliteConfig{ - SkipMigrations: false, - }, dbFileName) - require.NoError(t, err) - - dbToCleanup := db.DB - t.Cleanup(func() { - require.NoError( - t, dbToCleanup.Close(), - ) - }) - - ctxb := t.Context() - require.NoError( - t, db.ApplyAllMigrations(ctxb, GetMigrations()), - ) - - version, dirty, err := db.GetSchemaVersion() - require.NoError(t, err) - - // Now reset the schema version to 0 and make sure that - // we can apply the migrations again. - require.Equal(t, lastMigration.SchemaVersion, version) - require.False(t, dirty) - - require.NoError( - t, db.SetSchemaVersion( - database.NilVersion, false, - ), - ) - dropMigrationTrackerEntries(t, db.BaseDB) - - // Make sure that we reset the schema version. - version, dirty, err = db.GetSchemaVersion() - require.NoError(t, err) - require.Equal(t, -1, version) - require.False(t, dirty) - } - }) - - t.Run("Postgres", func(t *testing.T) { - // First create a temporary Postgres database to run - // the migrations on. - fixture := NewTestPgFixture( - t, DefaultPostgresFixtureLifetime, - ) - t.Cleanup(func() { - fixture.TearDown(t) - }) - - dbName := randomDBName(t) - - // Next instantiate the database and run the migrations - // including the custom migrations. - t.Logf("Creating new Postgres DB '%s' for testing "+ - "migrations", dbName) - - _, err := fixture.db.ExecContext( - t.Context(), "CREATE DATABASE "+dbName, - ) - require.NoError(t, err) - - cfg := fixture.GetConfig(dbName) - var db *PostgresStore - - // Run the migration 3 times to test that the migrations - // are idempotent. - for i := 0; i < 3; i++ { - cfg.SkipMigrations = false - db, err = NewPostgresStore(cfg) - require.NoError(t, err) - - ctxb := t.Context() - require.NoError( - t, db.ApplyAllMigrations(ctxb, GetMigrations()), - ) - - version, dirty, err := db.GetSchemaVersion() - require.NoError(t, err) - - // Now reset the schema version to 0 and make sure that - // we can apply the migrations again. - require.Equal(t, lastMigration.SchemaVersion, version) - require.False(t, dirty) - - require.NoError( - t, db.SetSchemaVersion( - database.NilVersion, false, - ), - ) - dropMigrationTrackerEntries(t, db.BaseDB) - - // Make sure that we reset the schema version. - version, dirty, err = db.GetSchemaVersion() - require.NoError(t, err) - require.Equal(t, -1, version) - require.False(t, dirty) - } - }) -} - // TestMigrationBug19RC1 tests a bug that was present in the migration code // at the v0.19.0-rc1 release. // The bug was fixed in: https://github.com/lightningnetwork/lnd/pull/9647 diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index 0ce7780d5..8b10e4967 100644 --- a/sqldb/sqlc/graph.sql.go +++ b/sqldb/sqlc/graph.sql.go @@ -290,7 +290,7 @@ func (q *Queries) DeleteZombieChannel(ctx context.Context, arg DeleteZombieChann const getChannelAndNodesBySCID = `-- name: GetChannelAndNodesBySCID :one SELECT - c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, + c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, c.signature, c.funding_pk_script, c.merkle_root_hash, n1.pub_key AS node1_pub_key, n2.pub_key AS node2_pub_key FROM graph_channels c @@ -319,6 +319,9 @@ type GetChannelAndNodesBySCIDRow struct { Node2Signature []byte Bitcoin1Signature []byte Bitcoin2Signature []byte + Signature []byte + FundingPkScript []byte + MerkleRootHash []byte Node1PubKey []byte Node2PubKey []byte } @@ -340,6 +343,9 @@ func (q *Queries) GetChannelAndNodesBySCID(ctx context.Context, arg GetChannelAn &i.Node2Signature, &i.Bitcoin1Signature, &i.Bitcoin2Signature, + &i.Signature, + &i.FundingPkScript, + &i.MerkleRootHash, &i.Node1PubKey, &i.Node2PubKey, ) @@ -348,7 +354,7 @@ func (q *Queries) GetChannelAndNodesBySCID(ctx context.Context, arg GetChannelAn const getChannelByOutpointWithPolicies = `-- name: GetChannelByOutpointWithPolicies :one SELECT - c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, + c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, c.signature, c.funding_pk_script, c.merkle_root_hash, n1.pub_key AS node1_pubkey, n2.pub_key AS node2_pubkey, @@ -454,6 +460,9 @@ func (q *Queries) GetChannelByOutpointWithPolicies(ctx context.Context, arg GetC &i.GraphChannel.Node2Signature, &i.GraphChannel.Bitcoin1Signature, &i.GraphChannel.Bitcoin2Signature, + &i.GraphChannel.Signature, + &i.GraphChannel.FundingPkScript, + &i.GraphChannel.MerkleRootHash, &i.Node1Pubkey, &i.Node2Pubkey, &i.Policy1ID, @@ -491,7 +500,7 @@ func (q *Queries) GetChannelByOutpointWithPolicies(ctx context.Context, arg GetC } const getChannelBySCID = `-- name: GetChannelBySCID :one -SELECT id, version, scid, node_id_1, node_id_2, outpoint, capacity, bitcoin_key_1, bitcoin_key_2, node_1_signature, node_2_signature, bitcoin_1_signature, bitcoin_2_signature FROM graph_channels +SELECT id, version, scid, node_id_1, node_id_2, outpoint, capacity, bitcoin_key_1, bitcoin_key_2, node_1_signature, node_2_signature, bitcoin_1_signature, bitcoin_2_signature, signature, funding_pk_script, merkle_root_hash FROM graph_channels WHERE scid = $1 AND version = $2 ` @@ -517,15 +526,18 @@ func (q *Queries) GetChannelBySCID(ctx context.Context, arg GetChannelBySCIDPara &i.Node2Signature, &i.Bitcoin1Signature, &i.Bitcoin2Signature, + &i.Signature, + &i.FundingPkScript, + &i.MerkleRootHash, ) return i, err } const getChannelBySCIDWithPolicies = `-- name: GetChannelBySCIDWithPolicies :one SELECT - c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, - n1.id, n1.version, n1.pub_key, n1.alias, n1.last_update, n1.color, n1.signature, - n2.id, n2.version, n2.pub_key, n2.alias, n2.last_update, n2.color, n2.signature, + c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, c.signature, c.funding_pk_script, c.merkle_root_hash, + n1.id, n1.version, n1.pub_key, n1.alias, n1.last_update, n1.color, n1.signature, n1.block_height, + n2.id, n2.version, n2.pub_key, n2.alias, n2.last_update, n2.color, n2.signature, n2.block_height, -- Policy 1 cp1.id AS policy1_id, @@ -630,6 +642,9 @@ func (q *Queries) GetChannelBySCIDWithPolicies(ctx context.Context, arg GetChann &i.GraphChannel.Node2Signature, &i.GraphChannel.Bitcoin1Signature, &i.GraphChannel.Bitcoin2Signature, + &i.GraphChannel.Signature, + &i.GraphChannel.FundingPkScript, + &i.GraphChannel.MerkleRootHash, &i.GraphNode.ID, &i.GraphNode.Version, &i.GraphNode.PubKey, @@ -637,6 +652,7 @@ func (q *Queries) GetChannelBySCIDWithPolicies(ctx context.Context, arg GetChann &i.GraphNode.LastUpdate, &i.GraphNode.Color, &i.GraphNode.Signature, + &i.GraphNode.BlockHeight, &i.GraphNode_2.ID, &i.GraphNode_2.Version, &i.GraphNode_2.PubKey, @@ -644,6 +660,7 @@ func (q *Queries) GetChannelBySCIDWithPolicies(ctx context.Context, arg GetChann &i.GraphNode_2.LastUpdate, &i.GraphNode_2.Color, &i.GraphNode_2.Signature, + &i.GraphNode_2.BlockHeight, &i.Policy1ID, &i.Policy1NodeID, &i.Policy1Version, @@ -764,7 +781,7 @@ func (q *Queries) GetChannelFeaturesBatch(ctx context.Context, chanIds []int64) } const getChannelPolicyByChannelAndNode = `-- name: GetChannelPolicyByChannelAndNode :one -SELECT id, version, channel_id, node_id, timelock, fee_ppm, base_fee_msat, min_htlc_msat, max_htlc_msat, last_update, disabled, inbound_base_fee_msat, inbound_fee_rate_milli_msat, message_flags, channel_flags, signature +SELECT id, version, channel_id, node_id, timelock, fee_ppm, base_fee_msat, min_htlc_msat, max_htlc_msat, last_update, disabled, inbound_base_fee_msat, inbound_fee_rate_milli_msat, message_flags, channel_flags, signature, block_height, disable_flags FROM graph_channel_policies WHERE channel_id = $1 AND node_id = $2 @@ -797,6 +814,8 @@ func (q *Queries) GetChannelPolicyByChannelAndNode(ctx context.Context, arg GetC &i.MessageFlags, &i.ChannelFlags, &i.Signature, + &i.BlockHeight, + &i.DisableFlags, ) return i, err } @@ -852,7 +871,7 @@ func (q *Queries) GetChannelPolicyExtraTypesBatch(ctx context.Context, policyIds const getChannelsByIDs = `-- name: GetChannelsByIDs :many SELECT - c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, + c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, c.signature, c.funding_pk_script, c.merkle_root_hash, -- Minimal node data. n1.id AS node1_id, @@ -975,6 +994,9 @@ func (q *Queries) GetChannelsByIDs(ctx context.Context, ids []int64) ([]GetChann &i.GraphChannel.Node2Signature, &i.GraphChannel.Bitcoin1Signature, &i.GraphChannel.Bitcoin2Signature, + &i.GraphChannel.Signature, + &i.GraphChannel.FundingPkScript, + &i.GraphChannel.MerkleRootHash, &i.Node1ID, &i.Node1PubKey, &i.Node2ID, @@ -1025,7 +1047,7 @@ func (q *Queries) GetChannelsByIDs(ctx context.Context, ids []int64) ([]GetChann const getChannelsByOutpoints = `-- name: GetChannelsByOutpoints :many SELECT - c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, + c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, c.signature, c.funding_pk_script, c.merkle_root_hash, n1.pub_key AS node1_pubkey, n2.pub_key AS node2_pubkey FROM graph_channels c @@ -1074,6 +1096,9 @@ func (q *Queries) GetChannelsByOutpoints(ctx context.Context, outpoints []string &i.GraphChannel.Node2Signature, &i.GraphChannel.Bitcoin1Signature, &i.GraphChannel.Bitcoin2Signature, + &i.GraphChannel.Signature, + &i.GraphChannel.FundingPkScript, + &i.GraphChannel.MerkleRootHash, &i.Node1Pubkey, &i.Node2Pubkey, ); err != nil { @@ -1092,9 +1117,9 @@ func (q *Queries) GetChannelsByOutpoints(ctx context.Context, outpoints []string const getChannelsByPolicyLastUpdateRange = `-- name: GetChannelsByPolicyLastUpdateRange :many SELECT - c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, - n1.id, n1.version, n1.pub_key, n1.alias, n1.last_update, n1.color, n1.signature, - n2.id, n2.version, n2.pub_key, n2.alias, n2.last_update, n2.color, n2.signature, + c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, c.signature, c.funding_pk_script, c.merkle_root_hash, + n1.id, n1.version, n1.pub_key, n1.alias, n1.last_update, n1.color, n1.signature, n1.block_height, + n2.id, n2.version, n2.pub_key, n2.alias, n2.last_update, n2.color, n2.signature, n2.block_height, -- Policy 1 (node_id_1) cp1.id AS policy1_id, @@ -1244,6 +1269,9 @@ func (q *Queries) GetChannelsByPolicyLastUpdateRange(ctx context.Context, arg Ge &i.GraphChannel.Node2Signature, &i.GraphChannel.Bitcoin1Signature, &i.GraphChannel.Bitcoin2Signature, + &i.GraphChannel.Signature, + &i.GraphChannel.FundingPkScript, + &i.GraphChannel.MerkleRootHash, &i.GraphNode.ID, &i.GraphNode.Version, &i.GraphNode.PubKey, @@ -1251,6 +1279,7 @@ func (q *Queries) GetChannelsByPolicyLastUpdateRange(ctx context.Context, arg Ge &i.GraphNode.LastUpdate, &i.GraphNode.Color, &i.GraphNode.Signature, + &i.GraphNode.BlockHeight, &i.GraphNode_2.ID, &i.GraphNode_2.Version, &i.GraphNode_2.PubKey, @@ -1258,6 +1287,7 @@ func (q *Queries) GetChannelsByPolicyLastUpdateRange(ctx context.Context, arg Ge &i.GraphNode_2.LastUpdate, &i.GraphNode_2.Color, &i.GraphNode_2.Signature, + &i.GraphNode_2.BlockHeight, &i.Policy1ID, &i.Policy1NodeID, &i.Policy1Version, @@ -1303,7 +1333,7 @@ func (q *Queries) GetChannelsByPolicyLastUpdateRange(ctx context.Context, arg Ge } const getChannelsBySCIDRange = `-- name: GetChannelsBySCIDRange :many -SELECT c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, +SELECT c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, c.signature, c.funding_pk_script, c.merkle_root_hash, n1.pub_key AS node1_pub_key, n2.pub_key AS node2_pub_key FROM graph_channels c @@ -1347,6 +1377,9 @@ func (q *Queries) GetChannelsBySCIDRange(ctx context.Context, arg GetChannelsByS &i.GraphChannel.Node2Signature, &i.GraphChannel.Bitcoin1Signature, &i.GraphChannel.Bitcoin2Signature, + &i.GraphChannel.Signature, + &i.GraphChannel.FundingPkScript, + &i.GraphChannel.MerkleRootHash, &i.Node1PubKey, &i.Node2PubKey, ); err != nil { @@ -1365,9 +1398,9 @@ func (q *Queries) GetChannelsBySCIDRange(ctx context.Context, arg GetChannelsByS const getChannelsBySCIDWithPolicies = `-- name: GetChannelsBySCIDWithPolicies :many SELECT - c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, - n1.id, n1.version, n1.pub_key, n1.alias, n1.last_update, n1.color, n1.signature, - n2.id, n2.version, n2.pub_key, n2.alias, n2.last_update, n2.color, n2.signature, + c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, c.signature, c.funding_pk_script, c.merkle_root_hash, + n1.id, n1.version, n1.pub_key, n1.alias, n1.last_update, n1.color, n1.signature, n1.block_height, + n2.id, n2.version, n2.pub_key, n2.alias, n2.last_update, n2.color, n2.signature, n2.block_height, -- Policy 1 cp1.id AS policy1_id, @@ -1490,6 +1523,9 @@ func (q *Queries) GetChannelsBySCIDWithPolicies(ctx context.Context, arg GetChan &i.GraphChannel.Node2Signature, &i.GraphChannel.Bitcoin1Signature, &i.GraphChannel.Bitcoin2Signature, + &i.GraphChannel.Signature, + &i.GraphChannel.FundingPkScript, + &i.GraphChannel.MerkleRootHash, &i.GraphNode.ID, &i.GraphNode.Version, &i.GraphNode.PubKey, @@ -1497,6 +1533,7 @@ func (q *Queries) GetChannelsBySCIDWithPolicies(ctx context.Context, arg GetChan &i.GraphNode.LastUpdate, &i.GraphNode.Color, &i.GraphNode.Signature, + &i.GraphNode.BlockHeight, &i.GraphNode_2.ID, &i.GraphNode_2.Version, &i.GraphNode_2.PubKey, @@ -1504,6 +1541,7 @@ func (q *Queries) GetChannelsBySCIDWithPolicies(ctx context.Context, arg GetChan &i.GraphNode_2.LastUpdate, &i.GraphNode_2.Color, &i.GraphNode_2.Signature, + &i.GraphNode_2.BlockHeight, &i.Policy1ID, &i.Policy1NodeID, &i.Policy1Version, @@ -1549,7 +1587,7 @@ func (q *Queries) GetChannelsBySCIDWithPolicies(ctx context.Context, arg GetChan } const getChannelsBySCIDs = `-- name: GetChannelsBySCIDs :many -SELECT id, version, scid, node_id_1, node_id_2, outpoint, capacity, bitcoin_key_1, bitcoin_key_2, node_1_signature, node_2_signature, bitcoin_1_signature, bitcoin_2_signature FROM graph_channels +SELECT id, version, scid, node_id_1, node_id_2, outpoint, capacity, bitcoin_key_1, bitcoin_key_2, node_1_signature, node_2_signature, bitcoin_1_signature, bitcoin_2_signature, signature, funding_pk_script, merkle_root_hash FROM graph_channels WHERE version = $1 AND scid IN (/*SLICE:scids*/?) ` @@ -1593,6 +1631,9 @@ func (q *Queries) GetChannelsBySCIDs(ctx context.Context, arg GetChannelsBySCIDs &i.Node2Signature, &i.Bitcoin1Signature, &i.Bitcoin2Signature, + &i.Signature, + &i.FundingPkScript, + &i.MerkleRootHash, ); err != nil { return nil, err } @@ -1756,7 +1797,7 @@ func (q *Queries) GetNodeAddressesBatch(ctx context.Context, ids []int64) ([]Gra } const getNodeByPubKey = `-- name: GetNodeByPubKey :one -SELECT id, version, pub_key, alias, last_update, color, signature +SELECT id, version, pub_key, alias, last_update, color, signature, block_height FROM graph_nodes WHERE pub_key = $1 AND version = $2 @@ -1778,6 +1819,7 @@ func (q *Queries) GetNodeByPubKey(ctx context.Context, arg GetNodeByPubKeyParams &i.LastUpdate, &i.Color, &i.Signature, + &i.BlockHeight, ) return i, err } @@ -1947,7 +1989,7 @@ func (q *Queries) GetNodeIDByPubKey(ctx context.Context, arg GetNodeIDByPubKeyPa } const getNodesByIDs = `-- name: GetNodesByIDs :many -SELECT id, version, pub_key, alias, last_update, color, signature +SELECT id, version, pub_key, alias, last_update, color, signature, block_height FROM graph_nodes WHERE id IN (/*SLICE:ids*/?) ` @@ -1979,6 +2021,7 @@ func (q *Queries) GetNodesByIDs(ctx context.Context, ids []int64) ([]GraphNode, &i.LastUpdate, &i.Color, &i.Signature, + &i.BlockHeight, ); err != nil { return nil, err } @@ -1994,7 +2037,7 @@ func (q *Queries) GetNodesByIDs(ctx context.Context, ids []int64) ([]GraphNode, } const getNodesByLastUpdateRange = `-- name: GetNodesByLastUpdateRange :many -SELECT id, version, pub_key, alias, last_update, color, signature +SELECT id, version, pub_key, alias, last_update, color, signature, block_height FROM graph_nodes WHERE last_update >= $1 AND last_update <= $2 @@ -2061,6 +2104,7 @@ func (q *Queries) GetNodesByLastUpdateRange(ctx context.Context, arg GetNodesByL &i.LastUpdate, &i.Color, &i.Signature, + &i.BlockHeight, ); err != nil { return nil, err } @@ -2143,7 +2187,7 @@ func (q *Queries) GetPruneTip(ctx context.Context) (GraphPruneLog, error) { } const getPublicV1ChannelsBySCID = `-- name: GetPublicV1ChannelsBySCID :many -SELECT id, version, scid, node_id_1, node_id_2, outpoint, capacity, bitcoin_key_1, bitcoin_key_2, node_1_signature, node_2_signature, bitcoin_1_signature, bitcoin_2_signature +SELECT id, version, scid, node_id_1, node_id_2, outpoint, capacity, bitcoin_key_1, bitcoin_key_2, node_1_signature, node_2_signature, bitcoin_1_signature, bitcoin_2_signature, signature, funding_pk_script, merkle_root_hash FROM graph_channels WHERE node_1_signature IS NOT NULL AND scid >= $1 @@ -2178,6 +2222,9 @@ func (q *Queries) GetPublicV1ChannelsBySCID(ctx context.Context, arg GetPublicV1 &i.Node2Signature, &i.Bitcoin1Signature, &i.Bitcoin2Signature, + &i.Signature, + &i.FundingPkScript, + &i.MerkleRootHash, ); err != nil { return nil, err } @@ -2704,7 +2751,7 @@ func (q *Queries) IsZombieChannel(ctx context.Context, arg IsZombieChannelParams } const listChannelsByNodeID = `-- name: ListChannelsByNodeID :many -SELECT c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, +SELECT c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, c.signature, c.funding_pk_script, c.merkle_root_hash, n1.pub_key AS node1_pubkey, n2.pub_key AS node2_pubkey, @@ -2820,6 +2867,9 @@ func (q *Queries) ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNo &i.GraphChannel.Node2Signature, &i.GraphChannel.Bitcoin1Signature, &i.GraphChannel.Bitcoin2Signature, + &i.GraphChannel.Signature, + &i.GraphChannel.FundingPkScript, + &i.GraphChannel.MerkleRootHash, &i.Node1Pubkey, &i.Node2Pubkey, &i.Policy1ID, @@ -2867,7 +2917,7 @@ func (q *Queries) ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNo } const listChannelsForNodeIDs = `-- name: ListChannelsForNodeIDs :many -SELECT c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, +SELECT c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, c.signature, c.funding_pk_script, c.merkle_root_hash, n1.pub_key AS node1_pubkey, n2.pub_key AS node2_pubkey, @@ -3004,6 +3054,9 @@ func (q *Queries) ListChannelsForNodeIDs(ctx context.Context, arg ListChannelsFo &i.GraphChannel.Node2Signature, &i.GraphChannel.Bitcoin1Signature, &i.GraphChannel.Bitcoin2Signature, + &i.GraphChannel.Signature, + &i.GraphChannel.FundingPkScript, + &i.GraphChannel.MerkleRootHash, &i.Node1Pubkey, &i.Node2Pubkey, &i.Policy1ID, @@ -3230,7 +3283,7 @@ func (q *Queries) ListChannelsWithPoliciesForCachePaginated(ctx context.Context, const listChannelsWithPoliciesPaginated = `-- name: ListChannelsWithPoliciesPaginated :many SELECT - c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, + c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity, c.bitcoin_key_1, c.bitcoin_key_2, c.node_1_signature, c.node_2_signature, c.bitcoin_1_signature, c.bitcoin_2_signature, c.signature, c.funding_pk_script, c.merkle_root_hash, -- Join node pubkeys n1.pub_key AS node1_pubkey, @@ -3347,6 +3400,9 @@ func (q *Queries) ListChannelsWithPoliciesPaginated(ctx context.Context, arg Lis &i.GraphChannel.Node2Signature, &i.GraphChannel.Bitcoin1Signature, &i.GraphChannel.Bitcoin2Signature, + &i.GraphChannel.Signature, + &i.GraphChannel.FundingPkScript, + &i.GraphChannel.MerkleRootHash, &i.Node1Pubkey, &i.Node2Pubkey, &i.Policy1ID, @@ -3436,7 +3492,7 @@ func (q *Queries) ListNodeIDsAndPubKeys(ctx context.Context, arg ListNodeIDsAndP } const listNodesPaginated = `-- name: ListNodesPaginated :many -SELECT id, version, pub_key, alias, last_update, color, signature +SELECT id, version, pub_key, alias, last_update, color, signature, block_height FROM graph_nodes WHERE version = $1 AND id > $2 ORDER BY id @@ -3466,6 +3522,7 @@ func (q *Queries) ListNodesPaginated(ctx context.Context, arg ListNodesPaginated &i.LastUpdate, &i.Color, &i.Signature, + &i.BlockHeight, ); err != nil { return nil, err } diff --git a/sqldb/sqlc/migrations/000009_graph_v2.down.sql b/sqldb/sqlc/migrations/000009_graph_v2.down.sql new file mode 100644 index 000000000..b566e4016 --- /dev/null +++ b/sqldb/sqlc/migrations/000009_graph_v2.down.sql @@ -0,0 +1,17 @@ +-- Remove the block_height column from graph_nodes +ALTER TABLE graph_nodes DROP COLUMN block_height; + +-- Remove the signature column from graph_channels +ALTER TABLE graph_channels DROP COLUMN signature; + +-- Remove the funding_pk_script column from graph_channels +ALTER TABLE graph_channels DROP COLUMN funding_pk_script; + +-- Remove the merkle_root_hash column from graph_channels +ALTER TABLE graph_channels DROP COLUMN merkle_root_hash; + +-- Remove the block_height column from graph_channel_policies +ALTER TABLE graph_channel_policies DROP COLUMN block_height; + +-- Remove the disable_flags column from graph_channel_policies +ALTER TABLE graph_channel_policies DROP COLUMN disable_flags; \ No newline at end of file diff --git a/sqldb/sqlc/migrations/000009_graph_v2.up.sql b/sqldb/sqlc/migrations/000009_graph_v2.up.sql new file mode 100644 index 000000000..19a3e99f9 --- /dev/null +++ b/sqldb/sqlc/migrations/000009_graph_v2.up.sql @@ -0,0 +1,23 @@ +-- The block height timestamp of this node's latest received node announcement. +-- It may be zero if we have not received a node announcement yet. +ALTER TABLE graph_nodes ADD COLUMN block_height BIGINT; + +-- The signature of the channel announcement. If this is null, then the channel +-- belongs to the source node and the channel has not been announced yet. +ALTER TABLE graph_channels ADD COLUMN signature BLOB; + +-- For v2 channels onwards, we cant necessarily derive the funding pk script +-- from the other fields in the announcement, so we store it here so that +-- we have easy access to it when we want to subscribe to channel closures. +ALTER TABLE graph_channels ADD COLUMN funding_pk_script BLOB; + +-- The optional merkel root hash advertised in the V2 channel announcement. +ALTER TABLE graph_channels ADD COLUMN merkle_root_hash BLOB; + +-- The block height timestamp of this channel's latest received channel-update +-- message (for v2 channel update messages). +ALTER TABLE graph_channel_policies ADD COLUMN block_height BIGINT; + +-- A bitfield describing the disabled flags for a v2 channel update. +ALTER TABLE graph_channel_policies ADD COLUMN disable_flags SMALLINT + CHECK (disable_flags >= 0 AND disable_flags <= 255); \ No newline at end of file diff --git a/sqldb/sqlc/models.go b/sqldb/sqlc/models.go index 24df0d680..359720096 100644 --- a/sqldb/sqlc/models.go +++ b/sqldb/sqlc/models.go @@ -42,6 +42,9 @@ type GraphChannel struct { Node2Signature []byte Bitcoin1Signature []byte Bitcoin2Signature []byte + Signature []byte + FundingPkScript []byte + MerkleRootHash []byte } type GraphChannelExtraType struct { @@ -72,6 +75,8 @@ type GraphChannelPolicy struct { MessageFlags sql.NullInt16 ChannelFlags sql.NullInt16 Signature []byte + BlockHeight sql.NullInt64 + DisableFlags sql.NullInt16 } type GraphChannelPolicyExtraType struct { @@ -85,13 +90,14 @@ type GraphClosedScid struct { } type GraphNode struct { - ID int64 - Version int16 - PubKey []byte - Alias sql.NullString - LastUpdate sql.NullInt64 - Color sql.NullString - Signature []byte + ID int64 + Version int16 + PubKey []byte + Alias sql.NullString + LastUpdate sql.NullInt64 + Color sql.NullString + Signature []byte + BlockHeight sql.NullInt64 } type GraphNodeAddress struct { From bc76383e158c2010a4531de8c9cb3ec655543073 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 3 Nov 2025 08:51:39 +0200 Subject: [PATCH 03/55] graph/db: rename V1Store to Store The underling store will store gossip messages across gossip versions and we will instead expose version parameters on many of the methods. So this interface really just abstracts the underlying store/schema type. --- config_builder.go | 2 +- graph/db/benchmark_test.go | 52 +++++++++--------- graph/db/graph.go | 48 ++++++++--------- graph/db/graph_test.go | 8 +-- graph/db/interfaces.go | 4 +- graph/db/kv_store.go | 6 +-- graph/db/sql_store.go | 88 +++++++++++++++---------------- graph/db/test_kvdb.go | 2 +- graph/db/test_postgres.go | 4 +- graph/db/test_sqlite.go | 4 +- itest/lnd_graph_migration_test.go | 4 +- 11 files changed, 111 insertions(+), 111 deletions(-) diff --git a/config_builder.go b/config_builder.go index 7ce63041e..3fe62cd2a 100644 --- a/config_builder.go +++ b/config_builder.go @@ -1096,7 +1096,7 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( // The graph store implementation we will use depends on whether // native SQL is enabled or not. - var graphStore graphdb.V1Store + var graphStore graphdb.Store // Instantiate a native SQL store if the flag is set. if d.cfg.DB.UseNativeSQL { diff --git a/graph/db/benchmark_test.go b/graph/db/benchmark_test.go index 4c9aef7c6..d7cc3fbc9 100644 --- a/graph/db/benchmark_test.go +++ b/graph/db/benchmark_test.go @@ -72,19 +72,19 @@ var ( // and a function to open the connection. type dbConnection struct { name string - open func(testing.TB) V1Store + open func(testing.TB) Store } // This var block defines the various database connections that we will use // for testing. Each connection is defined as a dbConnection struct that // contains a name and an open function. The open function is used to create -// a new V1Store instance for the given database type. +// a new Store instance for the given database type. var ( // kvdbBBoltConn is a connection to a kvdb-bbolt database called // channel.db. kvdbBBoltConn = dbConnection{ name: "kvdb-bbolt", - open: func(b testing.TB) V1Store { + open: func(b testing.TB) Store { return connectBBoltDB(b, bboltDBPath, kvdbBBoltFile) }, } @@ -93,7 +93,7 @@ var ( // channel.sqlite. kvdbSqliteConn = dbConnection{ name: "kvdb-sqlite", - open: func(b testing.TB) V1Store { + open: func(b testing.TB) Store { return connectKVDBSqlite( b, kvdbSqlitePath, kvdbSqliteFile, ) @@ -104,7 +104,7 @@ var ( // called lnd.sqlite. nativeSQLSqliteConn = dbConnection{ name: "native-sqlite", - open: func(b testing.TB) V1Store { + open: func(b testing.TB) Store { return connectNativeSQLite( b, sqldb.DefaultSQLiteConfig(), nativeSQLSqlitePath, nativeSQLSqliteFile, @@ -116,7 +116,7 @@ var ( // using a postgres connection string. kvdbPostgresConn = dbConnection{ name: "kvdb-postgres", - open: func(b testing.TB) V1Store { + open: func(b testing.TB) Store { return connectKVDBPostgres(b, kvdbPostgresDNS) }, } @@ -125,7 +125,7 @@ var ( // database using a postgres connection string. nativeSQLPostgresConn = dbConnection{ name: "native-postgres", - open: func(b testing.TB) V1Store { + open: func(b testing.TB) Store { return connectNativePostgres( b, sqldb.DefaultPostgresConfig(), nativeSQLPostgresDNS, @@ -134,10 +134,10 @@ var ( } ) -// connectNativePostgres creates a V1Store instance backed by a native Postgres +// connectNativePostgres creates a Store instance backed by a native Postgres // database for testing purposes. func connectNativePostgres(t testing.TB, cfg *sqldb.QueryConfig, - dsn string) V1Store { + dsn string) Store { return newSQLStore(t, cfg, sqlPostgres(t, dsn)) } @@ -157,10 +157,10 @@ func sqlPostgres(t testing.TB, dsn string) BatchedSQLQueries { return newSQLExecutor(t, store) } -// connectNativeSQLite creates a V1Store instance backed by a native SQLite +// connectNativeSQLite creates a Store instance backed by a native SQLite // database for testing purposes. func connectNativeSQLite(t testing.TB, cfg *sqldb.QueryConfig, dbPath, - file string) V1Store { + file string) Store { return newSQLStore(t, cfg, sqlSQLite(t, dbPath, file)) } @@ -205,9 +205,9 @@ func kvdbPostgres(t testing.TB, dsn string) kvdb.Backend { return kvStore } -// connectKVDBPostgres creates a V1Store instance backed by a kvdb-postgres +// connectKVDBPostgres creates a Store instance backed by a kvdb-postgres // database for testing purposes. -func connectKVDBPostgres(t testing.TB, dsn string) V1Store { +func connectKVDBPostgres(t testing.TB, dsn string) Store { return newKVStore(t, kvdbPostgres(t, dsn)) } @@ -231,14 +231,14 @@ func kvdbSqlite(t testing.TB, dbPath, fileName string) kvdb.Backend { return kvStore } -// connectKVDBSqlite creates a V1Store instance backed by a kvdb-sqlite +// connectKVDBSqlite creates a Store instance backed by a kvdb-sqlite // database for testing purposes. -func connectKVDBSqlite(t testing.TB, dbPath, fileName string) V1Store { +func connectKVDBSqlite(t testing.TB, dbPath, fileName string) Store { return newKVStore(t, kvdbSqlite(t, dbPath, fileName)) } // connectBBoltDB creates a new BBolt database connection for testing. -func connectBBoltDB(t testing.TB, dbPath, fileName string) V1Store { +func connectBBoltDB(t testing.TB, dbPath, fileName string) Store { return newKVStore(t, kvdbBBolt(t, dbPath, fileName)) } @@ -261,7 +261,7 @@ func kvdbBBolt(t testing.TB, dbPath, fileName string) kvdb.Backend { // newKVStore creates a new KVStore instance for testing using a provided // kvdb.Backend instance. -func newKVStore(t testing.TB, backend kvdb.Backend) V1Store { +func newKVStore(t testing.TB, backend kvdb.Backend) Store { store, err := NewKVStore(backend, testStoreOptions...) require.NoError(t, err) @@ -286,7 +286,7 @@ func newSQLExecutor(t testing.TB, db sqldb.DB) BatchedSQLQueries { // newSQLStore creates a new SQLStore instance for testing using a provided // sqldb.DB instance. func newSQLStore(t testing.TB, cfg *sqldb.QueryConfig, - db BatchedSQLQueries) V1Store { + db BatchedSQLQueries) Store { store, err := NewSQLStore( &SQLStoreConfig{ @@ -587,7 +587,7 @@ func BenchmarkCacheLoading(b *testing.B) { } } -// BenchmarkGraphReadMethods benchmarks various read calls of various V1Store +// BenchmarkGraphReadMethods benchmarks various read calls of various Store // implementations. // // NOTE: this is to be run against a local graph database. It can be run @@ -614,11 +614,11 @@ func BenchmarkGraphReadMethods(b *testing.B) { tests := []struct { name string - fn func(b testing.TB, store V1Store) + fn func(b testing.TB, store Store) }{ { name: "ForEachNode", - fn: func(b testing.TB, store V1Store) { + fn: func(b testing.TB, store Store) { err := store.ForEachNode( ctx, func(_ *models.Node) error { @@ -635,7 +635,7 @@ func BenchmarkGraphReadMethods(b *testing.B) { }, { name: "ForEachChannel", - fn: func(b testing.TB, store V1Store) { + fn: func(b testing.TB, store Store) { //nolint:ll err := store.ForEachChannel( ctx, func(_ *models.ChannelEdgeInfo, @@ -655,7 +655,7 @@ func BenchmarkGraphReadMethods(b *testing.B) { }, { name: "NodeUpdatesInHorizon", - fn: func(b testing.TB, store V1Store) { + fn: func(b testing.TB, store Store) { iter := store.NodeUpdatesInHorizon( time.Unix(0, 0), time.Now(), ) @@ -665,7 +665,7 @@ func BenchmarkGraphReadMethods(b *testing.B) { }, { name: "ForEachNodeCacheable", - fn: func(b testing.TB, store V1Store) { + fn: func(b testing.TB, store Store) { err := store.ForEachNodeCacheable( ctx, func(_ route.Vertex, _ *lnwire.FeatureVector) error { @@ -683,7 +683,7 @@ func BenchmarkGraphReadMethods(b *testing.B) { }, { name: "ForEachNodeCached", - fn: func(b testing.TB, store V1Store) { + fn: func(b testing.TB, store Store) { //nolint:ll err := store.ForEachNodeCached( ctx, false, func(context.Context, @@ -704,7 +704,7 @@ func BenchmarkGraphReadMethods(b *testing.B) { }, { name: "ChanUpdatesInHorizon", - fn: func(b testing.TB, store V1Store) { + fn: func(b testing.TB, store Store) { iter := store.ChanUpdatesInHorizon( time.Unix(0, 0), time.Now(), ) diff --git a/graph/db/graph.go b/graph/db/graph.go index fc1ffa300..37aad6a09 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -30,7 +30,7 @@ type ChannelGraph struct { graphCache *GraphCache - V1Store + Store *topologyManager quit chan struct{} @@ -38,7 +38,7 @@ type ChannelGraph struct { } // NewChannelGraph creates a new ChannelGraph instance with the given backend. -func NewChannelGraph(v1Store V1Store, +func NewChannelGraph(v1Store Store, options ...ChanGraphOption) (*ChannelGraph, error) { opts := defaultChanGraphOptions() @@ -47,7 +47,7 @@ func NewChannelGraph(v1Store V1Store, } g := &ChannelGraph{ - V1Store: v1Store, + Store: v1Store, topologyManager: newTopologyManager(), quit: make(chan struct{}), } @@ -161,7 +161,7 @@ func (c *ChannelGraph) populateCache(ctx context.Context) error { log.Info("Populating in-memory channel graph, this might take a " + "while...") - err := c.V1Store.ForEachNodeCacheable(ctx, func(node route.Vertex, + err := c.Store.ForEachNodeCacheable(ctx, func(node route.Vertex, features *lnwire.FeatureVector) error { c.graphCache.AddNodeFeatures(node, features) @@ -172,7 +172,7 @@ func (c *ChannelGraph) populateCache(ctx context.Context) error { return err } - err = c.V1Store.ForEachChannelCacheable( + err = c.Store.ForEachChannelCacheable( func(info *models.CachedEdgeInfo, policy1, policy2 *models.CachedEdgePolicy) error { @@ -208,7 +208,7 @@ func (c *ChannelGraph) ForEachNodeDirectedChannel(node route.Vertex, return c.graphCache.ForEachChannel(node, cb) } - return c.V1Store.ForEachNodeDirectedChannel(node, cb, reset) + return c.Store.ForEachNodeDirectedChannel(node, cb, reset) } // FetchNodeFeatures returns the features of the given node. If no features are @@ -224,7 +224,7 @@ func (c *ChannelGraph) FetchNodeFeatures(node route.Vertex) ( return c.graphCache.GetFeatures(node), nil } - return c.V1Store.FetchNodeFeatures(node) + return c.Store.FetchNodeFeatures(node) } // GraphSession will provide the call-back with access to a NodeTraverser @@ -238,7 +238,7 @@ func (c *ChannelGraph) GraphSession(cb func(graph NodeTraverser) error, return cb(c) } - return c.V1Store.GraphSession(cb, reset) + return c.Store.GraphSession(cb, reset) } // ForEachNodeCached iterates through all the stored vertices/nodes in the @@ -259,7 +259,7 @@ func (c *ChannelGraph) ForEachNodeCached(ctx context.Context, withAddrs bool, ) } - return c.V1Store.ForEachNodeCached(ctx, withAddrs, cb, reset) + return c.Store.ForEachNodeCached(ctx, withAddrs, cb, reset) } // AddNode adds a vertex/node to the graph database. If the node is not @@ -271,7 +271,7 @@ func (c *ChannelGraph) ForEachNodeCached(ctx context.Context, withAddrs bool, func (c *ChannelGraph) AddNode(ctx context.Context, node *models.Node, op ...batch.SchedulerOption) error { - err := c.V1Store.AddNode(ctx, node, op...) + err := c.Store.AddNode(ctx, node, op...) if err != nil { return err } @@ -296,7 +296,7 @@ func (c *ChannelGraph) AddNode(ctx context.Context, func (c *ChannelGraph) DeleteNode(ctx context.Context, nodePub route.Vertex) error { - err := c.V1Store.DeleteNode(ctx, nodePub) + err := c.Store.DeleteNode(ctx, nodePub) if err != nil { return err } @@ -317,7 +317,7 @@ func (c *ChannelGraph) DeleteNode(ctx context.Context, func (c *ChannelGraph) AddChannelEdge(ctx context.Context, edge *models.ChannelEdgeInfo, op ...batch.SchedulerOption) error { - err := c.V1Store.AddChannelEdge(ctx, edge, op...) + err := c.Store.AddChannelEdge(ctx, edge, op...) if err != nil { return err } @@ -339,7 +339,7 @@ func (c *ChannelGraph) AddChannelEdge(ctx context.Context, // If the cache is enabled, the edge will be added back to the graph cache if // we still have a record of this channel in the DB. func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error { - err := c.V1Store.MarkEdgeLive(chanID) + err := c.Store.MarkEdgeLive(chanID) if err != nil { return err } @@ -347,7 +347,7 @@ func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error { if c.graphCache != nil { // We need to add the channel back into our graph cache, // otherwise we won't use it for path finding. - infos, err := c.V1Store.FetchChanInfos([]uint64{chanID}) + infos, err := c.Store.FetchChanInfos([]uint64{chanID}) if err != nil { return err } @@ -385,7 +385,7 @@ func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error { func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning, markZombie bool, chanIDs ...uint64) error { - infos, err := c.V1Store.DeleteChannelEdges( + infos, err := c.Store.DeleteChannelEdges( strictZombiePruning, markZombie, chanIDs..., ) if err != nil { @@ -414,7 +414,7 @@ func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning, markZombie bool, func (c *ChannelGraph) DisconnectBlockAtHeight(height uint32) ( []*models.ChannelEdgeInfo, error) { - edges, err := c.V1Store.DisconnectBlockAtHeight(height) + edges, err := c.Store.DisconnectBlockAtHeight(height) if err != nil { return nil, err } @@ -442,7 +442,7 @@ func (c *ChannelGraph) PruneGraph(spentOutputs []*wire.OutPoint, blockHash *chainhash.Hash, blockHeight uint32) ( []*models.ChannelEdgeInfo, error) { - edges, nodes, err := c.V1Store.PruneGraph( + edges, nodes, err := c.Store.PruneGraph( spentOutputs, blockHash, blockHeight, ) if err != nil { @@ -487,7 +487,7 @@ func (c *ChannelGraph) PruneGraph(spentOutputs []*wire.OutPoint, // that we only maintain a graph of reachable nodes. In the event that a pruned // node gains more channels, it will be re-added back to the graph. func (c *ChannelGraph) PruneGraphNodes() error { - nodes, err := c.V1Store.PruneGraphNodes() + nodes, err := c.Store.PruneGraphNodes() if err != nil { return err } @@ -509,7 +509,7 @@ func (c *ChannelGraph) PruneGraphNodes() error { func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, isZombieChan func(time.Time, time.Time) bool) ([]uint64, error) { - unknown, knownZombies, err := c.V1Store.FilterKnownChanIDs(chansInfo) + unknown, knownZombies, err := c.Store.FilterKnownChanIDs(chansInfo) if err != nil { return nil, err } @@ -538,7 +538,7 @@ func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, // timestamps could bring it back from the dead, then we mark it // alive, and we let it be added to the set of IDs to query our // peer for. - err := c.V1Store.MarkEdgeLive( + err := c.Store.MarkEdgeLive( info.ShortChannelID.ToUint64(), ) // Since there is a chance that the edge could have been marked @@ -559,7 +559,7 @@ func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, func (c *ChannelGraph) MarkEdgeZombie(chanID uint64, pubKey1, pubKey2 [33]byte) error { - err := c.V1Store.MarkEdgeZombie(chanID, pubKey1, pubKey2) + err := c.Store.MarkEdgeZombie(chanID, pubKey1, pubKey2) if err != nil { return err } @@ -581,7 +581,7 @@ func (c *ChannelGraph) MarkEdgeZombie(chanID uint64, func (c *ChannelGraph) UpdateEdgePolicy(ctx context.Context, edge *models.ChannelEdgePolicy, op ...batch.SchedulerOption) error { - from, to, err := c.V1Store.UpdateEdgePolicy(ctx, edge, op...) + from, to, err := c.Store.UpdateEdgePolicy(ctx, edge, op...) if err != nil { return err } @@ -602,11 +602,11 @@ func (c *ChannelGraph) UpdateEdgePolicy(ctx context.Context, } // MakeTestGraph creates a new instance of the ChannelGraph for testing -// purposes. The backing V1Store implementation depends on the version of +// purposes. The backing Store implementation depends on the version of // NewTestDB included in the current build. // // NOTE: this is currently unused, but is left here for future use to show how -// NewTestDB can be used. As the SQL implementation of the V1Store is +// NewTestDB can be used. As the SQL implementation of the Store is // implemented, unit tests will be switched to use this function instead of // the existing MakeTestGraph helper. Once only this function is used, the // existing MakeTestGraph function will be removed and this one will be renamed. diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 3e5c93173..5cbd325cf 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -153,7 +153,7 @@ func TestNodeInsertionAndDeletion(t *testing.T) { // Check that the node's features are fetched correctly. This check // will check the database directly. - features, err = graph.V1Store.FetchNodeFeatures(node.PubKeyBytes) + features, err = graph.Store.FetchNodeFeatures(node.PubKeyBytes) require.NoError(t, err) require.Equal(t, testFeatures, features) @@ -1575,7 +1575,7 @@ func TestGraphTraversalCacheable(t *testing.T) { require.NoError(t, err) // Now skip the cache and query the DB directly. - err = graph.V1Store.ForEachNodeDirectedChannel( + err = graph.Store.ForEachNodeDirectedChannel( node, func(d *DirectedChannel) error { delete(chanIndex2, d.ChannelID) return nil @@ -3603,7 +3603,7 @@ func TestChannelEdgePruningUpdateIndexDeletion(t *testing.T) { graph := MakeTestGraph(t) // The update index only applies to the bbolt graph. - boltStore, ok := graph.V1Store.(*KVStore) + boltStore, ok := graph.Store.(*KVStore) if !ok { t.Skipf("skipping test that is aimed at a bbolt graph DB") } @@ -4161,7 +4161,7 @@ func TestEdgePolicyMissingMaxHTLC(t *testing.T) { graph := MakeTestGraph(t) // This test currently directly edits the bytes stored in the bbolt DB. - boltStore, ok := graph.V1Store.(*KVStore) + boltStore, ok := graph.Store.(*KVStore) if !ok { t.Skipf("skipping test that is aimed at a bbolt graph DB") } diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index 2d4da91f5..20408c904 100644 --- a/graph/db/interfaces.go +++ b/graph/db/interfaces.go @@ -28,9 +28,9 @@ type NodeTraverser interface { FetchNodeFeatures(nodePub route.Vertex) (*lnwire.FeatureVector, error) } -// V1Store represents the main interface for the channel graph database for all +// Store represents the main interface for the channel graph database for all // channels and nodes gossiped via the V1 gossip protocol as defined in BOLT 7. -type V1Store interface { //nolint:interfacebloat +type Store interface { //nolint:interfacebloat NodeTraverser // AddNode adds a vertex/node to the graph database. If the diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 34b21c9ab..27c3bbadc 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -199,8 +199,8 @@ type KVStore struct { } // A compile-time assertion to ensure that the KVStore struct implements the -// V1Store interface. -var _ V1Store = (*KVStore)(nil) +// Store interface. +var _ Store = (*KVStore)(nil) // NewKVStore allocates a new KVStore backed by a DB instance. The // returned instance has its own unique reject cache and channel cache. @@ -804,7 +804,7 @@ func (c *KVStore) DisabledChannelIDs() ([]uint64, error) { // returns an error, then the transaction is aborted and the iteration stops // early. // -// NOTE: this is part of the V1Store interface. +// NOTE: this is part of the Store interface. func (c *KVStore) ForEachNode(_ context.Context, cb func(*models.Node) error, reset func()) error { diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 3e2d74dc2..3e994836a 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -163,7 +163,7 @@ type BatchedSQLQueries interface { sqldb.BatchedTx[SQLQueries] } -// SQLStore is an implementation of the V1Store interface that uses a SQL +// SQLStore is an implementation of the Store interface that uses a SQL // database as the backend. type SQLStore struct { cfg *SQLStoreConfig @@ -183,9 +183,9 @@ type SQLStore struct { srcNodeMu sync.Mutex } -// A compile-time assertion to ensure that SQLStore implements the V1Store +// A compile-time assertion to ensure that SQLStore implements the Store // interface. -var _ V1Store = (*SQLStore)(nil) +var _ Store = (*SQLStore)(nil) // SQLStoreConfig holds the configuration for the SQLStore. type SQLStoreConfig struct { @@ -235,7 +235,7 @@ func NewSQLStore(cfg *SQLStoreConfig, db BatchedSQLQueries, // graph. If it is present from before, this will update that node's // information. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) AddNode(ctx context.Context, node *models.Node, opts ...batch.SchedulerOption) error { @@ -265,7 +265,7 @@ func (s *SQLStore) AddNode(ctx context.Context, // key. If the node isn't found in the database, then ErrGraphNodeNotFound is // returned. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) FetchNode(ctx context.Context, pubKey route.Vertex) (*models.Node, error) { @@ -289,7 +289,7 @@ func (s *SQLStore) FetchNode(ctx context.Context, // with a true boolean. Otherwise, an empty time.Time is returned with a false // boolean. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) HasNode(ctx context.Context, pubKey [33]byte) (time.Time, bool, error) { @@ -330,7 +330,7 @@ func (s *SQLStore) HasNode(ctx context.Context, // that the graph DB is aware of. The returned boolean indicates if the // given node is unknown to the graph DB or not. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) AddrsForNode(ctx context.Context, nodePub *btcec.PublicKey) (bool, []net.Addr, error) { @@ -372,7 +372,7 @@ func (s *SQLStore) AddrsForNode(ctx context.Context, // DeleteNode starts a new database transaction to remove a vertex/node // from the database according to the node's public key. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) DeleteNode(ctx context.Context, pubKey route.Vertex) error { @@ -423,7 +423,7 @@ func (s *SQLStore) FetchNodeFeatures(nodePub route.Vertex) ( // A channel is disabled when two of the associated ChanelEdgePolicies // have their disabled bit on. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) DisabledChannelIDs() ([]uint64, error) { var ( ctx = context.TODO() @@ -450,7 +450,7 @@ func (s *SQLStore) DisabledChannelIDs() ([]uint64, error) { // LookupAlias attempts to return the alias as advertised by the target node. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) LookupAlias(ctx context.Context, pub *btcec.PublicKey) (string, error) { @@ -488,7 +488,7 @@ func (s *SQLStore) LookupAlias(ctx context.Context, // a path finding algorithm in order to explore the reachability of another // node based off the source node. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) SourceNode(ctx context.Context) (*models.Node, error) { @@ -517,7 +517,7 @@ func (s *SQLStore) SourceNode(ctx context.Context) (*models.Node, // node is to be used as the center of a star-graph within path finding // algorithms. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) SetSourceNode(ctx context.Context, node *models.Node) error { @@ -562,7 +562,7 @@ func (s *SQLStore) SetSourceNode(ctx context.Context, // nodes to quickly determine if they have the same set of up to date node // announcements. // -// NOTE: This is part of the V1Store interface. +// NOTE: This is part of the Store interface. func (s *SQLStore) NodeUpdatesInHorizon(startTime, endTime time.Time, opts ...IteratorOption) iter.Seq2[*models.Node, error] { @@ -671,7 +671,7 @@ func (s *SQLStore) NodeUpdatesInHorizon(startTime, endTime time.Time, // supports. The chanPoint and chanID are used to uniquely identify the edge // globally within the database. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) AddChannelEdge(ctx context.Context, edge *models.ChannelEdgeInfo, opts ...batch.SchedulerOption) error { @@ -726,7 +726,7 @@ func (s *SQLStore) AddChannelEdge(ctx context.Context, // This represents the "newest" channel from the PoV of the chain. This method // can be used by peers to quickly determine if their graphs are in sync. // -// NOTE: This is part of the V1Store interface. +// NOTE: This is part of the Store interface. func (s *SQLStore) HighestChanID(ctx context.Context) (uint64, error) { var highestChanID uint64 err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { @@ -757,7 +757,7 @@ func (s *SQLStore) HighestChanID(ctx context.Context) (uint64, error) { // determined by the lexicographical ordering of the identity public keys of the // nodes on either side of the channel. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) UpdateEdgePolicy(ctx context.Context, edge *models.ChannelEdgePolicy, opts ...batch.SchedulerOption) (route.Vertex, route.Vertex, error) { @@ -855,7 +855,7 @@ func (s *SQLStore) updateEdgeCache(e *models.ChannelEdgePolicy, // channel's outpoint, whether we have a policy for the channel and the channel // peer's node information. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) ForEachSourceNodeChannel(ctx context.Context, cb func(chanPoint wire.OutPoint, havePolicy bool, otherNode *models.Node) error, reset func()) error { @@ -914,7 +914,7 @@ func (s *SQLStore) ForEachSourceNodeChannel(ctx context.Context, // returns an error, then the transaction is aborted and the iteration stops // early. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) ForEachNode(ctx context.Context, cb func(node *models.Node) error, reset func()) error { @@ -982,7 +982,7 @@ func (s *SQLStore) ForEachNodeCacheable(ctx context.Context, // // Unknown policies are passed into the callback as nil values. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) ForEachNodeChannel(ctx context.Context, nodePub route.Vertex, cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error, reset func()) error { @@ -1099,7 +1099,7 @@ func (s *SQLStore) updateChanCacheBatch(edgesToCache map[uint64]ChannelEdge) { // 5. Update cache after successful batch // 6. Repeat with updated pagination cursor until no more results // -// NOTE: This is part of the V1Store interface. +// NOTE: This is part of the Store interface. func (s *SQLStore) ChanUpdatesInHorizon(startTime, endTime time.Time, opts ...IteratorOption) iter.Seq2[ChannelEdge, error] { @@ -1268,7 +1268,7 @@ func (s *SQLStore) ChanUpdatesInHorizon(startTime, endTime time.Time, // result in an additional round-trip to the database, so it should only be used // if the addresses are actually needed. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) ForEachNodeCached(ctx context.Context, withAddrs bool, cb func(ctx context.Context, node route.Vertex, addrs []net.Addr, chans map[uint64]*DirectedChannel) error, reset func()) error { @@ -1564,7 +1564,7 @@ func (s *SQLStore) ForEachChannelCacheable(cb func(*models.CachedEdgeInfo, // for that particular channel edge routing policy will be passed into the // callback. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) ForEachChannel(ctx context.Context, cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error, reset func()) error { @@ -1582,7 +1582,7 @@ func (s *SQLStore) ForEachChannel(ctx context.Context, // timestamp info of the latest received channel update messages of the channel // will be included in the response. // -// NOTE: This is part of the V1Store interface. +// NOTE: This is part of the Store interface. func (s *SQLStore) FilterChannelRange(startHeight, endHeight uint32, withTimestamps bool) ([]BlockChannelRange, error) { @@ -1702,7 +1702,7 @@ func (s *SQLStore) FilterChannelRange(startHeight, endHeight uint32, // zombie. This method is used on an ad-hoc basis, when channels need to be // marked as zombies outside the normal pruning cycle. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) MarkEdgeZombie(chanID uint64, pubKey1, pubKey2 [33]byte) error { @@ -1736,7 +1736,7 @@ func (s *SQLStore) MarkEdgeZombie(chanID uint64, // MarkEdgeLive clears an edge from our zombie index, deeming it as live. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) MarkEdgeLive(chanID uint64) error { s.cacheMu.Lock() defer s.cacheMu.Unlock() @@ -1787,7 +1787,7 @@ func (s *SQLStore) MarkEdgeLive(chanID uint64) error { // zombie, then the two node public keys corresponding to this edge are also // returned. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte, error) { @@ -1830,7 +1830,7 @@ func (s *SQLStore) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte, // NumZombies returns the current number of zombie channels in the graph. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) NumZombies() (uint64, error) { var ( ctx = context.TODO() @@ -1865,7 +1865,7 @@ func (s *SQLStore) NumZombies() (uint64, error) { // that resurrects the channel from its zombie state. The markZombie bool // denotes whether to mark the channel as a zombie. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, chanIDs ...uint64) ([]*models.ChannelEdgeInfo, error) { @@ -1970,7 +1970,7 @@ func (s *SQLStore) DeleteChannelEdges(strictZombiePruning, markZombie bool, // within the database. In this case, the ChannelEdgePolicy's will be nil, and // the ChannelEdgeInfo will only include the public keys of each node. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) FetchChannelEdgesByID(chanID uint64) ( *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { @@ -2067,7 +2067,7 @@ func (s *SQLStore) FetchChannelEdgesByID(chanID uint64) ( // information for the channel itself is returned as well as two structs that // contain the routing policies for the channel in either direction. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { @@ -2137,7 +2137,7 @@ func (s *SQLStore) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( // it is not found, then the zombie index is checked and its result is returned // as the second boolean. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) HasChannelEdge(chanID uint64) (time.Time, time.Time, bool, bool, error) { @@ -2253,7 +2253,7 @@ func (s *SQLStore) HasChannelEdge(chanID uint64) (time.Time, time.Time, bool, // passed channel point (outpoint). If the passed channel doesn't exist within // the database, then ErrEdgeNotFound is returned. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) ChannelID(chanPoint *wire.OutPoint) (uint64, error) { var ( ctx = context.TODO() @@ -2288,7 +2288,7 @@ func (s *SQLStore) ChannelID(chanPoint *wire.OutPoint) (uint64, error) { // given public key is seen as a public node in the graph from the graph's // source node's point of view. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) IsPublicNode(pubKey [33]byte) (bool, error) { ctx := context.TODO() @@ -2313,7 +2313,7 @@ func (s *SQLStore) IsPublicNode(pubKey [33]byte) (bool, error) { // of the query. This can be used to respond to peer queries that are seeking to // fill in gaps in their view of the channel graph. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) FetchChanInfos(chanIDs []uint64) ([]ChannelEdge, error) { var ( ctx = context.TODO() @@ -2407,7 +2407,7 @@ func (s *SQLStore) forEachChanWithPoliciesInSCIDList(ctx context.Context, // channels another peer knows of that we don't. The ChannelUpdateInfos for the // known zombies is also returned. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo) ([]uint64, []ChannelUpdateInfo, error) { @@ -2531,7 +2531,7 @@ func (s *SQLStore) forEachChanInSCIDList(ctx context.Context, db SQLQueries, // NOTE: this prunes nodes across protocol versions. It will never prune the // source nodes. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) PruneGraphNodes() ([]route.Vertex, error) { var ctx = context.TODO() @@ -2560,7 +2560,7 @@ func (s *SQLStore) PruneGraphNodes() ([]route.Vertex, error) { // the target block along with any pruned nodes are returned if the function // succeeds without error. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) PruneGraph(spentOutputs []*wire.OutPoint, blockHash *chainhash.Hash, blockHeight uint32) ( []*models.ChannelEdgeInfo, []route.Vertex, error) { @@ -2715,7 +2715,7 @@ func (s *SQLStore) deleteChannels(ctx context.Context, db SQLQueries, // returned are the ones that need to be watched on chain to detect channel // closes on the resident blockchain. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) ChannelView() ([]EdgePoint, error) { var ( ctx = context.TODO() @@ -2781,7 +2781,7 @@ func (s *SQLStore) ChannelView() ([]EdgePoint, error) { // to tell if the graph is currently in sync with the current best known UTXO // state. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) PruneTip() (*chainhash.Hash, uint32, error) { var ( ctx = context.TODO() @@ -2843,7 +2843,7 @@ func (s *SQLStore) pruneGraphNodes(ctx context.Context, // Channels that were removed from the graph resulting from the // disconnected block are returned. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) DisconnectBlockAtHeight(height uint32) ( []*models.ChannelEdgeInfo, error) { @@ -2931,7 +2931,7 @@ func (s *SQLStore) DisconnectBlockAtHeight(height uint32) ( // AddEdgeProof sets the proof of an existing edge in the graph database. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) AddEdgeProof(scid lnwire.ShortChannelID, proof *models.ChannelAuthProof) error { @@ -2981,7 +2981,7 @@ func (s *SQLStore) AddEdgeProof(scid lnwire.ShortChannelID, // that we can ignore channel announcements that we know to be closed without // having to validate them and fetch a block. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) PutClosedScid(scid lnwire.ShortChannelID) error { var ( ctx = context.TODO() @@ -2996,7 +2996,7 @@ func (s *SQLStore) PutClosedScid(scid lnwire.ShortChannelID) error { // IsClosedScid checks whether a channel identified by the passed in scid is // closed. This helps avoid having to perform expensive validation checks. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) IsClosedScid(scid lnwire.ShortChannelID) (bool, error) { var ( ctx = context.TODO() @@ -3024,7 +3024,7 @@ func (s *SQLStore) IsClosedScid(scid lnwire.ShortChannelID) (bool, error) { // GraphSession will provide the call-back with access to a NodeTraverser // instance which can be used to perform queries against the channel graph. // -// NOTE: part of the V1Store interface. +// NOTE: part of the Store interface. func (s *SQLStore) GraphSession(cb func(graph NodeTraverser) error, reset func()) error { diff --git a/graph/db/test_kvdb.go b/graph/db/test_kvdb.go index f325d41f0..569e0949f 100644 --- a/graph/db/test_kvdb.go +++ b/graph/db/test_kvdb.go @@ -10,7 +10,7 @@ import ( ) // NewTestDB is a helper function that creates an BBolt database for testing. -func NewTestDB(t testing.TB) V1Store { +func NewTestDB(t testing.TB) Store { backend, backendCleanup, err := kvdb.GetTestBackend(t.TempDir(), "cgr") require.NoError(t, err) diff --git a/graph/db/test_postgres.go b/graph/db/test_postgres.go index 6134f0114..716210afa 100644 --- a/graph/db/test_postgres.go +++ b/graph/db/test_postgres.go @@ -13,7 +13,7 @@ import ( // NewTestDB is a helper function that creates a SQLStore backed by a SQL // database for testing. -func NewTestDB(t testing.TB) V1Store { +func NewTestDB(t testing.TB) Store { return NewTestDBWithFixture(t, nil) } @@ -31,7 +31,7 @@ func NewTestDBFixture(t *testing.T) *sqldb.TestPgFixture { // NewTestDBWithFixture is a helper function that creates a SQLStore backed by a // SQL database for testing. func NewTestDBWithFixture(t testing.TB, - pgFixture *sqldb.TestPgFixture) V1Store { + pgFixture *sqldb.TestPgFixture) Store { var querier BatchedSQLQueries if pgFixture == nil { diff --git a/graph/db/test_sqlite.go b/graph/db/test_sqlite.go index c1c6d808f..68fef6eb9 100644 --- a/graph/db/test_sqlite.go +++ b/graph/db/test_sqlite.go @@ -13,7 +13,7 @@ import ( // NewTestDB is a helper function that creates a SQLStore backed by a SQL // database for testing. -func NewTestDB(t testing.TB) V1Store { +func NewTestDB(t testing.TB) Store { return NewTestDBWithFixture(t, nil) } @@ -24,7 +24,7 @@ func NewTestDBFixture(_ *testing.T) *sqldb.TestPgFixture { // NewTestDBWithFixture is a helper function that creates a SQLStore backed by a // SQL database for testing. -func NewTestDBWithFixture(t testing.TB, _ *sqldb.TestPgFixture) V1Store { +func NewTestDBWithFixture(t testing.TB, _ *sqldb.TestPgFixture) Store { store, err := NewSQLStore( &SQLStoreConfig{ ChainHash: *chaincfg.MainNetParams.GenesisHash, diff --git a/itest/lnd_graph_migration_test.go b/itest/lnd_graph_migration_test.go index 81fe65e87..d4a17a424 100644 --- a/itest/lnd_graph_migration_test.go +++ b/itest/lnd_graph_migration_test.go @@ -60,7 +60,7 @@ func testGraphMigration(ht *lntest.HarnessTest) { // assertDBState is a helper function that asserts the state of the // graph DB. - assertDBState := func(db graphdb.V1Store) { + assertDBState := func(db graphdb.Store) { var ( numNodes int edges = make(map[uint64]bool) @@ -127,7 +127,7 @@ func testGraphMigration(ht *lntest.HarnessTest) { } func openNativeSQLGraphDB(ht *lntest.HarnessTest, - hn *node.HarnessNode) graphdb.V1Store { + hn *node.HarnessNode) graphdb.Store { db := openNativeSQLDB(ht, hn) From 3dfc835354f7787ae7ebccf567335c7f00396054 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 26 Oct 2025 12:05:08 +0200 Subject: [PATCH 04/55] graph/db: add V2 Node constructor We add a new NewV2Node constructor which takes a new NodeV2Fields as a parameter. This NodeV2Fields struct defines the fields that can be set in a models.Node if the version is V2. --- graph/db/models/node.go | 59 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/graph/db/models/node.go b/graph/db/models/node.go index 9a045d53c..7f3ce777a 100644 --- a/graph/db/models/node.go +++ b/graph/db/models/node.go @@ -27,6 +27,11 @@ type Node struct { // been updated. LastUpdate time.Time + // LastBlockHeight is the block height that timestamps the last update + // we received for this node. This is only used if this is a V2 node + // announcement. + LastBlockHeight uint32 + // Address is the TCP address this node is reachable over. Addresses []net.Addr @@ -49,8 +54,13 @@ type Node struct { // 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. + // compatible manner. This is only used for V1 node announcements. ExtraOpaqueData []byte + + // ExtraSignedFields is a map of extra fields that are covered by the + // node announcement's signature that we have not explicitly parsed. + // This is only used for version 2 node announcements and beyond. + ExtraSignedFields map[uint64][]byte } // NodeV1Fields houses the fields that are specific to a version 1 node @@ -103,6 +113,53 @@ func NewV1Node(pub route.Vertex, n *NodeV1Fields) *Node { } } +// NodeV2Fields houses the fields that are specific to a version 2 node +// announcement. +type NodeV2Fields struct { + // LastBlockHeight is the block height that timestamps the last update + // we received for this node. + LastBlockHeight uint32 + + // Address is the TCP address this node is reachable over. + Addresses []net.Addr + + // Color is the selected color for the node. + Color fn.Option[color.RGBA] + + // Alias is a nick-name for the node. The alias can be used to confirm + // a node's identity or to serve as a short ID for an address book. + Alias fn.Option[string] + + // Signature is the schnorr signature under the advertised public key + // which serves to authenticate the attributes announced by this node. + Signature []byte + + // Features is the list of protocol features supported by this node. + Features *lnwire.RawFeatureVector + + // ExtraSignedFields is a map of extra fields that are covered by the + // node announcement's signature that we have not explicitly parsed. + ExtraSignedFields map[uint64][]byte +} + +// NewV2Node creates a new version 2 node from the passed fields. +func NewV2Node(pub route.Vertex, n *NodeV2Fields) *Node { + return &Node{ + Version: lnwire.GossipVersion2, + PubKeyBytes: pub, + Addresses: n.Addresses, + AuthSigBytes: n.Signature, + Features: lnwire.NewFeatureVector( + n.Features, lnwire.Features, + ), + LastBlockHeight: n.LastBlockHeight, + Color: n.Color, + Alias: n.Alias, + LastUpdate: time.Unix(0, 0), + ExtraSignedFields: n.ExtraSignedFields, + } +} + // NewV1ShellNode creates a new shell version 1 node. func NewV1ShellNode(pubKey route.Vertex) *Node { return NewShellNode(lnwire.GossipVersion1, pubKey) From c61617200b7830994bf85fc38485241e513391a6 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 26 Oct 2025 12:16:13 +0200 Subject: [PATCH 05/55] sqldb: update node query for v2 Here we update the UpdateNode query so that it can be used to insert the new blockheight field for a v2 node. --- sqldb/sqlc/graph.sql.go | 46 ++++++++++++++++++++++-------------- sqldb/sqlc/queries/graph.sql | 18 +++++++++----- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index 8b10e4967..cdf15a90b 100644 --- a/sqldb/sqlc/graph.sql.go +++ b/sqldb/sqlc/graph.sql.go @@ -3673,9 +3673,9 @@ const upsertNode = `-- name: UpsertNode :one */ INSERT INTO graph_nodes ( - version, pub_key, alias, last_update, color, signature + version, pub_key, alias, last_update, block_height, color, signature ) VALUES ( - $1, $2, $3, $4, $5, $6 + $1, $2, $3, $4, $5, $6, $7 ) ON CONFLICT (pub_key, version) -- Update the following fields if a conflict occurs on pub_key @@ -3683,20 +3683,24 @@ ON CONFLICT (pub_key, version) DO UPDATE SET alias = EXCLUDED.alias, last_update = EXCLUDED.last_update, + block_height = EXCLUDED.block_height, color = EXCLUDED.color, signature = EXCLUDED.signature -WHERE graph_nodes.last_update IS NULL - OR EXCLUDED.last_update > graph_nodes.last_update +WHERE (graph_nodes.last_update IS NULL + OR EXCLUDED.last_update > graph_nodes.last_update) +AND (graph_nodes.block_height IS NULL + OR EXCLUDED.block_height >= graph_nodes.block_height) RETURNING id ` type UpsertNodeParams struct { - Version int16 - PubKey []byte - Alias sql.NullString - LastUpdate sql.NullInt64 - Color sql.NullString - Signature []byte + Version int16 + PubKey []byte + Alias sql.NullString + LastUpdate sql.NullInt64 + BlockHeight sql.NullInt64 + Color sql.NullString + Signature []byte } func (q *Queries) UpsertNode(ctx context.Context, arg UpsertNodeParams) (int64, error) { @@ -3705,6 +3709,7 @@ func (q *Queries) UpsertNode(ctx context.Context, arg UpsertNodeParams) (int64, arg.PubKey, arg.Alias, arg.LastUpdate, + arg.BlockHeight, arg.Color, arg.Signature, ) @@ -3801,9 +3806,9 @@ func (q *Queries) UpsertPruneLogEntry(ctx context.Context, arg UpsertPruneLogEnt const upsertSourceNode = `-- name: UpsertSourceNode :one INSERT INTO graph_nodes ( - version, pub_key, alias, last_update, color, signature + version, pub_key, alias, last_update, block_height, color, signature ) VALUES ( - $1, $2, $3, $4, $5, $6 + $1, $2, $3, $4, $5, $6, $7 ) ON CONFLICT (pub_key, version) -- Update the following fields if a conflict occurs on pub_key @@ -3811,20 +3816,24 @@ ON CONFLICT (pub_key, version) DO UPDATE SET alias = EXCLUDED.alias, last_update = EXCLUDED.last_update, + block_height = EXCLUDED.block_height, color = EXCLUDED.color, signature = EXCLUDED.signature WHERE graph_nodes.last_update IS NULL OR EXCLUDED.last_update >= graph_nodes.last_update +AND (graph_nodes.block_height IS NULL + OR EXCLUDED.block_height >= graph_nodes.block_height) RETURNING id ` type UpsertSourceNodeParams struct { - Version int16 - PubKey []byte - Alias sql.NullString - LastUpdate sql.NullInt64 - Color sql.NullString - Signature []byte + Version int16 + PubKey []byte + Alias sql.NullString + LastUpdate sql.NullInt64 + BlockHeight sql.NullInt64 + Color sql.NullString + Signature []byte } // We use a separate upsert for our own node since we want to be less strict @@ -3836,6 +3845,7 @@ func (q *Queries) UpsertSourceNode(ctx context.Context, arg UpsertSourceNodePara arg.PubKey, arg.Alias, arg.LastUpdate, + arg.BlockHeight, arg.Color, arg.Signature, ) diff --git a/sqldb/sqlc/queries/graph.sql b/sqldb/sqlc/queries/graph.sql index a8ff040d9..480437655 100644 --- a/sqldb/sqlc/queries/graph.sql +++ b/sqldb/sqlc/queries/graph.sql @@ -5,9 +5,9 @@ -- name: UpsertNode :one INSERT INTO graph_nodes ( - version, pub_key, alias, last_update, color, signature + version, pub_key, alias, last_update, block_height, color, signature ) VALUES ( - $1, $2, $3, $4, $5, $6 + $1, $2, $3, $4, $5, $6, $7 ) ON CONFLICT (pub_key, version) -- Update the following fields if a conflict occurs on pub_key @@ -15,10 +15,13 @@ ON CONFLICT (pub_key, version) DO UPDATE SET alias = EXCLUDED.alias, last_update = EXCLUDED.last_update, + block_height = EXCLUDED.block_height, color = EXCLUDED.color, signature = EXCLUDED.signature -WHERE graph_nodes.last_update IS NULL - OR EXCLUDED.last_update > graph_nodes.last_update +WHERE (graph_nodes.last_update IS NULL + OR EXCLUDED.last_update > graph_nodes.last_update) +AND (graph_nodes.block_height IS NULL + OR EXCLUDED.block_height >= graph_nodes.block_height) RETURNING id; -- We use a separate upsert for our own node since we want to be less strict @@ -26,9 +29,9 @@ RETURNING id; -- update the record even if the last_update is the same as what we have. -- name: UpsertSourceNode :one INSERT INTO graph_nodes ( - version, pub_key, alias, last_update, color, signature + version, pub_key, alias, last_update, block_height, color, signature ) VALUES ( - $1, $2, $3, $4, $5, $6 + $1, $2, $3, $4, $5, $6, $7 ) ON CONFLICT (pub_key, version) -- Update the following fields if a conflict occurs on pub_key @@ -36,10 +39,13 @@ ON CONFLICT (pub_key, version) DO UPDATE SET alias = EXCLUDED.alias, last_update = EXCLUDED.last_update, + block_height = EXCLUDED.block_height, color = EXCLUDED.color, signature = EXCLUDED.signature WHERE graph_nodes.last_update IS NULL OR EXCLUDED.last_update >= graph_nodes.last_update +AND (graph_nodes.block_height IS NULL + OR EXCLUDED.block_height >= graph_nodes.block_height) RETURNING id; -- name: GetNodesByIDs :many From 628b89cc40f34dc6840d10a4f856fa7b509cdd6e Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 26 Oct 2025 12:31:49 +0200 Subject: [PATCH 06/55] graph/db: explicit redirect to Store Instead of embedding Store in ChannelGraph so that any methods of the Store interface not implemented by the ChannelGraph are redirected to the underlying Store, we update things in this commit to instead make the "redirection" explicit. This is in preparation for changes we will make soon where some underlying store methods will take an explicit "version" parameter but then we will keep the ChannelGraph methods as is so that existing call-sites dont all need to be updated. We will then add "Versioned" ChannelGraph wrapper which decides the version use. Initially, most call-sites will just create a wrapped V1 ChannelGraph so that the logic remains as it is today. --- graph/db/graph.go | 243 +++++++++++++++++++++++++++++++++++++---- graph/db/graph_test.go | 8 +- 2 files changed, 226 insertions(+), 25 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index 37aad6a09..13b9af300 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -4,12 +4,14 @@ import ( "context" "errors" "fmt" + "iter" "net" "sync" "sync/atomic" "testing" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/batch" @@ -30,7 +32,7 @@ type ChannelGraph struct { graphCache *GraphCache - Store + db Store *topologyManager quit chan struct{} @@ -47,7 +49,7 @@ func NewChannelGraph(v1Store Store, } g := &ChannelGraph{ - Store: v1Store, + db: v1Store, topologyManager: newTopologyManager(), quit: make(chan struct{}), } @@ -161,7 +163,7 @@ func (c *ChannelGraph) populateCache(ctx context.Context) error { log.Info("Populating in-memory channel graph, this might take a " + "while...") - err := c.Store.ForEachNodeCacheable(ctx, func(node route.Vertex, + err := c.db.ForEachNodeCacheable(ctx, func(node route.Vertex, features *lnwire.FeatureVector) error { c.graphCache.AddNodeFeatures(node, features) @@ -172,7 +174,7 @@ func (c *ChannelGraph) populateCache(ctx context.Context) error { return err } - err = c.Store.ForEachChannelCacheable( + err = c.db.ForEachChannelCacheable( func(info *models.CachedEdgeInfo, policy1, policy2 *models.CachedEdgePolicy) error { @@ -208,7 +210,7 @@ func (c *ChannelGraph) ForEachNodeDirectedChannel(node route.Vertex, return c.graphCache.ForEachChannel(node, cb) } - return c.Store.ForEachNodeDirectedChannel(node, cb, reset) + return c.db.ForEachNodeDirectedChannel(node, cb, reset) } // FetchNodeFeatures returns the features of the given node. If no features are @@ -224,7 +226,7 @@ func (c *ChannelGraph) FetchNodeFeatures(node route.Vertex) ( return c.graphCache.GetFeatures(node), nil } - return c.Store.FetchNodeFeatures(node) + return c.db.FetchNodeFeatures(node) } // GraphSession will provide the call-back with access to a NodeTraverser @@ -238,7 +240,7 @@ func (c *ChannelGraph) GraphSession(cb func(graph NodeTraverser) error, return cb(c) } - return c.Store.GraphSession(cb, reset) + return c.db.GraphSession(cb, reset) } // ForEachNodeCached iterates through all the stored vertices/nodes in the @@ -259,7 +261,7 @@ func (c *ChannelGraph) ForEachNodeCached(ctx context.Context, withAddrs bool, ) } - return c.Store.ForEachNodeCached(ctx, withAddrs, cb, reset) + return c.db.ForEachNodeCached(ctx, withAddrs, cb, reset) } // AddNode adds a vertex/node to the graph database. If the node is not @@ -271,7 +273,7 @@ func (c *ChannelGraph) ForEachNodeCached(ctx context.Context, withAddrs bool, func (c *ChannelGraph) AddNode(ctx context.Context, node *models.Node, op ...batch.SchedulerOption) error { - err := c.Store.AddNode(ctx, node, op...) + err := c.db.AddNode(ctx, node, op...) if err != nil { return err } @@ -296,7 +298,7 @@ func (c *ChannelGraph) AddNode(ctx context.Context, func (c *ChannelGraph) DeleteNode(ctx context.Context, nodePub route.Vertex) error { - err := c.Store.DeleteNode(ctx, nodePub) + err := c.db.DeleteNode(ctx, nodePub) if err != nil { return err } @@ -317,7 +319,7 @@ func (c *ChannelGraph) DeleteNode(ctx context.Context, func (c *ChannelGraph) AddChannelEdge(ctx context.Context, edge *models.ChannelEdgeInfo, op ...batch.SchedulerOption) error { - err := c.Store.AddChannelEdge(ctx, edge, op...) + err := c.db.AddChannelEdge(ctx, edge, op...) if err != nil { return err } @@ -339,7 +341,7 @@ func (c *ChannelGraph) AddChannelEdge(ctx context.Context, // If the cache is enabled, the edge will be added back to the graph cache if // we still have a record of this channel in the DB. func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error { - err := c.Store.MarkEdgeLive(chanID) + err := c.db.MarkEdgeLive(chanID) if err != nil { return err } @@ -347,7 +349,7 @@ func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error { if c.graphCache != nil { // We need to add the channel back into our graph cache, // otherwise we won't use it for path finding. - infos, err := c.Store.FetchChanInfos([]uint64{chanID}) + infos, err := c.db.FetchChanInfos([]uint64{chanID}) if err != nil { return err } @@ -385,7 +387,7 @@ func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error { func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning, markZombie bool, chanIDs ...uint64) error { - infos, err := c.Store.DeleteChannelEdges( + infos, err := c.db.DeleteChannelEdges( strictZombiePruning, markZombie, chanIDs..., ) if err != nil { @@ -414,7 +416,7 @@ func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning, markZombie bool, func (c *ChannelGraph) DisconnectBlockAtHeight(height uint32) ( []*models.ChannelEdgeInfo, error) { - edges, err := c.Store.DisconnectBlockAtHeight(height) + edges, err := c.db.DisconnectBlockAtHeight(height) if err != nil { return nil, err } @@ -442,7 +444,7 @@ func (c *ChannelGraph) PruneGraph(spentOutputs []*wire.OutPoint, blockHash *chainhash.Hash, blockHeight uint32) ( []*models.ChannelEdgeInfo, error) { - edges, nodes, err := c.Store.PruneGraph( + edges, nodes, err := c.db.PruneGraph( spentOutputs, blockHash, blockHeight, ) if err != nil { @@ -487,7 +489,7 @@ func (c *ChannelGraph) PruneGraph(spentOutputs []*wire.OutPoint, // that we only maintain a graph of reachable nodes. In the event that a pruned // node gains more channels, it will be re-added back to the graph. func (c *ChannelGraph) PruneGraphNodes() error { - nodes, err := c.Store.PruneGraphNodes() + nodes, err := c.db.PruneGraphNodes() if err != nil { return err } @@ -509,7 +511,7 @@ func (c *ChannelGraph) PruneGraphNodes() error { func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, isZombieChan func(time.Time, time.Time) bool) ([]uint64, error) { - unknown, knownZombies, err := c.Store.FilterKnownChanIDs(chansInfo) + unknown, knownZombies, err := c.db.FilterKnownChanIDs(chansInfo) if err != nil { return nil, err } @@ -538,7 +540,7 @@ func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, // timestamps could bring it back from the dead, then we mark it // alive, and we let it be added to the set of IDs to query our // peer for. - err := c.Store.MarkEdgeLive( + err := c.db.MarkEdgeLive( info.ShortChannelID.ToUint64(), ) // Since there is a chance that the edge could have been marked @@ -559,7 +561,7 @@ func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo, func (c *ChannelGraph) MarkEdgeZombie(chanID uint64, pubKey1, pubKey2 [33]byte) error { - err := c.Store.MarkEdgeZombie(chanID, pubKey1, pubKey2) + err := c.db.MarkEdgeZombie(chanID, pubKey1, pubKey2) if err != nil { return err } @@ -581,7 +583,7 @@ func (c *ChannelGraph) MarkEdgeZombie(chanID uint64, func (c *ChannelGraph) UpdateEdgePolicy(ctx context.Context, edge *models.ChannelEdgePolicy, op ...batch.SchedulerOption) error { - from, to, err := c.Store.UpdateEdgePolicy(ctx, edge, op...) + from, to, err := c.db.UpdateEdgePolicy(ctx, edge, op...) if err != nil { return err } @@ -601,6 +603,205 @@ func (c *ChannelGraph) UpdateEdgePolicy(ctx context.Context, return nil } +// AddrsForNode returns all known addresses for the target node public key. +func (c *ChannelGraph) AddrsForNode(ctx context.Context, + nodePub *btcec.PublicKey) (bool, []net.Addr, error) { + + return c.db.AddrsForNode(ctx, nodePub) +} + +// ForEachSourceNodeChannel iterates through all channels of the source node. +func (c *ChannelGraph) ForEachSourceNodeChannel(ctx context.Context, + cb func(chanPoint wire.OutPoint, havePolicy bool, + otherNode *models.Node) error, reset func()) error { + + return c.db.ForEachSourceNodeChannel(ctx, cb, reset) +} + +// ForEachNodeChannel iterates through all channels of the given node. +func (c *ChannelGraph) ForEachNodeChannel(ctx context.Context, + nodePub route.Vertex, cb func(*models.ChannelEdgeInfo, + *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy) error, reset func()) error { + + return c.db.ForEachNodeChannel(ctx, nodePub, cb, reset) +} + +// ForEachNode iterates through all stored vertices/nodes in the graph. +func (c *ChannelGraph) ForEachNode(ctx context.Context, + cb func(*models.Node) error, reset func()) error { + + return c.db.ForEachNode(ctx, cb, reset) +} + +// ForEachNodeCacheable iterates through all stored vertices/nodes in the graph. +func (c *ChannelGraph) ForEachNodeCacheable(ctx context.Context, + cb func(route.Vertex, *lnwire.FeatureVector) error, + reset func()) error { + + return c.db.ForEachNodeCacheable(ctx, cb, reset) +} + +// LookupAlias attempts to return the alias as advertised by the target node. +func (c *ChannelGraph) LookupAlias(ctx context.Context, + pub *btcec.PublicKey) (string, error) { + + return c.db.LookupAlias(ctx, pub) +} + +// NodeUpdatesInHorizon returns all known lightning nodes with updates in the +// range. +func (c *ChannelGraph) NodeUpdatesInHorizon(startTime, endTime time.Time, + opts ...IteratorOption) iter.Seq2[*models.Node, error] { + + return c.db.NodeUpdatesInHorizon(startTime, endTime, opts...) +} + +// FetchNode attempts to look up a target node by its identity public key. +func (c *ChannelGraph) FetchNode(ctx context.Context, + nodePub route.Vertex) (*models.Node, error) { + + return c.db.FetchNode(ctx, nodePub) +} + +// HasNode determines if the graph has a vertex identified by the target node. +func (c *ChannelGraph) HasNode(ctx context.Context, + nodePub [33]byte) (time.Time, bool, error) { + + return c.db.HasNode(ctx, nodePub) +} + +// 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) +} + +// ForEachChannel iterates through all channel edges stored within the graph. +func (c *ChannelGraph) ForEachChannel(ctx context.Context, + cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy) error, reset func()) error { + + return c.db.ForEachChannel(ctx, cb, reset) +} + +// ForEachChannelCacheable iterates through all channel edges for the cache. +func (c *ChannelGraph) ForEachChannelCacheable(cb func(*models.CachedEdgeInfo, + *models.CachedEdgePolicy, *models.CachedEdgePolicy) error, + reset func()) error { + + return c.db.ForEachChannelCacheable(cb, reset) +} + +// DisabledChannelIDs returns the channel ids of disabled channels. +func (c *ChannelGraph) DisabledChannelIDs() ([]uint64, error) { + return c.db.DisabledChannelIDs() +} + +// HasChannelEdge returns true if the database knows of a channel edge. +func (c *ChannelGraph) HasChannelEdge(chanID uint64) (time.Time, time.Time, + bool, bool, error) { + + return c.db.HasChannelEdge(chanID) +} + +// AddEdgeProof sets the proof of an existing edge in the graph database. +func (c *ChannelGraph) AddEdgeProof(chanID lnwire.ShortChannelID, + proof *models.ChannelAuthProof) error { + + return c.db.AddEdgeProof(chanID, proof) +} + +// ChannelID attempts to lookup the 8-byte compact channel ID. +func (c *ChannelGraph) ChannelID(chanPoint *wire.OutPoint) (uint64, error) { + return c.db.ChannelID(chanPoint) +} + +// HighestChanID returns the "highest" known channel ID in the channel graph. +func (c *ChannelGraph) HighestChanID(ctx context.Context) (uint64, error) { + return c.db.HighestChanID(ctx) +} + +// ChanUpdatesInHorizon returns all known channel edges with updates in the +// horizon. +func (c *ChannelGraph) ChanUpdatesInHorizon(startTime, endTime time.Time, + opts ...IteratorOption) iter.Seq2[ChannelEdge, error] { + + return c.db.ChanUpdatesInHorizon(startTime, endTime, opts...) +} + +// FilterChannelRange returns channel IDs within the passed block height range. +func (c *ChannelGraph) FilterChannelRange(startHeight, endHeight uint32, + withTimestamps bool) ([]BlockChannelRange, error) { + + return c.db.FilterChannelRange(startHeight, endHeight, withTimestamps) +} + +// FetchChanInfos returns the set of channel edges for the passed channel IDs. +func (c *ChannelGraph) FetchChanInfos(chanIDs []uint64) ([]ChannelEdge, error) { + return c.db.FetchChanInfos(chanIDs) +} + +// FetchChannelEdgesByOutpoint attempts to lookup directed edges by funding +// outpoint. +func (c *ChannelGraph) FetchChannelEdgesByOutpoint(op *wire.OutPoint) ( + *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy, error) { + + return c.db.FetchChannelEdgesByOutpoint(op) +} + +// FetchChannelEdgesByID attempts to lookup directed edges by channel ID. +func (c *ChannelGraph) FetchChannelEdgesByID(chanID uint64) ( + *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy, error) { + + return c.db.FetchChannelEdgesByID(chanID) +} + +// ChannelView returns the verifiable edge information for each active channel. +func (c *ChannelGraph) ChannelView() ([]EdgePoint, error) { + return c.db.ChannelView() +} + +// IsZombieEdge returns whether the edge is considered zombie. +func (c *ChannelGraph) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte, + error) { + + return c.db.IsZombieEdge(chanID) +} + +// NumZombies returns the current number of zombie channels in the graph. +func (c *ChannelGraph) NumZombies() (uint64, error) { + return c.db.NumZombies() +} + +// PutClosedScid stores a SCID for a closed channel in the database. +func (c *ChannelGraph) PutClosedScid(scid lnwire.ShortChannelID) error { + return c.db.PutClosedScid(scid) +} + +// IsClosedScid checks whether a channel identified by the scid is closed. +func (c *ChannelGraph) IsClosedScid(scid lnwire.ShortChannelID) (bool, error) { + return c.db.IsClosedScid(scid) +} + +// SourceNode returns the source node of the graph. +func (c *ChannelGraph) SourceNode(ctx context.Context) (*models.Node, error) { + return c.db.SourceNode(ctx) +} + +// SetSourceNode sets the source node within the graph database. +func (c *ChannelGraph) SetSourceNode(ctx context.Context, + node *models.Node) error { + + return c.db.SetSourceNode(ctx, node) +} + +// PruneTip returns the block height and hash of the latest pruning block. +func (c *ChannelGraph) PruneTip() (*chainhash.Hash, uint32, error) { + return c.db.PruneTip() +} + // 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 5cbd325cf..89729944c 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -153,7 +153,7 @@ func TestNodeInsertionAndDeletion(t *testing.T) { // Check that the node's features are fetched correctly. This check // will check the database directly. - features, err = graph.Store.FetchNodeFeatures(node.PubKeyBytes) + features, err = graph.db.FetchNodeFeatures(node.PubKeyBytes) require.NoError(t, err) require.Equal(t, testFeatures, features) @@ -1575,7 +1575,7 @@ func TestGraphTraversalCacheable(t *testing.T) { require.NoError(t, err) // Now skip the cache and query the DB directly. - err = graph.Store.ForEachNodeDirectedChannel( + err = graph.db.ForEachNodeDirectedChannel( node, func(d *DirectedChannel) error { delete(chanIndex2, d.ChannelID) return nil @@ -3603,7 +3603,7 @@ func TestChannelEdgePruningUpdateIndexDeletion(t *testing.T) { graph := MakeTestGraph(t) // The update index only applies to the bbolt graph. - boltStore, ok := graph.Store.(*KVStore) + boltStore, ok := graph.db.(*KVStore) if !ok { t.Skipf("skipping test that is aimed at a bbolt graph DB") } @@ -4161,7 +4161,7 @@ func TestEdgePolicyMissingMaxHTLC(t *testing.T) { graph := MakeTestGraph(t) // This test currently directly edits the bytes stored in the bbolt DB. - boltStore, ok := graph.Store.(*KVStore) + boltStore, ok := graph.db.(*KVStore) if !ok { t.Skipf("skipping test that is aimed at a bbolt graph DB") } From 1738e0cabcb7f861768abfdcd2de0c2a855a6239 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 26 Oct 2025 12:17:41 +0200 Subject: [PATCH 07/55] graph/db: allow v2 nodes Update the SQLStore node writer and readers to handle V2 ndoes. Currently no logic will actually add such nodes. The following commits will update what is needed so that CRUD for v2 nodes can be tested. --- graph/db/sql_store.go | 113 ++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 37 deletions(-) diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 3e994836a..55efabf15 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -538,7 +538,7 @@ func (s *SQLStore) SetSourceNode(ctx context.Context, // Make sure that if a source node for this version is already // set, then the ID is the same as the one we are about to set. dbSourceNodeID, _, err := s.getSourceNode( - ctx, db, lnwire.GossipVersion1, + ctx, db, node.Version, ) if err != nil && !errors.Is(err, ErrSourceNodeNotSet) { return fmt.Errorf("unable to fetch source node: %w", @@ -3489,6 +3489,19 @@ func buildNode(ctx context.Context, cfg *sqldb.QueryConfig, db SQLQueries, return buildNodeWithBatchData(dbNode, data) } +// isKnownGossipVersion checks whether the provided gossip version is known +// and supported. +func isKnownGossipVersion(v lnwire.GossipVersion) bool { + switch v { + case lnwire.GossipVersion1: + return true + case lnwire.GossipVersion2: + return true + default: + return false + } +} + // buildNodeWithBatchData builds a models.Node instance // from the provided sqlc.GraphNode and batchNodeData. If the node does have // features/addresses/extra fields, then the corresponding fields are expected @@ -3496,15 +3509,18 @@ func buildNode(ctx context.Context, cfg *sqldb.QueryConfig, db SQLQueries, func buildNodeWithBatchData(dbNode sqlc.GraphNode, batchData *batchNodeData) (*models.Node, error) { - if dbNode.Version != int16(lnwire.GossipVersion1) { - return nil, fmt.Errorf("unsupported node version: %d", - dbNode.Version) + v := lnwire.GossipVersion(dbNode.Version) + + if !isKnownGossipVersion(v) { + return nil, fmt.Errorf("unknown node version: %d", v) } - var pub [33]byte - copy(pub[:], dbNode.PubKey) + pub, err := route.NewVertexFromBytes(dbNode.PubKey) + if err != nil { + return nil, fmt.Errorf("unable to parse pubkey: %w", err) + } - node := models.NewV1ShellNode(pub) + node := models.NewShellNode(v, pub) if len(dbNode.Signature) == 0 { return node, nil @@ -3518,8 +3534,10 @@ func buildNodeWithBatchData(dbNode sqlc.GraphNode, if dbNode.LastUpdate.Valid { node.LastUpdate = time.Unix(dbNode.LastUpdate.Int64, 0) } + if dbNode.BlockHeight.Valid { + node.LastBlockHeight = uint32(dbNode.BlockHeight.Int64) + } - var err error if dbNode.Color.Valid { nodeColor, err := DecodeHexColor(dbNode.Color.String) if err != nil { @@ -3551,13 +3569,19 @@ func buildNodeWithBatchData(dbNode sqlc.GraphNode, // Use preloaded extra fields. if extraFields, exists := batchData.extraFields[dbNode.ID]; exists { - recs, err := lnwire.CustomRecords(extraFields).Serialize() - if err != nil { - return nil, fmt.Errorf("unable to serialize extra "+ - "signed fields: %w", err) - } - if len(recs) != 0 { - node.ExtraOpaqueData = recs + if v == lnwire.GossipVersion1 { + records := lnwire.CustomRecords(extraFields) + recs, err := records.Serialize() + if err != nil { + return nil, fmt.Errorf("unable to serialize "+ + "extra signed fields: %w", err) + } + + if len(recs) != 0 { + node.ExtraOpaqueData = recs + } + } else if len(extraFields) > 0 { + node.ExtraSignedFields = extraFields } } @@ -3637,10 +3661,13 @@ func upsertNodeAncillaryData(ctx context.Context, db SQLQueries, // Convert the flat extra opaque data into a map of TLV types to // values. - extra, err := marshalExtraOpaqueData(node.ExtraOpaqueData) - if err != nil { - return fmt.Errorf("unable to marshal extra opaque data: %w", - err) + extra := node.ExtraSignedFields + if node.Version == lnwire.GossipVersion1 { + extra, err = marshalExtraOpaqueData(node.ExtraOpaqueData) + if err != nil { + return fmt.Errorf("unable to marshal extra opaque "+ + "data: %w", err) + } } // Update the node's extra signed fields. @@ -3655,34 +3682,39 @@ func upsertNodeAncillaryData(ctx context.Context, db SQLQueries, // populateNodeParams populates the common node parameters from a models.Node. // This is a helper for building UpsertNodeParams and UpsertSourceNodeParams. func populateNodeParams(node *models.Node, - setParams func(lastUpdate sql.NullInt64, alias, + setParams func(lastUpdate, lastBlockHeight sql.NullInt64, alias, colorStr sql.NullString, signature []byte)) error { if !node.HaveAnnouncement() { return nil } + var ( + alias, colorStr sql.NullString + lastUpdate, lastBlockHeight sql.NullInt64 + ) + node.Color.WhenSome(func(rgba color.RGBA) { + colorStr = sqldb.SQLStrValid(EncodeHexColor(rgba)) + }) + node.Alias.WhenSome(func(s string) { + alias = sqldb.SQLStrValid(s) + }) + switch node.Version { case lnwire.GossipVersion1: - lastUpdate := sqldb.SQLInt64(node.LastUpdate.Unix()) - var alias, colorStr sql.NullString - - node.Color.WhenSome(func(rgba color.RGBA) { - colorStr = sqldb.SQLStrValid(EncodeHexColor(rgba)) - }) - node.Alias.WhenSome(func(s string) { - alias = sqldb.SQLStrValid(s) - }) - - setParams(lastUpdate, alias, colorStr, node.AuthSigBytes) + lastUpdate = sqldb.SQLInt64(node.LastUpdate.Unix()) case lnwire.GossipVersion2: - // No-op for now. + lastBlockHeight = sqldb.SQLInt64(int64(node.LastBlockHeight)) default: return fmt.Errorf("unknown gossip version: %d", node.Version) } + setParams( + lastUpdate, lastBlockHeight, alias, colorStr, node.AuthSigBytes, + ) + return nil } @@ -3690,20 +3722,22 @@ func populateNodeParams(node *models.Node, // strict UpsertNode query (requires timestamp to be increasing). func buildNodeUpsertParams(node *models.Node) (sqlc.UpsertNodeParams, error) { params := sqlc.UpsertNodeParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(node.Version), PubKey: node.PubKeyBytes[:], } err := populateNodeParams( - node, func(lastUpdate sql.NullInt64, alias, + node, func(lastUpdate, lastBlockHeight sql.NullInt64, alias, colorStr sql.NullString, signature []byte) { params.LastUpdate = lastUpdate + params.BlockHeight = lastBlockHeight params.Alias = alias params.Color = colorStr params.Signature = signature - }) + }, + ) return params, err } @@ -3714,14 +3748,15 @@ func buildSourceNodeUpsertParams(node *models.Node) ( sqlc.UpsertSourceNodeParams, error) { params := sqlc.UpsertSourceNodeParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(node.Version), PubKey: node.PubKeyBytes[:], } err := populateNodeParams( - node, func(lastUpdate sql.NullInt64, alias, + node, func(lastUpdate, lastBlock sql.NullInt64, alias, colorStr sql.NullString, signature []byte) { + params.BlockHeight = lastBlock params.LastUpdate = lastUpdate params.Alias = alias params.Color = colorStr @@ -3772,6 +3807,10 @@ func upsertSourceNode(ctx context.Context, db SQLQueries, func upsertNode(ctx context.Context, db SQLQueries, node *models.Node) (int64, error) { + if !isKnownGossipVersion(node.Version) { + return 0, fmt.Errorf("unknown gossip version: %d", node.Version) + } + params, err := buildNodeUpsertParams(node) if err != nil { return 0, err From 5a3c013e91ddd47a1af6ab6b4851a6849eb3fa37 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 26 Nov 2025 07:35:25 +0200 Subject: [PATCH 08/55] sqldb/sqlc: add NodeExists query --- sqldb/sqlc/graph.sql.go | 21 +++++++++++++++++++++ sqldb/sqlc/querier.go | 1 + sqldb/sqlc/queries/graph.sql | 8 ++++++++ 3 files changed, 30 insertions(+) diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index cdf15a90b..2c26eb5d2 100644 --- a/sqldb/sqlc/graph.sql.go +++ b/sqldb/sqlc/graph.sql.go @@ -3537,6 +3537,27 @@ func (q *Queries) ListNodesPaginated(ctx context.Context, arg ListNodesPaginated return items, nil } +const nodeExists = `-- name: NodeExists :one +SELECT EXISTS ( + SELECT 1 + FROM graph_nodes + WHERE pub_key = $1 + AND version = $2 +) AS node_exists +` + +type NodeExistsParams struct { + PubKey []byte + Version int16 +} + +func (q *Queries) NodeExists(ctx context.Context, arg NodeExistsParams) (bool, error) { + row := q.db.QueryRowContext(ctx, nodeExists, arg.PubKey, arg.Version) + var node_exists bool + err := row.Scan(&node_exists) + return node_exists, err +} + const upsertChanPolicyExtraType = `-- name: UpsertChanPolicyExtraType :exec /* ───────────────────────────────────────────── graph_channel_policy_extra_types table queries diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index 7b7b06495..aa8ab2ecf 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -125,6 +125,7 @@ type Querier interface { ListNodeIDsAndPubKeys(ctx context.Context, arg ListNodeIDsAndPubKeysParams) ([]ListNodeIDsAndPubKeysRow, error) ListNodesPaginated(ctx context.Context, arg ListNodesPaginatedParams) ([]GraphNode, error) NextInvoiceSettleIndex(ctx context.Context) (int64, error) + NodeExists(ctx context.Context, arg NodeExistsParams) (bool, error) OnAMPSubInvoiceCanceled(ctx context.Context, arg OnAMPSubInvoiceCanceledParams) error OnAMPSubInvoiceCreated(ctx context.Context, arg OnAMPSubInvoiceCreatedParams) error OnAMPSubInvoiceSettled(ctx context.Context, arg OnAMPSubInvoiceSettledParams) error diff --git a/sqldb/sqlc/queries/graph.sql b/sqldb/sqlc/queries/graph.sql index 480437655..2df865d9b 100644 --- a/sqldb/sqlc/queries/graph.sql +++ b/sqldb/sqlc/queries/graph.sql @@ -59,6 +59,14 @@ FROM graph_nodes WHERE pub_key = $1 AND version = $2; +-- name: NodeExists :one +SELECT EXISTS ( + SELECT 1 + FROM graph_nodes + WHERE pub_key = $1 + AND version = $2 +) AS node_exists; + -- name: GetNodeIDByPubKey :one SELECT id FROM graph_nodes From 97143a74ab94d9ae286e599d59b17f478daae6b5 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 26 Oct 2025 16:38:29 +0200 Subject: [PATCH 09/55] graph/db: split HasNode into two methods HasNode currently is very v1 specific since it returns a time.Time timestamp which is specific to V1 node announcements. However, it is mostly only used for the "exists" return value. So here we split it up into HasNode which just checks existence and HasV1Node which retains the same behavaiour as before. --- graph/builder.go | 2 +- graph/db/graph.go | 13 +++++++++++-- graph/db/graph_test.go | 6 +++--- graph/db/interfaces.go | 17 +++++++++++----- graph/db/kv_store.go | 44 ++++++++++++++++++++++++++++++++++++------ graph/db/sql_store.go | 31 +++++++++++++++++++++++++++-- routing/router_test.go | 8 ++++---- rpcserver.go | 4 ++-- 8 files changed, 100 insertions(+), 25 deletions(-) diff --git a/graph/builder.go b/graph/builder.go index 91040dd08..f9ce63eb8 100644 --- a/graph/builder.go +++ b/graph/builder.go @@ -874,7 +874,7 @@ func (b *Builder) assertNodeAnnFreshness(ctx context.Context, node route.Vertex, // node announcements, we will ignore such nodes. If we do know about // this node, check that this update brings info newer than what we // already have. - lastUpdate, exists, err := b.cfg.Graph.HasNode(ctx, node) + lastUpdate, exists, err := b.cfg.Graph.HasV1Node(ctx, node) if err != nil { return fmt.Errorf("unable to query for the "+ "existence of node: %w", err) diff --git a/graph/db/graph.go b/graph/db/graph.go index 13b9af300..e9bd4aafb 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -664,10 +664,19 @@ func (c *ChannelGraph) FetchNode(ctx context.Context, return c.db.FetchNode(ctx, nodePub) } -// HasNode determines if the graph has a vertex identified by the target node. -func (c *ChannelGraph) HasNode(ctx context.Context, +// HasV1Node determines if the graph has a vertex identified by the target node +// in the V1 graph. +func (c *ChannelGraph) HasV1Node(ctx context.Context, nodePub [33]byte) (time.Time, bool, error) { + return c.db.HasV1Node(ctx, nodePub) +} + +// HasNode determines if the graph has a vertex identified by the target node +// in the V1 graph. +func (c *ChannelGraph) HasNode(ctx context.Context, nodePub [33]byte) (bool, + error) { + return c.db.HasNode(ctx, nodePub) } diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 89729944c..c147b09fd 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -138,7 +138,7 @@ func TestNodeInsertionAndDeletion(t *testing.T) { dbNode, err := graph.FetchNode(ctx, testPub) require.NoError(t, err, "unable to locate node") - _, exists, err := graph.HasNode(ctx, dbNode.PubKeyBytes) + exists, err := graph.HasNode(ctx, dbNode.PubKeyBytes) require.NoError(t, err) require.True(t, exists) @@ -309,7 +309,7 @@ func TestPartialNode(t *testing.T) { dbNode2, err := graph.FetchNode(ctx, pubKey2) require.NoError(t, err) - _, exists, err := graph.HasNode(ctx, dbNode1.PubKeyBytes) + exists, err := graph.HasNode(ctx, dbNode1.PubKeyBytes) require.NoError(t, err) require.True(t, exists) @@ -318,7 +318,7 @@ func TestPartialNode(t *testing.T) { expectedNode1 := models.NewV1ShellNode(pubKey1) compareNodes(t, expectedNode1, dbNode1) - _, exists, err = graph.HasNode(ctx, dbNode2.PubKeyBytes) + exists, err = graph.HasNode(ctx, dbNode2.PubKeyBytes) require.NoError(t, err) require.True(t, exists) diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index 20408c904..59172833e 100644 --- a/graph/db/interfaces.go +++ b/graph/db/interfaces.go @@ -119,12 +119,19 @@ type Store interface { //nolint:interfacebloat FetchNode(ctx context.Context, nodePub route.Vertex) (*models.Node, error) + // HasV1Node determines if the graph has a vertex identified by + // the target node identity public key in the V1 graph. If the node + // exists in the database, a timestamp of when the data for the node + // was lasted updated is returned along with a true boolean. Otherwise, + // an empty time.Time is returned with a false boolean. + // This is specific to the V1 graph since only V1 node announcements + // use timestamps for their latest update timestamp. + HasV1Node(ctx context.Context, nodePub [33]byte) (time.Time, bool, + error) + // HasNode determines if the graph has a vertex identified by - // the target node identity public key. If the node exists in the - // database, a timestamp of when the data for the node was lasted - // updated is returned along with a true boolean. Otherwise, an empty - // time.Time is returned with a false boolean. - HasNode(ctx context.Context, nodePub [33]byte) (time.Time, bool, error) + // the target node identity public key. + HasNode(ctx context.Context, nodePub [33]byte) (bool, error) // IsPublicNode is a helper method that determines whether the node with // the given public key is seen as a public node in the graph from the diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 27c3bbadc..a582fd5e4 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -3433,11 +3433,12 @@ func (c *KVStore) fetchLightningNode(tx kvdb.RTx, return node, nil } -// HasNode determines if the graph has a vertex identified by the target node -// identity public key. If the node exists in the database, a timestamp of when -// the data for the node was lasted updated is returned along with a true -// boolean. Otherwise, an empty time.Time is returned with a false boolean. -func (c *KVStore) HasNode(_ context.Context, +// HasV1Node determines if the graph has a vertex identified by the +// target node identity public key. If the node exists in the database, a +// timestamp of when the data for the node was lasted updated is returned along +// with a true boolean. Otherwise, an empty time.Time is returned with a false +// boolean. +func (c *KVStore) HasV1Node(_ context.Context, nodePub [33]byte) (time.Time, bool, error) { var ( @@ -3457,7 +3458,6 @@ func (c *KVStore) HasNode(_ context.Context, // exit early. nodeBytes := nodes.Get(nodePub[:]) if nodeBytes == nil { - exists = false return nil } @@ -3485,6 +3485,38 @@ func (c *KVStore) HasNode(_ context.Context, return updateTime, exists, nil } +// HasNode determines if the graph has a vertex identified by the target node +// identity public key. +func (c *KVStore) HasNode(_ context.Context, nodePub [33]byte) (bool, error) { + var exists bool + err := kvdb.View(c.db, func(tx kvdb.RTx) error { + // First grab the nodes bucket which stores the mapping from + // pubKey to node information. + nodes := tx.ReadBucket(nodeBucket) + if nodes == nil { + return ErrGraphNotFound + } + + // If a key for this serialized public key isn't found, we can + // exit early. + nodeBytes := nodes.Get(nodePub[:]) + if nodeBytes == nil { + return nil + } + + exists = true + + return nil + }, func() { + exists = false + }) + if err != nil { + return exists, err + } + + return exists, nil +} + // nodeTraversal is used to traverse all channels of a node given by its // public key and passes channel information into the specified callback. // diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 55efabf15..d9fca5155 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -53,6 +53,7 @@ type SQLQueries interface { DeleteUnconnectedNodes(ctx context.Context) ([][]byte, error) DeleteNodeByPubKey(ctx context.Context, arg sqlc.DeleteNodeByPubKeyParams) (sql.Result, error) DeleteNode(ctx context.Context, id int64) error + NodeExists(ctx context.Context, arg sqlc.NodeExistsParams) (bool, error) GetExtraNodeTypes(ctx context.Context, nodeID int64) ([]sqlc.GraphNodeExtraType, error) GetNodeExtraTypesBatch(ctx context.Context, ids []int64) ([]sqlc.GraphNodeExtraType, error) @@ -283,14 +284,14 @@ func (s *SQLStore) FetchNode(ctx context.Context, return node, nil } -// HasNode determines if the graph has a vertex identified by the +// HasV1Node determines if the graph has a vertex identified by the // target node identity public key. If the node exists in the database, a // timestamp of when the data for the node was lasted updated is returned along // with a true boolean. Otherwise, an empty time.Time is returned with a false // boolean. // // NOTE: part of the Store interface. -func (s *SQLStore) HasNode(ctx context.Context, +func (s *SQLStore) HasV1Node(ctx context.Context, pubKey [33]byte) (time.Time, bool, error) { var ( @@ -326,6 +327,32 @@ func (s *SQLStore) HasNode(ctx context.Context, return lastUpdate, exists, nil } +// HasNode determines if the graph has a vertex identified by the +// target node identity public key. +// +// NOTE: part of the Store interface. +func (s *SQLStore) HasNode(ctx context.Context, pubKey [33]byte) (bool, error) { + var ( + v = lnwire.GossipVersion1 + exists bool + ) + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + var err error + exists, err = db.NodeExists(ctx, sqlc.NodeExistsParams{ + Version: int16(v), + PubKey: pubKey[:], + }) + + return err + }, sqldb.NoOpReset) + if err != nil { + return false, fmt.Errorf("unable to check if node (%x) "+ + "exists: %w", pubKey, err) + } + + return exists, nil +} + // AddrsForNode returns all known addresses for the target node public key // that the graph DB is aware of. The returned boolean indicates if the // given node is unknown to the graph DB or not. diff --git a/routing/router_test.go b/routing/router_test.go index 9f088917d..102afe85b 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -2717,11 +2717,11 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { copy(pub2[:], priv2.PubKey().SerializeCompressed()) // The two nodes we are about to add should not exist yet. - _, exists1, err := ctx.graph.HasNode(ctxb, pub1) + exists1, err := ctx.graph.HasNode(ctxb, pub1) require.NoError(t, err, "unable to query graph") require.False(t, exists1) - _, exists2, err := ctx.graph.HasNode(ctxb, pub2) + exists2, err := ctx.graph.HasNode(ctxb, pub2) require.NoError(t, err, "unable to query graph") require.False(t, exists2) @@ -2778,11 +2778,11 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { // After adding the edge between the two previously unknown nodes, they // should have been added to the graph. - _, exists1, err = ctx.graph.HasNode(ctxb, pub1) + exists1, err = ctx.graph.HasNode(ctxb, pub1) require.NoError(t, err, "unable to query graph") require.True(t, exists1) - _, exists2, err = ctx.graph.HasNode(ctxb, pub2) + exists2, err = ctx.graph.HasNode(ctxb, pub2) require.NoError(t, err, "unable to query graph") require.True(t, exists2) diff --git a/rpcserver.go b/rpcserver.go index fe0e6702b..f09278777 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -746,7 +746,7 @@ func (r *rpcServer) addDeps(ctx context.Context, s *server, return info.NodeKey1Bytes, info.NodeKey2Bytes, nil }, HasNode: func(nodePub route.Vertex) (bool, error) { - _, exists, err := graph.HasNode(ctx, nodePub) + exists, err := graph.HasNode(ctx, nodePub) return exists, err }, @@ -1822,7 +1822,7 @@ func (r *rpcServer) VerifyMessage(ctx context.Context, // // TODO(phlip9): Require valid nodes to have capital in active channels. graph := r.server.graphDB - _, active, err := graph.HasNode(ctx, pub) + active, err := graph.HasNode(ctx, pub) if err != nil { return nil, fmt.Errorf("failed to query graph: %w", err) } From c281810ab98e24f9099ffe574041e909774d0142 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 26 Oct 2025 17:06:30 +0200 Subject: [PATCH 10/55] graph/db: move gossip versions up one layer Update some of the node related graph CRUD methods to take a version rather than hardcoding them in the SQLStore layer. Move the version up one layer instead. This will make it easier to make it configurable later on. --- graph/db/graph.go | 14 +++++----- graph/db/graph_test.go | 4 ++- graph/db/interfaces.go | 27 +++++++++++++------ graph/db/kv_store.go | 59 ++++++++++++++++++++++++++++++++++------- graph/db/sql_store.go | 60 ++++++++++++++++++++++-------------------- 5 files changed, 111 insertions(+), 53 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index e9bd4aafb..47df50e5a 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -226,7 +226,7 @@ func (c *ChannelGraph) FetchNodeFeatures(node route.Vertex) ( return c.graphCache.GetFeatures(node), nil } - return c.db.FetchNodeFeatures(node) + return c.db.FetchNodeFeatures(lnwire.GossipVersion1, node) } // GraphSession will provide the call-back with access to a NodeTraverser @@ -298,7 +298,7 @@ func (c *ChannelGraph) AddNode(ctx context.Context, func (c *ChannelGraph) DeleteNode(ctx context.Context, nodePub route.Vertex) error { - err := c.db.DeleteNode(ctx, nodePub) + err := c.db.DeleteNode(ctx, lnwire.GossipVersion1, nodePub) if err != nil { return err } @@ -607,7 +607,7 @@ func (c *ChannelGraph) UpdateEdgePolicy(ctx context.Context, func (c *ChannelGraph) AddrsForNode(ctx context.Context, nodePub *btcec.PublicKey) (bool, []net.Addr, error) { - return c.db.AddrsForNode(ctx, nodePub) + return c.db.AddrsForNode(ctx, lnwire.GossipVersion1, nodePub) } // ForEachSourceNodeChannel iterates through all channels of the source node. @@ -646,7 +646,7 @@ func (c *ChannelGraph) ForEachNodeCacheable(ctx context.Context, func (c *ChannelGraph) LookupAlias(ctx context.Context, pub *btcec.PublicKey) (string, error) { - return c.db.LookupAlias(ctx, pub) + return c.db.LookupAlias(ctx, lnwire.GossipVersion1, pub) } // NodeUpdatesInHorizon returns all known lightning nodes with updates in the @@ -661,7 +661,7 @@ func (c *ChannelGraph) NodeUpdatesInHorizon(startTime, endTime time.Time, func (c *ChannelGraph) FetchNode(ctx context.Context, nodePub route.Vertex) (*models.Node, error) { - return c.db.FetchNode(ctx, nodePub) + return c.db.FetchNode(ctx, lnwire.GossipVersion1, nodePub) } // HasV1Node determines if the graph has a vertex identified by the target node @@ -677,7 +677,7 @@ func (c *ChannelGraph) HasV1Node(ctx context.Context, func (c *ChannelGraph) HasNode(ctx context.Context, nodePub [33]byte) (bool, error) { - return c.db.HasNode(ctx, nodePub) + return c.db.HasNode(ctx, lnwire.GossipVersion1, nodePub) } // IsPublicNode determines whether the node is seen as public in the graph. @@ -796,7 +796,7 @@ func (c *ChannelGraph) IsClosedScid(scid lnwire.ShortChannelID) (bool, error) { // SourceNode returns the source node of the graph. func (c *ChannelGraph) SourceNode(ctx context.Context) (*models.Node, error) { - return c.db.SourceNode(ctx) + return c.db.SourceNode(ctx, lnwire.GossipVersion1) } // SetSourceNode sets the source node within the graph database. diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index c147b09fd..16b08797a 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -153,7 +153,9 @@ func TestNodeInsertionAndDeletion(t *testing.T) { // Check that the node's features are fetched correctly. This check // will check the database directly. - features, err = graph.db.FetchNodeFeatures(node.PubKeyBytes) + features, err = graph.db.FetchNodeFeatures( + lnwire.GossipVersion1, node.PubKeyBytes, + ) require.NoError(t, err) require.Equal(t, testFeatures, features) diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index 59172833e..b6a8c10ea 100644 --- a/graph/db/interfaces.go +++ b/graph/db/interfaces.go @@ -31,7 +31,14 @@ type NodeTraverser interface { // Store represents the main interface for the channel graph database for all // channels and nodes gossiped via the V1 gossip protocol as defined in BOLT 7. type Store interface { //nolint:interfacebloat - NodeTraverser + // ForEachNodeDirectedChannel calls the callback for every channel of + // the given node. + ForEachNodeDirectedChannel(nodePub route.Vertex, + cb func(channel *DirectedChannel) error, reset func()) error + + // FetchNodeFeatures returns the features of the given node. + FetchNodeFeatures(v lnwire.GossipVersion, + nodePub route.Vertex) (*lnwire.FeatureVector, error) // AddNode adds a vertex/node to the graph database. If the // node is not in the database from before, this will add a new, @@ -45,7 +52,7 @@ type Store interface { //nolint:interfacebloat // AddrsForNode returns all known addresses for the target node public // key that the graph DB is aware of. The returned boolean indicates if // the given node is unknown to the graph DB or not. - AddrsForNode(ctx context.Context, + AddrsForNode(ctx context.Context, v lnwire.GossipVersion, nodePub *btcec.PublicKey) (bool, []net.Addr, error) // ForEachSourceNodeChannel iterates through all channels of the source @@ -100,11 +107,13 @@ type Store interface { //nolint:interfacebloat // LookupAlias attempts to return the alias as advertised by the target // node. - LookupAlias(ctx context.Context, pub *btcec.PublicKey) (string, error) + LookupAlias(ctx context.Context, v lnwire.GossipVersion, + pub *btcec.PublicKey) (string, error) // DeleteNode starts a new database transaction to remove a // vertex/node from the database according to the node's public key. - DeleteNode(ctx context.Context, nodePub route.Vertex) error + DeleteNode(ctx context.Context, v lnwire.GossipVersion, + nodePub route.Vertex) error // NodeUpdatesInHorizon returns all the known lightning node which have // an update timestamp within the passed range. This method can be used @@ -116,8 +125,8 @@ type Store interface { //nolint:interfacebloat // FetchNode attempts to look up a target node by its identity // public key. If the node isn't found in the database, then // ErrGraphNodeNotFound is returned. - FetchNode(ctx context.Context, nodePub route.Vertex) (*models.Node, - error) + FetchNode(ctx context.Context, v lnwire.GossipVersion, + nodePub route.Vertex) (*models.Node, error) // HasV1Node determines if the graph has a vertex identified by // the target node identity public key in the V1 graph. If the node @@ -131,7 +140,8 @@ type Store interface { //nolint:interfacebloat // HasNode determines if the graph has a vertex identified by // the target node identity public key. - HasNode(ctx context.Context, nodePub [33]byte) (bool, error) + HasNode(ctx context.Context, v lnwire.GossipVersion, + nodePub [33]byte) (bool, error) // IsPublicNode is a helper method that determines whether the node with // the given public key is seen as a public node in the graph from the @@ -334,7 +344,8 @@ type Store interface { //nolint:interfacebloat // treated as the center node within a star-graph. This method may be // used to kick off a path finding algorithm in order to explore the // reachability of another node based off the source node. - SourceNode(ctx context.Context) (*models.Node, error) + SourceNode(ctx context.Context, v lnwire.GossipVersion) (*models.Node, + error) // SetSourceNode sets the source node within the graph database. The // source node is to be used as the center of a star-graph within path diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index a582fd5e4..021f4a3c5 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -164,6 +164,11 @@ var ( // // maps: scid -> []byte{} closedScidBucket = []byte("closed-scid") + + // ErrVersionNotSupportedForKVDB is returned with KVStore queries are + // made using a gossip version other than V1. + ErrVersionNotSupportedForKVDB = errors.New("only gossip v1 is " + + "supported for kvdb graph store") ) const ( @@ -376,7 +381,7 @@ func initKVStore(db kvdb.Backend) error { // unknown to the graph DB or not. // // NOTE: this is part of the channeldb.AddrSource interface. -func (c *KVStore) AddrsForNode(ctx context.Context, +func (c *KVStore) AddrsForNode(ctx context.Context, v lnwire.GossipVersion, nodePub *btcec.PublicKey) (bool, []net.Addr, error) { pubKey, err := route.NewVertexFromBytes(nodePub.SerializeCompressed()) @@ -384,7 +389,7 @@ func (c *KVStore) AddrsForNode(ctx context.Context, return false, nil, err } - node, err := c.FetchNode(ctx, pubKey) + node, err := c.FetchNode(ctx, v, pubKey) // We don't consider it an error if the graph is unaware of the node. switch { case err != nil && !errors.Is(err, ErrGraphNodeNotFound): @@ -666,8 +671,12 @@ func (c *KVStore) ForEachNodeDirectedChannel(nodePub route.Vertex, // known for the node, an empty feature vector is returned. // // NOTE: this is part of the graphdb.NodeTraverser interface. -func (c *KVStore) FetchNodeFeatures(nodePub route.Vertex) ( - *lnwire.FeatureVector, error) { +func (c *KVStore) FetchNodeFeatures(v lnwire.GossipVersion, + nodePub route.Vertex) (*lnwire.FeatureVector, error) { + + if v != lnwire.GossipVersion1 { + return nil, ErrVersionNotSupportedForKVDB + } return c.fetchNodeFeatures(nil, nodePub) } @@ -901,7 +910,13 @@ func (c *KVStore) ForEachNodeCacheable(_ context.Context, // as the center node within a star-graph. This method may be used to kick off // a path finding algorithm in order to explore the reachability of another // node based off the source node. -func (c *KVStore) SourceNode(_ context.Context) (*models.Node, error) { +func (c *KVStore) SourceNode(_ context.Context, + v lnwire.GossipVersion) (*models.Node, error) { + + if v != lnwire.GossipVersion1 { + return nil, ErrVersionNotSupportedForKVDB + } + return sourceNode(c.db) } @@ -955,6 +970,10 @@ func sourceNodeWithTx(nodes kvdb.RBucket) (*models.Node, error) { func (c *KVStore) SetSourceNode(_ context.Context, node *models.Node) error { + if node.Version != lnwire.GossipVersion1 { + return ErrVersionNotSupportedForKVDB + } + nodePubBytes := node.PubKeyBytes[:] return kvdb.Update(c.db, func(tx kvdb.RwTx) error { @@ -1021,9 +1040,13 @@ func addLightningNode(tx kvdb.RwTx, node *models.Node) error { // LookupAlias attempts to return the alias as advertised by the target node. // TODO(roasbeef): currently assumes that aliases are unique... -func (c *KVStore) LookupAlias(_ context.Context, +func (c *KVStore) LookupAlias(_ context.Context, v lnwire.GossipVersion, pub *btcec.PublicKey) (string, error) { + if v != lnwire.GossipVersion1 { + return "", ErrVersionNotSupportedForKVDB + } + var alias string err := kvdb.View(c.db, func(tx kvdb.RTx) error { @@ -1060,9 +1083,13 @@ func (c *KVStore) LookupAlias(_ context.Context, // DeleteNode starts a new database transaction to remove a vertex/node // from the database according to the node's public key. -func (c *KVStore) DeleteNode(_ context.Context, +func (c *KVStore) DeleteNode(_ context.Context, v lnwire.GossipVersion, nodePub route.Vertex) error { + if v != lnwire.GossipVersion1 { + return ErrVersionNotSupportedForKVDB + } + // TODO(roasbeef): ensure dangling edges are removed... return kvdb.Update(c.db, func(tx kvdb.RwTx) error { nodes := tx.ReadWriteBucket(nodeBucket) @@ -3370,9 +3397,13 @@ func (c *KVStore) fetchNodeTx(tx kvdb.RTx, nodePub route.Vertex) (*models.Node, // FetchNode attempts to look up a target node by its identity public // key. If the node isn't found in the database, then ErrGraphNodeNotFound is // returned. -func (c *KVStore) FetchNode(_ context.Context, +func (c *KVStore) FetchNode(_ context.Context, v lnwire.GossipVersion, nodePub route.Vertex) (*models.Node, error) { + if v != lnwire.GossipVersion1 { + return nil, ErrVersionNotSupportedForKVDB + } + return c.fetchLightningNode(nil, nodePub) } @@ -3487,7 +3518,13 @@ func (c *KVStore) HasV1Node(_ context.Context, // HasNode determines if the graph has a vertex identified by the target node // identity public key. -func (c *KVStore) HasNode(_ context.Context, nodePub [33]byte) (bool, error) { +func (c *KVStore) HasNode(_ context.Context, v lnwire.GossipVersion, + nodePub [33]byte) (bool, error) { + + if v != lnwire.GossipVersion1 { + return false, ErrVersionNotSupportedForKVDB + } + var exists bool err := kvdb.View(c.db, func(tx kvdb.RTx) error { // First grab the nodes bucket which stores the mapping from @@ -4366,6 +4403,10 @@ func (c *nodeTraverserSession) FetchNodeFeatures(nodePub route.Vertex) ( func putLightningNode(nodeBucket, aliasBucket, updateIndex kvdb.RwBucket, node *models.Node) error { + if node.Version != lnwire.GossipVersion1 { + return ErrVersionNotSupportedForKVDB + } + var ( scratch [16]byte b bytes.Buffer diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index d9fca5155..0df0985b7 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -267,13 +267,15 @@ func (s *SQLStore) AddNode(ctx context.Context, // returned. // // NOTE: part of the Store interface. -func (s *SQLStore) FetchNode(ctx context.Context, +func (s *SQLStore) FetchNode(ctx context.Context, v lnwire.GossipVersion, pubKey route.Vertex) (*models.Node, error) { var node *models.Node err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { var err error - _, node, err = getNodeByPubKey(ctx, s.cfg.QueryCfg, db, pubKey) + _, node, err = getNodeByPubKey( + ctx, s.cfg.QueryCfg, db, v, pubKey, + ) return err }, sqldb.NoOpReset) @@ -331,11 +333,10 @@ func (s *SQLStore) HasV1Node(ctx context.Context, // target node identity public key. // // NOTE: part of the Store interface. -func (s *SQLStore) HasNode(ctx context.Context, pubKey [33]byte) (bool, error) { - var ( - v = lnwire.GossipVersion1 - exists bool - ) +func (s *SQLStore) HasNode(ctx context.Context, v lnwire.GossipVersion, + pubKey [33]byte) (bool, error) { + + var exists bool err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { var err error exists, err = db.NodeExists(ctx, sqlc.NodeExistsParams{ @@ -358,7 +359,7 @@ func (s *SQLStore) HasNode(ctx context.Context, pubKey [33]byte) (bool, error) { // given node is unknown to the graph DB or not. // // NOTE: part of the Store interface. -func (s *SQLStore) AddrsForNode(ctx context.Context, +func (s *SQLStore) AddrsForNode(ctx context.Context, v lnwire.GossipVersion, nodePub *btcec.PublicKey) (bool, []net.Addr, error) { var ( @@ -370,7 +371,7 @@ func (s *SQLStore) AddrsForNode(ctx context.Context, // does. dbID, err := db.GetNodeIDByPubKey( ctx, sqlc.GetNodeIDByPubKeyParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), PubKey: nodePub.SerializeCompressed(), }, ) @@ -400,13 +401,13 @@ func (s *SQLStore) AddrsForNode(ctx context.Context, // from the database according to the node's public key. // // NOTE: part of the Store interface. -func (s *SQLStore) DeleteNode(ctx context.Context, +func (s *SQLStore) DeleteNode(ctx context.Context, v lnwire.GossipVersion, pubKey route.Vertex) error { err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { res, err := db.DeleteNodeByPubKey( ctx, sqlc.DeleteNodeByPubKeyParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), PubKey: pubKey[:], }, ) @@ -438,12 +439,12 @@ func (s *SQLStore) DeleteNode(ctx context.Context, // known for the node, an empty feature vector is returned. // // NOTE: this is part of the graphdb.NodeTraverser interface. -func (s *SQLStore) FetchNodeFeatures(nodePub route.Vertex) ( - *lnwire.FeatureVector, error) { +func (s *SQLStore) FetchNodeFeatures(v lnwire.GossipVersion, + nodePub route.Vertex) (*lnwire.FeatureVector, error) { ctx := context.TODO() - return fetchNodeFeatures(ctx, s.db, nodePub) + return fetchNodeFeatures(ctx, s.db, v, nodePub) } // DisabledChannelIDs returns the channel ids of disabled channels. @@ -478,14 +479,14 @@ func (s *SQLStore) DisabledChannelIDs() ([]uint64, error) { // LookupAlias attempts to return the alias as advertised by the target node. // // NOTE: part of the Store interface. -func (s *SQLStore) LookupAlias(ctx context.Context, +func (s *SQLStore) LookupAlias(ctx context.Context, v lnwire.GossipVersion, pub *btcec.PublicKey) (string, error) { var alias string err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { dbNode, err := db.GetNodeByPubKey( ctx, sqlc.GetNodeByPubKeyParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), PubKey: pub.SerializeCompressed(), }, ) @@ -516,20 +517,20 @@ func (s *SQLStore) LookupAlias(ctx context.Context, // node based off the source node. // // NOTE: part of the Store interface. -func (s *SQLStore) SourceNode(ctx context.Context) (*models.Node, - error) { +func (s *SQLStore) SourceNode(ctx context.Context, + v lnwire.GossipVersion) (*models.Node, error) { var node *models.Node err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { - _, nodePub, err := s.getSourceNode( - ctx, db, lnwire.GossipVersion1, - ) + _, nodePub, err := s.getSourceNode(ctx, db, v) if err != nil { return fmt.Errorf("unable to fetch V1 source node: %w", err) } - _, node, err = getNodeByPubKey(ctx, s.cfg.QueryCfg, db, nodePub) + _, node, err = getNodeByPubKey( + ctx, s.cfg.QueryCfg, db, v, nodePub, + ) return err }, sqldb.NoOpReset) @@ -919,7 +920,8 @@ func (s *SQLStore) ForEachSourceNodeChannel(ctx context.Context, } _, otherNode, err := getNodeByPubKey( - ctx, s.cfg.QueryCfg, db, otherNodePub, + ctx, s.cfg.QueryCfg, db, + lnwire.GossipVersion1, otherNodePub, ) if err != nil { return fmt.Errorf("unable to fetch "+ @@ -3104,7 +3106,7 @@ func (s *sqlNodeTraverser) FetchNodeFeatures(nodePub route.Vertex) ( ctx := context.TODO() - return fetchNodeFeatures(ctx, s.db, nodePub) + return fetchNodeFeatures(ctx, s.db, lnwire.GossipVersion1, nodePub) } // forEachNodeDirectedChannel iterates through all channels of a given @@ -3466,11 +3468,12 @@ func updateChanEdgePolicy(ctx context.Context, tx SQLQueries, // getNodeByPubKey attempts to look up a target node by its public key. func getNodeByPubKey(ctx context.Context, cfg *sqldb.QueryConfig, db SQLQueries, - pubKey route.Vertex) (int64, *models.Node, error) { + v lnwire.GossipVersion, pubKey route.Vertex) (int64, *models.Node, + error) { dbNode, err := db.GetNodeByPubKey( ctx, sqlc.GetNodeByPubKeyParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), PubKey: pubKey[:], }, ) @@ -3926,12 +3929,13 @@ func upsertNodeFeatures(ctx context.Context, db SQLQueries, nodeID int64, // fetchNodeFeatures fetches the features for a node with the given public key. func fetchNodeFeatures(ctx context.Context, queries SQLQueries, - nodePub route.Vertex) (*lnwire.FeatureVector, error) { + v lnwire.GossipVersion, nodePub route.Vertex) (*lnwire.FeatureVector, + error) { rows, err := queries.GetNodeFeaturesByPubKey( ctx, sqlc.GetNodeFeaturesByPubKeyParams{ PubKey: nodePub[:], - Version: int16(lnwire.GossipVersion1), + Version: int16(v), }, ) if err != nil { From d8337be9680cdef29d5b58a8d27252158d3c56ab Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 26 Oct 2025 17:09:41 +0200 Subject: [PATCH 11/55] graph/db: add helper variable for tests Add an isSQLDB variable that will let us quickly check in tests if the backing DB is KVStore or SQLStore. --- graph/db/test_kvdb.go | 3 +++ graph/db/test_postgres.go | 3 +++ graph/db/test_sqlite.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/graph/db/test_kvdb.go b/graph/db/test_kvdb.go index 569e0949f..22904f858 100644 --- a/graph/db/test_kvdb.go +++ b/graph/db/test_kvdb.go @@ -9,6 +9,9 @@ import ( "github.com/stretchr/testify/require" ) +// isSQLDB indicates that this build does not use a SQL database. +var isSQLDB = false + // NewTestDB is a helper function that creates an BBolt database for testing. func NewTestDB(t testing.TB) Store { backend, backendCleanup, err := kvdb.GetTestBackend(t.TempDir(), "cgr") diff --git a/graph/db/test_postgres.go b/graph/db/test_postgres.go index 716210afa..a1bd61a2c 100644 --- a/graph/db/test_postgres.go +++ b/graph/db/test_postgres.go @@ -11,6 +11,9 @@ import ( "github.com/stretchr/testify/require" ) +// isSQLDB indicates that this build uses a SQL database. +var isSQLDB = true + // NewTestDB is a helper function that creates a SQLStore backed by a SQL // database for testing. func NewTestDB(t testing.TB) Store { diff --git a/graph/db/test_sqlite.go b/graph/db/test_sqlite.go index 68fef6eb9..137ab7aa9 100644 --- a/graph/db/test_sqlite.go +++ b/graph/db/test_sqlite.go @@ -11,6 +11,9 @@ import ( "github.com/stretchr/testify/require" ) +// isSQLDB indicates that this build uses a SQL database. +var isSQLDB = true + // NewTestDB is a helper function that creates a SQLStore backed by a SQL // database for testing. func NewTestDB(t testing.TB) Store { From 4db51980792fd65a81be90d770ff57ba5cd8cc25 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 26 Oct 2025 17:32:01 +0200 Subject: [PATCH 12/55] graph/db: introduce VersionedGraph For now, all instances will use V1 only so as not to change behaviour yet. --- autopilot/prefattach_test.go | 6 ++- graph/builder.go | 11 +++-- graph/builder_test.go | 12 +++-- graph/db/graph.go | 95 +++++++++++++++++++++--------------- graph/db/graph_test.go | 28 +++++------ graph/notifications_test.go | 12 +++-- routing/router_test.go | 24 +++++---- rpcserver.go | 8 +-- server.go | 10 +++- 9 files changed, 122 insertions(+), 84 deletions(-) diff --git a/autopilot/prefattach_test.go b/autopilot/prefattach_test.go index 78b738d3e..5439f02aa 100644 --- a/autopilot/prefattach_test.go +++ b/autopilot/prefattach_test.go @@ -31,12 +31,14 @@ type testGraph interface { } type testDBGraph struct { - db *graphdb.ChannelGraph + db *graphdb.VersionedGraph databaseChannelGraph } func newDiskChanGraph(t *testing.T) (testGraph, error) { - graphDB := graphdb.MakeTestGraph(t) + graphDB := graphdb.NewVersionedGraph( + graphdb.MakeTestGraph(t), lnwire.GossipVersion1, + ) require.NoError(t, graphDB.Start()) t.Cleanup(func() { require.NoError(t, graphDB.Stop()) diff --git a/graph/builder.go b/graph/builder.go index f9ce63eb8..3e1571396 100644 --- a/graph/builder.go +++ b/graph/builder.go @@ -111,7 +111,8 @@ type Builder struct { bestHeight atomic.Uint32 - cfg *Config + cfg *Config + v1Graph *graphdb.VersionedGraph // newBlocks is a channel in which new blocks connected to the end of // the main chain are sent over, and blocks updated after a call to @@ -146,7 +147,11 @@ var _ ChannelGraphSource = (*Builder)(nil) // NewBuilder constructs a new Builder. func NewBuilder(cfg *Config) (*Builder, error) { return &Builder{ - cfg: cfg, + cfg: cfg, + // For now, we'll just use V1 graph reader. + v1Graph: graphdb.NewVersionedGraph( + cfg.Graph, lnwire.GossipVersion1, + ), channelEdgeMtx: multimutex.NewMutex[uint64](), statTicker: ticker.New(defaultStatInterval), stats: new(builderStats), @@ -1266,7 +1271,7 @@ func (b *Builder) GetChannelByID(chanID lnwire.ShortChannelID) ( func (b *Builder) FetchNode(ctx context.Context, node route.Vertex) (*models.Node, error) { - return b.cfg.Graph.FetchNode(ctx, node) + return b.v1Graph.FetchNode(ctx, node) } // ForAllOutgoingChannels is used to iterate over all outgoing channels owned by diff --git a/graph/builder_test.go b/graph/builder_test.go index 72f271985..ca57a44c9 100644 --- a/graph/builder_test.go +++ b/graph/builder_test.go @@ -359,7 +359,7 @@ func TestWakeUpOnStaleBranch(t *testing.T) { // Create new router with same graph database. router, err := NewBuilder(&Config{ SelfNode: selfNode.PubKeyBytes, - Graph: ctx.graph, + Graph: ctx.graph.ChannelGraph, Chain: ctx.chain, ChainView: ctx.chainView, ChannelPruneExpiry: time.Hour * 24, @@ -1595,7 +1595,9 @@ func parseTestGraph(t *testing.T, useCache bool, path string) ( } return &testGraphInstance{ - graph: graph, + graph: graphdb.NewVersionedGraph( + graph, lnwire.GossipVersion1, + ), aliasMap: aliasMap, privKeyMap: privKeyMap, channelIDs: channelIDs, @@ -1690,7 +1692,7 @@ func asymmetricTestChannel(alias1, alias2 string, capacity btcutil.Amount, // assertChannelsPruned ensures that only the given channels are pruned from the // graph out of the set of all channels. -func assertChannelsPruned(t *testing.T, graph *graphdb.ChannelGraph, +func assertChannelsPruned(t *testing.T, graph *graphdb.VersionedGraph, channels []*testChannel, prunedChanIDs ...uint64) { t.Helper() @@ -1980,7 +1982,9 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, } return &testGraphInstance{ - graph: graph, + graph: graphdb.NewVersionedGraph( + graph, lnwire.GossipVersion1, + ), aliasMap: aliasMap, privKeyMap: privKeyMap, links: links, diff --git a/graph/db/graph.go b/graph/db/graph.go index 47df50e5a..ae7d93a73 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -293,23 +293,6 @@ func (c *ChannelGraph) AddNode(ctx context.Context, return nil } -// DeleteNode starts a new database transaction to remove a vertex/node -// from the database according to the node's public key. -func (c *ChannelGraph) DeleteNode(ctx context.Context, - nodePub route.Vertex) error { - - err := c.db.DeleteNode(ctx, lnwire.GossipVersion1, nodePub) - if err != nil { - return err - } - - if c.graphCache != nil { - c.graphCache.RemoveNode(nodePub) - } - - return nil -} - // AddChannelEdge adds a new (undirected, blank) edge to the graph database. An // undirected edge from the two target nodes are created. The information stored // denotes the static attributes of the channel, such as the channelID, the keys @@ -603,13 +586,6 @@ func (c *ChannelGraph) UpdateEdgePolicy(ctx context.Context, return nil } -// AddrsForNode returns all known addresses for the target node public key. -func (c *ChannelGraph) AddrsForNode(ctx context.Context, - nodePub *btcec.PublicKey) (bool, []net.Addr, error) { - - return c.db.AddrsForNode(ctx, lnwire.GossipVersion1, nodePub) -} - // ForEachSourceNodeChannel iterates through all channels of the source node. func (c *ChannelGraph) ForEachSourceNodeChannel(ctx context.Context, cb func(chanPoint wire.OutPoint, havePolicy bool, @@ -657,13 +633,6 @@ func (c *ChannelGraph) NodeUpdatesInHorizon(startTime, endTime time.Time, return c.db.NodeUpdatesInHorizon(startTime, endTime, opts...) } -// FetchNode attempts to look up a target node by its identity public key. -func (c *ChannelGraph) FetchNode(ctx context.Context, - nodePub route.Vertex) (*models.Node, error) { - - return c.db.FetchNode(ctx, lnwire.GossipVersion1, nodePub) -} - // HasV1Node determines if the graph has a vertex identified by the target node // in the V1 graph. func (c *ChannelGraph) HasV1Node(ctx context.Context, @@ -672,14 +641,6 @@ func (c *ChannelGraph) HasV1Node(ctx context.Context, return c.db.HasV1Node(ctx, nodePub) } -// HasNode determines if the graph has a vertex identified by the target node -// in the V1 graph. -func (c *ChannelGraph) HasNode(ctx context.Context, nodePub [33]byte) (bool, - error) { - - return c.db.HasNode(ctx, lnwire.GossipVersion1, nodePub) -} - // 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) @@ -811,6 +772,62 @@ func (c *ChannelGraph) PruneTip() (*chainhash.Hash, uint32, error) { return c.db.PruneTip() } +// VersionedGraph is a wrapper around ChannelGraph that will call underlying +// Store methods with a specific gossip version. +type VersionedGraph struct { + *ChannelGraph + v lnwire.GossipVersion +} + +// NewVersionedGraph creates a new VersionedGraph. +func NewVersionedGraph(c *ChannelGraph, + v lnwire.GossipVersion) *VersionedGraph { + + return &VersionedGraph{ + ChannelGraph: c, + v: v, + } +} + +// FetchNode attempts to look up a target node by its identity public key. +func (c *VersionedGraph) FetchNode(ctx context.Context, + nodePub route.Vertex) (*models.Node, error) { + + return c.db.FetchNode(ctx, c.v, nodePub) +} + +// 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) { + + return c.db.AddrsForNode(ctx, c.v, nodePub) +} + +// DeleteNode starts a new database transaction to remove a vertex/node +// from the database according to the node's public key. +func (c *VersionedGraph) DeleteNode(ctx context.Context, + nodePub route.Vertex) error { + + err := c.db.DeleteNode(ctx, c.v, nodePub) + if err != nil { + return err + } + + if c.graphCache != nil { + c.graphCache.RemoveNode(nodePub) + } + + return nil +} + +// HasNode determines if the graph has a vertex identified by the target node +// in the V1 graph. +func (c *VersionedGraph) HasNode(ctx context.Context, nodePub [33]byte) (bool, + error) { + + return c.db.HasNode(ctx, c.v, nodePub) +} + // 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 16b08797a..ff54717a9 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -98,7 +98,7 @@ func TestNodeInsertionAndDeletion(t *testing.T) { t.Parallel() ctx := t.Context() - graph := MakeTestGraph(t) + graph := NewVersionedGraph(MakeTestGraph(t), lnwire.GossipVersion1) // We'd like to test basic insertion/deletion for vertexes from the // graph, so we'll create a test vertex to start with. @@ -123,7 +123,7 @@ func TestNodeInsertionAndDeletion(t *testing.T) { // without any errors. node := nodeWithAddrs(testAddrs) require.NoError(t, graph.AddNode(ctx, node)) - assertNodeInCache(t, graph, node, testFeatures) + assertNodeInCache(t, graph.ChannelGraph, node, testFeatures) // Our AddNode implementation uses the batcher meaning that it is // possible that two updates for the same node announcement may be @@ -153,16 +153,14 @@ func TestNodeInsertionAndDeletion(t *testing.T) { // Check that the node's features are fetched correctly. This check // will check the database directly. - features, err = graph.db.FetchNodeFeatures( - lnwire.GossipVersion1, node.PubKeyBytes, - ) + features, err = graph.FetchNodeFeatures(node.PubKeyBytes) require.NoError(t, err) require.Equal(t, testFeatures, features) // Next, delete the node from the graph, this should purge all data // related to the node. require.NoError(t, graph.DeleteNode(ctx, testPub)) - assertNodeNotInCache(t, graph, testPub) + assertNodeNotInCache(t, graph.ChannelGraph, testPub) // Attempting to delete the node again should return an error since // the node is no longer known. @@ -287,7 +285,7 @@ func TestPartialNode(t *testing.T) { t.Parallel() ctx := t.Context() - graph := MakeTestGraph(t) + graph := NewVersionedGraph(MakeTestGraph(t), lnwire.GossipVersion1) // To insert a partial node, we need to add a channel edge that has // node keys for nodes we are not yet aware @@ -301,8 +299,8 @@ func TestPartialNode(t *testing.T) { // Both of the nodes should now be in both the graph (as partial/shell) // nodes _and_ the cache should also have an awareness of both nodes. - assertNodeInCache(t, graph, &node1, nil) - assertNodeInCache(t, graph, &node2, nil) + assertNodeInCache(t, graph.ChannelGraph, &node1, nil) + assertNodeInCache(t, graph.ChannelGraph, &node2, nil) // Next, fetch the node2 from the database to ensure everything was // serialized properly. @@ -332,7 +330,7 @@ func TestPartialNode(t *testing.T) { // Next, delete the node from the graph, this should purge all data // related to the node. require.NoError(t, graph.DeleteNode(ctx, pubKey1)) - assertNodeNotInCache(t, graph, testPub) + assertNodeNotInCache(t, graph.ChannelGraph, testPub) // Finally, attempt to fetch the node again. This should fail as the // node should have been deleted from the database. @@ -3750,7 +3748,7 @@ func TestPruneGraphNodes(t *testing.T) { t.Parallel() ctx := t.Context() - graph := MakeTestGraph(t) + graph := NewVersionedGraph(MakeTestGraph(t), lnwire.GossipVersion1) // We'll start off by inserting our source node, to ensure that it's // the only node left after we prune the graph. @@ -3801,7 +3799,7 @@ func TestPruneGraphNodes(t *testing.T) { // source node (which can't be pruned), and node 1+2. Nodes 1 and two // should still be left in the graph as there's half of an advertised // edge between them. - assertNumNodes(t, graph, 3) + assertNumNodes(t, graph.ChannelGraph, 3) // Finally, we'll ensure that node3, the only fully unconnected node as // properly deleted from the graph and not another node in its place. @@ -3816,7 +3814,7 @@ func TestAddChannelEdgeShellNodes(t *testing.T) { t.Parallel() ctx := t.Context() - graph := MakeTestGraph(t) + graph := NewVersionedGraph(MakeTestGraph(t), lnwire.GossipVersion1) // To start, we'll create two nodes, and only add one of them to the // channel graph. @@ -3855,7 +3853,7 @@ func TestNodePruningUpdateIndexDeletion(t *testing.T) { t.Parallel() ctx := t.Context() - graph := MakeTestGraph(t) + graph := NewVersionedGraph(MakeTestGraph(t), lnwire.GossipVersion1) // We'll first populate our graph with a single node that will be // removed shortly. @@ -4836,7 +4834,7 @@ func TestLightningNodePersistence(t *testing.T) { ctx := t.Context() // Create a new test graph instance. - graph := MakeTestGraph(t) + graph := NewVersionedGraph(MakeTestGraph(t), lnwire.GossipVersion1) nodeAnnBytes, err := hex.DecodeString(testNodeAnn) require.NoError(t, err) diff --git a/graph/notifications_test.go b/graph/notifications_test.go index 3a30486c9..e3f48712e 100644 --- a/graph/notifications_test.go +++ b/graph/notifications_test.go @@ -1042,7 +1042,7 @@ func TestEncodeHexColor(t *testing.T) { type testCtx struct { builder *Builder - graph *graphdb.ChannelGraph + graph *graphdb.VersionedGraph aliases map[string]route.Vertex @@ -1059,7 +1059,9 @@ type testCtx struct { func createTestCtxSingleNode(t *testing.T, startingHeight uint32) *testCtx { - graph := graphdb.MakeTestGraph(t) + graph := graphdb.NewVersionedGraph( + graphdb.MakeTestGraph(t), lnwire.GossipVersion1, + ) sourceNode := createTestNode(t) require.NoError(t, @@ -1086,7 +1088,7 @@ func (c *testCtx) RestartBuilder(t *testing.T) { // start it. builder, err := NewBuilder(&Config{ SelfNode: selfNode.PubKeyBytes, - Graph: c.graph, + Graph: c.graph.ChannelGraph, Chain: c.chain, ChainView: c.chainView, Notifier: c.builder.cfg.Notifier, @@ -1108,7 +1110,7 @@ func (c *testCtx) RestartBuilder(t *testing.T) { } type testGraphInstance struct { - graph *graphdb.ChannelGraph + graph *graphdb.VersionedGraph // aliasMap is a map from a node's alias to its public key. This type is // provided in order to allow easily look up from the human memorable @@ -1157,7 +1159,7 @@ func createTestCtxFromGraphInstanceAssumeValid(t *testing.T, graphBuilder, err := NewBuilder(&Config{ SelfNode: selfnode.PubKeyBytes, - Graph: graphInstance.graph, + Graph: graphInstance.graph.ChannelGraph, Chain: chain, ChainView: chainView, Notifier: notifier, diff --git a/routing/router_test.go b/routing/router_test.go index 102afe85b..9dfe5c726 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -61,7 +61,8 @@ type testCtx struct { graphBuilder *mockGraphBuilder - graph *graphdb.ChannelGraph + graph *graphdb.ChannelGraph + v1Graph *graphdb.VersionedGraph aliases map[string]route.Vertex @@ -170,9 +171,12 @@ func createTestCtxFromGraphInstanceAssumeValid(t *testing.T, router: router, graphBuilder: graphBuilder, graph: graphInstance.graph, - aliases: graphInstance.aliasMap, - privKeys: graphInstance.privKeyMap, - channelIDs: graphInstance.channelIDs, + v1Graph: graphdb.NewVersionedGraph( + graphInstance.graph, lnwire.GossipVersion1, + ), + aliases: graphInstance.aliasMap, + privKeys: graphInstance.privKeyMap, + channelIDs: graphInstance.channelIDs, } t.Cleanup(func() { @@ -2717,11 +2721,11 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { copy(pub2[:], priv2.PubKey().SerializeCompressed()) // The two nodes we are about to add should not exist yet. - exists1, err := ctx.graph.HasNode(ctxb, pub1) + exists1, err := ctx.v1Graph.HasNode(ctxb, pub1) require.NoError(t, err, "unable to query graph") require.False(t, exists1) - exists2, err := ctx.graph.HasNode(ctxb, pub2) + exists2, err := ctx.v1Graph.HasNode(ctxb, pub2) require.NoError(t, err, "unable to query graph") require.False(t, exists2) @@ -2778,11 +2782,11 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { // After adding the edge between the two previously unknown nodes, they // should have been added to the graph. - exists1, err = ctx.graph.HasNode(ctxb, pub1) + exists1, err = ctx.v1Graph.HasNode(ctxb, pub1) require.NoError(t, err, "unable to query graph") require.True(t, exists1) - exists2, err = ctx.graph.HasNode(ctxb, pub2) + exists2, err = ctx.v1Graph.HasNode(ctxb, pub2) require.NoError(t, err, "unable to query graph") require.True(t, exists2) @@ -2907,12 +2911,12 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { _, _, err = ctx.router.FindRoute(req) require.NoError(t, err, "unable to find any routes") - copy1, err := ctx.graph.FetchNode(ctxb, pub1) + copy1, err := ctx.v1Graph.FetchNode(ctxb, pub1) require.NoError(t, err, "unable to fetch node") require.Equal(t, n1.Alias, copy1.Alias) - copy2, err := ctx.graph.FetchNode(ctxb, pub2) + copy2, err := ctx.v1Graph.FetchNode(ctxb, pub2) require.NoError(t, err, "unable to fetch node") require.Equal(t, n2.Alias, copy2.Alias) diff --git a/rpcserver.go b/rpcserver.go index f09278777..d465825dc 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -746,7 +746,7 @@ func (r *rpcServer) addDeps(ctx context.Context, s *server, return info.NodeKey1Bytes, info.NodeKey2Bytes, nil }, HasNode: func(nodePub route.Vertex) (bool, error) { - exists, err := graph.HasNode(ctx, nodePub) + exists, err := s.v1Graph.HasNode(ctx, nodePub) return exists, err }, @@ -1821,7 +1821,7 @@ func (r *rpcServer) VerifyMessage(ctx context.Context, // channels signed the message. // // TODO(phlip9): Require valid nodes to have capital in active channels. - graph := r.server.graphDB + graph := r.server.v1Graph active, err := graph.HasNode(ctx, pub) if err != nil { return nil, fmt.Errorf("failed to query graph: %w", err) @@ -7143,7 +7143,7 @@ func (r *rpcServer) GetNodeInfo(ctx context.Context, "include_channels") } - graph := r.server.graphDB + graph := r.server.v1Graph // First, parse the hex-encoded public key into a full in-memory public // key object we can work with for querying. @@ -8298,7 +8298,7 @@ func (r *rpcServer) ForwardingHistory(ctx context.Context, return "", err } - peer, err := r.server.graphDB.FetchNode(ctx, vertex) + peer, err := r.server.v1Graph.FetchNode(ctx, vertex) if err != nil { return "", err } diff --git a/server.go b/server.go index 4e8ad037c..48c584ff0 100644 --- a/server.go +++ b/server.go @@ -321,6 +321,7 @@ type server struct { fundingMgr *funding.Manager graphDB *graphdb.ChannelGraph + v1Graph *graphdb.VersionedGraph chanStateDB *channeldb.ChannelStateDB @@ -669,12 +670,17 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr, HtlcInterceptor: invoiceHtlcModifier, } - addrSource := channeldb.NewMultiAddrSource(dbs.ChanStateDB, dbs.GraphDB) + v1Graph := graphdb.NewVersionedGraph( + dbs.GraphDB, lnwire.GossipVersion1, + ) + + addrSource := channeldb.NewMultiAddrSource(dbs.ChanStateDB, v1Graph) s := &server{ cfg: cfg, implCfg: implCfg, graphDB: dbs.GraphDB, + v1Graph: v1Graph, chanStateDB: dbs.ChanStateDB.ChannelStateDB(), addrSource: addrSource, miscDB: dbs.ChanStateDB, @@ -5207,7 +5213,7 @@ func (s *server) fetchNodeAdvertisedAddrs(ctx context.Context, return nil, err } - node, err := s.graphDB.FetchNode(ctx, vertex) + node, err := s.v1Graph.FetchNode(ctx, vertex) if err != nil { return nil, err } From d2b47cdbaed3f4cdf3ecd87df07c7970d13f9c28 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 26 Oct 2025 17:41:02 +0200 Subject: [PATCH 13/55] graph/db: let node helpers take version --- graph/db/graph_test.go | 202 ++++++++++++++++++++++++----------------- 1 file changed, 119 insertions(+), 83 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index ff54717a9..894d308f7 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -69,28 +69,55 @@ var ( } ) -func createNode(priv *btcec.PrivateKey) *models.Node { +func createNode(t testing.TB, v lnwire.GossipVersion, + priv *btcec.PrivateKey) *models.Node { + pubKey := route.NewVertex(priv.PubKey()) - return models.NewV1Node( - pubKey, &models.NodeV1Fields{ - LastUpdate: nextUpdateTime(), - Color: color.RGBA{1, 2, 3, 0}, - Alias: "kek" + hex.EncodeToString(pubKey[:]), - Addresses: testAddrs, - Features: testFeatures.RawFeatureVector, - AuthSigBytes: testSig.Serialize(), - }, - ) + switch v { + case lnwire.GossipVersion1: + return models.NewV1Node( + pubKey, &models.NodeV1Fields{ + LastUpdate: nextUpdateTime(), + Color: color.RGBA{1, 2, 3, 0}, + Alias: "kek" + hex.EncodeToString( + pubKey[:], + ), + Addresses: testAddrs, + Features: testFeatures.RawFeatureVector, + AuthSigBytes: testSig.Serialize(), + }, + ) + case lnwire.GossipVersion2: + return models.NewV2Node( + pubKey, &models.NodeV2Fields{ + Signature: testSig.Serialize(), + LastBlockHeight: nextBlockHeight(), + Color: fn.Some( + color.RGBA{1, 2, 3, 0}, + ), + Alias: fn.Some( + "kek" + hex.EncodeToString(pubKey[:]), + ), + Features: testFeatures. + RawFeatureVector, + Addresses: testAddrs, + }, + ) + } + + t.Fatalf("unknown gossip version: %v", v) + + return nil } -func createTestVertex(t testing.TB) *models.Node { +func createTestVertex(t testing.TB, v lnwire.GossipVersion) *models.Node { t.Helper() priv, err := btcec.NewPrivateKey() require.NoError(t, err) - return createNode(priv) + return createNode(t, v, priv) } // TestNodeInsertionAndDeletion tests the CRUD operations for a Node. @@ -105,7 +132,6 @@ func TestNodeInsertionAndDeletion(t *testing.T) { timeStamp := int64(1232342) nodeWithAddrs := func(addrs []net.Addr) *models.Node { timeStamp++ - return models.NewV1Node( testPub, &models.NodeV1Fields{ AuthSigBytes: testSig.Serialize(), @@ -347,7 +373,7 @@ func TestAliasLookup(t *testing.T) { // We'd like to test the alias index within the database, so first // create a new test node. - testNode := createTestVertex(t) + testNode := createTestVertex(t, lnwire.GossipVersion1) // Add the node to the graph's database, this should also insert an // entry into the alias index for this node. @@ -362,7 +388,7 @@ func TestAliasLookup(t *testing.T) { require.Equal(t, testNode.Alias.UnwrapOr(""), dbAlias) // Ensure that looking up a non-existent alias results in an error. - node := createTestVertex(t) + node := createTestVertex(t, lnwire.GossipVersion1) nodePub, err = node.PubKey() require.NoError(t, err, "unable to generate pubkey") _, err = graph.LookupAlias(ctx, nodePub) @@ -378,7 +404,7 @@ func TestSourceNode(t *testing.T) { // We'd like to test the setting/getting of the source node, so we // first create a fake node to use within the test. - testNode := createTestVertex(t) + testNode := createTestVertex(t, lnwire.GossipVersion1) // Attempt to fetch the source node, this should return an error as the // source node hasn't yet been set. @@ -407,10 +433,10 @@ func TestSetSourceNodeSameTimestamp(t *testing.T) { t.Parallel() ctx := t.Context() - graph := MakeTestGraph(t) + graph := NewVersionedGraph(MakeTestGraph(t), lnwire.GossipVersion1) // Create and set the initial source node. - testNode := createTestVertex(t) + testNode := createTestVertex(t, lnwire.GossipVersion1) require.NoError(t, graph.SetSourceNode(ctx, testNode)) // Verify the source node was set correctly. @@ -462,8 +488,8 @@ func TestEdgeInsertionDeletion(t *testing.T) { // We'd like to test the insertion/deletion of edges, so we create two // vertexes to connect. - node1 := createTestVertex(t) - node2 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) + node2 := createTestVertex(t, lnwire.GossipVersion1) // In addition to the fake vertexes we create some fake channel // identifiers. @@ -585,15 +611,15 @@ func TestDisconnectBlockAtHeight(t *testing.T) { graph := MakeTestGraph(t) - sourceNode := createTestVertex(t) + sourceNode := createTestVertex(t, lnwire.GossipVersion1) if err := graph.SetSourceNode(ctx, sourceNode); err != nil { t.Fatalf("unable to set source node: %v", err) } // We'd like to test the insertion/deletion of edges, so we create two // vertexes to connect. - node1 := createTestVertex(t) - node2 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) + node2 := createTestVertex(t, lnwire.GossipVersion1) // In addition to the fake vertexes we create some fake channel // identifiers. @@ -878,12 +904,12 @@ func TestEdgeInfoUpdates(t *testing.T) { // We'd like to test the update of edges inserted into the database, so // we create two vertexes to connect. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node1); err != nil { t.Fatalf("unable to add node: %v", err) } assertNodeInCache(t, graph, node1, testFeatures) - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node2); err != nil { t.Fatalf("unable to add node: %v", err) } @@ -972,8 +998,8 @@ func TestEdgePolicyCRUD(t *testing.T) { graph := MakeTestGraph(t) - node1 := createTestVertex(t) - node2 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) + node2 := createTestVertex(t, lnwire.GossipVersion1) // Create an edge. Don't add it to the DB yet. edgeInfo, edge1, edge2 := createChannelEdge(node1, node2) @@ -1274,8 +1300,8 @@ func TestAddEdgeProof(t *testing.T) { graph := MakeTestGraph(t) // Add an edge with no proof. - node1 := createTestVertex(t) - node2 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) + node2 := createTestVertex(t, lnwire.GossipVersion1) edge1, _, _ := createChannelEdge(node1, node2, withSkipProofs()) require.NoError(t, graph.AddChannelEdge(ctx, edge1)) @@ -1333,7 +1359,7 @@ func TestForEachSourceNodeChannel(t *testing.T) { graph := MakeTestGraph(t) // Create a source node (A) and set it as such in the DB. - nodeA := createTestVertex(t) + nodeA := createTestVertex(t, lnwire.GossipVersion1) require.NoError(t, graph.SetSourceNode(ctx, nodeA)) // Now, create a few more nodes (B, C, D) along with some channels @@ -1349,9 +1375,9 @@ func TestForEachSourceNodeChannel(t *testing.T) { // outgoing policy but for the A-C channel, we will set only an incoming // policy. - nodeB := createTestVertex(t) - nodeC := createTestVertex(t) - nodeD := createTestVertex(t) + nodeB := createTestVertex(t, lnwire.GossipVersion1) + nodeC := createTestVertex(t, lnwire.GossipVersion1) + nodeD := createTestVertex(t, lnwire.GossipVersion1) abEdge, abPolicy1, abPolicy2 := createChannelEdge(nodeA, nodeB) require.NoError(t, graph.AddChannelEdge(ctx, abEdge)) @@ -1650,7 +1676,7 @@ func fillTestGraph(t testing.TB, graph *ChannelGraph, numNodes, nodes := make([]*models.Node, numNodes) nodeIndex := map[string]struct{}{} for i := 0; i < numNodes; i++ { - node := createTestVertex(t) + node := createTestVertex(t, lnwire.GossipVersion1) nodes[i] = node nodeIndex[node.Alias.UnwrapOr("")] = struct{}{} @@ -1839,7 +1865,7 @@ func TestGraphPruning(t *testing.T) { graph := MakeTestGraph(t) - sourceNode := createTestVertex(t) + sourceNode := createTestVertex(t, lnwire.GossipVersion1) if err := graph.SetSourceNode(ctx, sourceNode); err != nil { t.Fatalf("unable to set source node: %v", err) } @@ -1850,7 +1876,7 @@ func TestGraphPruning(t *testing.T) { const numNodes = 5 graphNodes := make([]*models.Node, numNodes) for i := 0; i < numNodes; i++ { - node := createTestVertex(t) + node := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node); err != nil { t.Fatalf("unable to add node: %v", err) @@ -2041,8 +2067,8 @@ func TestHighestChanID(t *testing.T) { // Next, we'll insert two channels into the database, with each channel // connecting the same two nodes. - node1 := createTestVertex(t) - node2 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) + node2 := createTestVertex(t, lnwire.GossipVersion1) // The first channel with be at height 10, while the other will be at // height 100. @@ -2105,11 +2131,11 @@ func TestChanUpdatesInHorizon(t *testing.T) { } // We'll start by creating two nodes which will seed our test graph. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node1); err != nil { t.Fatalf("unable to add node: %v", err) } - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node2); err != nil { t.Fatalf("unable to add node: %v", err) } @@ -2270,7 +2296,7 @@ func TestNodeUpdatesInHorizon(t *testing.T) { const numNodes = 10 nodeAnns := make([]models.Node, 0, numNodes) for i := 0; i < numNodes; i++ { - nodeAnn := createTestVertex(t) + nodeAnn := createTestVertex(t, lnwire.GossipVersion1) // The node ann will use the current end time as its last // update them, then we'll add 10 seconds in order to create @@ -2361,7 +2387,7 @@ func testNodeUpdatesWithBatchSize(t *testing.T, ctx context.Context, var nodeAnns []models.Node for i := 0; i < 25; i++ { - nodeAnn := createTestVertex(t) + nodeAnn := createTestVertex(t, lnwire.GossipVersion1) nodeAnn.LastUpdate = startTime.Add( time.Duration(i) * time.Hour, ) @@ -2524,7 +2550,7 @@ func TestNodeUpdatesInHorizonEarlyTermination(t *testing.T) { // one hour apart. startTime := time.Unix(1234567890, 0) for i := 0; i < 100; i++ { - nodeAnn := createTestVertex(t) + nodeAnn := createTestVertex(t, lnwire.GossipVersion1) nodeAnn.LastUpdate = startTime.Add(time.Duration(i) * time.Hour) require.NoError(t, graph.AddNode(ctx, nodeAnn)) } @@ -2573,8 +2599,8 @@ func TestChanUpdatesInHorizonBoundaryConditions(t *testing.T) { // Create a fresh graph for each test, then add two new // nodes to the graph. graph := MakeTestGraph(t) - node1 := createTestVertex(t) - node2 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) + node2 := createTestVertex(t, lnwire.GossipVersion1) require.NoError(t, graph.AddNode(ctx, node1)) require.NoError(t, graph.AddNode(ctx, node2)) @@ -2742,11 +2768,11 @@ func TestFilterKnownChanIDs(t *testing.T) { }, filteredIDs) // We'll start by creating two nodes which will seed our test graph. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node1); err != nil { t.Fatalf("unable to add node: %v", err) } - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node2); err != nil { t.Fatalf("unable to add node: %v", err) } @@ -2897,10 +2923,10 @@ func TestStressTestChannelGraphAPI(t *testing.T) { graph := MakeTestGraph(t) - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) require.NoError(t, graph.AddNode(ctx, node1)) - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) require.NoError(t, graph.AddNode(ctx, node2)) // We need to update the node's timestamp since this call to @@ -3189,10 +3215,10 @@ func TestFilterChannelRange(t *testing.T) { // We'll first populate our graph with two nodes. All channels created // below will be made between these two nodes. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) require.NoError(t, graph.AddNode(ctx, node1)) - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) require.NoError(t, graph.AddNode(ctx, node2)) // If we try to filter a channel range before we have any channels @@ -3408,11 +3434,11 @@ func TestFetchChanInfos(t *testing.T) { // We'll first populate our graph with two nodes. All channels created // below will be made between these two nodes. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node1); err != nil { t.Fatalf("unable to add node: %v", err) } - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node2); err != nil { t.Fatalf("unable to add node: %v", err) } @@ -3510,11 +3536,11 @@ func TestIncompleteChannelPolicies(t *testing.T) { graph := MakeTestGraph(t) // Create two nodes. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node1); err != nil { t.Fatalf("unable to add node: %v", err) } - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node2); err != nil { t.Fatalf("unable to add node: %v", err) } @@ -3608,18 +3634,18 @@ func TestChannelEdgePruningUpdateIndexDeletion(t *testing.T) { t.Skipf("skipping test that is aimed at a bbolt graph DB") } - sourceNode := createTestVertex(t) + sourceNode := createTestVertex(t, lnwire.GossipVersion1) if err := graph.SetSourceNode(ctx, sourceNode); err != nil { t.Fatalf("unable to set source node: %v", err) } // We'll first populate our graph with two nodes. All channels created // below will be made between these two nodes. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node1); err != nil { t.Fatalf("unable to add node: %v", err) } - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node2); err != nil { t.Fatalf("unable to add node: %v", err) } @@ -3752,7 +3778,7 @@ func TestPruneGraphNodes(t *testing.T) { // We'll start off by inserting our source node, to ensure that it's // the only node left after we prune the graph. - sourceNode := createTestVertex(t) + sourceNode := createTestVertex(t, lnwire.GossipVersion1) if err := graph.SetSourceNode(ctx, sourceNode); err != nil { t.Fatalf("unable to set source node: %v", err) } @@ -3760,15 +3786,15 @@ func TestPruneGraphNodes(t *testing.T) { // With the source node inserted, we'll now add three nodes to the // channel graph, at the end of the scenario, only two of these nodes // should still be in the graph. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node1); err != nil { t.Fatalf("unable to add node: %v", err) } - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node2); err != nil { t.Fatalf("unable to add node: %v", err) } - node3 := createTestVertex(t) + node3 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node3); err != nil { t.Fatalf("unable to add node: %v", err) } @@ -3818,9 +3844,9 @@ func TestAddChannelEdgeShellNodes(t *testing.T) { // To start, we'll create two nodes, and only add one of them to the // channel graph. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) require.NoError(t, graph.SetSourceNode(ctx, node1)) - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) // We'll now create an edge between the two nodes, as a result, node2 // should be inserted into the database as a shell node. @@ -3857,7 +3883,7 @@ func TestNodePruningUpdateIndexDeletion(t *testing.T) { // We'll first populate our graph with a single node that will be // removed shortly. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node1); err != nil { t.Fatalf("unable to add node: %v", err) } @@ -3899,6 +3925,7 @@ func TestNodePruningUpdateIndexDeletion(t *testing.T) { var ( updateTime = prand.Int63() updateTimeMu sync.Mutex + updateBlock = prand.Uint32() ) func nextUpdateTime() time.Time { @@ -3910,6 +3937,15 @@ func nextUpdateTime() time.Time { return time.Unix(updateTime, 0) } +func nextBlockHeight() uint32 { + updateTimeMu.Lock() + defer updateTimeMu.Unlock() + + updateBlock++ + + return updateBlock +} + // TestNodeIsPublic ensures that we properly detect nodes that are seen as // public within the network graph. func TestNodeIsPublic(t *testing.T) { @@ -3925,19 +3961,19 @@ func TestNodeIsPublic(t *testing.T) { // participant to replicate real-world scenarios (private edges being in // some graphs but not others, etc.). aliceGraph := MakeTestGraph(t) - aliceNode := createTestVertex(t) + aliceNode := createTestVertex(t, lnwire.GossipVersion1) if err := aliceGraph.SetSourceNode(ctx, aliceNode); err != nil { t.Fatalf("unable to set source node: %v", err) } bobGraph := MakeTestGraph(t) - bobNode := createTestVertex(t) + bobNode := createTestVertex(t, lnwire.GossipVersion1) if err := bobGraph.SetSourceNode(ctx, bobNode); err != nil { t.Fatalf("unable to set source node: %v", err) } carolGraph := MakeTestGraph(t) - carolNode := createTestVertex(t) + carolNode := createTestVertex(t, lnwire.GossipVersion1) if err := carolGraph.SetSourceNode(ctx, carolNode); err != nil { t.Fatalf("unable to set source node: %v", err) } @@ -4076,13 +4112,13 @@ func TestDisabledChannelIDs(t *testing.T) { graph := MakeTestGraph(t) // Create first node and add it to the graph. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node1); err != nil { t.Fatalf("unable to add node: %v", err) } // Create second node and add it to the graph. - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node2); err != nil { t.Fatalf("unable to add node: %v", err) } @@ -4168,11 +4204,11 @@ func TestEdgePolicyMissingMaxHTLC(t *testing.T) { // We'd like to test the update of edges inserted into the database, so // we create two vertexes to connect. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) if err := graph.AddNode(ctx, node1); err != nil { t.Fatalf("unable to add node: %v", err) } - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) edgeInfo, edge1, edge2 := createChannelEdge(node1, node2) if err := graph.AddNode(ctx, node2); err != nil { @@ -4304,8 +4340,8 @@ func TestGraphZombieIndex(t *testing.T) { // We'll start by creating our test graph along with a test edge. graph := MakeTestGraph(t) - node1 := createTestVertex(t) - node2 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) + node2 := createTestVertex(t, lnwire.GossipVersion1) // Swap the nodes if the second's pubkey is smaller than the first. // Without this, the comparisons at the end will fail probabilistically. @@ -4453,7 +4489,7 @@ func TestLightningNodeSigVerification(t *testing.T) { } // Create a Node from the same private key. - node := createNode(priv) + node := createNode(t, lnwire.GossipVersion1, priv) // And finally check that we can verify the same signature from the // pubkey returned from the lightning node. @@ -4490,13 +4526,13 @@ func TestBatchedAddChannelEdge(t *testing.T) { graph := MakeTestGraph(t) - sourceNode := createTestVertex(t) + sourceNode := createTestVertex(t, lnwire.GossipVersion1) require.Nil(t, graph.SetSourceNode(ctx, sourceNode)) // We'd like to test the insertion/deletion of edges, so we create two // vertexes to connect. - node1 := createTestVertex(t) - node2 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) + node2 := createTestVertex(t, lnwire.GossipVersion1) // In addition to the fake vertexes we create some fake channel // identifiers. @@ -4569,9 +4605,9 @@ func TestBatchedUpdateEdgePolicy(t *testing.T) { // We'd like to test the update of edges inserted into the database, so // we create two vertexes to connect. - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) require.NoError(t, graph.AddNode(ctx, node1)) - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) require.NoError(t, graph.AddNode(ctx, node2)) // Create an edge and add it to the db. @@ -4677,9 +4713,9 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) { // option turned off. graph.graphCache = nil - node1 := createTestVertex(t) + node1 := createTestVertex(t, lnwire.GossipVersion1) require.NoError(t, graph.AddNode(ctx, node1)) - node2 := createTestVertex(t) + node2 := createTestVertex(t, lnwire.GossipVersion1) require.NoError(t, graph.AddNode(ctx, node2)) // Create an edge and add it to the db. From 7a5a588176a05baa4f5207e046aee615088f88b0 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 26 Nov 2025 07:49:41 +0200 Subject: [PATCH 14/55] graph/db: add TestVersionedDBs This will be used to run sub-tests with v2 data. --- graph/db/graph_test.go | 68 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 894d308f7..d7ceb06aa 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -120,22 +120,47 @@ func createTestVertex(t testing.TB, v lnwire.GossipVersion) *models.Node { return createNode(t, v, priv) } -// TestNodeInsertionAndDeletion tests the CRUD operations for a Node. -func TestNodeInsertionAndDeletion(t *testing.T) { +type versionedTest struct { + name string + test func(t *testing.T, v lnwire.GossipVersion) +} + +var versionedTests = []versionedTest{ + { + name: "node crud", + test: testNodeInsertionAndDeletion, + }, +} + +// TestVersionedDBs runs various tests against both v1 and v2 versioned +// backends. +func TestVersionedDBs(t *testing.T) { t.Parallel() - ctx := t.Context() - graph := NewVersionedGraph(MakeTestGraph(t), lnwire.GossipVersion1) + for _, vt := range versionedTests { + vt := vt - // We'd like to test basic insertion/deletion for vertexes from the - // graph, so we'll create a test vertex to start with. - timeStamp := int64(1232342) + t.Run(vt.name+"/v1", func(t *testing.T) { + vt.test(t, lnwire.GossipVersion1) + }) + + if !isSQLDB { + continue + } + + t.Run(vt.name+"/v2", func(t *testing.T) { + vt.test(t, lnwire.GossipVersion2) + }) + } +} + +// testNodeInsertionAndDeletion tests the CRUD operations for a Node. +func testNodeInsertionAndDeletion(t *testing.T, v lnwire.GossipVersion) { nodeWithAddrs := func(addrs []net.Addr) *models.Node { - timeStamp++ return models.NewV1Node( testPub, &models.NodeV1Fields{ AuthSigBytes: testSig.Serialize(), - LastUpdate: time.Unix(timeStamp, 0), + LastUpdate: nextUpdateTime(), Color: color.RGBA{1, 2, 3, 0}, Alias: "kek", Features: testFeatures.RawFeatureVector, @@ -145,6 +170,31 @@ func TestNodeInsertionAndDeletion(t *testing.T) { ) } + if v == lnwire.GossipVersion2 { + nodeWithAddrs = func(addrs []net.Addr) *models.Node { + return models.NewV2Node( + testPub, &models.NodeV2Fields{ + Signature: testSig.Serialize(), + LastBlockHeight: nextBlockHeight(), + Color: fn.Some( + color.RGBA{1, 2, 3, 0}, + ), + Alias: fn.Some("kek"), + Features: testFeatures. + RawFeatureVector, + Addresses: addrs, + ExtraSignedFields: map[uint64][]byte{ + 20: {0x1, 0x2, 0x3}, + 21: {0x4, 0x5, 0x6, 0x7}, + }, + }, + ) + } + } + + ctx := t.Context() + graph := NewVersionedGraph(MakeTestGraph(t), v) + // First, insert the node into the graph DB. This should succeed // without any errors. node := nodeWithAddrs(testAddrs) From 7bb38651d941feb23870180e530901450ee18ec6 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 26 Oct 2025 17:46:50 +0200 Subject: [PATCH 15/55] graph/db: convert other node CRUD methods Convert a few more CRUD methods to handle v2 nodes. Then update some more tests to be run against both v1 and v2 nodes. --- graph/db/graph.go | 26 ++++++++------- graph/db/graph_test.go | 27 +++++++++------ graph/db/sql_store.go | 2 +- routing/pathfind_test.go | 72 +++++++++++++++++++++++----------------- routing/router_test.go | 6 ++-- rpcserver.go | 6 ++-- server.go | 6 ++-- 7 files changed, 82 insertions(+), 63 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index ae7d93a73..3fbb00e93 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -618,13 +618,6 @@ func (c *ChannelGraph) ForEachNodeCacheable(ctx context.Context, return c.db.ForEachNodeCacheable(ctx, cb, reset) } -// LookupAlias attempts to return the alias as advertised by the target node. -func (c *ChannelGraph) LookupAlias(ctx context.Context, - pub *btcec.PublicKey) (string, error) { - - return c.db.LookupAlias(ctx, lnwire.GossipVersion1, pub) -} - // NodeUpdatesInHorizon returns all known lightning nodes with updates in the // range. func (c *ChannelGraph) NodeUpdatesInHorizon(startTime, endTime time.Time, @@ -755,11 +748,6 @@ func (c *ChannelGraph) IsClosedScid(scid lnwire.ShortChannelID) (bool, error) { return c.db.IsClosedScid(scid) } -// SourceNode returns the source node of the graph. -func (c *ChannelGraph) SourceNode(ctx context.Context) (*models.Node, error) { - return c.db.SourceNode(ctx, lnwire.GossipVersion1) -} - // SetSourceNode sets the source node within the graph database. func (c *ChannelGraph) SetSourceNode(ctx context.Context, node *models.Node) error { @@ -828,6 +816,20 @@ func (c *VersionedGraph) HasNode(ctx context.Context, nodePub [33]byte) (bool, return c.db.HasNode(ctx, c.v, nodePub) } +// LookupAlias attempts to return the alias as advertised by the target node. +func (c *VersionedGraph) LookupAlias(ctx context.Context, + pub *btcec.PublicKey) (string, error) { + + return c.db.LookupAlias(ctx, c.v, pub) +} + +// SourceNode returns the source node of the graph. +func (c *VersionedGraph) SourceNode(ctx context.Context) (*models.Node, + error) { + + return c.db.SourceNode(ctx, c.v) +} + // 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 d7ceb06aa..33d1b2bbe 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -130,6 +130,14 @@ var versionedTests = []versionedTest{ name: "node crud", test: testNodeInsertionAndDeletion, }, + { + name: "source node", + test: testSourceNode, + }, + { + name: "alias lookup", + test: testAliasLookup, + }, } // TestVersionedDBs runs various tests against both v1 and v2 versioned @@ -414,16 +422,15 @@ func TestPartialNode(t *testing.T) { require.ErrorIs(t, err, ErrGraphNodeNotFound) } -// TestAliasLookup tests the alias lookup functionality of the graph store. -func TestAliasLookup(t *testing.T) { - t.Parallel() +// testAliasLookup tests the alias lookup functionality of the graph store. +func testAliasLookup(t *testing.T, v lnwire.GossipVersion) { ctx := t.Context() - graph := MakeTestGraph(t) + graph := NewVersionedGraph(MakeTestGraph(t), v) // We'd like to test the alias index within the database, so first // create a new test node. - testNode := createTestVertex(t, lnwire.GossipVersion1) + testNode := createTestVertex(t, v) // Add the node to the graph's database, this should also insert an // entry into the alias index for this node. @@ -438,23 +445,23 @@ func TestAliasLookup(t *testing.T) { require.Equal(t, testNode.Alias.UnwrapOr(""), dbAlias) // Ensure that looking up a non-existent alias results in an error. - node := createTestVertex(t, lnwire.GossipVersion1) + node := createTestVertex(t, v) nodePub, err = node.PubKey() require.NoError(t, err, "unable to generate pubkey") _, err = graph.LookupAlias(ctx, nodePub) require.ErrorIs(t, err, ErrNodeAliasNotFound) } -// TestSourceNode tests the source node functionality of the graph store. -func TestSourceNode(t *testing.T) { +// testSourceNode tests the source node functionality of the graph store. +func testSourceNode(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 setting/getting of the source node, so we // first create a fake node to use within the test. - testNode := createTestVertex(t, lnwire.GossipVersion1) + testNode := createTestVertex(t, v) // Attempt to fetch the source node, this should return an error as the // source node hasn't yet been set. diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 0df0985b7..2c794fa95 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -524,7 +524,7 @@ func (s *SQLStore) SourceNode(ctx context.Context, err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { _, nodePub, err := s.getSourceNode(ctx, db, v) if err != nil { - return fmt.Errorf("unable to fetch V1 source node: %w", + return fmt.Errorf("unable to fetch source node: %w", err) } diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 473bd413e..132d61fb8 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -417,7 +417,10 @@ func parseTestGraph(t *testing.T, useCache bool, path string) ( } return &testGraphInstance{ - graph: graph, + graph: graph, + v1Graph: graphdb.NewVersionedGraph( + graph, lnwire.GossipVersion1, + ), mcBackend: mcBackend, aliasMap: aliasMap, privKeyMap: privKeyMap, @@ -487,6 +490,7 @@ type testChannel struct { type testGraphInstance struct { graph *graphdb.ChannelGraph + v1Graph *graphdb.VersionedGraph mcBackend kvdb.Backend // aliasMap is a map from a node's alias to its public key. This type is @@ -781,7 +785,10 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, } return &testGraphInstance{ - graph: graph, + graph: graph, + v1Graph: graphdb.NewVersionedGraph( + graph, lnwire.GossipVersion1, + ), mcBackend: graphBackend, aliasMap: aliasMap, privKeyMap: privKeyMap, @@ -1095,7 +1102,7 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc expectedHops := test.expectedHops expectedHopCount := len(expectedHops) - sourceNode, err := graphInstance.graph.SourceNode(ctx) + sourceNode, err := graphInstance.v1Graph.SourceNode(ctx) require.NoError(t, err, "unable to fetch source node") sourceVertex := route.Vertex(sourceNode.PubKeyBytes) @@ -1107,7 +1114,7 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc paymentAmt := lnwire.NewMSatFromSatoshis(test.paymentAmt) target := graphInstance.aliasMap[test.target] path, err := dbFindPath( - graphInstance.graph, nil, &mockBandwidthHints{}, + graphInstance.v1Graph, nil, &mockBandwidthHints{}, &RestrictParams{ FeeLimit: test.feeLimit, ProbabilitySource: noProbabilitySource, @@ -1237,7 +1244,7 @@ func runPathFindingWithAdditionalEdges(t *testing.T, useCache bool) { ctx := t.Context() - sourceNode, err := graph.graph.SourceNode(ctx) + sourceNode, err := graph.v1Graph.SourceNode(ctx) require.NoError(t, err, "unable to fetch source node") paymentAmt := lnwire.NewMSatFromSatoshis(100) @@ -1282,7 +1289,8 @@ func runPathFindingWithAdditionalEdges(t *testing.T, useCache bool) { []*unifiedEdge, error) { return dbFindPath( - graph.graph, additionalEdges, &mockBandwidthHints{}, + graph.v1Graph, additionalEdges, + &mockBandwidthHints{}, r, testPathFindingConfig, sourceNode.PubKeyBytes, doge.PubKeyBytes, paymentAmt, 0, 0, @@ -1322,7 +1330,7 @@ func runPathFindingWithBlindedPathDuplicateHop(t *testing.T, useCache bool) { ctx := t.Context() - sourceNode, err := graph.graph.SourceNode(ctx) + sourceNode, err := graph.v1Graph.SourceNode(ctx) require.NoError(t, err, "unable to fetch source node") paymentAmt := lnwire.NewMSatFromSatoshis(100) @@ -1396,7 +1404,7 @@ func runPathFindingWithBlindedPathDuplicateHop(t *testing.T, useCache bool) { []*unifiedEdge, error) { return dbFindPath( - graph.graph, blindedPath, &mockBandwidthHints{}, + graph.v1Graph, blindedPath, &mockBandwidthHints{}, r, testPathFindingConfig, sourceNode.PubKeyBytes, dummyTarget, paymentAmt, 0, 0, @@ -1456,7 +1464,7 @@ func runPathFindingWithRedundantAdditionalEdges(t *testing.T, useCache bool) { } path, err := dbFindPath( - ctx.graph, additionalEdges, ctx.bandwidthHints, + ctx.v1Graph, additionalEdges, ctx.bandwidthHints, &ctx.restrictParams, &ctx.pathFindingConfig, ctx.source, target, paymentAmt, ctx.timePref, 0, ) @@ -1809,7 +1817,7 @@ func runPathNotAvailable(t *testing.T, useCache bool) { ctx := t.Context() - sourceNode, err := graph.graph.SourceNode(ctx) + sourceNode, err := graph.v1Graph.SourceNode(ctx) require.NoError(t, err, "unable to fetch source node") // With the test graph loaded, we'll test that queries for target that @@ -1822,7 +1830,7 @@ func runPathNotAvailable(t *testing.T, useCache bool) { copy(unknownNode[:], unknownNodeBytes) _, err = dbFindPath( - graph.graph, nil, &mockBandwidthHints{}, + graph.v1Graph, nil, &mockBandwidthHints{}, noRestrictions, testPathFindingConfig, sourceNode.PubKeyBytes, unknownNode, 100, 0, 0, ) @@ -1865,14 +1873,14 @@ func runDestTLVGraphFallback(t *testing.T, useCache bool) { ctx := newPathFindingTestContext(t, useCache, testChannels, "roasbeef") - sourceNode, err := ctx.graph.SourceNode(t.Context()) + sourceNode, err := ctx.v1Graph.SourceNode(t.Context()) require.NoError(t, err, "unable to fetch source node") find := func(r *RestrictParams, target route.Vertex) ([]*unifiedEdge, error) { return dbFindPath( - ctx.graph, nil, &mockBandwidthHints{}, + ctx.v1Graph, nil, &mockBandwidthHints{}, r, testPathFindingConfig, sourceNode.PubKeyBytes, target, 100, 0, 0, ) @@ -2084,7 +2092,7 @@ func runPathInsufficientCapacity(t *testing.T, useCache bool) { require.NoError(t, err, "unable to create graph") ctx := t.Context() - sourceNode, err := graph.graph.SourceNode(ctx) + sourceNode, err := graph.v1Graph.SourceNode(ctx) require.NoError(t, err, "unable to fetch source node") // Next, test that attempting to find a path in which the current @@ -2099,7 +2107,7 @@ func runPathInsufficientCapacity(t *testing.T, useCache bool) { payAmt := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin) _, err = dbFindPath( - graph.graph, nil, &mockBandwidthHints{}, + graph.v1Graph, nil, &mockBandwidthHints{}, noRestrictions, testPathFindingConfig, sourceNode.PubKeyBytes, target, payAmt, 0, 0, ) @@ -2115,7 +2123,7 @@ func runRouteFailMinHTLC(t *testing.T, useCache bool) { require.NoError(t, err, "unable to create graph") ctx := t.Context() - sourceNode, err := graph.graph.SourceNode(ctx) + sourceNode, err := graph.v1Graph.SourceNode(ctx) require.NoError(t, err, "unable to fetch source node") // We'll not attempt to route an HTLC of 10 SAT from roasbeef to Son @@ -2124,7 +2132,7 @@ func runRouteFailMinHTLC(t *testing.T, useCache bool) { target := graph.aliasMap["songoku"] payAmt := lnwire.MilliSatoshi(10) _, err = dbFindPath( - graph.graph, nil, &mockBandwidthHints{}, + graph.v1Graph, nil, &mockBandwidthHints{}, noRestrictions, testPathFindingConfig, sourceNode.PubKeyBytes, target, payAmt, 0, 0, ) @@ -2200,7 +2208,7 @@ func runRouteFailDisabledEdge(t *testing.T, useCache bool) { require.NoError(t, err, "unable to create graph") ctx := t.Context() - sourceNode, err := graph.graph.SourceNode(ctx) + sourceNode, err := graph.v1Graph.SourceNode(ctx) require.NoError(t, err, "unable to fetch source node") // First, we'll try to route from roasbeef -> sophon. This should @@ -2208,7 +2216,7 @@ func runRouteFailDisabledEdge(t *testing.T, useCache bool) { target := graph.aliasMap["sophon"] payAmt := lnwire.NewMSatFromSatoshis(105000) _, err = dbFindPath( - graph.graph, nil, &mockBandwidthHints{}, + graph.v1Graph, nil, &mockBandwidthHints{}, noRestrictions, testPathFindingConfig, sourceNode.PubKeyBytes, target, payAmt, 0, 0, ) @@ -2232,7 +2240,7 @@ func runRouteFailDisabledEdge(t *testing.T, useCache bool) { } _, err = dbFindPath( - graph.graph, nil, &mockBandwidthHints{}, + graph.v1Graph, nil, &mockBandwidthHints{}, noRestrictions, testPathFindingConfig, sourceNode.PubKeyBytes, target, payAmt, 0, 0, ) @@ -2252,7 +2260,7 @@ func runRouteFailDisabledEdge(t *testing.T, useCache bool) { // If we attempt to route through that edge, we should get a failure as // it is no longer eligible. _, err = dbFindPath( - graph.graph, nil, &mockBandwidthHints{}, + graph.v1Graph, nil, &mockBandwidthHints{}, noRestrictions, testPathFindingConfig, sourceNode.PubKeyBytes, target, payAmt, 0, 0, ) @@ -2269,7 +2277,7 @@ func runPathSourceEdgesBandwidth(t *testing.T, useCache bool) { require.NoError(t, err, "unable to create graph") ctx := t.Context() - sourceNode, err := graph.graph.SourceNode(ctx) + sourceNode, err := graph.v1Graph.SourceNode(ctx) require.NoError(t, err, "unable to fetch source node") // First, we'll try to route from roasbeef -> sophon. This should @@ -2278,7 +2286,7 @@ func runPathSourceEdgesBandwidth(t *testing.T, useCache bool) { target := graph.aliasMap["sophon"] payAmt := lnwire.NewMSatFromSatoshis(50000) path, err := dbFindPath( - graph.graph, nil, &mockBandwidthHints{}, + graph.v1Graph, nil, &mockBandwidthHints{}, noRestrictions, testPathFindingConfig, sourceNode.PubKeyBytes, target, payAmt, 0, 0, ) @@ -2299,7 +2307,7 @@ func runPathSourceEdgesBandwidth(t *testing.T, useCache bool) { // Since both these edges has a bandwidth of zero, no path should be // found. _, err = dbFindPath( - graph.graph, nil, bandwidths, + graph.v1Graph, nil, bandwidths, noRestrictions, testPathFindingConfig, sourceNode.PubKeyBytes, target, payAmt, 0, 0, ) @@ -2314,7 +2322,7 @@ func runPathSourceEdgesBandwidth(t *testing.T, useCache bool) { // Now, if we attempt to route again, we should find the path via // phamnuven, as the other source edge won't be considered. path, err = dbFindPath( - graph.graph, nil, bandwidths, + graph.v1Graph, nil, bandwidths, noRestrictions, testPathFindingConfig, sourceNode.PubKeyBytes, target, payAmt, 0, 0, ) @@ -2340,7 +2348,7 @@ func runPathSourceEdgesBandwidth(t *testing.T, useCache bool) { // Since we ignore disable flags for local channels, a path should // still be found. path, err = dbFindPath( - graph.graph, nil, bandwidths, + graph.v1Graph, nil, bandwidths, noRestrictions, testPathFindingConfig, sourceNode.PubKeyBytes, target, payAmt, 0, 0, ) @@ -3184,6 +3192,7 @@ func runInboundFees(t *testing.T, useCache bool) { type pathFindingTestContext struct { t *testing.T graph *graphdb.ChannelGraph + v1Graph *graphdb.VersionedGraph restrictParams RestrictParams bandwidthHints bandwidthHints pathFindingConfig PathFindingConfig @@ -3201,7 +3210,7 @@ func newPathFindingTestContext(t *testing.T, useCache bool, ) require.NoError(t, err, "unable to create graph") - sourceNode, err := testGraphInstance.graph.SourceNode( + sourceNode, err := testGraphInstance.v1Graph.SourceNode( t.Context(), ) require.NoError(t, err, "unable to fetch source node") @@ -3212,6 +3221,7 @@ func newPathFindingTestContext(t *testing.T, useCache bool, source: route.Vertex(sourceNode.PubKeyBytes), pathFindingConfig: *testPathFindingConfig, graph: testGraphInstance.graph, + v1Graph: testGraphInstance.v1Graph, restrictParams: *noRestrictions, bandwidthHints: &mockBandwidthHints{}, } @@ -3247,7 +3257,7 @@ func (c *pathFindingTestContext) findPath(target route.Vertex, error) { return dbFindPath( - c.graph, nil, c.bandwidthHints, &c.restrictParams, + c.v1Graph, nil, c.bandwidthHints, &c.restrictParams, &c.pathFindingConfig, c.source, target, amt, c.timePref, 0, ) } @@ -3255,7 +3265,7 @@ func (c *pathFindingTestContext) findPath(target route.Vertex, func (c *pathFindingTestContext) findBlindedPaths( restrictions *blindedPathRestrictions) ([][]blindedHop, error) { - return dbFindBlindedPaths(c.graph, restrictions) + return dbFindBlindedPaths(c.v1Graph, restrictions) } func (c *pathFindingTestContext) assertPath(path []*unifiedEdge, @@ -3277,7 +3287,7 @@ func (c *pathFindingTestContext) assertPath(path []*unifiedEdge, // dbFindPath calls findPath after getting a db transaction from the database // graph. -func dbFindPath(graph *graphdb.ChannelGraph, +func dbFindPath(graph *graphdb.VersionedGraph, additionalEdges map[route.Vertex][]AdditionalEdge, bandwidthHints bandwidthHints, r *RestrictParams, cfg *PathFindingConfig, @@ -3315,7 +3325,7 @@ func dbFindPath(graph *graphdb.ChannelGraph, // dbFindBlindedPaths calls findBlindedPaths after getting a db transaction from // the database graph. -func dbFindBlindedPaths(graph *graphdb.ChannelGraph, +func dbFindBlindedPaths(graph *graphdb.VersionedGraph, restrictions *blindedPathRestrictions) ([][]blindedHop, error) { sourceNode, err := graph.SourceNode(context.Background()) diff --git a/routing/router_test.go b/routing/router_test.go index 9dfe5c726..cb4393c06 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -132,7 +132,7 @@ func createTestCtxFromGraphInstanceAssumeValid(t *testing.T, ) require.NoError(t, err) - sourceNode, err := graphInstance.graph.SourceNode(t.Context()) + sourceNode, err := graphInstance.v1Graph.SourceNode(t.Context()) require.NoError(t, err) sessionSource := &SessionSource{ GraphSessionFactory: graphInstance.graph, @@ -1205,7 +1205,7 @@ func TestFindPathFeeWeighting(t *testing.T) { var preImage [32]byte copy(preImage[:], bytes.Repeat([]byte{9}, 32)) - sourceNode, err := ctx.graph.SourceNode(t.Context()) + sourceNode, err := ctx.v1Graph.SourceNode(t.Context()) require.NoError(t, err, "unable to fetch source node") amt := lnwire.MilliSatoshi(100) @@ -1216,7 +1216,7 @@ func TestFindPathFeeWeighting(t *testing.T) { // the edge weighting, we should select the direct path over the 2 hop // path even though the direct path has a higher potential time lock. path, err := dbFindPath( - ctx.graph, nil, &mockBandwidthHints{}, + ctx.v1Graph, nil, &mockBandwidthHints{}, noRestrictions, testPathFindingConfig, sourceNode.PubKeyBytes, target, amt, 0, 0, diff --git a/rpcserver.go b/rpcserver.go index d465825dc..06e015507 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -704,7 +704,7 @@ func (r *rpcServer) addDeps(ctx context.Context, s *server, invoiceHtlcModifier *invoices.HtlcModificationInterceptor) error { // Set up router rpc backend. - selfNode, err := s.graphDB.SourceNode(ctx) + selfNode, err := s.v1Graph.SourceNode(ctx) if err != nil { return err } @@ -4995,7 +4995,7 @@ func createRPCOpenChannel(ctx context.Context, r *rpcServer, // Look up our channel peer's node alias if the caller requests it. if peerAliasLookup { - peerAlias, err := r.server.graphDB.LookupAlias(ctx, nodePub) + peerAlias, err := r.server.v1Graph.LookupAlias(ctx, nodePub) if err != nil { peerAlias = fmt.Sprintf("unable to lookup "+ "peer alias: %v", err) @@ -7909,7 +7909,7 @@ const feeBase float64 = 1000000 func (r *rpcServer) FeeReport(ctx context.Context, _ *lnrpc.FeeReportRequest) (*lnrpc.FeeReportResponse, error) { - channelGraph := r.server.graphDB + channelGraph := r.server.v1Graph selfNode, err := channelGraph.SourceNode(ctx) if err != nil { return nil, err diff --git a/server.go b/server.go index 48c584ff0..78b1971e1 100644 --- a/server.go +++ b/server.go @@ -988,7 +988,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr, MinProbability: routingConfig.MinRouteProbability, } - sourceNode, err := dbs.GraphDB.SourceNode(ctx) + sourceNode, err := s.v1Graph.SourceNode(ctx) if err != nil { return nil, fmt.Errorf("error getting source node: %w", err) } @@ -3440,7 +3440,7 @@ func (s *server) updateAndBroadcastSelfNode(ctx context.Context, // Update the on-disk version of our announcement. // Load and modify self node istead of creating anew instance so we // don't risk overwriting any existing values. - selfNode, err := s.graphDB.SourceNode(ctx) + selfNode, err := s.v1Graph.SourceNode(ctx) if err != nil { return fmt.Errorf("unable to get current source node: %w", err) } @@ -5624,7 +5624,7 @@ func (s *server) setSelfNode(ctx context.Context, nodePub route.Vertex, nodeLastUpdate = time.Now() ) - srcNode, err := s.graphDB.SourceNode(ctx) + srcNode, err := s.v1Graph.SourceNode(ctx) switch { case err == nil: // If we have a source node persisted in the DB already, then we From fcbfeb33e00ca836cf9a7fe720bacdc26ba490a6 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 3 Nov 2025 13:34:55 +0200 Subject: [PATCH 16/55] docs: update release notes --- docs/release-notes/release-notes-0.21.0.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index 1ba4f3fd9..0040da730 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -176,6 +176,8 @@ * Freeze the [graph SQL migration code](https://github.com/lightningnetwork/lnd/pull/10338) to prevent the need for maintenance as the sqlc code evolves. +* Prepare the graph DB for handling gossip [V2 + nodes](https://github.com/lightningnetwork/lnd/pull/10339). ## Code Health From 6fda2e8da9ca1de6be8c6323f98d54691ab93e5d Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 07:50:55 +0200 Subject: [PATCH 17/55] graph/db: remove unused sig field from ChannelEdgePolicy Remove the cached parsed signature field and its lazy getter method from ChannelEdgePolicy. This field was unused throughout the codebase and the signature is already stored as raw bytes in SigBytes. The SetSigBytes method is updated to remove the cache invalidation logic. --- graph/db/models/channel_edge_policy.go | 34 +------------------------- routing/localchans/manager.go | 2 +- 2 files changed, 2 insertions(+), 34 deletions(-) diff --git a/graph/db/models/channel_edge_policy.go b/graph/db/models/channel_edge_policy.go index 48d748ee0..a2661ef6f 100644 --- a/graph/db/models/channel_edge_policy.go +++ b/graph/db/models/channel_edge_policy.go @@ -4,7 +4,6 @@ import ( "fmt" "time" - "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/lnwire" ) @@ -17,13 +16,9 @@ import ( type ChannelEdgePolicy struct { // SigBytes is the raw bytes of the signature of the channel edge // policy. We'll only parse these if the caller needs to access the - // signature for validation purposes. Do not set SigBytes directly, but - // use SetSigBytes instead to make sure that the cache is invalidated. + // signature for validation purposes. SigBytes []byte - // sig is a cached fully parsed signature. - sig *ecdsa.Signature - // ChannelID is the unique channel ID for the channel. The first 3 // bytes are the block height, the next 3 the index within the block, // and the last 2 bytes are the output index for the channel. @@ -84,33 +79,6 @@ type ChannelEdgePolicy struct { ExtraOpaqueData lnwire.ExtraOpaqueData } -// Signature is a channel announcement signature, which is needed for proper -// edge policy announcement. -// -// NOTE: By having this method to access an attribute, we ensure we only need -// to fully deserialize the signature if absolutely necessary. -func (c *ChannelEdgePolicy) Signature() (*ecdsa.Signature, error) { - if c.sig != nil { - return c.sig, nil - } - - sig, err := ecdsa.ParseSignature(c.SigBytes) - if err != nil { - return nil, err - } - - c.sig = sig - - return sig, nil -} - -// SetSigBytes updates the signature and invalidates the cached parsed -// signature. -func (c *ChannelEdgePolicy) SetSigBytes(sig []byte) { - c.SigBytes = sig - c.sig = nil -} - // IsDisabled determines whether the edge has the disabled bit set. func (c *ChannelEdgePolicy) IsDisabled() bool { return c.ChannelFlags.IsDisabled() diff --git a/routing/localchans/manager.go b/routing/localchans/manager.go index a48486e7b..1a7c1d585 100644 --- a/routing/localchans/manager.go +++ b/routing/localchans/manager.go @@ -449,7 +449,7 @@ func (r *Manager) updateEdge(chanPoint wire.OutPoint, } // Clear signature to help prevent usage of the previous signature. - edge.SetSigBytes(nil) + edge.SigBytes = nil return nil } From 34f74c71ce1fa79db4ec68b147c6540b131f1e41 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 08:07:17 +0200 Subject: [PATCH 18/55] graph/db: use route.Vertex for channel keys Replace [33]byte with route.Vertex for NodeKey1Bytes, NodeKey2Bytes, BitcoinKey1Bytes, and BitcoinKey2Bytes in ChannelEdgeInfo. Since route.Vertex is defined as [33]byte, this change is functionally equivalent but provides better type safety and consistency with the rest of the routing subsystem. OtherNodeKeyBytes is also updated to return route.Vertex. --- graph/db/kv_store.go | 6 +++--- graph/db/models/channel_edge_info.go | 17 +++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 021f4a3c5..85b93caed 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -1262,9 +1262,9 @@ func (c *KVStore) addChannelEdge(tx kvdb.RwTx, // Mark edge policies for both sides as unknown. This is to enable // efficient incoming channel lookup for a node. - keys := []*[33]byte{ - &edge.NodeKey1Bytes, - &edge.NodeKey2Bytes, + keys := []route.Vertex{ + edge.NodeKey1Bytes, + edge.NodeKey2Bytes, } for _, key := range keys { err := putChanEdgePolicyUnknown(edges, edge.ChannelID, key[:]) diff --git a/graph/db/models/channel_edge_info.go b/graph/db/models/channel_edge_info.go index 0d6cd04f0..cfa7e1e6a 100644 --- a/graph/db/models/channel_edge_info.go +++ b/graph/db/models/channel_edge_info.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" ) // ChannelEdgeInfo represents a fully authenticated channel along with all its @@ -29,16 +30,16 @@ type ChannelEdgeInfo struct { ChainHash chainhash.Hash // NodeKey1Bytes is the raw public key of the first node. - NodeKey1Bytes [33]byte + NodeKey1Bytes route.Vertex - // NodeKey2Bytes is the raw public key of the first node. - NodeKey2Bytes [33]byte + // NodeKey2Bytes is the raw public key of the second node. + NodeKey2Bytes route.Vertex // BitcoinKey1Bytes is the raw public key of the first node. - BitcoinKey1Bytes [33]byte + BitcoinKey1Bytes route.Vertex - // BitcoinKey2Bytes is the raw public key of the first node. - BitcoinKey2Bytes [33]byte + // BitcoinKey2Bytes is the raw public key of the second node. + BitcoinKey2Bytes route.Vertex // Features is the list of protocol features supported by this channel // edge. @@ -90,7 +91,7 @@ func (c *ChannelEdgeInfo) NodeKey2() (*btcec.PublicKey, error) { // OtherNodeKeyBytes returns the node key bytes of the other end of the channel. func (c *ChannelEdgeInfo) OtherNodeKeyBytes(thisNodeKey []byte) ( - [33]byte, error) { + route.Vertex, error) { switch { case bytes.Equal(c.NodeKey1Bytes[:], thisNodeKey): @@ -98,7 +99,7 @@ func (c *ChannelEdgeInfo) OtherNodeKeyBytes(thisNodeKey []byte) ( case bytes.Equal(c.NodeKey2Bytes[:], thisNodeKey): return c.NodeKey1Bytes, nil default: - return [33]byte{}, fmt.Errorf("node not participating in " + + return route.Vertex{}, fmt.Errorf("node not participating in " + "this channel") } } From b13b0bb92211d73de7dbaeff6163e654c4eff869 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 08:18:21 +0200 Subject: [PATCH 19/55] graph/db: expose version in maybeCreateShellNode Add a version parameter to maybeCreateShellNode to allow callers to specify which gossip protocol version should be used when creating shell nodes. Currently all callers pass GossipVersion1, but this change sets up the foundation for v2 support. Shell nodes are lightweight node entries containing only a protocol version and public key, created before full node information is available. --- graph/db/sql_store.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 2c794fa95..a1592eacf 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -4239,14 +4239,20 @@ func marshalExtraOpaqueData(data []byte) (map[uint64][]byte, error) { func insertChannel(ctx context.Context, db SQLQueries, edge *models.ChannelEdgeInfo) error { + v := lnwire.GossipVersion1 + // Make sure that at least a "shell" entry for each node is present in // the nodes table. - node1DBID, err := maybeCreateShellNode(ctx, db, 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, edge.NodeKey2Bytes) + node2DBID, err := maybeCreateShellNode( + ctx, db, v, edge.NodeKey2Bytes, + ) if err != nil { return fmt.Errorf("unable to create shell node: %w", err) } @@ -4257,7 +4263,7 @@ func insertChannel(ctx context.Context, db SQLQueries, } createParams := sqlc.CreateChannelParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), Scid: channelIDToBytes(edge.ChannelID), NodeID1: node1DBID, NodeID2: node2DBID, @@ -4326,12 +4332,12 @@ func insertChannel(ctx context.Context, db SQLQueries, // created. The ID of the node is returned. A shell node only has a protocol // version and public key persisted. func maybeCreateShellNode(ctx context.Context, db SQLQueries, - pubKey route.Vertex) (int64, error) { + v lnwire.GossipVersion, pubKey route.Vertex) (int64, error) { dbNode, err := db.GetNodeByPubKey( ctx, sqlc.GetNodeByPubKeyParams{ PubKey: pubKey[:], - Version: int16(lnwire.GossipVersion1), + Version: int16(v), }, ) // The node exists. Return the ID. @@ -4344,7 +4350,7 @@ func maybeCreateShellNode(ctx context.Context, db SQLQueries, // Otherwise, the node does not exist, so we create a shell entry for // it. id, err := db.UpsertNode(ctx, sqlc.UpsertNodeParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), PubKey: pubKey[:], }) if err != nil { From ec46480a8694b6e3a2fce027ac5cc6b82f9f64ce Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 08:21:11 +0200 Subject: [PATCH 20/55] sqldb/sqlc: update graph CreateChannel query for v2 Add three new optional fields to the CreateChannel SQL query to support v2 channel announcements: - signature: single schnorr signature (replaces four ECDSA sigs) - funding_pk_script: the funding output script - merkle_root_hash: for taproot channels These fields are NULL for v1 channels and populated for v2 channels. --- sqldb/sqlc/graph.sql.go | 10 ++++++++-- sqldb/sqlc/queries/graph.sql | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index 2c26eb5d2..0096d87ec 100644 --- a/sqldb/sqlc/graph.sql.go +++ b/sqldb/sqlc/graph.sql.go @@ -78,9 +78,9 @@ INSERT INTO graph_channels ( version, scid, node_id_1, node_id_2, outpoint, capacity, bitcoin_key_1, bitcoin_key_2, node_1_signature, node_2_signature, bitcoin_1_signature, - bitcoin_2_signature + bitcoin_2_signature, signature, funding_pk_script, merkle_root_hash ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 ) RETURNING id ` @@ -98,6 +98,9 @@ type CreateChannelParams struct { Node2Signature []byte Bitcoin1Signature []byte Bitcoin2Signature []byte + Signature []byte + FundingPkScript []byte + MerkleRootHash []byte } func (q *Queries) CreateChannel(ctx context.Context, arg CreateChannelParams) (int64, error) { @@ -114,6 +117,9 @@ func (q *Queries) CreateChannel(ctx context.Context, arg CreateChannelParams) (i arg.Node2Signature, arg.Bitcoin1Signature, arg.Bitcoin2Signature, + arg.Signature, + arg.FundingPkScript, + arg.MerkleRootHash, ) var id int64 err := row.Scan(&id) diff --git a/sqldb/sqlc/queries/graph.sql b/sqldb/sqlc/queries/graph.sql index 2df865d9b..549cc930b 100644 --- a/sqldb/sqlc/queries/graph.sql +++ b/sqldb/sqlc/queries/graph.sql @@ -297,9 +297,9 @@ INSERT INTO graph_channels ( version, scid, node_id_1, node_id_2, outpoint, capacity, bitcoin_key_1, bitcoin_key_2, node_1_signature, node_2_signature, bitcoin_1_signature, - bitcoin_2_signature + bitcoin_2_signature, signature, funding_pk_script, merkle_root_hash ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 ) RETURNING id; From 4480352d599b2ca33df02f93356903a3965db113 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 09:40:23 +0200 Subject: [PATCH 21/55] multi: add version to models.ChannelEdgeInfo And set it to V1 version everywhere. Add a Version field to ChannelEdgeInfo to distinguish between v1 and v2 channel announcements. Set it to GossipVersion1 for all existing channels. Both KV and SQL stores now validate that only v1 channels are currently supported, returning an error for v2 channels. The KV store automatically sets version to v1 when deserializing (since all persisted channels in KV format are v1). This versioning is essential for handling the different field requirements and validation logic between v1 and v2 channels. --- autopilot/prefattach_test.go | 1 + discovery/gossiper.go | 1 + discovery/gossiper_test.go | 1 + graph/builder_test.go | 12 ++++++++++++ graph/db/channel_cache_test.go | 2 ++ graph/db/graph_test.go | 5 +++++ graph/db/kv_store.go | 10 ++++++++++ graph/db/models/channel_edge_info.go | 3 +++ graph/db/sql_store.go | 11 ++++++++++- graph/notifications_test.go | 4 ++++ lnrpc/devrpc/dev_server.go | 1 + lnrpc/invoicesrpc/addinvoice_test.go | 1 + netann/chan_status_manager_test.go | 1 + netann/channel_announcement_test.go | 1 + routing/localchans/manager.go | 1 + routing/localchans/manager_test.go | 6 ++++++ routing/pathfind_test.go | 2 ++ routing/router_test.go | 2 ++ 18 files changed, 64 insertions(+), 1 deletion(-) diff --git a/autopilot/prefattach_test.go b/autopilot/prefattach_test.go index 5439f02aa..a4026d73d 100644 --- a/autopilot/prefattach_test.go +++ b/autopilot/prefattach_test.go @@ -493,6 +493,7 @@ func (d *testDBGraph) addRandChannel(node1, node2 *btcec.PublicKey, chanID := randChanID() edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID.ToUint64(), Capacity: capacity, Features: lnwire.EmptyFeatureVector(), diff --git a/discovery/gossiper.go b/discovery/gossiper.go index ee83d5048..9d0338c22 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -2819,6 +2819,7 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(ctx context.Context, // With the proof validated (if necessary), we can now store it within // the database for our path finding and syncing needs. edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: scid.ToUint64(), ChainHash: ann.ChainHash, NodeKey1Bytes: ann.NodeID1, diff --git a/discovery/gossiper_test.go b/discovery/gossiper_test.go index efcedb0e9..efb7307da 100644 --- a/discovery/gossiper_test.go +++ b/discovery/gossiper_test.go @@ -273,6 +273,7 @@ func (r *mockGraphSource) GetChannelByID(chanID lnwire.ShortChannelID) ( } return &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, NodeKey1Bytes: pubKeys[0], NodeKey2Bytes: pubKeys[1], }, nil, nil, graphdb.ErrZombieEdge diff --git a/graph/builder_test.go b/graph/builder_test.go index ca57a44c9..b6fe88675 100644 --- a/graph/builder_test.go +++ b/graph/builder_test.go @@ -66,6 +66,7 @@ func TestAddProof(t *testing.T) { // After utxo was recreated adding the edge without the proof. edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID.ToUint64(), NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, @@ -152,6 +153,7 @@ func TestIgnoreChannelEdgePolicyForUnknownChannel(t *testing.T) { ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight) edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID.ToUint64(), NodeKey1Bytes: pub1, NodeKey2Bytes: pub2, @@ -273,6 +275,7 @@ func TestWakeUpOnStaleBranch(t *testing.T) { node2 := createTestNode(t) edge1 := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID1, NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, @@ -293,6 +296,7 @@ func TestWakeUpOnStaleBranch(t *testing.T) { } edge2 := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID2, NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, @@ -483,6 +487,7 @@ func TestDisconnectedBlocks(t *testing.T) { node2 := createTestNode(t) edge1 := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID1, NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, @@ -505,6 +510,7 @@ func TestDisconnectedBlocks(t *testing.T) { } edge2 := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID2, NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, @@ -639,6 +645,7 @@ func TestChansClosedOfflinePruneGraph(t *testing.T) { node2 := createTestNode(t) edge1 := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID1.ToUint64(), NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, @@ -1062,6 +1069,7 @@ func TestIsStaleNode(t *testing.T) { ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight) edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID.ToUint64(), NodeKey1Bytes: pub1, NodeKey2Bytes: pub2, @@ -1142,6 +1150,7 @@ func TestIsKnownEdge(t *testing.T) { ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight) edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID.ToUint64(), NodeKey1Bytes: pub1, NodeKey2Bytes: pub2, @@ -1202,6 +1211,7 @@ func TestIsStaleEdgePolicy(t *testing.T) { } edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID.ToUint64(), NodeKey1Bytes: pub1, NodeKey2Bytes: pub2, @@ -1514,6 +1524,7 @@ func parseTestGraph(t *testing.T, useCache bool, path string) ( // We first insert the existence of the edge between the two // nodes. edgeInfo := models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: edge.ChannelID, AuthProof: &testAuthProof, ChannelPoint: fundingPoint, @@ -1885,6 +1896,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, // We first insert the existence of the edge between the two // nodes. edgeInfo := models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: channelID, AuthProof: &testAuthProof, ChannelPoint: *fundingPoint, diff --git a/graph/db/channel_cache_test.go b/graph/db/channel_cache_test.go index 767958d9a..04f6d0331 100644 --- a/graph/db/channel_cache_test.go +++ b/graph/db/channel_cache_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/lightningnetwork/lnd/graph/db/models" + "github.com/lightningnetwork/lnd/lnwire" ) // TestChannelCache checks the behavior of the channelCache with respect to @@ -101,6 +102,7 @@ func assertHasChanEntries(t *testing.T, c *channelCache, start, end uint64) { func channelForInt(i uint64) ChannelEdge { return ChannelEdge{ Info: &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: i, }, } diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 33d1b2bbe..7c4255bcb 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -563,6 +563,7 @@ func TestEdgeInsertionDeletion(t *testing.T) { node2Pub, err := node2.PubKey() require.NoError(t, err, "unable to generate node key") edgeInfo := models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID, ChainHash: *chaincfg.MainNetParams.GenesisHash, AuthProof: &models.ChannelAuthProof{ @@ -638,6 +639,7 @@ func createEdge(height, txIndex uint32, txPosition uint16, outPointIndex uint32, node1Pub, _ := node1.PubKey() node2Pub, _ := node2.PubKey() edgeInfo := models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: shortChanID.ToUint64(), ChainHash: *chaincfg.MainNetParams.GenesisHash, AuthProof: &models.ChannelAuthProof{ @@ -894,6 +896,7 @@ func createChannelEdge(node1, node2 *models.Node, // Add the new edge to the database, this should proceed without any // errors. edgeInfo := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID, ChainHash: *chaincfg.MainNetParams.GenesisHash, ChannelPoint: outpoint, @@ -1775,6 +1778,7 @@ func fillTestGraph(t testing.TB, graph *ChannelGraph, numNodes, } edgeInfo := models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID, ChainHash: *chaincfg.MainNetParams.GenesisHash, AuthProof: &models.ChannelAuthProof{ @@ -1957,6 +1961,7 @@ func TestGraphPruning(t *testing.T) { channelPoints = append(channelPoints, &op) edgeInfo := models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID, ChainHash: *chaincfg.MainNetParams.GenesisHash, AuthProof: &models.ChannelAuthProof{ diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 85b93caed..74072781b 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -3945,6 +3945,7 @@ func (c *KVStore) FetchChannelEdgesByID(chanID uint64) ( // party as this is the only information we have about // it and return an error signaling so. edgeInfo = &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, NodeKey1Bytes: pubKey1, NodeKey2Bytes: pubKey2, } @@ -4704,6 +4705,12 @@ func deserializeLightningNode(r io.Reader) (*models.Node, error) { func putChanEdgeInfo(edgeIndex kvdb.RwBucket, edgeInfo *models.ChannelEdgeInfo, chanID [8]byte) error { + // We only support V1 channel edges in the KV store. + if edgeInfo.Version != lnwire.GossipVersion1 { + return fmt.Errorf("only V1 channel edges supported, got V%d", + edgeInfo.Version) + } + var b bytes.Buffer if _, err := b.Write(edgeInfo.NodeKey1Bytes[:]); err != nil { @@ -4866,6 +4873,9 @@ func deserializeChanEdgeInfo(r io.Reader) (*models.ChannelEdgeInfo, error) { edgeInfo models.ChannelEdgeInfo ) + // All channel edges in the KV store are V1. + edgeInfo.Version = lnwire.GossipVersion1 + if _, err := io.ReadFull(r, edgeInfo.NodeKey1Bytes[:]); err != nil { return nil, err } diff --git a/graph/db/models/channel_edge_info.go b/graph/db/models/channel_edge_info.go index cfa7e1e6a..4c783a6a9 100644 --- a/graph/db/models/channel_edge_info.go +++ b/graph/db/models/channel_edge_info.go @@ -20,6 +20,9 @@ import ( // policy of a channel are stored within a ChannelEdgePolicy for each direction // of the channel. type ChannelEdgeInfo struct { + // Version is the gossip version that this channel was advertised on. + Version lnwire.GossipVersion + // ChannelID is the unique channel ID for the channel. The first 3 // bytes are the block height, the next 3 the index within the block, // and the last 2 bytes are the output index for the channel. diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index a1592eacf..aeeeb378d 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -2038,7 +2038,9 @@ func (s *SQLStore) FetchChannelEdgesByID(chanID uint64) ( // populate the edge info with the public keys of each // party as this is the only information we have about // it. - edge = &models.ChannelEdgeInfo{} + edge = &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, + } copy(edge.NodeKey1Bytes[:], zombie.NodeKey1) copy(edge.NodeKey2Bytes[:], zombie.NodeKey2) @@ -4241,6 +4243,12 @@ func insertChannel(ctx context.Context, db SQLQueries, 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) + } + // Make sure that at least a "shell" entry for each node is present in // the nodes table. node1DBID, err := maybeCreateShellNode( @@ -4458,6 +4466,7 @@ func buildEdgeInfoWithBatchData(chain chainhash.Hash, copy(btcKey2[:], dbChan.BitcoinKey2) channel := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChainHash: chain, ChannelID: byteOrder.Uint64(dbChan.Scid), NodeKey1Bytes: node1, diff --git a/graph/notifications_test.go b/graph/notifications_test.go index e3f48712e..e220a0b44 100644 --- a/graph/notifications_test.go +++ b/graph/notifications_test.go @@ -448,6 +448,7 @@ func TestEdgeUpdateNotification(t *testing.T) { // Finally, to conclude our test set up, we'll create a channel // update to announce the created channel between the two nodes. edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID.ToUint64(), NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, @@ -642,6 +643,7 @@ func TestNodeUpdateNotification(t *testing.T) { require.NoError(t, testFeatures.Encode(testFeaturesBuf)) edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID.ToUint64(), NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, @@ -828,6 +830,7 @@ func TestNotificationCancellation(t *testing.T) { ntfnClient.Cancel() edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID.ToUint64(), NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, @@ -904,6 +907,7 @@ func TestChannelCloseNotification(t *testing.T) { // Finally, to conclude our test set up, we'll create a channel // announcement to announce the created channel between the two nodes. edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID.ToUint64(), NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, diff --git a/lnrpc/devrpc/dev_server.go b/lnrpc/devrpc/dev_server.go index 08f01e147..31db5fe0a 100644 --- a/lnrpc/devrpc/dev_server.go +++ b/lnrpc/devrpc/dev_server.go @@ -274,6 +274,7 @@ func (s *Server) ImportGraph(ctx context.Context, rpcEdge := rpcEdge edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: rpcEdge.ChannelId, ChainHash: *s.cfg.ActiveNetParams.GenesisHash, Capacity: btcutil.Amount(rpcEdge.Capacity), diff --git a/lnrpc/invoicesrpc/addinvoice_test.go b/lnrpc/invoicesrpc/addinvoice_test.go index 9394ce26d..ceab2bec4 100644 --- a/lnrpc/invoicesrpc/addinvoice_test.go +++ b/lnrpc/invoicesrpc/addinvoice_test.go @@ -306,6 +306,7 @@ var shouldIncludeChannelTestCases = []struct { "FetchChannelEdgesByID", mock.Anything, ).Once().Return( &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, NodeKey1Bytes: selectedPolicy, }, &models.ChannelEdgePolicy{ diff --git a/netann/chan_status_manager_test.go b/netann/chan_status_manager_test.go index 024b77932..1ec7cea23 100644 --- a/netann/chan_status_manager_test.go +++ b/netann/chan_status_manager_test.go @@ -101,6 +101,7 @@ func createEdgePolicies(t *testing.T, channel *channeldb.OpenChannel, dir2 |= lnwire.ChanUpdateDirection return &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelPoint: channel.FundingOutpoint, NodeKey1Bytes: pubkey1, NodeKey2Bytes: pubkey2, diff --git a/netann/channel_announcement_test.go b/netann/channel_announcement_test.go index 38949e046..49f61a57f 100644 --- a/netann/channel_announcement_test.go +++ b/netann/channel_announcement_test.go @@ -47,6 +47,7 @@ func TestCreateChanAnnouncement(t *testing.T) { BitcoinSig2Bytes: expChanAnn.BitcoinSig2.ToSignatureBytes(), } chanInfo := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChainHash: expChanAnn.ChainHash, ChannelID: expChanAnn.ShortChannelID.ToUint64(), ChannelPoint: wire.OutPoint{Index: 1}, diff --git a/routing/localchans/manager.go b/routing/localchans/manager.go index 1a7c1d585..f47e34a16 100644 --- a/routing/localchans/manager.go +++ b/routing/localchans/manager.go @@ -329,6 +329,7 @@ func (r *Manager) createEdge(channel *channeldb.OpenChannel, } info := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: shortChanID.ToUint64(), ChainHash: channel.ChainHash, Features: lnwire.EmptyFeatureVector(), diff --git a/routing/localchans/manager_test.go b/routing/localchans/manager_test.go index 5df344bba..108fe0a7a 100644 --- a/routing/localchans/manager_test.go +++ b/routing/localchans/manager_test.go @@ -212,6 +212,7 @@ func TestManager(t *testing.T) { channelSet: []channel{ { edgeInfo: &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, Capacity: chanCap, ChannelPoint: chanPointValid, }, @@ -230,6 +231,7 @@ func TestManager(t *testing.T) { channelSet: []channel{ { edgeInfo: &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, Capacity: chanCap, ChannelPoint: chanPointValid, }, @@ -248,6 +250,7 @@ func TestManager(t *testing.T) { channelSet: []channel{ { edgeInfo: &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, Capacity: chanCap, ChannelPoint: chanPointValid, }, @@ -270,6 +273,7 @@ func TestManager(t *testing.T) { channelSet: []channel{ { edgeInfo: &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, Capacity: chanCap, ChannelPoint: chanPointValid, }, @@ -392,6 +396,7 @@ func TestCreateEdgeLower(t *testing.T) { require.NoError(t, err) expectedInfo := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: 8, ChainHash: channel.ChainHash, Features: lnwire.EmptyFeatureVector(), @@ -485,6 +490,7 @@ func TestCreateEdgeHigher(t *testing.T) { require.NoError(t, err) expectedInfo := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: 8, ChainHash: channel.ChainHash, Features: lnwire.EmptyFeatureVector(), diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 132d61fb8..b7256e3a8 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -346,6 +346,7 @@ func parseTestGraph(t *testing.T, useCache bool, path string) ( // We first insert the existence of the edge between the two // nodes. edgeInfo := models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: edge.ChannelID, AuthProof: &testAuthProof, ChannelPoint: fundingPoint, @@ -678,6 +679,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, // We first insert the existence of the edge between the two // nodes. edgeInfo := models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: channelID, AuthProof: &testAuthProof, ChannelPoint: *fundingPoint, diff --git a/routing/router_test.go b/routing/router_test.go index cb4393c06..bb0b2f85c 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -2739,6 +2739,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { require.NoError(t, err, "unable to create channel edge") edge := &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID.ToUint64(), NodeKey1Bytes: pub1, NodeKey2Bytes: pub2, @@ -2819,6 +2820,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { require.NoError(t, err, "unable to create channel edge") edge = &models.ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, ChannelID: chanID.ToUint64(), Features: lnwire.EmptyFeatureVector(), AuthProof: nil, From 7a260ee1030e29baf2797996e9578b026f6ce044 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 10:19:40 +0200 Subject: [PATCH 22/55] multi: update models.ChannelAuthProof with v2 field Also update it to more closely match the persisted version which has the v1 and v2 only fields as optional. Refactor ChannelAuthProof to support both v1 and v2 channel announcements: - Add Version field to distinguish v1 from v2 proofs - Wrap v1-specific fields (NodeSig1/2, BitcoinSig1/2) in fn.Option since v2 doesn't use them - Add optional Signature field for v2's single schnorr signature - Add constructor functions NewV1ChannelAuthProof and NewV2ChannelAuthProof to enforce correct initialization - Add getter methods (NodeSig1(), BitcoinSig1(), etc.) that safely unwrap options, returning empty slices when not present The IsEmpty() check is updated to handle both versions correctly. Both stores validate v1-only for now. --- discovery/gossiper.go | 46 ++++++----- graph/builder_test.go | 60 +++++++-------- graph/db/graph_test.go | 85 +++++++++++---------- graph/db/kv_store.go | 42 +++++++--- graph/db/models/channel_auth_proof.go | 106 +++++++++++++++++++++++--- graph/db/sql_store.go | 36 +++++---- graph/notifications_test.go | 60 +++++++-------- netann/channel_announcement.go | 8 +- netann/channel_announcement_test.go | 12 +-- routing/pathfind_test.go | 12 +-- rpcserver.go | 8 +- 11 files changed, 300 insertions(+), 175 deletions(-) diff --git a/discovery/gossiper.go b/discovery/gossiper.go index 9d0338c22..2cbd4e873 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -2504,25 +2504,25 @@ func (d *AuthenticatedGossiper) updateChannel(ctx context.Context, ExtraOpaqueData: info.ExtraOpaqueData, } chanAnn.NodeSig1, err = lnwire.NewSigFromECDSARawSignature( - info.AuthProof.NodeSig1Bytes, + info.AuthProof.NodeSig1(), ) if err != nil { return nil, nil, err } chanAnn.NodeSig2, err = lnwire.NewSigFromECDSARawSignature( - info.AuthProof.NodeSig2Bytes, + info.AuthProof.NodeSig2(), ) if err != nil { return nil, nil, err } chanAnn.BitcoinSig1, err = lnwire.NewSigFromECDSARawSignature( - info.AuthProof.BitcoinSig1Bytes, + info.AuthProof.BitcoinSig1(), ) if err != nil { return nil, nil, err } chanAnn.BitcoinSig2, err = lnwire.NewSigFromECDSARawSignature( - info.AuthProof.BitcoinSig2Bytes, + info.AuthProof.BitcoinSig2(), ) if err != nil { return nil, nil, err @@ -2808,12 +2808,12 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(ctx context.Context, // If the proof checks out, then we'll save the proof itself to // the database so we can fetch it later when gossiping with // other nodes. - proof = &models.ChannelAuthProof{ - NodeSig1Bytes: ann.NodeSig1.ToSignatureBytes(), - NodeSig2Bytes: ann.NodeSig2.ToSignatureBytes(), - BitcoinSig1Bytes: ann.BitcoinSig1.ToSignatureBytes(), - BitcoinSig2Bytes: ann.BitcoinSig2.ToSignatureBytes(), - } + proof = models.NewV1ChannelAuthProof( + ann.NodeSig1.ToSignatureBytes(), + ann.NodeSig2.ToSignatureBytes(), + ann.BitcoinSig1.ToSignatureBytes(), + ann.BitcoinSig2.ToSignatureBytes(), + ) } // With the proof validated (if necessary), we can now store it within @@ -3758,21 +3758,25 @@ func (d *AuthenticatedGossiper) handleAnnSig(ctx context.Context, // We now have both halves of the channel announcement proof, then // we'll reconstruct the initial announcement so we can validate it // shortly below. - var dbProof models.ChannelAuthProof + var dbProof *models.ChannelAuthProof if isFirstNode { - dbProof.NodeSig1Bytes = ann.NodeSignature.ToSignatureBytes() - dbProof.NodeSig2Bytes = oppProof.NodeSignature.ToSignatureBytes() - dbProof.BitcoinSig1Bytes = ann.BitcoinSignature.ToSignatureBytes() - dbProof.BitcoinSig2Bytes = oppProof.BitcoinSignature.ToSignatureBytes() + dbProof = models.NewV1ChannelAuthProof( + ann.NodeSignature.ToSignatureBytes(), + oppProof.NodeSignature.ToSignatureBytes(), + ann.BitcoinSignature.ToSignatureBytes(), + oppProof.BitcoinSignature.ToSignatureBytes(), + ) } else { - dbProof.NodeSig1Bytes = oppProof.NodeSignature.ToSignatureBytes() - dbProof.NodeSig2Bytes = ann.NodeSignature.ToSignatureBytes() - dbProof.BitcoinSig1Bytes = oppProof.BitcoinSignature.ToSignatureBytes() - dbProof.BitcoinSig2Bytes = ann.BitcoinSignature.ToSignatureBytes() + dbProof = models.NewV1ChannelAuthProof( + oppProof.NodeSignature.ToSignatureBytes(), + ann.NodeSignature.ToSignatureBytes(), + oppProof.BitcoinSignature.ToSignatureBytes(), + ann.BitcoinSignature.ToSignatureBytes(), + ) } chanAnn, e1Ann, e2Ann, err := netann.CreateChanAnnouncement( - &dbProof, chanInfo, e1, e2, + dbProof, chanInfo, e1, e2, ) if err != nil { log.Error(err) @@ -3798,7 +3802,7 @@ func (d *AuthenticatedGossiper) handleAnnSig(ctx context.Context, // attest to the bitcoin keys by validating the signatures of // announcement. If proof is valid then we'll populate the channel edge // with it, so we can announce it on peer connect. - err = d.cfg.Graph.AddProof(ann.ShortChannelID, &dbProof) + err = d.cfg.Graph.AddProof(ann.ShortChannelID, dbProof) if err != nil { err := fmt.Errorf("unable add proof to the channel chanID=%v:"+ " %v", ann.ChannelID, err) diff --git a/graph/builder_test.go b/graph/builder_test.go index b6fe88675..f2648a002 100644 --- a/graph/builder_test.go +++ b/graph/builder_test.go @@ -279,12 +279,12 @@ func TestWakeUpOnStaleBranch(t *testing.T) { ChannelID: chanID1, NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), Features: lnwire.EmptyFeatureVector(), FundingScript: fn.Some(fundingScript1), } @@ -300,12 +300,12 @@ func TestWakeUpOnStaleBranch(t *testing.T) { ChannelID: chanID2, NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), Features: lnwire.EmptyFeatureVector(), FundingScript: fn.Some(fundingScript2), } @@ -493,12 +493,12 @@ func TestDisconnectedBlocks(t *testing.T) { NodeKey2Bytes: node2.PubKeyBytes, BitcoinKey1Bytes: node1.PubKeyBytes, BitcoinKey2Bytes: node2.PubKeyBytes, - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), Features: lnwire.EmptyFeatureVector(), FundingScript: fn.Some([]byte{}), } @@ -516,12 +516,12 @@ func TestDisconnectedBlocks(t *testing.T) { NodeKey2Bytes: node2.PubKeyBytes, BitcoinKey1Bytes: node1.PubKeyBytes, BitcoinKey2Bytes: node2.PubKeyBytes, - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), Features: lnwire.EmptyFeatureVector(), FundingScript: fn.Some([]byte{}), } @@ -649,12 +649,12 @@ func TestChansClosedOfflinePruneGraph(t *testing.T) { ChannelID: chanID1.ToUint64(), NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), ChannelPoint: *chanUTXO, Capacity: chanValue, Features: lnwire.EmptyFeatureVector(), diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 7c4255bcb..fcae25e8a 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -566,12 +566,12 @@ func TestEdgeInsertionDeletion(t *testing.T) { Version: lnwire.GossipVersion1, ChannelID: chanID, ChainHash: *chaincfg.MainNetParams.GenesisHash, - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), Features: lnwire.EmptyFeatureVector(), ChannelPoint: outpoint, Capacity: 9000, @@ -642,12 +642,12 @@ func createEdge(height, txIndex uint32, txPosition uint16, outPointIndex uint32, Version: lnwire.GossipVersion1, ChannelID: shortChanID.ToUint64(), ChainHash: *chaincfg.MainNetParams.GenesisHash, - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), ChannelPoint: outpoint, Capacity: 9000, ExtraOpaqueData: make([]byte, 0), @@ -821,17 +821,20 @@ func assertEdgeInfoEqual(t *testing.T, e1 *models.ChannelEdgeInfo, } require.True(t, bytes.Equal( - e1.AuthProof.NodeSig1Bytes, e2.AuthProof.NodeSig1Bytes, + e1.AuthProof.NodeSig1(), + e2.AuthProof.NodeSig1(), )) require.True(t, bytes.Equal( - e1.AuthProof.NodeSig2Bytes, e2.AuthProof.NodeSig2Bytes, + e1.AuthProof.NodeSig2(), + e2.AuthProof.NodeSig2(), )) require.True(t, bytes.Equal( - e1.AuthProof.BitcoinSig1Bytes, - e2.AuthProof.BitcoinSig1Bytes, + e1.AuthProof.BitcoinSig1(), + e2.AuthProof.BitcoinSig1(), )) require.True(t, bytes.Equal( - e1.AuthProof.BitcoinSig2Bytes, e2.AuthProof.BitcoinSig2Bytes, + e1.AuthProof.BitcoinSig2(), + e2.AuthProof.BitcoinSig2(), )) if e1.ChannelPoint != e2.ChannelPoint { @@ -917,12 +920,12 @@ func createChannelEdge(node1, node2 *models.Node, return edgeInfo, nil, nil } - edgeInfo.AuthProof = &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - } + edgeInfo.AuthProof = models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) edge1 := &models.ChannelEdgePolicy{ SigBytes: testSig.Serialize(), @@ -1373,12 +1376,12 @@ func TestAddEdgeProof(t *testing.T) { require.Equal(t, edge1, dbEdge) // Now, add the edge proof. - proof := &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - } + proof := models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) // First, add the proof to the rest of the channel edge info and try // to call AddChannelEdge again - this should fail due to the channel @@ -1781,12 +1784,12 @@ func fillTestGraph(t testing.TB, graph *ChannelGraph, numNodes, Version: lnwire.GossipVersion1, ChannelID: chanID, ChainHash: *chaincfg.MainNetParams.GenesisHash, - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), Features: lnwire.EmptyFeatureVector(), ChannelPoint: op, Capacity: 1000, @@ -1964,12 +1967,12 @@ func TestGraphPruning(t *testing.T) { Version: lnwire.GossipVersion1, ChannelID: chanID, ChainHash: *chaincfg.MainNetParams.GenesisHash, - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), Features: lnwire.EmptyFeatureVector(), ChannelPoint: op, Capacity: 1000, diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 74072781b..b137c6240 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -1398,6 +1398,12 @@ func (c *KVStore) HasChannelEdge( func (c *KVStore) AddEdgeProof(chanID lnwire.ShortChannelID, proof *models.ChannelAuthProof) error { + // We only support v1 channel proofs in the KVStore. + if proof.Version != lnwire.GossipVersion1 { + return fmt.Errorf("only v1 channel proofs supported, got v%d", + proof.Version) + } + // Construct the channel's primary key which is the 8-byte channel ID. var chanKey [8]byte binary.BigEndian.PutUint64(chanKey[:], chanID.ToUint64()) @@ -4738,10 +4744,10 @@ func putChanEdgeInfo(edgeIndex kvdb.RwBucket, authProof := edgeInfo.AuthProof var nodeSig1, nodeSig2, bitcoinSig1, bitcoinSig2 []byte if authProof != nil { - nodeSig1 = authProof.NodeSig1Bytes - nodeSig2 = authProof.NodeSig2Bytes - bitcoinSig1 = authProof.BitcoinSig1Bytes - bitcoinSig2 = authProof.BitcoinSig2Bytes + nodeSig1 = authProof.NodeSig1() + nodeSig2 = authProof.NodeSig2() + bitcoinSig1 = authProof.BitcoinSig1() + bitcoinSig2 = authProof.BitcoinSig2() } if err := wire.WriteVarBytes(&b, 0, nodeSig1); err != nil { @@ -4899,24 +4905,42 @@ func deserializeChanEdgeInfo(r io.Reader) (*models.ChannelEdgeInfo, error) { return nil, err } - proof := &models.ChannelAuthProof{} + proof := &models.ChannelAuthProof{ + // KV store always uses v1. + Version: lnwire.GossipVersion1, + } - proof.NodeSig1Bytes, err = wire.ReadVarBytes(r, 0, 80, "sigs") + nodeSig1, err := wire.ReadVarBytes(r, 0, 80, "sigs") if err != nil { return nil, err } - proof.NodeSig2Bytes, err = wire.ReadVarBytes(r, 0, 80, "sigs") + if len(nodeSig1) > 0 { + proof.NodeSig1Bytes = fn.Some(nodeSig1) + } + + nodeSig2, err := wire.ReadVarBytes(r, 0, 80, "sigs") if err != nil { return nil, err } - proof.BitcoinSig1Bytes, err = wire.ReadVarBytes(r, 0, 80, "sigs") + if len(nodeSig2) > 0 { + proof.NodeSig2Bytes = fn.Some(nodeSig2) + } + + bitcoinSig1, err := wire.ReadVarBytes(r, 0, 80, "sigs") if err != nil { return nil, err } - proof.BitcoinSig2Bytes, err = wire.ReadVarBytes(r, 0, 80, "sigs") + if len(bitcoinSig1) > 0 { + proof.BitcoinSig1Bytes = fn.Some(bitcoinSig1) + } + + bitcoinSig2, err := wire.ReadVarBytes(r, 0, 80, "sigs") if err != nil { return nil, err } + if len(bitcoinSig2) > 0 { + proof.BitcoinSig2Bytes = fn.Some(bitcoinSig2) + } if !proof.IsEmpty() { edgeInfo.AuthProof = proof diff --git a/graph/db/models/channel_auth_proof.go b/graph/db/models/channel_auth_proof.go index daf120b10..1e1fb03a5 100644 --- a/graph/db/models/channel_auth_proof.go +++ b/graph/db/models/channel_auth_proof.go @@ -1,35 +1,119 @@ package models +import ( + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/lnwire" +) + // ChannelAuthProof is the authentication proof (the signature portion) for a -// channel. Using the four signatures contained in the struct, and some +// channel. +// +// For v1 channels: +// Using the four node and bitcoin signatures contained in the struct, and some // auxiliary knowledge (the funding script, node identities, and outpoint) nodes // on the network are able to validate the authenticity and existence of a // channel. Each of these signatures signs the following digest: chanID || // nodeID1 || nodeID2 || bitcoinKey1|| bitcoinKey2 || 2-byte-feature-len || // features. +// +// For v2 channels: +// The single schnorr signature signs the tlv fields of the v2 channel +// announcement message which are in the signed range. type ChannelAuthProof struct { + // Version is the version of the channel announcement. + Version lnwire.GossipVersion + // NodeSig1Bytes are the raw bytes of the first node signature encoded // in DER format. - NodeSig1Bytes []byte + // + // NOTE: v1 channel announcements only. + NodeSig1Bytes fn.Option[[]byte] // NodeSig2Bytes are the raw bytes of the second node signature // encoded in DER format. - NodeSig2Bytes []byte + // + // NOTE: v1 channel announcements only. + NodeSig2Bytes fn.Option[[]byte] // BitcoinSig1Bytes are the raw bytes of the first bitcoin signature // encoded in DER format. - BitcoinSig1Bytes []byte + // + // NOTE: v1 channel announcements only. + BitcoinSig1Bytes fn.Option[[]byte] // BitcoinSig2Bytes are the raw bytes of the second bitcoin signature // encoded in DER format. - BitcoinSig2Bytes []byte + // + // NOTE: v1 channel announcements only. + BitcoinSig2Bytes fn.Option[[]byte] + + // Signature is the raw bytes of the single schnorr signature for v2 + // channel announcements. + // + // NOTE: v2 channel announcements only. + Signature fn.Option[[]byte] } -// IsEmpty check is the authentication proof is empty Proof is empty if at -// least one of the signatures are equal to nil. +// IsEmpty check is the authentication proof is empty Proof is empty. func (c *ChannelAuthProof) IsEmpty() bool { - return len(c.NodeSig1Bytes) == 0 || - len(c.NodeSig2Bytes) == 0 || - len(c.BitcoinSig1Bytes) == 0 || - len(c.BitcoinSig2Bytes) == 0 + // For v1 channel announcements, we either have all four signatures or + // none. + if c.Version == lnwire.GossipVersion1 { + return c.NodeSig1Bytes.IsNone() + } + + // For v2 channel announcements, we only have a single signature. + return c.Signature.IsNone() +} + +// NewV1ChannelAuthProof creates a new ChannelAuthProof for a v1 channel +// announcement. +func NewV1ChannelAuthProof(nodeSig1, nodeSig2, bitcoinSig1, + bitcoinSig2 []byte) *ChannelAuthProof { + + return &ChannelAuthProof{ + Version: lnwire.GossipVersion1, + NodeSig1Bytes: fn.Some(nodeSig1), + NodeSig2Bytes: fn.Some(nodeSig2), + BitcoinSig1Bytes: fn.Some(bitcoinSig1), + BitcoinSig2Bytes: fn.Some(bitcoinSig2), + } +} + +// NewV2ChannelAuthProof creates a new ChannelAuthProof for a v2 channel +// announcement. +func NewV2ChannelAuthProof(signature []byte) *ChannelAuthProof { + return &ChannelAuthProof{ + Version: lnwire.GossipVersion2, + Signature: fn.Some(signature), + } +} + +// NodeSig1 returns the first node signature bytes, or an empty slice if not +// present. +func (c *ChannelAuthProof) NodeSig1() []byte { + return c.NodeSig1Bytes.UnwrapOr([]byte{}) +} + +// NodeSig2 returns the second node signature bytes, or an empty slice if not +// present. +func (c *ChannelAuthProof) NodeSig2() []byte { + return c.NodeSig2Bytes.UnwrapOr([]byte{}) +} + +// BitcoinSig1 returns the first bitcoin signature bytes, or an empty slice if +// not present. +func (c *ChannelAuthProof) BitcoinSig1() []byte { + return c.BitcoinSig1Bytes.UnwrapOr([]byte{}) +} + +// BitcoinSig2 returns the second bitcoin signature bytes, or an empty slice if +// not present. +func (c *ChannelAuthProof) BitcoinSig2() []byte { + return c.BitcoinSig2Bytes.UnwrapOr([]byte{}) +} + +// Sig returns the v2 signature bytes, or an empty slice if not present. +func (c *ChannelAuthProof) Sig() []byte { + return c.Signature.UnwrapOr([]byte{}) } diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index aeeeb378d..253582669 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -2966,6 +2966,12 @@ 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", + proof.Version) + } + var ( ctx = context.TODO() scidBytes = channelIDToBytes(scid.ToUint64()) @@ -2975,10 +2981,10 @@ func (s *SQLStore) AddEdgeProof(scid lnwire.ShortChannelID, res, err := db.AddV1ChannelProof( ctx, sqlc.AddV1ChannelProofParams{ Scid: scidBytes, - Node1Signature: proof.NodeSig1Bytes, - Node2Signature: proof.NodeSig2Bytes, - Bitcoin1Signature: proof.BitcoinSig1Bytes, - Bitcoin2Signature: proof.BitcoinSig2Bytes, + Node1Signature: proof.NodeSig1(), + Node2Signature: proof.NodeSig2(), + Bitcoin1Signature: proof.BitcoinSig1(), + Bitcoin2Signature: proof.BitcoinSig2(), }, ) if err != nil { @@ -4284,10 +4290,10 @@ func insertChannel(ctx context.Context, db SQLQueries, if edge.AuthProof != nil { proof := edge.AuthProof - createParams.Node1Signature = proof.NodeSig1Bytes - createParams.Node2Signature = proof.NodeSig2Bytes - createParams.Bitcoin1Signature = proof.BitcoinSig1Bytes - createParams.Bitcoin2Signature = proof.BitcoinSig2Bytes + createParams.Node1Signature = proof.NodeSig1() + createParams.Node2Signature = proof.NodeSig2() + createParams.Bitcoin1Signature = proof.BitcoinSig1() + createParams.Bitcoin2Signature = proof.BitcoinSig2() } // Insert the new channel record. @@ -4483,12 +4489,16 @@ func buildEdgeInfoWithBatchData(chain chainhash.Hash, // 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 { - channel.AuthProof = &models.ChannelAuthProof{ - NodeSig1Bytes: dbChan.Node1Signature, - NodeSig2Bytes: dbChan.Node2Signature, - BitcoinSig1Bytes: dbChan.Bitcoin1Signature, - BitcoinSig2Bytes: dbChan.Bitcoin2Signature, + // For v1 channels, we have four signatures. + if dbChan.Version == int16(lnwire.GossipVersion1) { + channel.AuthProof = models.NewV1ChannelAuthProof( + dbChan.Node1Signature, + dbChan.Node2Signature, + dbChan.Bitcoin1Signature, + dbChan.Bitcoin2Signature, + ) } + // TODO(elle): Add v2 support when needed. } return channel, nil diff --git a/graph/notifications_test.go b/graph/notifications_test.go index e220a0b44..fab5b4ce6 100644 --- a/graph/notifications_test.go +++ b/graph/notifications_test.go @@ -68,12 +68,12 @@ var ( _ = testSScalar.SetByteSlice(testSBytes) testSig = ecdsa.NewSignature(testRScalar, testSScalar) - testAuthProof = models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - } + testAuthProof = *models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) ) func createTestNode(t *testing.T) *models.Node { @@ -452,12 +452,12 @@ func TestEdgeUpdateNotification(t *testing.T) { ChannelID: chanID.ToUint64(), NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), Features: lnwire.EmptyFeatureVector(), ChannelPoint: *chanPoint, Capacity: chanValue, @@ -648,12 +648,12 @@ func TestNodeUpdateNotification(t *testing.T) { NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, Features: lnwire.EmptyFeatureVector(), - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), FundingScript: fn.Some(script), } copy(edge.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed()) @@ -834,12 +834,12 @@ func TestNotificationCancellation(t *testing.T) { ChannelID: chanID.ToUint64(), NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), Features: lnwire.EmptyFeatureVector(), ChannelPoint: *chanPoint, Capacity: chanValue, @@ -911,12 +911,12 @@ func TestChannelCloseNotification(t *testing.T) { ChannelID: chanID.ToUint64(), NodeKey1Bytes: node1.PubKeyBytes, NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: &models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - }, + AuthProof: models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ), Features: lnwire.EmptyFeatureVector(), ChannelPoint: *chanUtxo, Capacity: chanValue, diff --git a/netann/channel_announcement.go b/netann/channel_announcement.go index 9bb21c401..fee3c5abf 100644 --- a/netann/channel_announcement.go +++ b/netann/channel_announcement.go @@ -56,25 +56,25 @@ func CreateChanAnnouncement(chanProof *models.ChannelAuthProof, var err error chanAnn.BitcoinSig1, err = lnwire.NewSigFromECDSARawSignature( - chanProof.BitcoinSig1Bytes, + chanProof.BitcoinSig1(), ) if err != nil { return nil, nil, nil, err } chanAnn.BitcoinSig2, err = lnwire.NewSigFromECDSARawSignature( - chanProof.BitcoinSig2Bytes, + chanProof.BitcoinSig2(), ) if err != nil { return nil, nil, nil, err } chanAnn.NodeSig1, err = lnwire.NewSigFromECDSARawSignature( - chanProof.NodeSig1Bytes, + chanProof.NodeSig1(), ) if err != nil { return nil, nil, nil, err } chanAnn.NodeSig2, err = lnwire.NewSigFromECDSARawSignature( - chanProof.NodeSig2Bytes, + chanProof.NodeSig2(), ) if err != nil { return nil, nil, nil, err diff --git a/netann/channel_announcement_test.go b/netann/channel_announcement_test.go index 49f61a57f..02d1d5617 100644 --- a/netann/channel_announcement_test.go +++ b/netann/channel_announcement_test.go @@ -40,12 +40,12 @@ func TestCreateChanAnnouncement(t *testing.T) { ExtraOpaqueData: []byte{0x1}, } - chanProof := &models.ChannelAuthProof{ - NodeSig1Bytes: expChanAnn.NodeSig1.ToSignatureBytes(), - NodeSig2Bytes: expChanAnn.NodeSig2.ToSignatureBytes(), - BitcoinSig1Bytes: expChanAnn.BitcoinSig1.ToSignatureBytes(), - BitcoinSig2Bytes: expChanAnn.BitcoinSig2.ToSignatureBytes(), - } + chanProof := models.NewV1ChannelAuthProof( + expChanAnn.NodeSig1.ToSignatureBytes(), + expChanAnn.NodeSig2.ToSignatureBytes(), + expChanAnn.BitcoinSig1.ToSignatureBytes(), + expChanAnn.BitcoinSig2.ToSignatureBytes(), + ) chanInfo := &models.ChannelEdgeInfo{ Version: lnwire.GossipVersion1, ChainHash: expChanAnn.ChainHash, diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index b7256e3a8..bbb31fdaf 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -99,12 +99,12 @@ var ( _ = testSScalar.SetByteSlice(testSBytes) testSig = ecdsa.NewSignature(testRScalar, testSScalar) - testAuthProof = models.ChannelAuthProof{ - NodeSig1Bytes: testSig.Serialize(), - NodeSig2Bytes: testSig.Serialize(), - BitcoinSig1Bytes: testSig.Serialize(), - BitcoinSig2Bytes: testSig.Serialize(), - } + testAuthProof = *models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) ) // noProbabilitySource is used in testing to return the same probability 1 for diff --git a/rpcserver.go b/rpcserver.go index 06e015507..0c496397b 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -6980,10 +6980,10 @@ func marshalDBEdge(edgeInfo *models.ChannelEdgeInfo, // channel announcement. if includeAuthProof && edgeInfo.AuthProof != nil { edge.AuthProof = &lnrpc.ChannelAuthProof{ - NodeSig1: edgeInfo.AuthProof.NodeSig1Bytes, - BitcoinSig1: edgeInfo.AuthProof.BitcoinSig1Bytes, - NodeSig2: edgeInfo.AuthProof.NodeSig2Bytes, - BitcoinSig2: edgeInfo.AuthProof.BitcoinSig2Bytes, + NodeSig1: edgeInfo.AuthProof.NodeSig1(), + BitcoinSig1: edgeInfo.AuthProof.BitcoinSig1(), + NodeSig2: edgeInfo.AuthProof.NodeSig2(), + BitcoinSig2: edgeInfo.AuthProof.BitcoinSig2(), } } From ba6222b52188d75aa2d13dee7f752bb630aae258 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 12:20:21 +0200 Subject: [PATCH 23/55] multi: add and use V1 constructor for models.ChannelEdgeInfo This makes it clear what fields must/can be set for a V1 channel. Introduce NewV1Channel constructor to create v1 channel edges with proper initialization and validation. The constructor: - Takes required fields (chanID, chainHash, node keys) as parameters - Takes v1-specific fields (bitcoin keys, extra opaque data) via ChannelV1Fields struct - Accepts optional fields (capacity, channel point, features, proof) via functional options (WithCapacity, WithChannelPoint, etc.) - Validates that if an AuthProof is provided, its version matches the channel version This makes it clear which fields are required vs optional for v1 channels and prevents incorrectly initialized channel edges. All tests and production code updated to use the constructor. --- autopilot/prefattach_test.go | 23 +- discovery/gossiper.go | 33 ++- discovery/gossiper_test.go | 14 +- graph/builder_test.go | 340 +++++++++++++-------------- graph/db/channel_cache_test.go | 15 +- graph/db/graph_test.go | 310 ++++++++++++++---------- graph/db/kv_store.go | 11 +- graph/db/models/channel_edge_info.go | 93 ++++++++ graph/db/sql_store.go | 55 +++-- graph/notifications_test.go | 155 ++++++------ lnrpc/devrpc/dev_server.go | 23 +- lnrpc/invoicesrpc/addinvoice_test.go | 16 +- netann/chan_status_manager_test.go | 21 +- netann/channel_announcement_test.go | 28 +-- routing/localchans/manager.go | 41 +++- routing/localchans/manager_test.go | 145 +++++++----- routing/pathfind_test.go | 66 +++--- routing/router_test.go | 39 ++- 18 files changed, 853 insertions(+), 575 deletions(-) diff --git a/autopilot/prefattach_test.go b/autopilot/prefattach_test.go index a4026d73d..30ec7ffc6 100644 --- a/autopilot/prefattach_test.go +++ b/autopilot/prefattach_test.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" graphdb "github.com/lightningnetwork/lnd/graph/db" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/lnwire" @@ -492,16 +493,20 @@ func (d *testDBGraph) addRandChannel(node1, node2 *btcec.PublicKey, } chanID := randChanID() - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID.ToUint64(), - Capacity: capacity, - Features: lnwire.EmptyFeatureVector(), + nodeKey1 := route.NewVertex(lnNode1) + nodeKey2 := route.NewVertex(lnNode2) + btcKey1 := route.NewVertex(lnNode1) + btcKey2 := route.NewVertex(lnNode2) + edge, err := models.NewV1Channel( + chanID.ToUint64(), chainhash.Hash{}, nodeKey1, nodeKey2, + &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, models.WithCapacity(capacity), + ) + if err != nil { + return nil, nil, err } - copy(edge.NodeKey1Bytes[:], lnNode1.SerializeCompressed()) - copy(edge.NodeKey2Bytes[:], lnNode2.SerializeCompressed()) - copy(edge.BitcoinKey1Bytes[:], lnNode1.SerializeCompressed()) - copy(edge.BitcoinKey2Bytes[:], lnNode2.SerializeCompressed()) if err := d.db.AddChannelEdge(ctx, edge); err != nil { return nil, nil, err diff --git a/discovery/gossiper.go b/discovery/gossiper.go index 2cbd4e873..b26435c56 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -2818,19 +2818,26 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(ctx context.Context, // With the proof validated (if necessary), we can now store it within // the database for our path finding and syncing needs. - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: scid.ToUint64(), - ChainHash: ann.ChainHash, - NodeKey1Bytes: ann.NodeID1, - NodeKey2Bytes: ann.NodeID2, - BitcoinKey1Bytes: ann.BitcoinKey1, - BitcoinKey2Bytes: ann.BitcoinKey2, - AuthProof: proof, - Features: lnwire.NewFeatureVector( - ann.Features, lnwire.Features, - ), - ExtraOpaqueData: ann.ExtraOpaqueData, + edge, err := models.NewV1Channel( + scid.ToUint64(), ann.ChainHash, ann.NodeID1, ann.NodeID2, + &models.ChannelV1Fields{ + BitcoinKey1Bytes: ann.BitcoinKey1, + BitcoinKey2Bytes: ann.BitcoinKey2, + ExtraOpaqueData: ann.ExtraOpaqueData, + }, + models.WithChanProof(proof), models.WithFeatures(ann.Features), + ) + if err != nil { + key := newRejectCacheKey( + ann.GossipVersion(), + scid.ToUint64(), + sourceToPub(nMsg.source), + ) + _, _ = d.recentRejects.Put(key, &cachedReject{}) + + log.Errorf("unable to create channel edge: %v", err) + nMsg.err <- err + return nil, false } // If there were any optional message fields provided, we'll include diff --git a/discovery/gossiper_test.go b/discovery/gossiper_test.go index efb7307da..b1231b0e3 100644 --- a/discovery/gossiper_test.go +++ b/discovery/gossiper_test.go @@ -272,11 +272,15 @@ func (r *mockGraphSource) GetChannelByID(chanID lnwire.ShortChannelID) ( return nil, nil, nil, graphdb.ErrEdgeNotFound } - return &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - NodeKey1Bytes: pubKeys[0], - NodeKey2Bytes: pubKeys[1], - }, nil, nil, graphdb.ErrZombieEdge + zombieEdge, err := models.NewV1Channel( + 0, chainhash.Hash{}, pubKeys[0], pubKeys[1], + &models.ChannelV1Fields{}, + ) + if err != nil { + return nil, nil, nil, err + } + + return zombieEdge, nil, nil, graphdb.ErrZombieEdge } edges := r.edges[chanID.ToUint64()] diff --git a/graph/builder_test.go b/graph/builder_test.go index f2648a002..25b2d28b1 100644 --- a/graph/builder_test.go +++ b/graph/builder_test.go @@ -17,10 +17,10 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainntnfs" - "github.com/lightningnetwork/lnd/fn/v2" graphdb "github.com/lightningnetwork/lnd/graph/db" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/htlcswitch" @@ -65,18 +65,17 @@ func TestAddProof(t *testing.T) { ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight) // After utxo was recreated adding the edge without the proof. - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID.ToUint64(), - NodeKey1Bytes: node1.PubKeyBytes, - NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: nil, - Features: lnwire.EmptyFeatureVector(), - FundingScript: fn.Some(script), - } - copy(edge.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed()) - copy(edge.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed()) + btcKey1 := route.NewVertex(bitcoinKey1) + btcKey2 := route.NewVertex(bitcoinKey2) + edge, err := models.NewV1Channel( + chanID.ToUint64(), *chaincfg.SimNetParams.GenesisHash, + node1.PubKeyBytes, node2.PubKeyBytes, &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, models.WithFundingScript(script), + ) + require.NoError(t, err) require.NoError(t, ctx.builder.AddEdge(ctxb, edge)) // Now we'll attempt to update the proof and check that it has been @@ -152,17 +151,20 @@ func TestIgnoreChannelEdgePolicyForUnknownChannel(t *testing.T) { } ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight) - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID.ToUint64(), - NodeKey1Bytes: pub1, - NodeKey2Bytes: pub2, - BitcoinKey1Bytes: pub1, - BitcoinKey2Bytes: pub2, - AuthProof: nil, - Features: lnwire.EmptyFeatureVector(), - FundingScript: fn.Some(script), - } + pub1Vertex, err := route.NewVertexFromBytes(pub1[:]) + require.NoError(t, err) + pub2Vertex, err := route.NewVertexFromBytes(pub2[:]) + require.NoError(t, err) + + edge, err := models.NewV1Channel( + chanID.ToUint64(), *chaincfg.SimNetParams.GenesisHash, + pub1Vertex, pub2Vertex, &models.ChannelV1Fields{ + BitcoinKey1Bytes: pub1Vertex, + BitcoinKey2Bytes: pub2Vertex, + }, models.WithFundingScript(script), + ) + require.NoError(t, err) + edgePolicy := &models.ChannelEdgePolicy{ SigBytes: testSig.Serialize(), ChannelID: edge.ChannelID, @@ -274,43 +276,42 @@ func TestWakeUpOnStaleBranch(t *testing.T) { node1 := createTestNode(t) node2 := createTestNode(t) - edge1 := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID1, - NodeKey1Bytes: node1.PubKeyBytes, - NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - Features: lnwire.EmptyFeatureVector(), - FundingScript: fn.Some(fundingScript1), - } - copy(edge1.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed()) - copy(edge1.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed()) + btcKey1, err := route.NewVertexFromBytes( + bitcoinKey1.SerializeCompressed(), + ) + require.NoError(t, err) + btcKey2, err := route.NewVertexFromBytes( + bitcoinKey2.SerializeCompressed(), + ) + require.NoError(t, err) + + edge1, err := models.NewV1Channel( + chanID1, *chaincfg.SimNetParams.GenesisHash, + node1.PubKeyBytes, node2.PubKeyBytes, &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, models.WithChanProof(models.NewV1ChannelAuthProof( + testSig.Serialize(), testSig.Serialize(), + testSig.Serialize(), testSig.Serialize(), + )), models.WithFundingScript(fundingScript1), + ) + require.NoError(t, err) if err := ctx.builder.AddEdge(ctxb, edge1); err != nil { t.Fatalf("unable to add edge: %v", err) } - edge2 := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID2, - NodeKey1Bytes: node1.PubKeyBytes, - NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - Features: lnwire.EmptyFeatureVector(), - FundingScript: fn.Some(fundingScript2), - } - copy(edge2.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed()) - copy(edge2.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed()) + edge2, err := models.NewV1Channel( + chanID2, *chaincfg.SimNetParams.GenesisHash, node1.PubKeyBytes, + node2.PubKeyBytes, &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, models.WithChanProof(models.NewV1ChannelAuthProof( + testSig.Serialize(), testSig.Serialize(), + testSig.Serialize(), testSig.Serialize(), + )), models.WithFundingScript(fundingScript2), + ) + require.NoError(t, err) if err := ctx.builder.AddEdge(ctxb, edge2); err != nil { t.Fatalf("unable to add edge: %v", err) @@ -486,47 +487,39 @@ func TestDisconnectedBlocks(t *testing.T) { node1 := createTestNode(t) node2 := createTestNode(t) - edge1 := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID1, - NodeKey1Bytes: node1.PubKeyBytes, - NodeKey2Bytes: node2.PubKeyBytes, - BitcoinKey1Bytes: node1.PubKeyBytes, - BitcoinKey2Bytes: node2.PubKeyBytes, - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - Features: lnwire.EmptyFeatureVector(), - FundingScript: fn.Some([]byte{}), - } - copy(edge1.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed()) - copy(edge1.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed()) + btcKey1 := route.NewVertex(bitcoinKey1) + btcKey2 := route.NewVertex(bitcoinKey2) + + proof := models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) + + edge1, err := models.NewV1Channel( + chanID1, *chaincfg.SimNetParams.GenesisHash, node1.PubKeyBytes, + node2.PubKeyBytes, &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, models.WithChanProof(proof), + models.WithFundingScript([]byte{}), + ) + require.NoError(t, err) if err := ctx.builder.AddEdge(ctxb, edge1); err != nil { t.Fatalf("unable to add edge: %v", err) } - edge2 := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID2, - NodeKey1Bytes: node1.PubKeyBytes, - NodeKey2Bytes: node2.PubKeyBytes, - BitcoinKey1Bytes: node1.PubKeyBytes, - BitcoinKey2Bytes: node2.PubKeyBytes, - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - Features: lnwire.EmptyFeatureVector(), - FundingScript: fn.Some([]byte{}), - } - copy(edge2.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed()) - copy(edge2.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed()) + edge2, err := models.NewV1Channel( + chanID2, *chaincfg.SimNetParams.GenesisHash, node1.PubKeyBytes, + node2.PubKeyBytes, &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, models.WithChanProof(proof), + models.WithFundingScript([]byte{}), + ) + require.NoError(t, err) if err := ctx.builder.AddEdge(ctxb, edge2); err != nil { t.Fatalf("unable to add edge: %v", err) @@ -644,24 +637,28 @@ func TestChansClosedOfflinePruneGraph(t *testing.T) { node1 := createTestNode(t) node2 := createTestNode(t) - edge1 := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID1.ToUint64(), - NodeKey1Bytes: node1.PubKeyBytes, - NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - ChannelPoint: *chanUTXO, - Capacity: chanValue, - Features: lnwire.EmptyFeatureVector(), - FundingScript: fn.Some(script), - } - copy(edge1.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed()) - copy(edge1.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed()) + btcKey1 := route.NewVertex(bitcoinKey1) + btcKey2 := route.NewVertex(bitcoinKey2) + + proof := models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) + + edge1, err := models.NewV1Channel( + chanID1.ToUint64(), *chaincfg.SimNetParams.GenesisHash, + node1.PubKeyBytes, node2.PubKeyBytes, &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, models.WithChanProof(proof), + models.WithCapacity(chanValue), + models.WithChannelPoint(*chanUTXO), + models.WithFundingScript(script), + ) + require.NoError(t, err) + if err := ctx.builder.AddEdge(ctxb, edge1); err != nil { t.Fatalf("unable to add edge: %v", err) } @@ -1068,17 +1065,15 @@ func TestIsStaleNode(t *testing.T) { } ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight) - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID.ToUint64(), - NodeKey1Bytes: pub1, - NodeKey2Bytes: pub2, - BitcoinKey1Bytes: pub1, - BitcoinKey2Bytes: pub2, - AuthProof: nil, - Features: lnwire.EmptyFeatureVector(), - FundingScript: fn.Some(script), - } + edge, err := models.NewV1Channel( + chanID.ToUint64(), *chaincfg.SimNetParams.GenesisHash, + pub1, pub2, &models.ChannelV1Fields{ + BitcoinKey1Bytes: pub1, + BitcoinKey2Bytes: pub2, + }, models.WithFundingScript(script), + ) + require.NoError(t, err) + if err := ctx.builder.AddEdge(ctxb, edge); err != nil { t.Fatalf("unable to add edge: %v", err) } @@ -1149,17 +1144,16 @@ func TestIsKnownEdge(t *testing.T) { } ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight) - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID.ToUint64(), - NodeKey1Bytes: pub1, - NodeKey2Bytes: pub2, - BitcoinKey1Bytes: pub1, - BitcoinKey2Bytes: pub2, - AuthProof: nil, - FundingScript: fn.Some(script), - Features: lnwire.EmptyFeatureVector(), - } + edge, err := models.NewV1Channel( + chanID.ToUint64(), *chaincfg.SimNetParams.GenesisHash, pub1, + pub2, &models.ChannelV1Fields{ + BitcoinKey1Bytes: pub1, + BitcoinKey2Bytes: pub2, + }, + models.WithFundingScript(script), + ) + require.NoError(t, err) + if err := ctx.builder.AddEdge(ctxb, edge); err != nil { t.Fatalf("unable to add edge: %v", err) } @@ -1210,17 +1204,15 @@ func TestIsStaleEdgePolicy(t *testing.T) { t.Fatalf("router failed to detect fresh edge policy") } - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID.ToUint64(), - NodeKey1Bytes: pub1, - NodeKey2Bytes: pub2, - BitcoinKey1Bytes: pub1, - BitcoinKey2Bytes: pub2, - AuthProof: nil, - Features: lnwire.EmptyFeatureVector(), - FundingScript: fn.Some(script), - } + edge, err := models.NewV1Channel( + chanID.ToUint64(), *chaincfg.SimNetParams.GenesisHash, pub1, + pub2, &models.ChannelV1Fields{ + BitcoinKey1Bytes: pub1, + BitcoinKey2Bytes: pub2, + }, models.WithFundingScript(script), + ) + require.NoError(t, err) + if err := ctx.builder.AddEdge(ctxb, edge); err != nil { t.Fatalf("unable to add edge: %v", err) } @@ -1523,19 +1515,27 @@ func parseTestGraph(t *testing.T, useCache bool, path string) ( // We first insert the existence of the edge between the two // nodes. - edgeInfo := models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: edge.ChannelID, - AuthProof: &testAuthProof, - ChannelPoint: fundingPoint, - Capacity: btcutil.Amount(edge.Capacity), - Features: lnwire.EmptyFeatureVector(), - } + var node1Vertex, node2Vertex route.Vertex + copy(node1Vertex[:], node1Bytes) + copy(node2Vertex[:], node2Bytes) - copy(edgeInfo.NodeKey1Bytes[:], node1Bytes) - copy(edgeInfo.NodeKey2Bytes[:], node2Bytes) - copy(edgeInfo.BitcoinKey1Bytes[:], node1Bytes) - copy(edgeInfo.BitcoinKey2Bytes[:], node2Bytes) + var btcKey1, btcKey2 route.Vertex + copy(btcKey1[:], node1Bytes) + copy(btcKey2[:], node2Bytes) + + edgeInfo, err := models.NewV1Channel( + edge.ChannelID, *chaincfg.SimNetParams.GenesisHash, + node1Vertex, node2Vertex, &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, + models.WithChanProof(&testAuthProof), + models.WithChannelPoint(fundingPoint), + models.WithCapacity(btcutil.Amount(edge.Capacity)), + ) + if err != nil { + return nil, err + } shortID := lnwire.NewShortChanIDFromInt(edge.ChannelID) links[shortID] = &mockLink{ @@ -1544,7 +1544,7 @@ func parseTestGraph(t *testing.T, useCache bool, path string) ( ), } - err = graph.AddChannelEdge(ctx, &edgeInfo) + err = graph.AddChannelEdge(ctx, edgeInfo) if err != nil && !errors.Is(err, graphdb.ErrEdgeAlreadyExist) { return nil, err } @@ -1584,12 +1584,12 @@ func parseTestGraph(t *testing.T, useCache bool, path string) ( } // We also store the channel IDs info for each of the node. - node1Vertex, err := route.NewVertexFromBytes(node1Bytes) + node1Vertex, err = route.NewVertexFromBytes(node1Bytes) if err != nil { return nil, err } - node2Vertex, err := route.NewVertexFromBytes(node2Bytes) + node2Vertex, err = route.NewVertexFromBytes(node2Bytes) if err != nil { return nil, err } @@ -1895,21 +1895,21 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, // We first insert the existence of the edge between the two // nodes. - edgeInfo := models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: channelID, - AuthProof: &testAuthProof, - ChannelPoint: *fundingPoint, - Capacity: testChannel.Capacity, - - NodeKey1Bytes: node1Vertex, - BitcoinKey1Bytes: node1Vertex, - NodeKey2Bytes: node2Vertex, - BitcoinKey2Bytes: node2Vertex, - Features: lnwire.EmptyFeatureVector(), + edgeInfo, err := models.NewV1Channel( + channelID, *chaincfg.SimNetParams.GenesisHash, + node1Vertex, node2Vertex, &models.ChannelV1Fields{ + BitcoinKey1Bytes: node1Vertex, + BitcoinKey2Bytes: node2Vertex, + }, + models.WithChanProof(&testAuthProof), + models.WithChannelPoint(*fundingPoint), + models.WithCapacity(testChannel.Capacity), + ) + if err != nil { + return nil, err } - err = graph.AddChannelEdge(ctx, &edgeInfo) + err = graph.AddChannelEdge(ctx, edgeInfo) if err != nil && !errors.Is(err, graphdb.ErrEdgeAlreadyExist) { diff --git a/graph/db/channel_cache_test.go b/graph/db/channel_cache_test.go index 04f6d0331..27ff65432 100644 --- a/graph/db/channel_cache_test.go +++ b/graph/db/channel_cache_test.go @@ -4,8 +4,9 @@ import ( "reflect" "testing" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/graph/db/models" - "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" ) // TestChannelCache checks the behavior of the channelCache with respect to @@ -100,10 +101,14 @@ func assertHasChanEntries(t *testing.T, c *channelCache, start, end uint64) { // channelForInt generates a unique ChannelEdge given an integer. func channelForInt(i uint64) ChannelEdge { + info, err := models.NewV1Channel( + i, chainhash.Hash{}, route.Vertex{}, route.Vertex{}, + &models.ChannelV1Fields{}, + ) + if err != nil { + panic(err) + } return ChannelEdge{ - Info: &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: i, - }, + Info: info, } } diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index fcae25e8a..c9e703f4d 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -379,7 +379,7 @@ func TestPartialNode(t *testing.T) { // Create an edge attached to these nodes and add it to the graph. edgeInfo, _ := createEdge(140, 0, 0, 0, &node1, &node2) - require.NoError(t, graph.AddChannelEdge(ctx, &edgeInfo)) + require.NoError(t, graph.AddChannelEdge(ctx, edgeInfo)) // Both of the nodes should now be in both the graph (as partial/shell) // nodes _and_ the cache should also have an awareness of both nodes. @@ -562,31 +562,50 @@ func TestEdgeInsertionDeletion(t *testing.T) { require.NoError(t, err, "unable to generate node key") node2Pub, err := node2.PubKey() require.NoError(t, err, "unable to generate node key") - edgeInfo := models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID, - ChainHash: *chaincfg.MainNetParams.GenesisHash, - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - Features: lnwire.EmptyFeatureVector(), - ChannelPoint: outpoint, - Capacity: 9000, - } - copy(edgeInfo.NodeKey1Bytes[:], node1Pub.SerializeCompressed()) - copy(edgeInfo.NodeKey2Bytes[:], node2Pub.SerializeCompressed()) - copy(edgeInfo.BitcoinKey1Bytes[:], node1Pub.SerializeCompressed()) - copy(edgeInfo.BitcoinKey2Bytes[:], node2Pub.SerializeCompressed()) - require.NoError(t, graph.AddChannelEdge(ctx, &edgeInfo)) - assertEdgeWithNoPoliciesInCache(t, graph, &edgeInfo) + 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) + + proof := models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) + + 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) + + require.NoError(t, graph.AddChannelEdge(ctx, edgeInfo)) + assertEdgeWithNoPoliciesInCache(t, graph, 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). @@ -623,7 +642,7 @@ func TestEdgeInsertionDeletion(t *testing.T) { } func createEdge(height, txIndex uint32, txPosition uint16, outPointIndex uint32, - node1, node2 *models.Node) (models.ChannelEdgeInfo, + node1, node2 *models.Node) (*models.ChannelEdgeInfo, lnwire.ShortChannelID) { shortChanID := lnwire.ShortChannelID{ @@ -638,26 +657,37 @@ func createEdge(height, txIndex uint32, txPosition uint16, outPointIndex uint32, node1Pub, _ := node1.PubKey() node2Pub, _ := node2.PubKey() - edgeInfo := models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: shortChanID.ToUint64(), - ChainHash: *chaincfg.MainNetParams.GenesisHash, - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - ChannelPoint: outpoint, - Capacity: 9000, - ExtraOpaqueData: make([]byte, 0), - Features: lnwire.EmptyFeatureVector(), - } - copy(edgeInfo.NodeKey1Bytes[:], node1Pub.SerializeCompressed()) - copy(edgeInfo.NodeKey2Bytes[:], node2Pub.SerializeCompressed()) - copy(edgeInfo.BitcoinKey1Bytes[:], node1Pub.SerializeCompressed()) - copy(edgeInfo.BitcoinKey2Bytes[:], node2Pub.SerializeCompressed()) + node1Vertex, _ := route.NewVertexFromBytes( + node1Pub.SerializeCompressed(), + ) + 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(), + ) + + 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), + ) return edgeInfo, shortChanID } @@ -716,20 +746,20 @@ func TestDisconnectBlockAtHeight(t *testing.T) { edgeInfo3, _ := createEdge(height-1, 0, 0, 2, node1, node2) // Now add all these new edges to the database. - if err := graph.AddChannelEdge(ctx, &edgeInfo); err != nil { + if err := graph.AddChannelEdge(ctx, edgeInfo); err != nil { t.Fatalf("unable to create channel edge: %v", err) } - if err := graph.AddChannelEdge(ctx, &edgeInfo2); err != nil { + if err := graph.AddChannelEdge(ctx, edgeInfo2); err != nil { t.Fatalf("unable to create channel edge: %v", err) } - if err := graph.AddChannelEdge(ctx, &edgeInfo3); err != nil { + if err := graph.AddChannelEdge(ctx, edgeInfo3); err != nil { t.Fatalf("unable to create channel edge: %v", err) } - assertEdgeWithNoPoliciesInCache(t, graph, &edgeInfo) - assertEdgeWithNoPoliciesInCache(t, graph, &edgeInfo2) - assertEdgeWithNoPoliciesInCache(t, graph, &edgeInfo3) + assertEdgeWithNoPoliciesInCache(t, graph, edgeInfo) + assertEdgeWithNoPoliciesInCache(t, graph, edgeInfo2) + assertEdgeWithNoPoliciesInCache(t, graph, edgeInfo3) // Call DisconnectBlockAtHeight, which should prune every channel // that has a funding height of 'height' or greater. @@ -739,7 +769,7 @@ func TestDisconnectBlockAtHeight(t *testing.T) { } assertNoEdge(t, graph, edgeInfo.ChannelID) assertNoEdge(t, graph, edgeInfo2.ChannelID) - assertEdgeWithNoPoliciesInCache(t, graph, &edgeInfo3) + assertEdgeWithNoPoliciesInCache(t, graph, edgeInfo3) // The two edges should have been removed. if len(removed) != 2 { @@ -898,35 +928,50 @@ func createChannelEdge(node1, node2 *models.Node, // Add the new edge to the database, this should proceed without any // errors. - edgeInfo := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID, - ChainHash: *chaincfg.MainNetParams.GenesisHash, - ChannelPoint: outpoint, - Capacity: 1000, - ExtraOpaqueData: []byte{ - 1, 1, 1, - 2, 2, 2, 2, - 3, 3, 3, 3, 3, - }, - Features: lnwire.EmptyFeatureVector(), - } - copy(edgeInfo.NodeKey1Bytes[:], firstNode[:]) - copy(edgeInfo.NodeKey2Bytes[:], secondNode[:]) - copy(edgeInfo.BitcoinKey1Bytes[:], firstNode[:]) - copy(edgeInfo.BitcoinKey2Bytes[:], secondNode[:]) + var node1Key, node2Key route.Vertex + copy(node1Key[:], firstNode[:]) + copy(node2Key[:], secondNode[:]) + extraData := []byte{ + 1, 1, 1, + 2, 2, 2, 2, + 3, 3, 3, 3, 3, + } + + var edgeInfo *models.ChannelEdgeInfo if opts.skipProofs { + edgeInfo, _ = models.NewV1Channel( + chanID, *chaincfg.MainNetParams.GenesisHash, node1Key, + node2Key, &models.ChannelV1Fields{ + BitcoinKey1Bytes: node1Key, + BitcoinKey2Bytes: node2Key, + ExtraOpaqueData: extraData, + }, models.WithChannelPoint(outpoint), + models.WithCapacity(1000), + ) + return edgeInfo, nil, nil } - edgeInfo.AuthProof = models.NewV1ChannelAuthProof( + proof := models.NewV1ChannelAuthProof( testSig.Serialize(), testSig.Serialize(), testSig.Serialize(), testSig.Serialize(), ) + edgeInfo, _ = models.NewV1Channel( + chanID, *chaincfg.MainNetParams.GenesisHash, node1Key, node2Key, + &models.ChannelV1Fields{ + BitcoinKey1Bytes: node1Key, + BitcoinKey2Bytes: node2Key, + ExtraOpaqueData: extraData, + }, + models.WithChanProof(proof), + models.WithChannelPoint(outpoint), + models.WithCapacity(1000), + ) + edge1 := &models.ChannelEdgePolicy{ SigBytes: testSig.Serialize(), ChannelID: chanID, @@ -1780,25 +1825,29 @@ func fillTestGraph(t testing.TB, graph *ChannelGraph, numNodes, Index: 0, } - edgeInfo := models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID, - ChainHash: *chaincfg.MainNetParams.GenesisHash, - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - Features: lnwire.EmptyFeatureVector(), - ChannelPoint: op, - Capacity: 1000, - } - copy(edgeInfo.NodeKey1Bytes[:], node1.PubKeyBytes[:]) - copy(edgeInfo.NodeKey2Bytes[:], node2.PubKeyBytes[:]) - copy(edgeInfo.BitcoinKey1Bytes[:], node1.PubKeyBytes[:]) - copy(edgeInfo.BitcoinKey2Bytes[:], node2.PubKeyBytes[:]) - err := graph.AddChannelEdge(ctx, &edgeInfo) + var node1Key, node2Key route.Vertex + copy(node1Key[:], node1.PubKeyBytes[:]) + copy(node2Key[:], node2.PubKeyBytes[:]) + + proof := models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) + + edgeInfo, err := models.NewV1Channel( + chanID, *chaincfg.MainNetParams.GenesisHash, + node1Key, node2Key, &models.ChannelV1Fields{ + BitcoinKey1Bytes: node1Key, + BitcoinKey2Bytes: node2Key, + }, + models.WithChanProof(proof), + models.WithChannelPoint(op), + models.WithCapacity(1000), + ) + require.NoError(t, err) + err = graph.AddChannelEdge(ctx, edgeInfo) require.NoError(t, err) // Create and add an edge with random data that points @@ -1963,28 +2012,31 @@ func TestGraphPruning(t *testing.T) { channelPoints = append(channelPoints, &op) - edgeInfo := models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID, - ChainHash: *chaincfg.MainNetParams.GenesisHash, - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - Features: lnwire.EmptyFeatureVector(), - ChannelPoint: op, - Capacity: 1000, - } - copy(edgeInfo.NodeKey1Bytes[:], graphNodes[i].PubKeyBytes[:]) - copy(edgeInfo.NodeKey2Bytes[:], graphNodes[i+1].PubKeyBytes[:]) - copy(edgeInfo.BitcoinKey1Bytes[:], graphNodes[i].PubKeyBytes[:]) - copy( - edgeInfo.BitcoinKey2Bytes[:], - graphNodes[i+1].PubKeyBytes[:], + var node1Key, node2Key route.Vertex + copy(node1Key[:], graphNodes[i].PubKeyBytes[:]) + copy(node2Key[:], graphNodes[i+1].PubKeyBytes[:]) + + proof := models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), ) - if err := graph.AddChannelEdge(ctx, &edgeInfo); err != nil { + + edgeInfo, err := models.NewV1Channel( + chanID, *chaincfg.MainNetParams.GenesisHash, + node1Key, node2Key, &models.ChannelV1Fields{ + BitcoinKey1Bytes: node1Key, + BitcoinKey2Bytes: node2Key, + }, + models.WithChanProof(proof), + models.WithChannelPoint(op), + models.WithCapacity(1000), + ) + if err != nil { + t.Fatalf("unable to create edge: %v", err) + } + if err := graph.AddChannelEdge(ctx, edgeInfo); err != nil { t.Fatalf("unable to add node: %v", err) } @@ -2140,10 +2192,10 @@ func TestHighestChanID(t *testing.T) { edge1, _ := createEdge(10, 0, 0, 0, node1, node2) edge2, chanID2 := createEdge(100, 0, 0, 0, node1, node2) - if err := graph.AddChannelEdge(ctx, &edge1); err != nil { + if err := graph.AddChannelEdge(ctx, edge1); err != nil { t.Fatalf("unable to create channel edge: %v", err) } - if err := graph.AddChannelEdge(ctx, &edge2); err != nil { + if err := graph.AddChannelEdge(ctx, edge2); err != nil { t.Fatalf("unable to create channel edge: %v", err) } @@ -2160,7 +2212,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) - if err := graph.AddChannelEdge(ctx, &edge3); err != nil { + if err := graph.AddChannelEdge(ctx, edge3); err != nil { t.Fatalf("unable to create channel edge: %v", err) } bestID, err = graph.HighestChanID(ctx) @@ -2216,7 +2268,7 @@ func TestChanUpdatesInHorizon(t *testing.T) { uint32(i*10), 0, 0, 0, node1, node2, ) - if err := graph.AddChannelEdge(ctx, &channel); err != nil { + if err := graph.AddChannelEdge(ctx, channel); err != nil { t.Fatalf("unable to create channel edge: %v", err) } @@ -2245,7 +2297,7 @@ func TestChanUpdatesInHorizon(t *testing.T) { } edges = append(edges, ChannelEdge{ - Info: &channel, + Info: channel, Policy1: edge1, Policy2: edge2, }) @@ -2683,7 +2735,7 @@ func TestChanUpdatesInHorizonBoundaryConditions(t *testing.T) { uint32(i*10), 0, 0, 0, node1, node2, ) require.NoError( - t, graph.AddChannelEdge(ctx, &channel), + t, graph.AddChannelEdge(ctx, channel), ) edge1 := newEdgePolicy( @@ -2851,7 +2903,7 @@ func TestFilterKnownChanIDs(t *testing.T) { uint32(i*10), 0, 0, 0, node1, node2, ) - if err := graph.AddChannelEdge(ctx, &channel); err != nil { + if err := graph.AddChannelEdge(ctx, channel); err != nil { t.Fatalf("unable to create channel edge: %v", err) } @@ -2866,7 +2918,7 @@ func TestFilterKnownChanIDs(t *testing.T) { channel, chanID := createEdge( uint32(i*10+1), 0, 0, 0, node1, node2, ) - if err := graph.AddChannelEdge(ctx, &channel); err != nil { + if err := graph.AddChannelEdge(ctx, channel); err != nil { t.Fatalf("unable to create channel edge: %v", err) } err := graph.DeleteChannelEdges(false, true, channel.ChannelID) @@ -3027,7 +3079,7 @@ func TestStressTestChannelGraphAPI(t *testing.T) { ) newChan := &chanInfo{ - info: channel, + info: *channel, id: chanID, } chans = append(chans, newChan) @@ -3339,12 +3391,12 @@ func TestFilterChannelRange(t *testing.T) { channel1, chanID1 := createEdge( chanHeight, uint32(i+1), 0, 0, node1, node2, ) - require.NoError(t, graph.AddChannelEdge(ctx, &channel1)) + require.NoError(t, graph.AddChannelEdge(ctx, channel1)) channel2, chanID2 := createEdge( chanHeight, uint32(i+2), 0, 0, node1, node2, ) - require.NoError(t, graph.AddChannelEdge(ctx, &channel2)) + require.NoError(t, graph.AddChannelEdge(ctx, channel2)) chanInfo1 := NewChannelUpdateInfo( chanID1, time.Time{}, time.Time{}, @@ -3520,7 +3572,7 @@ func TestFetchChanInfos(t *testing.T) { uint32(i*10), 0, 0, 0, node1, node2, ) - if err := graph.AddChannelEdge(ctx, &channel); err != nil { + if err := graph.AddChannelEdge(ctx, channel); err != nil { t.Fatalf("unable to create channel edge: %v", err) } @@ -3544,7 +3596,7 @@ func TestFetchChanInfos(t *testing.T) { } edges = append(edges, ChannelEdge{ - Info: &channel, + Info: channel, Policy1: edge1, Policy2: edge2, }) @@ -3561,7 +3613,7 @@ func TestFetchChanInfos(t *testing.T) { zombieChan, zombieChanID := createEdge( 666, 0, 0, 0, node1, node2, ) - if err := graph.AddChannelEdge(ctx, &zombieChan); err != nil { + if err := graph.AddChannelEdge(ctx, zombieChan); err != nil { t.Fatalf("unable to create channel edge: %v", err) } err := graph.DeleteChannelEdges(false, true, zombieChan.ChannelID) @@ -3614,7 +3666,7 @@ func TestIncompleteChannelPolicies(t *testing.T) { uint32(0), 0, 0, 0, node1, node2, ) - if err := graph.AddChannelEdge(ctx, &channel); err != nil { + if err := graph.AddChannelEdge(ctx, channel); err != nil { t.Fatalf("unable to create channel edge: %v", err) } @@ -3718,7 +3770,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) - if err := graph.AddChannelEdge(ctx, &edgeInfo); err != nil { + if err := graph.AddChannelEdge(ctx, edgeInfo); err != nil { t.Fatalf("unable to add edge: %v", err) } @@ -3867,7 +3919,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) - if err := graph.AddChannelEdge(ctx, &edgeInfo); err != nil { + if err := graph.AddChannelEdge(ctx, edgeInfo); err != nil { t.Fatalf("unable to add edge: %v", err) } @@ -3916,7 +3968,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) - require.NoError(t, graph.AddChannelEdge(ctx, &edgeInfo)) + require.NoError(t, graph.AddChannelEdge(ctx, edgeInfo)) // Ensure that node1 was inserted as a full node, while node2 only has // a shell node present. @@ -3930,7 +3982,7 @@ func TestAddChannelEdgeShellNodes(t *testing.T) { // Show that attempting to add the channel again will result in an // error. - err = graph.AddChannelEdge(ctx, &edgeInfo) + err = graph.AddChannelEdge(ctx, edgeInfo) require.ErrorIs(t, err, ErrEdgeAlreadyExist) // Show that updating the shell node to a full node record works. @@ -4049,7 +4101,7 @@ func TestNodeIsPublic(t *testing.T) { // 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} + edges := []*models.ChannelEdgeInfo{aliceBobEdge, bobCarolEdge} graphs := []*ChannelGraph{aliceGraph, bobGraph, carolGraph} for _, graph := range graphs { for _, node := range nodes { @@ -4131,7 +4183,7 @@ func TestNodeIsPublic(t *testing.T) { } bobCarolEdge.AuthProof = nil - if err := graph.AddChannelEdge(ctx, &bobCarolEdge); err != nil { + if err := graph.AddChannelEdge(ctx, bobCarolEdge); err != nil { t.Fatalf("unable to add edge: %v", err) } } @@ -4634,7 +4686,7 @@ func TestBatchedAddChannelEdge(t *testing.T) { // Create a third edge, this with a block height of 155. edgeInfo3, _ := createEdge(height-1, 0, 0, 2, node1, node2) - edges := []models.ChannelEdgeInfo{edgeInfo, edgeInfo2, edgeInfo3} + edges := []models.ChannelEdgeInfo{*edgeInfo, *edgeInfo2, *edgeInfo3} errChan := make(chan error, len(edges)) errTimeout := errors.New("timeout adding batched channel") diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index b137c6240..14185f11a 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -3950,11 +3950,14 @@ func (c *KVStore) FetchChannelEdgesByID(chanID uint64) ( // populate the edge info with the public keys of each // party as this is the only information we have about // it and return an error signaling so. - edgeInfo = &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - NodeKey1Bytes: pubKey1, - NodeKey2Bytes: pubKey2, + zombieEdge, err := models.NewV1Channel( + 0, chainhash.Hash{}, pubKey1, pubKey2, + &models.ChannelV1Fields{}, + ) + if err != nil { + return err } + edgeInfo = zombieEdge return ErrZombieEdge } diff --git a/graph/db/models/channel_edge_info.go b/graph/db/models/channel_edge_info.go index 4c783a6a9..7e872c5cd 100644 --- a/graph/db/models/channel_edge_info.go +++ b/graph/db/models/channel_edge_info.go @@ -76,6 +76,99 @@ type ChannelEdgeInfo struct { ExtraOpaqueData []byte } +// EdgeModifier is a functional option that modifies a ChannelEdgeInfo. +type EdgeModifier func(*ChannelEdgeInfo) + +// WithChannelPoint sets the channel point (funding outpoint) on the edge. +func WithChannelPoint(cp wire.OutPoint) EdgeModifier { + return func(e *ChannelEdgeInfo) { + e.ChannelPoint = cp + } +} + +// WithFeatures sets the feature vector on the edge. +func WithFeatures(f *lnwire.RawFeatureVector) EdgeModifier { + return func(e *ChannelEdgeInfo) { + e.Features = lnwire.NewFeatureVector(f, lnwire.Features) + } +} + +// WithCapacity sets the capacity on the edge. +func WithCapacity(c btcutil.Amount) EdgeModifier { + return func(e *ChannelEdgeInfo) { + e.Capacity = c + } +} + +// WithChanProof sets the authentication proof on the edge. +func WithChanProof(proof *ChannelAuthProof) EdgeModifier { + return func(e *ChannelEdgeInfo) { + e.AuthProof = proof + } +} + +// WithFundingScript sets the funding script on the edge. +func WithFundingScript(script []byte) EdgeModifier { + return func(e *ChannelEdgeInfo) { + e.FundingScript = fn.Some(script) + } +} + +// ChannelV1Fields contains the fields that are specific to v1 channel +// announcements. +type ChannelV1Fields struct { + // BitcoinKey1Bytes is the raw public key of the first node. + BitcoinKey1Bytes route.Vertex + + // BitcoinKey2Bytes is the raw public key of the second node. + BitcoinKey2Bytes route.Vertex + + // 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. + ExtraOpaqueData []byte +} + +// NewV1Channel creates a new ChannelEdgeInfo for a v1 channel announcement. +// It takes the required fields for all channels (chanID, chainHash, node keys) +// and v1-specific fields, along with optional modifiers for setting additional +// fields like capacity, channel point, features, and auth proof. +// +// The constructor validates that if an AuthProof is provided via modifiers, its +// version matches the channel version (v1). +func NewV1Channel(chanID uint64, chainHash chainhash.Hash, node1, + node2 route.Vertex, v1Fields *ChannelV1Fields, + opts ...EdgeModifier) (*ChannelEdgeInfo, error) { + + edge := &ChannelEdgeInfo{ + Version: lnwire.GossipVersion1, + NodeKey1Bytes: node1, + NodeKey2Bytes: node2, + BitcoinKey1Bytes: v1Fields.BitcoinKey1Bytes, + BitcoinKey2Bytes: v1Fields.BitcoinKey2Bytes, + ChannelID: chanID, + ChainHash: chainHash, + Features: lnwire.EmptyFeatureVector(), + ExtraOpaqueData: v1Fields.ExtraOpaqueData, + } + + 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 diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 253582669..0ef5855bf 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -2038,11 +2038,22 @@ func (s *SQLStore) FetchChannelEdgesByID(chanID uint64) ( // populate the edge info with the public keys of each // party as this is the only information we have about // it. - edge = &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, + node1, err := route.NewVertexFromBytes(zombie.NodeKey1) + if err != nil { + return err } - copy(edge.NodeKey1Bytes[:], zombie.NodeKey1) - copy(edge.NodeKey2Bytes[:], zombie.NodeKey2) + node2, err := route.NewVertexFromBytes(zombie.NodeKey2) + if err != nil { + return err + } + zombieEdge, err := models.NewV1Channel( + 0, chainhash.Hash{}, node1, node2, + &models.ChannelV1Fields{}, + ) + if err != nil { + return err + } + edge = zombieEdge return ErrZombieEdge } else if err != nil { @@ -4467,22 +4478,28 @@ func buildEdgeInfoWithBatchData(chain chainhash.Hash, recs = make([]byte, 0) } - var btcKey1, btcKey2 route.Vertex - copy(btcKey1[:], dbChan.BitcoinKey1) - copy(btcKey2[:], dbChan.BitcoinKey2) + 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 := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChainHash: chain, - ChannelID: byteOrder.Uint64(dbChan.Scid), - NodeKey1Bytes: node1, - NodeKey2Bytes: node2, - BitcoinKey1Bytes: btcKey1, - BitcoinKey2Bytes: btcKey2, - ChannelPoint: *op, - Capacity: btcutil.Amount(dbChan.Capacity.Int64), - Features: fv, - ExtraOpaqueData: recs, + 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 diff --git a/graph/notifications_test.go b/graph/notifications_test.go index fab5b4ce6..b086fc562 100644 --- a/graph/notifications_test.go +++ b/graph/notifications_test.go @@ -14,6 +14,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainntnfs" @@ -447,24 +448,27 @@ func TestEdgeUpdateNotification(t *testing.T) { // Finally, to conclude our test set up, we'll create a channel // update to announce the created channel between the two nodes. - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID.ToUint64(), - NodeKey1Bytes: node1.PubKeyBytes, - NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - Features: lnwire.EmptyFeatureVector(), - ChannelPoint: *chanPoint, - Capacity: chanValue, - FundingScript: fn.Some(script), - } - copy(edge.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed()) - copy(edge.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed()) + btcKey1 := route.NewVertex(bitcoinKey1) + btcKey2 := route.NewVertex(bitcoinKey2) + + proof := models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) + + edge, err := models.NewV1Channel( + chanID.ToUint64(), *chaincfg.SimNetParams.GenesisHash, + node1.PubKeyBytes, node2.PubKeyBytes, &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, models.WithChanProof(proof), + models.WithChannelPoint(*chanPoint), + models.WithCapacity(chanValue), + models.WithFundingScript(script), + ) + require.NoError(t, err) if err := ctx.builder.AddEdge(ctxb, edge); err != nil { t.Fatalf("unable to add edge: %v", err) @@ -642,22 +646,25 @@ func TestNodeUpdateNotification(t *testing.T) { testFeaturesBuf := new(bytes.Buffer) require.NoError(t, testFeatures.Encode(testFeaturesBuf)) - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID.ToUint64(), - NodeKey1Bytes: node1.PubKeyBytes, - NodeKey2Bytes: node2.PubKeyBytes, - Features: lnwire.EmptyFeatureVector(), - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - FundingScript: fn.Some(script), - } - copy(edge.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed()) - copy(edge.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed()) + btcKey1 := route.NewVertex(bitcoinKey1) + btcKey2 := route.NewVertex(bitcoinKey2) + + proof := models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) + + edge, err := models.NewV1Channel( + chanID.ToUint64(), *chaincfg.SimNetParams.GenesisHash, + node1.PubKeyBytes, node2.PubKeyBytes, &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, models.WithChanProof(proof), + models.WithFundingScript(script), + ) + require.NoError(t, err) // Adding the edge will add the nodes to the graph, but with no info // except the pubkey known. @@ -829,24 +836,28 @@ func TestNotificationCancellation(t *testing.T) { // to the client. ntfnClient.Cancel() - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID.ToUint64(), - NodeKey1Bytes: node1.PubKeyBytes, - NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - Features: lnwire.EmptyFeatureVector(), - ChannelPoint: *chanPoint, - Capacity: chanValue, - FundingScript: fn.Some(script), - } - copy(edge.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed()) - copy(edge.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed()) + btcKey1 := route.NewVertex(bitcoinKey1) + btcKey2 := route.NewVertex(bitcoinKey2) + + proof := models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) + + edge, err := models.NewV1Channel( + chanID.ToUint64(), *chaincfg.SimNetParams.GenesisHash, + node1.PubKeyBytes, node2.PubKeyBytes, &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, models.WithChanProof(proof), + models.WithChannelPoint(*chanPoint), + models.WithCapacity(chanValue), + models.WithFundingScript(script), + ) + require.NoError(t, err) + if err := ctx.builder.AddEdge(ctxb, edge); err != nil { t.Fatalf("unable to add edge: %v", err) } @@ -906,24 +917,28 @@ func TestChannelCloseNotification(t *testing.T) { // Finally, to conclude our test set up, we'll create a channel // announcement to announce the created channel between the two nodes. - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID.ToUint64(), - NodeKey1Bytes: node1.PubKeyBytes, - NodeKey2Bytes: node2.PubKeyBytes, - AuthProof: models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ), - Features: lnwire.EmptyFeatureVector(), - ChannelPoint: *chanUtxo, - Capacity: chanValue, - FundingScript: fn.Some(script), - } - copy(edge.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed()) - copy(edge.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed()) + btcKey1 := route.NewVertex(bitcoinKey1) + btcKey2 := route.NewVertex(bitcoinKey2) + + proof := models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) + + edge, err := models.NewV1Channel( + chanID.ToUint64(), *chaincfg.SimNetParams.GenesisHash, + node1.PubKeyBytes, node2.PubKeyBytes, &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, models.WithChanProof(proof), + models.WithChannelPoint(*chanUtxo), + models.WithCapacity(chanValue), + models.WithFundingScript(script), + ) + require.NoError(t, err) + if err := ctx.builder.AddEdge(ctxb, edge); err != nil { t.Fatalf("unable to add edge: %v", err) } diff --git a/lnrpc/devrpc/dev_server.go b/lnrpc/devrpc/dev_server.go index 31db5fe0a..7dee1627a 100644 --- a/lnrpc/devrpc/dev_server.go +++ b/lnrpc/devrpc/dev_server.go @@ -224,7 +224,6 @@ func (s *Server) ImportGraph(ctx context.Context, // Obtain the pointer to the global singleton channel graph. graphDB := s.cfg.GraphDB - var err error for _, rpcNode := range graph.Nodes { pubKeyBytes, err := parsePubKey(rpcNode.PubKey) if err != nil { @@ -273,19 +272,12 @@ func (s *Server) ImportGraph(ctx context.Context, for _, rpcEdge := range graph.Edges { rpcEdge := rpcEdge - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: rpcEdge.ChannelId, - ChainHash: *s.cfg.ActiveNetParams.GenesisHash, - Capacity: btcutil.Amount(rpcEdge.Capacity), - } - - edge.NodeKey1Bytes, err = parsePubKey(rpcEdge.Node1Pub) + node1, err := parsePubKey(rpcEdge.Node1Pub) if err != nil { return nil, err } - edge.NodeKey2Bytes, err = parsePubKey(rpcEdge.Node2Pub) + node2, err := parsePubKey(rpcEdge.Node2Pub) if err != nil { return nil, err } @@ -294,7 +286,16 @@ func (s *Server) ImportGraph(ctx context.Context, if err != nil { return nil, err } - edge.ChannelPoint = *channelPoint + + edge, err := models.NewV1Channel( + rpcEdge.ChannelId, *s.cfg.ActiveNetParams.GenesisHash, + node1, node2, &models.ChannelV1Fields{}, + models.WithCapacity(btcutil.Amount(rpcEdge.Capacity)), + models.WithChannelPoint(*channelPoint), + ) + if err != nil { + return nil, err + } if err := graphDB.AddChannelEdge(ctx, edge); err != nil { return nil, fmt.Errorf("unable to add edge %v: %w", diff --git a/lnrpc/invoicesrpc/addinvoice_test.go b/lnrpc/invoicesrpc/addinvoice_test.go index ceab2bec4..ca83f8bab 100644 --- a/lnrpc/invoicesrpc/addinvoice_test.go +++ b/lnrpc/invoicesrpc/addinvoice_test.go @@ -6,10 +6,12 @@ import ( "testing" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/zpay32" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -305,10 +307,16 @@ var shouldIncludeChannelTestCases = []struct { h.Mock.On( "FetchChannelEdgesByID", mock.Anything, ).Once().Return( - &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - NodeKey1Bytes: selectedPolicy, - }, + func() *models.ChannelEdgeInfo { + edge, err := models.NewV1Channel( + 0, chainhash.Hash{}, selectedPolicy, + route.Vertex{}, + &models.ChannelV1Fields{}, + ) + require.NoError(h.t, err) + + return edge + }(), &models.ChannelEdgePolicy{ FeeBaseMSat: 1000, FeeProportionalMillionths: 20, diff --git a/netann/chan_status_manager_test.go b/netann/chan_status_manager_test.go index 1ec7cea23..5265d317f 100644 --- a/netann/chan_status_manager_test.go +++ b/netann/chan_status_manager_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" graphdb "github.com/lightningnetwork/lnd/graph/db" @@ -19,6 +20,7 @@ import ( "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/netann" + "github.com/lightningnetwork/lnd/routing/route" "github.com/stretchr/testify/require" ) @@ -100,12 +102,19 @@ func createEdgePolicies(t *testing.T, channel *channeldb.OpenChannel, // bit. dir2 |= lnwire.ChanUpdateDirection - return &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelPoint: channel.FundingOutpoint, - NodeKey1Bytes: pubkey1, - NodeKey2Bytes: pubkey2, - }, + pubkey1Vertex, err := route.NewVertexFromBytes(pubkey1[:]) + require.NoError(t, err) + pubkey2Vertex, err := route.NewVertexFromBytes(pubkey2[:]) + require.NoError(t, err) + + edgeInfo, err := models.NewV1Channel( + channel.ShortChanID().ToUint64(), chainhash.Hash{}, + pubkey1Vertex, pubkey2Vertex, &models.ChannelV1Fields{}, + models.WithChannelPoint(channel.FundingOutpoint), + ) + require.NoError(t, err) + + return edgeInfo, &models.ChannelEdgePolicy{ ChannelID: channel.ShortChanID().ToUint64(), ChannelFlags: dir1, diff --git a/netann/channel_announcement_test.go b/netann/channel_announcement_test.go index 02d1d5617..c46b90a9d 100644 --- a/netann/channel_announcement_test.go +++ b/netann/channel_announcement_test.go @@ -46,21 +46,19 @@ func TestCreateChanAnnouncement(t *testing.T) { expChanAnn.BitcoinSig1.ToSignatureBytes(), expChanAnn.BitcoinSig2.ToSignatureBytes(), ) - chanInfo := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChainHash: expChanAnn.ChainHash, - ChannelID: expChanAnn.ShortChannelID.ToUint64(), - ChannelPoint: wire.OutPoint{Index: 1}, - Capacity: btcutil.SatoshiPerBitcoin, - NodeKey1Bytes: key, - NodeKey2Bytes: key, - BitcoinKey1Bytes: key, - BitcoinKey2Bytes: key, - Features: lnwire.NewFeatureVector( - features, lnwire.Features, - ), - ExtraOpaqueData: expChanAnn.ExtraOpaqueData, - } + chanInfo, err := models.NewV1Channel( + expChanAnn.ShortChannelID.ToUint64(), expChanAnn.ChainHash, + key, key, &models.ChannelV1Fields{ + BitcoinKey1Bytes: key, + BitcoinKey2Bytes: key, + ExtraOpaqueData: expChanAnn.ExtraOpaqueData, + }, + models.WithChanProof(chanProof), + models.WithChannelPoint(wire.OutPoint{Index: 1}), + models.WithCapacity(btcutil.SatoshiPerBitcoin), + models.WithFeatures(features), + ) + require.NoError(t, err) chanAnn, _, _, err := CreateChanAnnouncement( chanProof, chanInfo, nil, nil, ) diff --git a/routing/localchans/manager.go b/routing/localchans/manager.go index f47e34a16..cc86d82cc 100644 --- a/routing/localchans/manager.go +++ b/routing/localchans/manager.go @@ -18,6 +18,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing" + "github.com/lightningnetwork/lnd/routing/route" ) // Manager manages the node's local channels. The only operation that is @@ -328,20 +329,36 @@ func (r *Manager) createEdge(channel *channeldb.OpenChannel, "script: %v", err) } - info := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: shortChanID.ToUint64(), - ChainHash: channel.ChainHash, - Features: lnwire.EmptyFeatureVector(), - Capacity: channel.Capacity, - ChannelPoint: channel.FundingOutpoint, - FundingScript: fn.Some(fundingScript), + nodeKey1, err := route.NewVertexFromBytes(nodeKey1Bytes) + if err != nil { + return nil, nil, err + } + nodeKey2, err := route.NewVertexFromBytes(nodeKey2Bytes) + if err != nil { + return nil, nil, err + } + bitcoinKey1, err := route.NewVertexFromBytes(bitcoinKey1Bytes) + if err != nil { + return nil, nil, err + } + bitcoinKey2, err := route.NewVertexFromBytes(bitcoinKey2Bytes) + if err != nil { + return nil, nil, err } - copy(info.NodeKey1Bytes[:], nodeKey1Bytes) - copy(info.NodeKey2Bytes[:], nodeKey2Bytes) - copy(info.BitcoinKey1Bytes[:], bitcoinKey1Bytes) - copy(info.BitcoinKey2Bytes[:], bitcoinKey2Bytes) + info, err := models.NewV1Channel( + shortChanID.ToUint64(), channel.ChainHash, nodeKey1, nodeKey2, + &models.ChannelV1Fields{ + BitcoinKey1Bytes: bitcoinKey1, + BitcoinKey2Bytes: bitcoinKey2, + }, + models.WithCapacity(channel.Capacity), + models.WithChannelPoint(channel.FundingOutpoint), + models.WithFundingScript(fundingScript), + ) + if err != nil { + return nil, nil, err + } // Construct a dummy channel edge policy with default values that will // be updated with the new values in the call to processChan below. diff --git a/routing/localchans/manager_test.go b/routing/localchans/manager_test.go index 108fe0a7a..6196330bd 100644 --- a/routing/localchans/manager_test.go +++ b/routing/localchans/manager_test.go @@ -13,13 +13,13 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/discovery" - "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing" + "github.com/lightningnetwork/lnd/routing/route" "github.com/stretchr/testify/require" ) @@ -211,11 +211,21 @@ func TestManager(t *testing.T) { newPolicy: newPolicy, channelSet: []channel{ { - edgeInfo: &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - Capacity: chanCap, - ChannelPoint: chanPointValid, - }, + //nolint:ll + edgeInfo: func() *models.ChannelEdgeInfo { + info, err := models.NewV1Channel( + 0, + chainhash.Hash{}, + route.Vertex{}, + route.Vertex{}, + &models.ChannelV1Fields{}, + models.WithCapacity(chanCap), + models.WithChannelPoint(chanPointValid), + ) + require.NoError(t, err) + + return info + }(), }, }, specifiedChanPoints: []wire.OutPoint{chanPointValid}, @@ -230,11 +240,21 @@ func TestManager(t *testing.T) { newPolicy: newPolicy, channelSet: []channel{ { - edgeInfo: &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - Capacity: chanCap, - ChannelPoint: chanPointValid, - }, + //nolint:ll + edgeInfo: func() *models.ChannelEdgeInfo { + info, err := models.NewV1Channel( + 0, + chainhash.Hash{}, + route.Vertex{}, + route.Vertex{}, + &models.ChannelV1Fields{}, + models.WithCapacity(chanCap), + models.WithChannelPoint(chanPointValid), + ) + require.NoError(t, err) + + return info + }(), }, }, specifiedChanPoints: []wire.OutPoint{}, @@ -249,11 +269,22 @@ func TestManager(t *testing.T) { newPolicy: newPolicy, channelSet: []channel{ { - edgeInfo: &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - Capacity: chanCap, - ChannelPoint: chanPointValid, - }, + //nolint:ll + edgeInfo: func() *models.ChannelEdgeInfo { + info, err := models.NewV1Channel( + 0, + chainhash.Hash{}, + route.Vertex{}, + route.Vertex{}, + &models.ChannelV1Fields{}, + models.WithCapacity(chanCap), + models.WithChannelPoint(chanPointValid), + ) + + require.NoError(t, err) + + return info + }(), }, }, specifiedChanPoints: []wire.OutPoint{chanPointMissing}, @@ -272,11 +303,22 @@ func TestManager(t *testing.T) { newPolicy: noMaxHtlcPolicy, channelSet: []channel{ { - edgeInfo: &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - Capacity: chanCap, - ChannelPoint: chanPointValid, - }, + //nolint:ll + edgeInfo: func() *models.ChannelEdgeInfo { + info, err := models.NewV1Channel( + 0, + chainhash.Hash{}, + route.Vertex{}, + route.Vertex{}, + &models.ChannelV1Fields{}, + models.WithCapacity(chanCap), + models.WithChannelPoint(chanPointValid), + ) + + require.NoError(t, err) + + return info + }(), }, }, specifiedChanPoints: []wire.OutPoint{chanPointValid}, @@ -395,23 +437,19 @@ func TestCreateEdgeLower(t *testing.T) { fundingScript, err := funding.MakeFundingScript(channel) require.NoError(t, err) - expectedInfo := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: 8, - ChainHash: channel.ChainHash, - Features: lnwire.EmptyFeatureVector(), - Capacity: 9, - ChannelPoint: channel.FundingOutpoint, - NodeKey1Bytes: sp, - NodeKey2Bytes: rp, - BitcoinKey1Bytes: [33]byte( - localMultisigKey.SerializeCompressed()), - BitcoinKey2Bytes: [33]byte( - remoteMultisigKey.SerializeCompressed()), - AuthProof: nil, - ExtraOpaqueData: nil, - FundingScript: fn.Some(fundingScript), - } + btcKey1 := route.NewVertex(localMultisigKey) + btcKey2 := route.NewVertex(remoteMultisigKey) + expectedInfo, err := models.NewV1Channel( + 8, channel.ChainHash, sp, rp, &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, + models.WithCapacity(9), + models.WithChannelPoint(channel.FundingOutpoint), + models.WithFundingScript(fundingScript), + ) + require.NoError(t, err) + expectedEdge := &models.ChannelEdgePolicy{ ChannelID: 8, LastUpdate: timestamp, @@ -489,23 +527,20 @@ func TestCreateEdgeHigher(t *testing.T) { fundingScript, err := funding.MakeFundingScript(channel) require.NoError(t, err) - expectedInfo := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: 8, - ChainHash: channel.ChainHash, - Features: lnwire.EmptyFeatureVector(), - Capacity: 9, - ChannelPoint: channel.FundingOutpoint, - NodeKey1Bytes: rp, - NodeKey2Bytes: sp, - BitcoinKey1Bytes: [33]byte( - remoteMultisigKey.SerializeCompressed()), - BitcoinKey2Bytes: [33]byte( - localMultisigKey.SerializeCompressed()), - AuthProof: nil, - ExtraOpaqueData: nil, - FundingScript: fn.Some(fundingScript), - } + btcKey1 := route.NewVertex(remoteMultisigKey) + btcKey2 := route.NewVertex(localMultisigKey) + expectedInfo, err := models.NewV1Channel( + 8, channel.ChainHash, rp, sp, + &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, + models.WithCapacity(9), + models.WithChannelPoint(channel.FundingOutpoint), + models.WithFundingScript(fundingScript), + ) + require.NoError(t, err) + expectedEdge := &models.ChannelEdgePolicy{ ChannelID: 8, LastUpdate: timestamp, diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index bbb31fdaf..ed291a6aa 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -19,6 +19,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" sphinx "github.com/lightningnetwork/lightning-onion" @@ -345,19 +346,28 @@ func parseTestGraph(t *testing.T, useCache bool, path string) ( // We first insert the existence of the edge between the two // nodes. - edgeInfo := models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: edge.ChannelID, - AuthProof: &testAuthProof, - ChannelPoint: fundingPoint, - Features: lnwire.EmptyFeatureVector(), - Capacity: btcutil.Amount(edge.Capacity), - } + var node1Vertex, node2Vertex route.Vertex + copy(node1Vertex[:], node1Bytes) + copy(node2Vertex[:], node2Bytes) - copy(edgeInfo.NodeKey1Bytes[:], node1Bytes) - copy(edgeInfo.NodeKey2Bytes[:], node2Bytes) - copy(edgeInfo.BitcoinKey1Bytes[:], node1Bytes) - copy(edgeInfo.BitcoinKey2Bytes[:], node2Bytes) + var btcKey1, btcKey2 route.Vertex + copy(btcKey1[:], node1Bytes) + copy(btcKey2[:], node2Bytes) + + edgeInfo, err := models.NewV1Channel( + edge.ChannelID, *chaincfg.SimNetParams.GenesisHash, + node1Vertex, node2Vertex, + &models.ChannelV1Fields{ + BitcoinKey1Bytes: btcKey1, + BitcoinKey2Bytes: btcKey2, + }, + models.WithChanProof(&testAuthProof), + models.WithChannelPoint(fundingPoint), + models.WithCapacity(btcutil.Amount(edge.Capacity)), + ) + if err != nil { + return nil, err + } shortID := lnwire.NewShortChanIDFromInt(edge.ChannelID) links[shortID] = &mockLink{ @@ -366,7 +376,7 @@ func parseTestGraph(t *testing.T, useCache bool, path string) ( ), } - err = graph.AddChannelEdge(ctx, &edgeInfo) + err = graph.AddChannelEdge(ctx, edgeInfo) if err != nil && !errors.Is(err, graphdb.ErrEdgeAlreadyExist) { return nil, err } @@ -396,12 +406,12 @@ func parseTestGraph(t *testing.T, useCache bool, path string) ( } // We also store the channel IDs info for each of the node. - node1Vertex, err := route.NewVertexFromBytes(node1Bytes) + node1Vertex, err = route.NewVertexFromBytes(node1Bytes) if err != nil { return nil, err } - node2Vertex, err := route.NewVertexFromBytes(node2Bytes) + node2Vertex, err = route.NewVertexFromBytes(node2Bytes) if err != nil { return nil, err } @@ -678,21 +688,21 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, // We first insert the existence of the edge between the two // nodes. - edgeInfo := models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: channelID, - AuthProof: &testAuthProof, - ChannelPoint: *fundingPoint, - Capacity: testChannel.Capacity, - Features: lnwire.EmptyFeatureVector(), - - NodeKey1Bytes: node1Vertex, - BitcoinKey1Bytes: node1Vertex, - NodeKey2Bytes: node2Vertex, - BitcoinKey2Bytes: node2Vertex, + edgeInfo, err := models.NewV1Channel( + channelID, *chaincfg.SimNetParams.GenesisHash, + node1Vertex, node2Vertex, &models.ChannelV1Fields{ + BitcoinKey1Bytes: node1Vertex, + BitcoinKey2Bytes: node2Vertex, + }, + models.WithChanProof(&testAuthProof), + models.WithChannelPoint(*fundingPoint), + models.WithCapacity(testChannel.Capacity), + ) + if err != nil { + return nil, err } - err = graph.AddChannelEdge(ctx, &edgeInfo) + err = graph.AddChannelEdge(ctx, edgeInfo) if err != nil && !errors.Is(err, graphdb.ErrEdgeAlreadyExist) { return nil, err } diff --git a/routing/router_test.go b/routing/router_test.go index bb0b2f85c..6f9d2c3f3 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -2738,16 +2738,14 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { ) require.NoError(t, err, "unable to create channel edge") - edge := &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID.ToUint64(), - NodeKey1Bytes: pub1, - NodeKey2Bytes: pub2, - BitcoinKey1Bytes: pub1, - BitcoinKey2Bytes: pub2, - Features: lnwire.EmptyFeatureVector(), - AuthProof: nil, - } + edge, err := models.NewV1Channel( + chanID.ToUint64(), chainhash.Hash{}, pub1, pub2, + &models.ChannelV1Fields{ + BitcoinKey1Bytes: pub1, + BitcoinKey2Bytes: pub2, + }, + ) + require.NoError(t, err) require.NoError(t, ctx.graph.AddChannelEdge(ctxb, edge)) // We must add the edge policy to be able to use the edge for route @@ -2819,16 +2817,17 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { 10000, 510) require.NoError(t, err, "unable to create channel edge") - edge = &models.ChannelEdgeInfo{ - Version: lnwire.GossipVersion1, - ChannelID: chanID.ToUint64(), - Features: lnwire.EmptyFeatureVector(), - AuthProof: nil, - } - copy(edge.NodeKey1Bytes[:], node1Bytes) - edge.NodeKey2Bytes = node2Bytes - copy(edge.BitcoinKey1Bytes[:], node1Bytes) - edge.BitcoinKey2Bytes = node2Bytes + node1Vertex, err := route.NewVertexFromBytes(node1Bytes) + require.NoError(t, err) + + edge, err = models.NewV1Channel( + chanID.ToUint64(), chainhash.Hash{}, node1Vertex, node2Bytes, + &models.ChannelV1Fields{ + BitcoinKey1Bytes: node1Vertex, + BitcoinKey2Bytes: node2Bytes, + }, + ) + require.NoError(t, err) require.NoError(t, ctx.graph.AddChannelEdge(ctxb, edge)) From 830b9db69c1dd96df9234eb647db463b0a360c43 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 13:00:31 +0200 Subject: [PATCH 24/55] multi: add ToChannelAnnouncement helper on ChannelEdgeInfo So that we have one place that converts from our `models` struct to the `lnwire.ChannelAnnouncement` struct. The commit also refactors netann.CreateChanAnnouncement to only take a ChannelEdgeInfo and get the proof from there instead of needing the proof to be passed in separately. Add ToChannelAnnouncement() method to ChannelEdgeInfo that converts the model struct to a lnwire.ChannelAnnouncement1 message. This: - Centralizes the conversion logic in one place instead of scattered across multiple call sites - Validates that AuthProof is present (can't create announcement without proof) - Currently only supports v1 channels, returning error for v2 Refactor netann.CreateChanAnnouncement to use this helper and remove the separate chanProof parameter since proof is now accessed from within ChannelEdgeInfo. This improves encapsulation and reduces parameter count. --- discovery/chan_series.go | 6 +-- discovery/gossiper.go | 49 ++++++---------------- graph/db/models/channel_edge_info.go | 63 ++++++++++++++++++++++++++++ netann/channel_announcement.go | 40 ++---------------- netann/channel_announcement_test.go | 4 +- 5 files changed, 82 insertions(+), 80 deletions(-) diff --git a/discovery/chan_series.go b/discovery/chan_series.go index 050b82b10..9ba9607e9 100644 --- a/discovery/chan_series.go +++ b/discovery/chan_series.go @@ -133,8 +133,7 @@ func (c *ChanSeries) UpdatesInHorizon(chain chainhash.Hash, //nolint:ll chanAnn, edge1, edge2, err := netann.CreateChanAnnouncement( - channel.Info.AuthProof, channel.Info, - channel.Policy1, channel.Policy2, + channel.Info, channel.Policy1, channel.Policy2, ) if err != nil { if !yield(nil, err) { @@ -281,8 +280,7 @@ func (c *ChanSeries) FetchChanAnns(chain chainhash.Hash, } chanAnn, edge1, edge2, err := netann.CreateChanAnnouncement( - channel.Info.AuthProof, channel.Info, channel.Policy1, - channel.Policy2, + channel.Info, channel.Policy1, channel.Policy2, ) if err != nil { return nil, err diff --git a/discovery/gossiper.go b/discovery/gossiper.go index b26435c56..88ac2418b 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -2096,10 +2096,14 @@ func (d *AuthenticatedGossiper) processRejectedEdge(_ context.Context, return nil, nil } + // Attach the proof to the channel info before creating the + // announcement. + chanInfo.AuthProof = proof + // We'll then create then validate the new fully assembled // announcement. chanAnn, e1Ann, e2Ann, err := netann.CreateChanAnnouncement( - proof, chanInfo, e1, e2, + chanInfo, e1, e2, ) if err != nil { return nil, err @@ -2492,44 +2496,13 @@ func (d *AuthenticatedGossiper) updateChannel(ctx context.Context, // have a full channel announcement for this channel. var chanAnn *lnwire.ChannelAnnouncement1 if info.AuthProof != nil { - chanID := lnwire.NewShortChanIDFromInt(info.ChannelID) - chanAnn = &lnwire.ChannelAnnouncement1{ - ShortChannelID: chanID, - NodeID1: info.NodeKey1Bytes, - NodeID2: info.NodeKey2Bytes, - ChainHash: info.ChainHash, - BitcoinKey1: info.BitcoinKey1Bytes, - Features: lnwire.NewRawFeatureVector(), - BitcoinKey2: info.BitcoinKey2Bytes, - ExtraOpaqueData: info.ExtraOpaqueData, - } - chanAnn.NodeSig1, err = lnwire.NewSigFromECDSARawSignature( - info.AuthProof.NodeSig1(), - ) - if err != nil { - return nil, nil, err - } - chanAnn.NodeSig2, err = lnwire.NewSigFromECDSARawSignature( - info.AuthProof.NodeSig2(), - ) - if err != nil { - return nil, nil, err - } - chanAnn.BitcoinSig1, err = lnwire.NewSigFromECDSARawSignature( - info.AuthProof.BitcoinSig1(), - ) - if err != nil { - return nil, nil, err - } - chanAnn.BitcoinSig2, err = lnwire.NewSigFromECDSARawSignature( - info.AuthProof.BitcoinSig2(), - ) + chanAnn, err = info.ToChannelAnnouncement() if err != nil { return nil, nil, err } } - return chanAnn, chanUpdate, err + return chanAnn, chanUpdate, nil } // SyncManager returns the gossiper's SyncManager instance. @@ -3703,7 +3676,7 @@ func (d *AuthenticatedGossiper) handleAnnSig(ctx context.Context, ann.ChannelID, peerID) ca, _, _, err := netann.CreateChanAnnouncement( - chanInfo.AuthProof, chanInfo, e1, e2, + chanInfo, e1, e2, ) if err != nil { log.Errorf("unable to gen ann: %v", @@ -3782,8 +3755,12 @@ func (d *AuthenticatedGossiper) handleAnnSig(ctx context.Context, ) } + // Attach the proof to the channel info before creating the + // announcement. + chanInfo.AuthProof = dbProof + chanAnn, e1Ann, e2Ann, err := netann.CreateChanAnnouncement( - dbProof, chanInfo, e1, e2, + chanInfo, e1, e2, ) if err != nil { log.Error(err) diff --git a/graph/db/models/channel_edge_info.go b/graph/db/models/channel_edge_info.go index 7e872c5cd..9f9477064 100644 --- a/graph/db/models/channel_edge_info.go +++ b/graph/db/models/channel_edge_info.go @@ -199,3 +199,66 @@ func (c *ChannelEdgeInfo) OtherNodeKeyBytes(thisNodeKey []byte) ( "this channel") } } + +// ToChannelAnnouncement converts the ChannelEdgeInfo to a +// lnwire.ChannelAnnouncement1 message. Returns an error if AuthProof is nil +// or if the version is not v1. +func (c *ChannelEdgeInfo) ToChannelAnnouncement() ( + *lnwire.ChannelAnnouncement1, error) { + + // We currently only support v1 channel announcements. + if c.Version != lnwire.GossipVersion1 { + return nil, fmt.Errorf("unsupported channel version: %d", + c.Version) + } + + // If there's no auth proof, we can't create a full channel + // announcement. + if c.AuthProof == nil { + return nil, fmt.Errorf("cannot create channel announcement " + + "without auth proof") + } + + chanID := lnwire.NewShortChanIDFromInt(c.ChannelID) + chanAnn := &lnwire.ChannelAnnouncement1{ + ShortChannelID: chanID, + NodeID1: c.NodeKey1Bytes, + NodeID2: c.NodeKey2Bytes, + ChainHash: c.ChainHash, + BitcoinKey1: c.BitcoinKey1Bytes, + BitcoinKey2: c.BitcoinKey2Bytes, + Features: c.Features.RawFeatureVector, + ExtraOpaqueData: c.ExtraOpaqueData, + } + + var err error + chanAnn.NodeSig1, err = lnwire.NewSigFromECDSARawSignature( + c.AuthProof.NodeSig1(), + ) + if err != nil { + return nil, err + } + + chanAnn.NodeSig2, err = lnwire.NewSigFromECDSARawSignature( + c.AuthProof.NodeSig2(), + ) + if err != nil { + return nil, err + } + + chanAnn.BitcoinSig1, err = lnwire.NewSigFromECDSARawSignature( + c.AuthProof.BitcoinSig1(), + ) + if err != nil { + return nil, err + } + + chanAnn.BitcoinSig2, err = lnwire.NewSigFromECDSARawSignature( + c.AuthProof.BitcoinSig2(), + ) + if err != nil { + return nil, err + } + + return chanAnn, nil +} diff --git a/netann/channel_announcement.go b/netann/channel_announcement.go index fee3c5abf..14f65c3f5 100644 --- a/netann/channel_announcement.go +++ b/netann/channel_announcement.go @@ -34,48 +34,14 @@ const ( // function is used to transform out database structs into the corresponding wire // structs for announcing new channels to other peers, or simply syncing up a // peer's initial routing table upon connect. -func CreateChanAnnouncement(chanProof *models.ChannelAuthProof, - chanInfo *models.ChannelEdgeInfo, +func CreateChanAnnouncement(chanInfo *models.ChannelEdgeInfo, e1, e2 *models.ChannelEdgePolicy) (*lnwire.ChannelAnnouncement1, *lnwire.ChannelUpdate1, *lnwire.ChannelUpdate1, error) { // First, using the parameters of the channel, along with the channel - // authentication chanProof, we'll create re-create the original + // authentication proof, we'll create re-create the original // authenticated channel announcement. - chanID := lnwire.NewShortChanIDFromInt(chanInfo.ChannelID) - chanAnn := &lnwire.ChannelAnnouncement1{ - ShortChannelID: chanID, - NodeID1: chanInfo.NodeKey1Bytes, - NodeID2: chanInfo.NodeKey2Bytes, - ChainHash: chanInfo.ChainHash, - BitcoinKey1: chanInfo.BitcoinKey1Bytes, - BitcoinKey2: chanInfo.BitcoinKey2Bytes, - Features: chanInfo.Features.RawFeatureVector, - ExtraOpaqueData: chanInfo.ExtraOpaqueData, - } - - var err error - chanAnn.BitcoinSig1, err = lnwire.NewSigFromECDSARawSignature( - chanProof.BitcoinSig1(), - ) - if err != nil { - return nil, nil, nil, err - } - chanAnn.BitcoinSig2, err = lnwire.NewSigFromECDSARawSignature( - chanProof.BitcoinSig2(), - ) - if err != nil { - return nil, nil, nil, err - } - chanAnn.NodeSig1, err = lnwire.NewSigFromECDSARawSignature( - chanProof.NodeSig1(), - ) - if err != nil { - return nil, nil, nil, err - } - chanAnn.NodeSig2, err = lnwire.NewSigFromECDSARawSignature( - chanProof.NodeSig2(), - ) + chanAnn, err := chanInfo.ToChannelAnnouncement() if err != nil { return nil, nil, nil, err } diff --git a/netann/channel_announcement_test.go b/netann/channel_announcement_test.go index c46b90a9d..11e72bb4a 100644 --- a/netann/channel_announcement_test.go +++ b/netann/channel_announcement_test.go @@ -59,9 +59,7 @@ func TestCreateChanAnnouncement(t *testing.T) { models.WithFeatures(features), ) require.NoError(t, err) - chanAnn, _, _, err := CreateChanAnnouncement( - chanProof, chanInfo, nil, nil, - ) + chanAnn, _, _, err := CreateChanAnnouncement(chanInfo, nil, nil) require.NoError(t, err, "unable to create channel announcement") assert.Equal(t, chanAnn, expChanAnn) From 44f41714152cdbdd493df635d9ed5b85c21b73ed Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 13:11:02 +0200 Subject: [PATCH 25/55] graph/db: make some ChannelEdgeInfo fields optional Since not all will be required for V2 channels. Wrap BitcoinKey1Bytes and BitcoinKey2Bytes in fn.Option since these fields are only required for v1 channel announcements. V2 channels may or may not have bitcoin keys present in their announcement. NewV1Channel constructor wraps the bitcoin keys with fn.Some(). All access sites updated to unwrap the options, using UnwrapOr for non-critical paths and UnwrapOrErr where the keys must be present (e.g., KV serialization, ToChannelAnnouncement). --- graph/db/graph_test.go | 13 +++++++--- graph/db/kv_store.go | 38 +++++++++++++++++++++++----- graph/db/models/channel_edge_info.go | 27 +++++++++++++++----- graph/db/sql_store.go | 20 +++++++++------ 4 files changed, 72 insertions(+), 26 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index c9e703f4d..5c9c0308d 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -838,10 +838,14 @@ func assertEdgeInfoEqual(t *testing.T, e1 *models.ChannelEdgeInfo, if !bytes.Equal(e1.NodeKey2Bytes[:], e2.NodeKey2Bytes[:]) { t.Fatalf("nodekey2 doesn't match") } - if !bytes.Equal(e1.BitcoinKey1Bytes[:], e2.BitcoinKey1Bytes[:]) { + btcKey1E1 := e1.BitcoinKey1Bytes.UnwrapOr(route.Vertex{}) + btcKey1E2 := e2.BitcoinKey1Bytes.UnwrapOr(route.Vertex{}) + if !bytes.Equal(btcKey1E1[:], btcKey1E2[:]) { t.Fatalf("bitcoinkey1 doesn't match") } - if !bytes.Equal(e1.BitcoinKey2Bytes[:], e2.BitcoinKey2Bytes[:]) { + btcKey2E1 := e1.BitcoinKey2Bytes.UnwrapOr(route.Vertex{}) + btcKey2E2 := e2.BitcoinKey2Bytes.UnwrapOr(route.Vertex{}) + if !bytes.Equal(btcKey2E1[:], btcKey2E2[:]) { t.Fatalf("bitcoinkey2 doesn't match") } @@ -2040,9 +2044,10 @@ func TestGraphPruning(t *testing.T) { t.Fatalf("unable to add node: %v", err) } + btcKey1 := edgeInfo.BitcoinKey1Bytes.UnwrapOr(route.Vertex{}) + btcKey2 := edgeInfo.BitcoinKey2Bytes.UnwrapOr(route.Vertex{}) pkScript, err := genMultiSigP2WSH( - edgeInfo.BitcoinKey1Bytes[:], - edgeInfo.BitcoinKey2Bytes[:], + btcKey1[:], btcKey2[:], ) if err != nil { t.Fatalf("unable to gen multi-sig p2wsh: %v", err) diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 14185f11a..f5a1ec219 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -4112,9 +4112,14 @@ func (c *KVStore) ChannelView() ([]EdgePoint, error) { return err } + btcKey1 := edgeInfo.BitcoinKey1Bytes.UnwrapOr( + route.Vertex{}, + ) + btcKey2 := edgeInfo.BitcoinKey2Bytes.UnwrapOr( + route.Vertex{}, + ) pkScript, err := genMultiSigP2WSH( - edgeInfo.BitcoinKey1Bytes[:], - edgeInfo.BitcoinKey2Bytes[:], + btcKey1[:], btcKey2[:], ) if err != nil { return err @@ -4728,10 +4733,24 @@ func putChanEdgeInfo(edgeIndex kvdb.RwBucket, if _, err := b.Write(edgeInfo.NodeKey2Bytes[:]); err != nil { return err } - if _, err := b.Write(edgeInfo.BitcoinKey1Bytes[:]); err != nil { + + btc1Key, err := edgeInfo.BitcoinKey1Bytes.UnwrapOrErr( + fmt.Errorf("edge missing bitcoin key 1"), + ) + if err != nil { return err } - if _, err := b.Write(edgeInfo.BitcoinKey2Bytes[:]); err != nil { + btc2Key, err := edgeInfo.BitcoinKey2Bytes.UnwrapOrErr( + fmt.Errorf("edge missing bitcoin key 2"), + ) + if err != nil { + return err + } + + if _, err := b.Write(btc1Key[:]); err != nil { + return err + } + if _, err := b.Write(btc2Key[:]); err != nil { return err } @@ -4769,7 +4788,7 @@ func putChanEdgeInfo(edgeIndex kvdb.RwBucket, if err := WriteOutpoint(&b, &edgeInfo.ChannelPoint); err != nil { return err } - err := binary.Write(&b, byteOrder, uint64(edgeInfo.Capacity)) + err = binary.Write(&b, byteOrder, uint64(edgeInfo.Capacity)) if err != nil { return err } @@ -4891,12 +4910,17 @@ func deserializeChanEdgeInfo(r io.Reader) (*models.ChannelEdgeInfo, error) { if _, err := io.ReadFull(r, edgeInfo.NodeKey2Bytes[:]); err != nil { return nil, err } - if _, err := io.ReadFull(r, edgeInfo.BitcoinKey1Bytes[:]); err != nil { + + var btcKey1, btcKey2 route.Vertex + if _, err := io.ReadFull(r, btcKey1[:]); err != nil { return nil, err } - if _, err := io.ReadFull(r, edgeInfo.BitcoinKey2Bytes[:]); err != nil { + edgeInfo.BitcoinKey1Bytes = fn.Some(btcKey1) + + if _, err := io.ReadFull(r, btcKey2[:]); err != nil { return nil, err } + edgeInfo.BitcoinKey2Bytes = fn.Some(btcKey2) featureBytes, err := wire.ReadVarBytes(r, 0, 900, "features") if err != nil { diff --git a/graph/db/models/channel_edge_info.go b/graph/db/models/channel_edge_info.go index 9f9477064..aea69e919 100644 --- a/graph/db/models/channel_edge_info.go +++ b/graph/db/models/channel_edge_info.go @@ -39,10 +39,10 @@ type ChannelEdgeInfo struct { NodeKey2Bytes route.Vertex // BitcoinKey1Bytes is the raw public key of the first node. - BitcoinKey1Bytes route.Vertex + BitcoinKey1Bytes fn.Option[route.Vertex] // BitcoinKey2Bytes is the raw public key of the second node. - BitcoinKey2Bytes route.Vertex + BitcoinKey2Bytes fn.Option[route.Vertex] // Features is the list of protocol features supported by this channel // edge. @@ -147,8 +147,8 @@ func NewV1Channel(chanID uint64, chainHash chainhash.Hash, node1, Version: lnwire.GossipVersion1, NodeKey1Bytes: node1, NodeKey2Bytes: node2, - BitcoinKey1Bytes: v1Fields.BitcoinKey1Bytes, - BitcoinKey2Bytes: v1Fields.BitcoinKey2Bytes, + BitcoinKey1Bytes: fn.Some(v1Fields.BitcoinKey1Bytes), + BitcoinKey2Bytes: fn.Some(v1Fields.BitcoinKey2Bytes), ChannelID: chanID, ChainHash: chainHash, Features: lnwire.EmptyFeatureVector(), @@ -219,19 +219,32 @@ func (c *ChannelEdgeInfo) ToChannelAnnouncement() ( "without auth proof") } + btc1, err := c.BitcoinKey1Bytes.UnwrapOrErr( + fmt.Errorf("bitcoin key 1 missing for v1 channel announcement"), + ) + if err != nil { + return nil, err + } + + btc2, err := c.BitcoinKey2Bytes.UnwrapOrErr( + fmt.Errorf("bitcoin key 2 missing for v1 channel announcement"), + ) + if err != nil { + return nil, err + } + chanID := lnwire.NewShortChanIDFromInt(c.ChannelID) chanAnn := &lnwire.ChannelAnnouncement1{ ShortChannelID: chanID, NodeID1: c.NodeKey1Bytes, NodeID2: c.NodeKey2Bytes, ChainHash: c.ChainHash, - BitcoinKey1: c.BitcoinKey1Bytes, - BitcoinKey2: c.BitcoinKey2Bytes, + BitcoinKey1: btc1, + BitcoinKey2: btc2, Features: c.Features.RawFeatureVector, ExtraOpaqueData: c.ExtraOpaqueData, } - var err error chanAnn.NodeSig1, err = lnwire.NewSigFromECDSARawSignature( c.AuthProof.NodeSig1(), ) diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 0ef5855bf..281975e0c 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -4288,15 +4288,19 @@ func insertChannel(ctx context.Context, db SQLQueries, } createParams := sqlc.CreateChannelParams{ - Version: int16(v), - Scid: channelIDToBytes(edge.ChannelID), - NodeID1: node1DBID, - NodeID2: node2DBID, - Outpoint: edge.ChannelPoint.String(), - Capacity: capacity, - BitcoinKey1: edge.BitcoinKey1Bytes[:], - BitcoinKey2: edge.BitcoinKey2Bytes[:], + Version: int16(v), + Scid: channelIDToBytes(edge.ChannelID), + NodeID1: node1DBID, + NodeID2: node2DBID, + Outpoint: edge.ChannelPoint.String(), + Capacity: capacity, } + edge.BitcoinKey1Bytes.WhenSome(func(vertex route.Vertex) { + createParams.BitcoinKey1 = vertex[:] + }) + edge.BitcoinKey2Bytes.WhenSome(func(vertex route.Vertex) { + createParams.BitcoinKey2 = vertex[:] + }) if edge.AuthProof != nil { proof := edge.AuthProof From 06a669bc89273d9e89a83c84b1e656a7d4946d28 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 13:15:19 +0200 Subject: [PATCH 26/55] graph/db: add FundingPKScript method on ChannelEdgeInfo Add FundingPKScript() method that returns the funding output's pkScript for the channel. The implementation is version-aware: - V1: generates a 2-of-2 multisig P2WSH script from the two bitcoin keys - V2: will use taproot script (to be implemented) This encapsulates the script generation logic and makes it clear which bitcoin keys are being used. Replaces direct calls to genMultiSigP2WSH with the cleaner method call. --- graph/db/graph_test.go | 11 +++------- graph/db/kv_store.go | 10 +-------- graph/db/models/channel_edge_info.go | 33 ++++++++++++++++++++++++++++ graph/db/sql_store.go | 1 + 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 5c9c0308d..db308fe0b 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -2044,14 +2044,9 @@ func TestGraphPruning(t *testing.T) { t.Fatalf("unable to add node: %v", err) } - btcKey1 := edgeInfo.BitcoinKey1Bytes.UnwrapOr(route.Vertex{}) - btcKey2 := edgeInfo.BitcoinKey2Bytes.UnwrapOr(route.Vertex{}) - pkScript, err := genMultiSigP2WSH( - btcKey1[:], btcKey2[:], - ) - if err != nil { - t.Fatalf("unable to gen multi-sig p2wsh: %v", err) - } + pkScript, err := edgeInfo.FundingPKScript() + require.NoError(t, err) + edgePoints = append(edgePoints, EdgePoint{ FundingPkScript: pkScript, OutPoint: op, diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index f5a1ec219..0854a506f 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -4112,15 +4112,7 @@ func (c *KVStore) ChannelView() ([]EdgePoint, error) { return err } - btcKey1 := edgeInfo.BitcoinKey1Bytes.UnwrapOr( - route.Vertex{}, - ) - btcKey2 := edgeInfo.BitcoinKey2Bytes.UnwrapOr( - route.Vertex{}, - ) - pkScript, err := genMultiSigP2WSH( - btcKey1[:], btcKey2[:], - ) + pkScript, err := edgeInfo.FundingPKScript() if err != nil { return err } diff --git a/graph/db/models/channel_edge_info.go b/graph/db/models/channel_edge_info.go index aea69e919..3e4a06587 100644 --- a/graph/db/models/channel_edge_info.go +++ b/graph/db/models/channel_edge_info.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" ) @@ -200,6 +201,38 @@ func (c *ChannelEdgeInfo) OtherNodeKeyBytes(thisNodeKey []byte) ( } } +// FundingPKScript returns the funding output's pkScript for the channel. +func (c *ChannelEdgeInfo) FundingPKScript() ([]byte, error) { + switch c.Version { + case lnwire.GossipVersion1: + btc1Key, err := c.BitcoinKey1Bytes.UnwrapOrErr( + fmt.Errorf("missing bitcoin key 1"), + ) + if err != nil { + return nil, err + } + btc2Key, err := c.BitcoinKey2Bytes.UnwrapOrErr( + fmt.Errorf("missing bitcoin key 2"), + ) + if err != nil { + return nil, err + } + + witnessScript, err := input.GenMultiSigScript( + btc1Key[:], btc2Key[:], + ) + if err != nil { + return nil, err + } + + return input.WitnessScriptHash(witnessScript) + + default: + return nil, fmt.Errorf("unsupported channel version: %d", + c.Version) + } +} + // ToChannelAnnouncement converts the ChannelEdgeInfo to a // lnwire.ChannelAnnouncement1 message. Returns an error if AuthProof is nil // or if the version is not v1. diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 281975e0c..e515b35c8 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -2768,6 +2768,7 @@ func (s *SQLStore) ChannelView() ([]EdgePoint, error) { handleChannel := func(_ context.Context, channel sqlc.ListChannelsPaginatedRow) error { + // TODO(elle): update to handle V2 channels. pkScript, err := genMultiSigP2WSH( channel.BitcoinKey1, channel.BitcoinKey2, ) From 25bbb39030039878fdf573fde78a7a41fc392414 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 19:04:28 +0200 Subject: [PATCH 27/55] docs: update release notes Update the Database section to reference both PRs that prepare the graph DB for gossip v2 support: - PR 10339: node handling - PR 10379: channel handling (this PR) --- docs/release-notes/release-notes-0.21.0.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index 0040da730..fc2a3fdce 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -176,8 +176,9 @@ * Freeze the [graph SQL migration code](https://github.com/lightningnetwork/lnd/pull/10338) to prevent the need for maintenance as the sqlc code evolves. -* Prepare the graph DB for handling gossip [V2 - nodes](https://github.com/lightningnetwork/lnd/pull/10339). +* 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). ## Code Health From 6842252bf8a57d22dbba2ae6690349fa497938ce Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 16:17:42 +0200 Subject: [PATCH 28/55] graph/db: add v2 channel announcement fields to ChannelEdgeInfo This commit extends ChannelEdgeInfo to support v2 (taproot) channel announcements by adding: - MerkleRootHash for the optional Merkle tree commitment - ExtraSignedFields for additional signed TLV fields - ChannelV2Fields struct to encapsulate v2-specific optional fields - NewV2Channel constructor for creating v2 channel edges - FundingPKScript handling for MuSig2 key aggregation with optional taproot tweaks --- graph/db/models/channel_edge_info.go | 127 +++++++++++++++++++ graph/db/models/channel_edge_info_test.go | 147 ++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 graph/db/models/channel_edge_info_test.go diff --git a/graph/db/models/channel_edge_info.go b/graph/db/models/channel_edge_info.go index 3e4a06587..4855c4ee4 100644 --- a/graph/db/models/channel_edge_info.go +++ b/graph/db/models/channel_edge_info.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -40,9 +41,15 @@ type ChannelEdgeInfo struct { NodeKey2Bytes route.Vertex // 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 second 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 @@ -68,13 +75,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. @@ -170,6 +192,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 second 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 @@ -227,6 +296,64 @@ 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", + )) + } + + // By default, the tweak is empty which results in a BIP86 + // output. If we have a merkle root, we'll use that as the + // tweak. + muSig2Opt := musig2.WithBIP86KeyTweak() + c.MerkleRootHash.WhenSome(func(hash chainhash.Hash) { + muSig2Opt = musig2.WithTaprootKeyTweak(hash[:]) + }) + + // Compute the output key. + combinedKey, _, _, err := musig2.AggregateKeys( + []*btcec.PublicKey{pubKey1, pubKey2}, true, muSig2Opt, + ) + 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/models/channel_edge_info_test.go b/graph/db/models/channel_edge_info_test.go new file mode 100644 index 000000000..464ca212d --- /dev/null +++ b/graph/db/models/channel_edge_info_test.go @@ -0,0 +1,147 @@ +package models + +import ( + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +// TestFundingPKScriptV2 tests the FundingPKScript method for v2 channels +// which uses MuSig2 key aggregation. +func TestFundingPKScriptV2(t *testing.T) { + t.Parallel() + + // Generate two test keys for bitcoin keys. + privKey1, err := btcec.NewPrivateKey() + require.NoError(t, err) + pubKey1 := privKey1.PubKey() + + privKey2, err := btcec.NewPrivateKey() + require.NoError(t, err) + pubKey2 := privKey2.PubKey() + + // Convert to route.Vertex format. + var btcKey1, btcKey2 route.Vertex + copy(btcKey1[:], pubKey1.SerializeCompressed()) + copy(btcKey2[:], pubKey2.SerializeCompressed()) + + // Create a test merkle root. + var merkleRoot chainhash.Hash + copy(merkleRoot[:], []byte("test-merkle-root-hash-32-bytes!")) + + t.Run("v2 with btc keys, no merkle root (BIP86)", func(t *testing.T) { + t.Parallel() + + edge := &ChannelEdgeInfo{ + Version: lnwire.GossipVersion2, + BitcoinKey1Bytes: fn.Some(btcKey1), + BitcoinKey2Bytes: fn.Some(btcKey2), + } + + pkScript, err := edge.FundingPKScript() + require.NoError(t, err) + require.NotEmpty(t, pkScript) + + // Verify it's a valid taproot script (OP_1 <32-byte-key>). + require.Len(t, pkScript, 34) + require.Equal(t, byte(0x51), pkScript[0]) // OP_1 + + // Manually compute expected script using BIP86 tweak. + combinedKey, _, _, err := musig2.AggregateKeys( + []*btcec.PublicKey{pubKey1, pubKey2}, true, + musig2.WithBIP86KeyTweak(), + ) + require.NoError(t, err) + + expectedScript, err := input.PayToTaprootScript( + combinedKey.FinalKey, + ) + require.NoError(t, err) + require.Equal(t, expectedScript, pkScript) + }) + + t.Run("v2 with bitcoin keys and merkle root", func(t *testing.T) { + t.Parallel() + + edge := &ChannelEdgeInfo{ + Version: lnwire.GossipVersion2, + BitcoinKey1Bytes: fn.Some(btcKey1), + BitcoinKey2Bytes: fn.Some(btcKey2), + MerkleRootHash: fn.Some(merkleRoot), + } + + pkScript, err := edge.FundingPKScript() + require.NoError(t, err) + require.NotEmpty(t, pkScript) + + // Verify it's a valid taproot script. + require.Len(t, pkScript, 34) + require.Equal(t, byte(0x51), pkScript[0]) // OP_1 + + // Manually compute expected script with taproot tweak. + combinedKey, _, _, err := musig2.AggregateKeys( + []*btcec.PublicKey{pubKey1, pubKey2}, true, + musig2.WithTaprootKeyTweak(merkleRoot[:]), + ) + require.NoError(t, err) + + expectedScript, err := input.PayToTaprootScript( + combinedKey.FinalKey, + ) + require.NoError(t, err) + require.Equal(t, expectedScript, pkScript) + }) + + t.Run("v2 no btc keys returns stored script", func(t *testing.T) { + t.Parallel() + + storedScript := []byte{0x51, 0x20} // OP_1 + push 32 + storedScript = append(storedScript, make([]byte, 32)...) + + edge := &ChannelEdgeInfo{ + Version: lnwire.GossipVersion2, + FundingScript: fn.Some(storedScript), + } + + pkScript, err := edge.FundingPKScript() + require.NoError(t, err) + require.Equal(t, storedScript, pkScript) + }) + + t.Run("v2 no btc keys and no stored script errors", func(t *testing.T) { + t.Parallel() + + edge := &ChannelEdgeInfo{ + Version: lnwire.GossipVersion2, + } + + _, err := edge.FundingPKScript() + require.Error(t, err) + require.Contains(t, err.Error(), "expected a funding pk script") + }) + + t.Run("v2 one btc key returns stored script", func(t *testing.T) { + t.Parallel() + + storedScript := []byte{0x51, 0x20} + storedScript = append(storedScript, make([]byte, 32)...) + + // Only key1 set, key2 missing. + edge := &ChannelEdgeInfo{ + Version: lnwire.GossipVersion2, + BitcoinKey1Bytes: fn.Some(btcKey1), + FundingScript: fn.Some(storedScript), + } + + pkScript, err := edge.FundingPKScript() + require.NoError(t, err) + require.Equal(t, storedScript, pkScript) + }) +} From fa24d336c164465333ef1c726b00cd205f5cbca9 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 16:44:15 +0200 Subject: [PATCH 29/55] graph/db: update SQLStore to read and write v2 channels Extends the SQL store to support v2 (taproot) channel announcements: - Add version validation in AddChannelEdge - Store v2-specific fields: FundingPkScript, MerkleRootHash, Signature - Update buildEdgeInfoWithBatchData to reconstruct v2 channels from DB with optional bitcoin keys and funding script handling - Add WithMerkleRootHash edge modifier for ChannelV2Fields --- graph/db/models/channel_edge_info.go | 17 +++ graph/db/sql_store.go | 188 ++++++++++++++++++--------- 2 files changed, 147 insertions(+), 58 deletions(-) diff --git a/graph/db/models/channel_edge_info.go b/graph/db/models/channel_edge_info.go index 4855c4ee4..aae7b91f0 100644 --- a/graph/db/models/channel_edge_info.go +++ b/graph/db/models/channel_edge_info.go @@ -137,6 +137,13 @@ func WithFundingScript(script []byte) EdgeModifier { } } +// 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 { @@ -201,6 +208,14 @@ type ChannelV2Fields struct { // BitcoinKey2Bytes is the raw public key of the second 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. // @@ -219,6 +234,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 e515b35c8..cbb3d145a 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -703,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...), @@ -720,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 { @@ -4259,26 +4264,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) } @@ -4302,6 +4297,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 @@ -4310,6 +4311,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. @@ -4333,10 +4335,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 { @@ -4448,9 +4453,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. @@ -4474,45 +4479,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, @@ -4520,7 +4530,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 86732fae9950a3ba677810494f008dd045276dcc Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 16:45:27 +0200 Subject: [PATCH 30/55] graph/db: refactor TestNodeIsPublic to use require assertions Replace verbose t.Fatal patterns with concise require assertions following the project's testing guidelines. --- graph/db/graph_test.go | 42 ++++++++++++------------------------------ 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index db308fe0b..36282715b 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -4079,21 +4079,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) @@ -4127,19 +4124,9 @@ 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) - 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) } } } @@ -4155,9 +4142,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}, @@ -4174,18 +4159,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 6bb3bdc83d4ced291a4f2ab06802551b433fe5cb Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 16:57:04 +0200 Subject: [PATCH 31/55] graph/db: add version parameter to createEdge test helper Extend the createEdge test helper to accept a gossip version, enabling creation of both v1 and v2 test channels. V2 channels include the appropriate auth proof (single signature), merkle root hash, and funding script fields. --- graph/db/graph_test.go | 178 +++++++++++++++++++++++++++++------------ 1 file changed, 125 insertions(+), 53 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 36282715b..d7203266a 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -378,7 +378,9 @@ 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) @@ -641,9 +643,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, @@ -664,30 +666,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 } @@ -731,19 +777,22 @@ 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 { @@ -2189,8 +2238,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) @@ -2211,7 +2260,9 @@ 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) } @@ -2265,7 +2316,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 { @@ -2732,7 +2784,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), @@ -2900,7 +2953,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 { @@ -2916,7 +2970,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) @@ -3074,8 +3129,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{ @@ -3389,12 +3445,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)) @@ -3569,7 +3627,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 { @@ -3611,7 +3670,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) @@ -3663,7 +3722,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 { @@ -3769,7 +3828,9 @@ 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) } @@ -3918,7 +3979,9 @@ 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) } @@ -3967,7 +4030,9 @@ 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 @@ -4092,8 +4157,12 @@ 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. @@ -4654,19 +4723,22 @@ 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 a73189ccec2ceac629528077806b4781ccab32d3 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 17:12:51 +0200 Subject: [PATCH 32/55] graph/db: add version parameter to DeleteChannelEdges Thread the gossip version through DeleteChannelEdges to enable version-aware channel deletion. The KVStore rejects non-v1 versions while SQLStore properly passes the version to the underlying queries. This prepares for v2 channel zombie handling (strict zombie pruning for v2 is marked as TODO). --- graph/db/graph.go | 3 ++- graph/db/graph_test.go | 20 +++++++++++++++----- graph/db/interfaces.go | 5 +++-- graph/db/kv_store.go | 9 +++++++-- graph/db/sql_store.go | 24 ++++++++++++++++-------- 5 files changed, 43 insertions(+), 18 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index 3fbb00e93..f2a5ffb0e 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/graph_test.go b/graph/db/graph_test.go index d7203266a..d819582d5 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -713,7 +713,9 @@ func createEdge(version lnwire.GossipVersion, height, txIndex uint32, // Create a test funding script. fundingScript := []byte{0x00, 0x20} - fundingScript = append(fundingScript, bytes.Repeat([]byte{0xbb}, 32)...) + fundingScript = append( + fundingScript, bytes.Repeat([]byte{0xbb}, 32)..., + ) proof := models.NewV2ChannelAuthProof(testSig.Serialize()) @@ -777,7 +779,9 @@ func TestDisconnectBlockAtHeight(t *testing.T) { // Create an edge which has its block height at 156. height := uint32(156) - edgeInfo, _ := createEdge(lnwire.GossipVersion1, 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 @@ -2238,8 +2242,12 @@ func TestHighestChanID(t *testing.T) { // The first channel with be at height 10, while the other will be at // height 100. - edge1, _ := createEdge(lnwire.GossipVersion1, 10, 0, 0, 0, node1, node2) - edge2, chanID2 := createEdge(lnwire.GossipVersion1, 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) @@ -4723,7 +4731,9 @@ func TestBatchedAddChannelEdge(t *testing.T) { // Create an edge which has its block height at 156. height := uint32(156) - edgeInfo, _ := createEdge(lnwire.GossipVersion1, 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 diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index b6a8c10ea..24cfae9ab 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 0854a506f..38b6c975f 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 cbb3d145a..fe8aa6291 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -1900,8 +1900,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() @@ -1934,7 +1935,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 @@ -1962,7 +1963,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 { @@ -2377,7 +2378,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 @@ -2425,7 +2426,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 { @@ -2435,7 +2436,7 @@ func (s *SQLStore) forEachChanWithPoliciesInSCIDList(ctx context.Context, return db.GetChannelsBySCIDWithPolicies( ctx, sqlc.GetChannelsBySCIDWithPoliciesParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), Scids: scids, }, ) @@ -6027,12 +6028,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) @@ -6051,7 +6059,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 315f5ed741220ac105287454dd260e2572c81d9b Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 17:19:53 +0200 Subject: [PATCH 33/55] sqldb/sqlc: add AddV2ChannelProof query Add SQL query to update the signature column for v2 channel auth proofs. Unlike v1 which requires four separate signatures, v2 channels use a single aggregated signature. --- 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 0096d87ec..c08ba967f 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 aa8ab2ecf..05bf91877 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 549cc930b..4f71fc8fa 100644 --- a/sqldb/sqlc/queries/graph.sql +++ b/sqldb/sqlc/queries/graph.sql @@ -312,6 +312,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 99d622ca0397542e35723d7aaf4bb3ac2fc60442 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 17:23:32 +0200 Subject: [PATCH 34/55] graph/db: support v2 channel proofs in AddEdgeProof Update AddEdgeProof to handle both v1 (four separate signatures) and v2 (single aggregated signature) channel authentication proofs using the appropriate SQL queries. --- graph/db/graph_test.go | 138 ++++++++++++++++++----------------------- graph/db/sql_store.go | 41 ++++++++---- 2 files changed, 92 insertions(+), 87 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index d819582d5..ea82ea5fa 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: "add edge proof", + test: testAddEdgeProof, + }, } // TestVersionedDBs runs various tests against both v1 and v2 versioned @@ -644,8 +648,10 @@ func TestEdgeInsertionDeletion(t *testing.T) { } func createEdge(version lnwire.GossipVersion, height, txIndex uint32, - txPosition uint16, outPointIndex uint32, node1, node2 *models.Node) ( - *models.ChannelEdgeInfo, lnwire.ShortChannelID) { + txPosition uint16, outPointIndex uint32, node1, node2 *models.Node, + skipProof ...bool) (*models.ChannelEdgeInfo, lnwire.ShortChannelID) { + + shouldSkipProof := len(skipProof) > 0 && skipProof[0] shortChanID := lnwire.ShortChannelID{ BlockHeight: height, @@ -677,12 +683,19 @@ func createEdge(version lnwire.GossipVersion, height, txIndex uint32, node2Pub.SerializeCompressed(), ) - proof := models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ) + opts := []models.EdgeModifier{ + models.WithChannelPoint(outpoint), + models.WithCapacity(9000), + } + if !shouldSkipProof { + proof := models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) + opts = append(opts, models.WithChanProof(proof)) + } edgeInfo, _ = models.NewV1Channel( shortChanID.ToUint64(), @@ -694,9 +707,7 @@ func createEdge(version lnwire.GossipVersion, height, txIndex uint32, BitcoinKey2Bytes: btcKey2, ExtraOpaqueData: make([]byte, 0), }, - models.WithChanProof(proof), - models.WithChannelPoint(outpoint), - models.WithCapacity(9000), + opts..., ) case lnwire.GossipVersion2: @@ -717,7 +728,16 @@ func createEdge(version lnwire.GossipVersion, height, txIndex uint32, fundingScript, bytes.Repeat([]byte{0xbb}, 32)..., ) - proof := models.NewV2ChannelAuthProof(testSig.Serialize()) + opts := []models.EdgeModifier{ + models.WithChannelPoint(outpoint), + models.WithCapacity(9000), + } + if !shouldSkipProof { + proof := models.NewV2ChannelAuthProof( + testSig.Serialize(), + ) + opts = append(opts, models.WithChanProof(proof)) + } edgeInfo, _ = models.NewV2Channel( shortChanID.ToUint64(), @@ -731,9 +751,7 @@ func createEdge(version lnwire.GossipVersion, height, txIndex uint32, FundingScript: fn.Some(fundingScript), ExtraSignedFields: make(map[uint64][]byte), }, - models.WithChanProof(proof), - models.WithChannelPoint(outpoint), - models.WithCapacity(9000), + opts..., ) } @@ -940,29 +958,9 @@ func assertEdgeInfoEqual(t *testing.T, e1 *models.ChannelEdgeInfo, } } -type createEdgeConfig struct { - skipProofs bool -} - -type createEdgeOpt func(*createEdgeConfig) - -// withSkipProofs will let createChannelEdge create an edge without auth -// proofs. In this case, createChannelEdge will then also not create policies. -func withSkipProofs() createEdgeOpt { - return func(cfg *createEdgeConfig) { - cfg.skipProofs = true - } -} - -func createChannelEdge(node1, node2 *models.Node, - options ...createEdgeOpt) (*models.ChannelEdgeInfo, +func createChannelEdge(node1, node2 *models.Node) (*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) { - var opts createEdgeConfig - for _, o := range options { - o(&opts) - } - var ( firstNode [33]byte secondNode [33]byte @@ -995,21 +993,6 @@ func createChannelEdge(node1, node2 *models.Node, 3, 3, 3, 3, 3, } - var edgeInfo *models.ChannelEdgeInfo - if opts.skipProofs { - edgeInfo, _ = models.NewV1Channel( - chanID, *chaincfg.MainNetParams.GenesisHash, node1Key, - node2Key, &models.ChannelV1Fields{ - BitcoinKey1Bytes: node1Key, - BitcoinKey2Bytes: node2Key, - ExtraOpaqueData: extraData, - }, models.WithChannelPoint(outpoint), - models.WithCapacity(1000), - ) - - return edgeInfo, nil, nil - } - proof := models.NewV1ChannelAuthProof( testSig.Serialize(), testSig.Serialize(), @@ -1017,7 +1000,7 @@ func createChannelEdge(node1, node2 *models.Node, testSig.Serialize(), ) - edgeInfo, _ = models.NewV1Channel( + edgeInfo, _ := models.NewV1Channel( chanID, *chaincfg.MainNetParams.GenesisHash, node1Key, node2Key, &models.ChannelV1Fields{ BitcoinKey1Bytes: node1Key, @@ -1457,42 +1440,48 @@ func newEdgePolicy(chanID uint64, updateTime int64) *models.ChannelEdgePolicy { } } -// TestAddEdgeProof tests the ability to add an edge proof to an existing edge. -func TestAddEdgeProof(t *testing.T) { +// testAddEdgeProof tests the ability to add an edge proof to an existing edge. +func testAddEdgeProof(t *testing.T, v lnwire.GossipVersion) { t.Parallel() ctx := t.Context() - graph := MakeTestGraph(t) + graph := NewVersionedGraph(MakeTestGraph(t), v) // Add an edge with no proof. - node1 := createTestVertex(t, lnwire.GossipVersion1) - node2 := createTestVertex(t, lnwire.GossipVersion1) - edge1, _, _ := createChannelEdge(node1, node2, withSkipProofs()) + node1 := createTestVertex(t, v) + node2 := createTestVertex(t, v) + + // Create edge without proof (skipProof = true). + edge1, _ := createEdge(v, 100, 0, 0, 0, node1, node2, true) require.NoError(t, graph.AddChannelEdge(ctx, edge1)) - // Fetch the edge and assert that the proof is nil and that the rest - // of the edge info is correct. + // Fetch the edge and assert that the proof is nil. dbEdge, _, _, err := graph.FetchChannelEdgesByID(edge1.ChannelID) require.NoError(t, err) require.Nil(t, dbEdge.AuthProof) - require.Equal(t, edge1, dbEdge) - // Now, add the edge proof. - proof := models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ) + // Create a proof appropriate for the version. + var proof *models.ChannelAuthProof + switch v { + case lnwire.GossipVersion1: + proof = models.NewV1ChannelAuthProof( + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + testSig.Serialize(), + ) + case lnwire.GossipVersion2: + proof = models.NewV2ChannelAuthProof(testSig.Serialize()) + } // First, add the proof to the rest of the channel edge info and try // to call AddChannelEdge again - this should fail due to the channel // already existing. edge1.AuthProof = proof err = graph.AddChannelEdge(ctx, edge1) - require.Error(t, err, ErrEdgeAlreadyExist) + require.ErrorIs(t, err, ErrEdgeAlreadyExist) - // Now add just the proof. + // Now add just the proof via AddEdgeProof. scid1 := lnwire.NewShortChanIDFromInt(edge1.ChannelID) require.NoError(t, graph.AddEdgeProof(scid1, proof)) @@ -1500,19 +1489,16 @@ func TestAddEdgeProof(t *testing.T) { dbEdge, _, _, err = graph.FetchChannelEdgesByID(edge1.ChannelID) require.NoError(t, err) require.NotNil(t, dbEdge.AuthProof) - require.Equal(t, edge1, dbEdge) // For completeness, also test the case where we insert a new edge with - // an edge proof. Show that the proof is present from the get go. - edge2, _, _ := createChannelEdge(node1, node2) + // an edge proof from the start. Show that the proof is present. + edge2, _ := createEdge(v, 200, 0, 0, 1, node1, node2) require.NoError(t, graph.AddChannelEdge(ctx, edge2)) - // Fetch the edge and assert that the proof is nil and that the rest - // of the edge info is correct. + // Fetch the edge and assert that the proof is set. dbEdge2, _, _, err := graph.FetchChannelEdgesByID(edge2.ChannelID) require.NoError(t, err) require.NotNil(t, dbEdge2.AuthProof) - require.Equal(t, edge2, dbEdge2) } // TestForEachSourceNodeChannel tests that the ForEachSourceNodeChannel diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index fe8aa6291..18ab23314 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -82,6 +82,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) @@ -2984,9 +2985,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) } @@ -2996,15 +2996,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 dcd0b9dac7f13fbf087c5b1cc672f539b29c4cff Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 17:34:22 +0200 Subject: [PATCH 35/55] graph/db: make TestPartialNode version-aware Convert TestPartialNode to a versioned test that runs for both v1 and v2 gossip versions, ensuring shell node creation works correctly for both channel types. --- graph/db/graph_test.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index ea82ea5fa..6584e123b 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -142,6 +142,10 @@ var versionedTests = []versionedTest{ name: "add edge proof", test: testAddEdgeProof, }, + { + name: "partial node", + test: testPartialNode, + }, } // TestVersionedDBs runs various tests against both v1 and v2 versioned @@ -367,24 +371,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) @@ -392,7 +394,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) @@ -405,7 +407,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) @@ -414,7 +416,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 b3cf9a806f336ad0f02ff8fa334e2ad6a983c2d8 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 17:39:58 +0200 Subject: [PATCH 36/55] multi: add DeleteChannelEdges to VersionedGraph Add version-aware DeleteChannelEdges method to VersionedGraph and update call sites in graph builder, rpcserver, and server to use versioned graphs. This ensures channel deletion operations are properly scoped to the correct gossip version. --- 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 3e1571396..96a7582eb 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 f2a5ffb0e..302813f49 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 6584e123b..ed6a5f580 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -153,6 +153,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 0c496397b..927635a84 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -3196,7 +3196,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 @@ -3237,7 +3237,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 78b1971e1..7ba74176a 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 a99604c7d7770f4ceaf48539a1eb17a886f3c8d5 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 18:01:27 +0200 Subject: [PATCH 37/55] sqldb/sqlc: add IsPublicV2Node query Add SQL query to determine if a node has public v2 channels. Unlike v1 which requires all four individual signatures, v2 channels are considered public when the single aggregated signature is present. --- sqldb/sqlc/graph.sql.go | 30 ++++++++++++++++++++++++++++++ sqldb/sqlc/querier.go | 1 + sqldb/sqlc/queries/graph.sql | 22 ++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index c08ba967f..135f00d6e 100644 --- a/sqldb/sqlc/graph.sql.go +++ b/sqldb/sqlc/graph.sql.go @@ -2751,6 +2751,36 @@ 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 + -- 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 + + UNION ALL + + SELECT 1 + FROM graph_channels c + JOIN graph_nodes n ON n.id = c.node_id_2 + WHERE c.version = 2 + AND COALESCE(length(c.signature), 0) > 0 + 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 05bf91877..d26f845d0 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 4f71fc8fa..1817fbf3b 100644 --- a/sqldb/sqlc/queries/graph.sql +++ b/sqldb/sqlc/queries/graph.sql @@ -112,6 +112,28 @@ 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 + -- 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 + + UNION ALL + + SELECT 1 + FROM graph_channels c + JOIN graph_nodes n ON n.id = c.node_id_2 + WHERE c.version = 2 + AND COALESCE(length(c.signature), 0) > 0 + AND n.pub_key = $1 +); + -- name: DeleteUnconnectedNodes :many DELETE FROM graph_nodes WHERE From 971c0f6eb8e7ccdb0ee8ffb77b9c4fcf81bbfde5 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 18:03:42 +0200 Subject: [PATCH 38/55] graph/db: add version parameter to IsPublicNode Make IsPublicNode version-aware by routing to the appropriate SQL query based on gossip version. V1 and v2 have different criteria for determining node publicity (v1 requires four signatures, v2 requires one). Convert TestNodeIsPublic to versioned test for both protocols. --- graph/builder.go | 2 +- graph/db/graph.go | 7 ++++++- graph/db/graph_test.go | 36 ++++++++++++++++++------------------ graph/db/interfaces.go | 2 +- graph/db/kv_store.go | 8 +++++++- graph/db/sql_store.go | 15 +++++++++++++-- 6 files changed, 46 insertions(+), 24 deletions(-) diff --git a/graph/builder.go b/graph/builder.go index 96a7582eb..1f3309c04 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 302813f49..6c4b0a150 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 ed6a5f580..2f106dd63 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -146,6 +146,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 @@ -4125,9 +4129,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() @@ -4139,33 +4143,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() @@ -4181,7 +4181,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() @@ -4212,7 +4212,7 @@ func TestNodeIsPublic(t *testing.T) { } checkNodes( []*models.Node{aliceNode}, - []*ChannelGraph{bobGraph, carolGraph}, + []*VersionedGraph{bobGraph, carolGraph}, false, ) @@ -4240,7 +4240,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 24cfae9ab..5658fa5c7 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 38b6c975f..b3442bde6 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -4005,7 +4005,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 18ab23314..2f0b1c56f 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -50,6 +50,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 @@ -2338,13 +2339,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 d88fa01162dc345bfe42bce03d3837bd962f20b4 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 18:28:20 +0200 Subject: [PATCH 39/55] graph/db: add version parameter to FetchChannelEdgesByID/Outpoint Make channel edge fetching version-aware by adding gossip version parameter to FetchChannelEdgesByID and FetchChannelEdgesByOutpoint. Add corresponding methods to VersionedGraph. V2 policy building is marked as TODO. --- graph/db/graph.go | 25 ++++++++++++++++++++++-- graph/db/interfaces.go | 4 ++-- graph/db/kv_store.go | 16 ++++++++++++---- graph/db/sql_store.go | 43 +++++++++++++++++++++++++++++++++++------- 4 files changed, 73 insertions(+), 15 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index 6c4b0a150..0f4dda661 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 5658fa5c7..c6848132e 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 b3442bde6..0887ff0d8 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -3811,8 +3811,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 ( @@ -3821,6 +3821,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. @@ -3897,10 +3901,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 2f0b1c56f..7cebedeef 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -2008,8 +2008,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 ( @@ -2018,11 +2018,18 @@ 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) { @@ -2031,7 +2038,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) { @@ -2118,8 +2125,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 ( @@ -2127,11 +2134,18 @@ 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) { @@ -4662,6 +4676,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 03050a589d852c8d8b458bb68d83631853201d4f Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 18:36:54 +0200 Subject: [PATCH 40/55] graph/db: add version parameter to IsZombieEdge Make IsZombieEdge version-aware and add corresponding method to VersionedGraph. Convert TestEdgeInsertionDeletion to versioned test using the createEdge helper for both v1 and v2 channel testing. --- graph/db/graph.go | 9 ++- graph/db/graph_test.go | 136 +++++++++++++++++++++++------------------ graph/db/interfaces.go | 3 +- graph/db/kv_store.go | 9 ++- graph/db/sql_store.go | 11 +++- 5 files changed, 103 insertions(+), 65 deletions(-) diff --git a/graph/db/graph.go b/graph/db/graph.go index 0f4dda661..41396aceb 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 2f106dd63..a6dcb106d 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -142,6 +142,10 @@ var versionedTests = []versionedTest{ name: "add edge proof", test: testAddEdgeProof, }, + { + name: "edge insertion deletion", + test: testEdgeInsertionDeletion, + }, { name: "partial node", test: testPartialNode, @@ -549,88 +553,104 @@ func TestSetSourceNodeSameTimestamp(t *testing.T) { require.Equal(t, testNode.LastUpdate, updatedNode.LastUpdate) } -// 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) + node1 := createTestVertex(t, v) + node2 := createTestVertex(t, v) - // In addition to the fake vertexes we create some fake channel - // identifiers. - chanID := uint64(prand.Int63()) + // Create a fake channel and add it to the graph. + const ( + blockHeight = 1234 + txIndex = 1 + txPosition = 0 + outPointIndex = 9 + ) + + edgeInfo, shortChanID := createEdge( + v, blockHeight, txIndex, txPosition, outPointIndex, node1, + node2, + ) + chanID := shortChanID.ToUint64() outpoint := wire.OutPoint{ Hash: rev, - Index: 9, + Index: outPointIndex, } - // 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) - - proof := models.NewV1ChannelAuthProof( - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - testSig.Serialize(), - ) - - 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) - 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). - _, e1, e2, err := graph.FetchChannelEdgesByID(chanID) + // Ensure that both policies are returned as unknown (nil) and that + // the edge info round-trips correctly. + dbEdge, e1, e2, err := graph.FetchChannelEdgesByID(chanID) require.NoError(t, err) require.Nil(t, e1) require.Nil(t, e2) + // Verify core fields match. + require.Equal(t, edgeInfo.ChannelID, dbEdge.ChannelID) + require.Equal(t, edgeInfo.Version, dbEdge.Version) + require.Equal(t, edgeInfo.NodeKey1Bytes, dbEdge.NodeKey1Bytes) + require.Equal(t, edgeInfo.NodeKey2Bytes, dbEdge.NodeKey2Bytes) + require.Equal(t, edgeInfo.ChainHash, dbEdge.ChainHash) + require.Equal(t, edgeInfo.ChannelPoint, dbEdge.ChannelPoint) + require.Equal(t, edgeInfo.Capacity, dbEdge.Capacity) + + // Verify auth proof round-trips. + require.NotNil(t, dbEdge.AuthProof) + require.Equal(t, edgeInfo.AuthProof.Version, dbEdge.AuthProof.Version) + + // Verify version-specific fields. + switch v { + case lnwire.GossipVersion1: + require.Equal(t, + edgeInfo.BitcoinKey1Bytes, dbEdge.BitcoinKey1Bytes, + ) + require.Equal(t, + edgeInfo.BitcoinKey2Bytes, dbEdge.BitcoinKey2Bytes, + ) + require.Equal(t, + edgeInfo.ExtraOpaqueData, dbEdge.ExtraOpaqueData, + ) + + case lnwire.GossipVersion2: + require.Equal(t, + edgeInfo.BitcoinKey1Bytes, dbEdge.BitcoinKey1Bytes, + ) + require.Equal(t, + edgeInfo.BitcoinKey2Bytes, dbEdge.BitcoinKey2Bytes, + ) + require.Equal(t, + edgeInfo.MerkleRootHash, dbEdge.MerkleRootHash, + ) + require.Equal(t, + edgeInfo.FundingScript, dbEdge.FundingScript, + ) + require.Equal(t, + edgeInfo.ExtraSignedFields, dbEdge.ExtraSignedFields, + ) + } + + // Also verify fetching by outpoint returns the same data. + dbEdge2, _, _, err := graph.FetchChannelEdgesByOutpoint(&outpoint) + require.NoError(t, err) + require.Equal(t, dbEdge.ChannelID, dbEdge2.ChannelID) + // 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 c6848132e..24a48e4c7 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 0887ff0d8..550ed3270 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -4257,14 +4257,19 @@ 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 7cebedeef..c55110338 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -1824,8 +1824,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() @@ -1834,11 +1834,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 a39f94eb4a5de6ad6188e167a53f7f215bc94800 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 19 Nov 2025 19:06:08 +0200 Subject: [PATCH 41/55] docs: update release notes for gossip v2 graph preparation Add reference to the PR implementing v2 channel support in the graph database layer. --- docs/release-notes/release-notes-0.21.0.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index fc2a3fdce..1c74a245e 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -176,9 +176,10 @@ * Freeze the [graph SQL migration code](https://github.com/lightningnetwork/lnd/pull/10338) to prevent the need for maintenance as the sqlc code evolves. -* Prepare the graph DB for handling gossip V2 +* 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](https://github.com/lightningnetwork/lnd/pull/10380). ## Code Health From 6328c4d897c48c3cfce0161970e929152971341d Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 29 Jan 2026 11:45:07 +0200 Subject: [PATCH 42/55] graph/db: treat empty channel signatures as missing This commit improves handling of missing channel signatures in the database: - Return nil from auth proof accessors instead of empty slices so that missing signatures are stored as NULL in SQL. - Update public channel checks to require signature length > 0, which properly handles existing empty bytea values in the database. - Add regression test covering empty v1 and v2 channel signatures to prevent future issues. --- graph/db/graph_test.go | 57 +++++++++++++++++++++++++++ graph/db/kv_store.go | 7 +++- graph/db/models/channel_auth_proof.go | 26 ++++++------ sqldb/sqlc/graph.sql.go | 10 ++--- sqldb/sqlc/queries/graph.sql | 12 +++--- 5 files changed, 85 insertions(+), 27 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index a6dcb106d..5148f63a8 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -154,6 +154,10 @@ var versionedTests = []versionedTest{ name: "node is public", test: testNodeIsPublic, }, + { + name: "node is public empty channel signature", + test: testIsPublicNodeEmptyChannelSignature, + }, } // TestVersionedDBs runs various tests against both v1 and v2 versioned @@ -4265,6 +4269,59 @@ func testNodeIsPublic(t *testing.T, v lnwire.GossipVersion) { ) } +// testIsPublicNodeEmptyChannelSignature ensures empty channel signatures don't +// mark nodes as public. +func testIsPublicNodeEmptyChannelSignature(t *testing.T, + v lnwire.GossipVersion) { + + t.Parallel() + ctx := t.Context() + + testGraph := MakeTestGraph(t) + graph := NewVersionedGraph(testGraph, v) + + // Set a source node as it's required for IsPublicNode. + sourceNode := createTestVertex(t, v) + err := graph.SetSourceNode(ctx, sourceNode) + require.NoError(t, err) + + node1 := createTestVertex(t, v) + + node1.LastUpdate = nextUpdateTime() + + err = graph.AddNode(ctx, node1) + require.NoError(t, err) + + // Create an edge between source node and node1, with + // empty signatures. This tests that empty signatures + // don't mark nodes as public. + edgeInfo, _ := createEdge( + v, 10, 0, 0, 0, sourceNode, node1, + true, + ) + + switch v { + case lnwire.GossipVersion1: + edgeInfo.AuthProof = + models.NewV1ChannelAuthProof( + []byte{}, []byte{}, + []byte{}, []byte{}, + ) + case lnwire.GossipVersion2: + edgeInfo.AuthProof = + models.NewV2ChannelAuthProof([]byte{}) + } + + err = graph.AddChannelEdge(ctx, edgeInfo) + require.NoError(t, err) + + // node1 should NOT be considered public because the + // channel announcement has empty signatures. + isPublic, err := graph.IsPublicNode(node1.PubKeyBytes) + require.NoError(t, err) + require.False(t, isPublic) +} + // BenchmarkIsPublicNode measures the performance of IsPublicNode when checking // a large number of nodes. func BenchmarkIsPublicNode(b *testing.B) { diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 550ed3270..eb4c3b9a1 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -3377,8 +3377,11 @@ func (c *KVStore) isPublic(tx kvdb.RTx, nodePub route.Vertex, } // Since the edge _does_ extend to the source node, we'll also - // need to ensure that this is a public edge. - if info.AuthProof != nil { + // need to ensure that this is a public edge with valid + // signatures (not empty). + if info.AuthProof != nil && !info.AuthProof.IsEmpty() && + len(info.AuthProof.BitcoinSig1()) > 0 { + nodeIsPublic = true return errDone } diff --git a/graph/db/models/channel_auth_proof.go b/graph/db/models/channel_auth_proof.go index 1e1fb03a5..e4acc2f8b 100644 --- a/graph/db/models/channel_auth_proof.go +++ b/graph/db/models/channel_auth_proof.go @@ -89,31 +89,29 @@ func NewV2ChannelAuthProof(signature []byte) *ChannelAuthProof { } } -// NodeSig1 returns the first node signature bytes, or an empty slice if not -// present. +// NodeSig1 returns the first node signature bytes, or nil if not present. func (c *ChannelAuthProof) NodeSig1() []byte { - return c.NodeSig1Bytes.UnwrapOr([]byte{}) + return c.NodeSig1Bytes.UnwrapOr(nil) } -// NodeSig2 returns the second node signature bytes, or an empty slice if not -// present. +// NodeSig2 returns the second node signature bytes, or nil if not present. func (c *ChannelAuthProof) NodeSig2() []byte { - return c.NodeSig2Bytes.UnwrapOr([]byte{}) + return c.NodeSig2Bytes.UnwrapOr(nil) } -// BitcoinSig1 returns the first bitcoin signature bytes, or an empty slice if -// not present. +// BitcoinSig1 returns the first bitcoin signature bytes, or nil if not +// present. func (c *ChannelAuthProof) BitcoinSig1() []byte { - return c.BitcoinSig1Bytes.UnwrapOr([]byte{}) + return c.BitcoinSig1Bytes.UnwrapOr(nil) } -// BitcoinSig2 returns the second bitcoin signature bytes, or an empty slice if -// not present. +// BitcoinSig2 returns the second bitcoin signature bytes, or nil if not +// present. func (c *ChannelAuthProof) BitcoinSig2() []byte { - return c.BitcoinSig2Bytes.UnwrapOr([]byte{}) + return c.BitcoinSig2Bytes.UnwrapOr(nil) } -// Sig returns the v2 signature bytes, or an empty slice if not present. +// Sig returns the v2 signature bytes, or nil if not present. func (c *ChannelAuthProof) Sig() []byte { - return c.Signature.UnwrapOr([]byte{}) + return c.Signature.UnwrapOr(nil) } diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index 135f00d6e..b8bb88408 100644 --- a/sqldb/sqlc/graph.sql.go +++ b/sqldb/sqlc/graph.sql.go @@ -2085,7 +2085,7 @@ WHERE last_update >= $1 SELECT 1 FROM graph_channels c WHERE c.version = 1 - AND c.bitcoin_1_signature IS NOT NULL + AND COALESCE(length(c.bitcoin_1_signature), 0) > 0 AND (c.node_id_1 = graph_nodes.id OR c.node_id_2 = graph_nodes.id) ) ) @@ -2211,7 +2211,7 @@ func (q *Queries) GetPruneTip(ctx context.Context) (GraphPruneLog, error) { const getPublicV1ChannelsBySCID = `-- name: GetPublicV1ChannelsBySCID :many SELECT id, version, scid, node_id_1, node_id_2, outpoint, capacity, bitcoin_key_1, bitcoin_key_2, node_1_signature, node_2_signature, bitcoin_1_signature, bitcoin_2_signature, signature, funding_pk_script, merkle_root_hash FROM graph_channels -WHERE node_1_signature IS NOT NULL +WHERE COALESCE(length(node_1_signature), 0) > 0 AND scid >= $1 AND scid < $2 ` @@ -2732,14 +2732,14 @@ SELECT EXISTS ( -- one of the signatures since we only ever set them -- together. WHERE c.version = 1 - AND c.bitcoin_1_signature IS NOT NULL + AND COALESCE(length(c.bitcoin_1_signature), 0) > 0 AND n.pub_key = $1 UNION ALL SELECT 1 FROM graph_channels c JOIN graph_nodes n ON n.id = c.node_id_2 WHERE c.version = 1 - AND c.bitcoin_1_signature IS NOT NULL + AND COALESCE(length(c.bitcoin_1_signature), 0) > 0 AND n.pub_key = $1 ) ` @@ -2760,7 +2760,7 @@ SELECT EXISTS ( -- 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 COALESCE(length(c.signature), 0) > 0 AND n.pub_key = $1 UNION ALL diff --git a/sqldb/sqlc/queries/graph.sql b/sqldb/sqlc/queries/graph.sql index 1817fbf3b..0ad71f878 100644 --- a/sqldb/sqlc/queries/graph.sql +++ b/sqldb/sqlc/queries/graph.sql @@ -101,14 +101,14 @@ SELECT EXISTS ( -- one of the signatures since we only ever set them -- together. WHERE c.version = 1 - AND c.bitcoin_1_signature IS NOT NULL + AND COALESCE(length(c.bitcoin_1_signature), 0) > 0 AND n.pub_key = $1 UNION ALL SELECT 1 FROM graph_channels c JOIN graph_nodes n ON n.id = c.node_id_2 WHERE c.version = 1 - AND c.bitcoin_1_signature IS NOT NULL + AND COALESCE(length(c.bitcoin_1_signature), 0) > 0 AND n.pub_key = $1 ); @@ -121,7 +121,7 @@ SELECT EXISTS ( -- 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 COALESCE(length(c.signature), 0) > 0 AND n.pub_key = $1 UNION ALL @@ -251,7 +251,7 @@ WHERE last_update >= @start_time SELECT 1 FROM graph_channels c WHERE c.version = 1 - AND c.bitcoin_1_signature IS NOT NULL + AND COALESCE(length(c.bitcoin_1_signature), 0) > 0 AND (c.node_id_1 = graph_nodes.id OR c.node_id_2 = graph_nodes.id) ) ) @@ -731,7 +731,7 @@ WHERE c.version = $1 -- name: GetPublicV1ChannelsBySCID :many SELECT * FROM graph_channels -WHERE node_1_signature IS NOT NULL +WHERE COALESCE(length(node_1_signature), 0) > 0 AND scid >= @start_scid AND scid < @end_scid; @@ -1217,4 +1217,4 @@ ON CONFLICT (channel_id, node_id, version) message_flags = EXCLUDED.message_flags, channel_flags = EXCLUDED.channel_flags, signature = EXCLUDED.signature -RETURNING id; \ No newline at end of file +RETURNING id; From 0aa93c93e7a96b6219260d3f61fd22ec9570d3fa Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 3 Feb 2026 10:42:54 +0200 Subject: [PATCH 43/55] graph/db: add v2 policy fields to database layer Extend channel policy queries and structs to support v2-specific fields: - Add BlockHeight field to track the block height for v2 policy updates. - Add DisableFlags field for v2 channel disable messages. Both fields are nullable (sql.NullInt64/Int16) to maintain backwards compatibility with v1 channels. The fields are initialized as null in updateChanEdgePolicy and threaded through all policy-related queries (GetChannelBySCIDWithPolicies, ListChannelsByNodeID, UpsertEdgePolicy, etc.) and the extractChannelPolicies helper. This commit includes both the hand-written SQL query updates and the corresponding sqlc-generated Go code. --- graph/db/sql_store.go | 38 ++++++++++ sqldb/sqlc/graph.sql.go | 138 ++++++++++++++++++++++++++++++++--- sqldb/sqlc/queries/graph.sql | 62 +++++++++++++--- 3 files changed, 214 insertions(+), 24 deletions(-) diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index c55110338..9a62f2f5d 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -3517,6 +3517,8 @@ func updateChanEdgePolicy(ctx context.Context, tx SQLQueries, InboundBaseFeeMsat: inboundBase, InboundFeeRateMilliMsat: inboundRate, Signature: edge.SigBytes, + BlockHeight: sql.NullInt64{}, + DisableFlags: sql.NullInt16{}, }) if err != nil { return node1Pub, node2Pub, isNode1, @@ -4830,6 +4832,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, Disabled: r.Policy1Disabled, MessageFlags: r.Policy1MessageFlags, ChannelFlags: r.Policy1ChannelFlags, + BlockHeight: r.Policy1BlockHeight, + DisableFlags: r.Policy1DisableFlags, } } if r.Policy2Timelock.Valid { @@ -4844,6 +4848,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, Disabled: r.Policy2Disabled, MessageFlags: r.Policy2MessageFlags, ChannelFlags: r.Policy2ChannelFlags, + BlockHeight: r.Policy2BlockHeight, + DisableFlags: r.Policy2DisableFlags, } } @@ -4868,6 +4874,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy1MessageFlags, ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, + BlockHeight: r.Policy1BlockHeight, + DisableFlags: r.Policy1DisableFlags, } } if r.Policy2ID.Valid { @@ -4888,6 +4896,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy2MessageFlags, ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, + BlockHeight: r.Policy2BlockHeight, + DisableFlags: r.Policy2DisableFlags, } } @@ -4912,6 +4922,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy1MessageFlags, ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, + BlockHeight: r.Policy1BlockHeight, + DisableFlags: r.Policy1DisableFlags, } } if r.Policy2ID.Valid { @@ -4932,6 +4944,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy2MessageFlags, ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, + BlockHeight: r.Policy2BlockHeight, + DisableFlags: r.Policy2DisableFlags, } } @@ -4956,6 +4970,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy1MessageFlags, ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, + BlockHeight: r.Policy1BlockHeight, + DisableFlags: r.Policy1DisableFlags, } } if r.Policy2ID.Valid { @@ -4976,6 +4992,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy2MessageFlags, ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, + BlockHeight: r.Policy2BlockHeight, + DisableFlags: r.Policy2DisableFlags, } } @@ -5000,6 +5018,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy1MessageFlags, ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, + BlockHeight: r.Policy1BlockHeight, + DisableFlags: r.Policy1DisableFlags, } } if r.Policy2ID.Valid { @@ -5020,6 +5040,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy2MessageFlags, ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, + BlockHeight: r.Policy2BlockHeight, + DisableFlags: r.Policy2DisableFlags, } } @@ -5044,6 +5066,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy1MessageFlags, ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, + BlockHeight: r.Policy1BlockHeight, + DisableFlags: r.Policy1DisableFlags, } } if r.Policy2ID.Valid { @@ -5064,6 +5088,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy2MessageFlags, ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, + BlockHeight: r.Policy2BlockHeight, + DisableFlags: r.Policy2DisableFlags, } } @@ -5088,6 +5114,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy1MessageFlags, ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, + BlockHeight: r.Policy1BlockHeight, + DisableFlags: r.Policy1DisableFlags, } } if r.Policy2ID.Valid { @@ -5108,6 +5136,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy2MessageFlags, ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, + BlockHeight: r.Policy2BlockHeight, + DisableFlags: r.Policy2DisableFlags, } } @@ -5132,6 +5162,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy1MessageFlags, ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, + BlockHeight: r.Policy1BlockHeight, + DisableFlags: r.Policy1DisableFlags, } } if r.Policy2ID.Valid { @@ -5152,6 +5184,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy2MessageFlags, ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, + BlockHeight: r.Policy2BlockHeight, + DisableFlags: r.Policy2DisableFlags, } } @@ -5176,6 +5210,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy1MessageFlags, ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, + BlockHeight: r.Policy1BlockHeight, + DisableFlags: r.Policy1DisableFlags, } } if r.Policy2ID.Valid { @@ -5196,6 +5232,8 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, MessageFlags: r.Policy2MessageFlags, ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, + BlockHeight: r.Policy2BlockHeight, + DisableFlags: r.Policy2DisableFlags, } } diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index b8bb88408..1d569b18e 100644 --- a/sqldb/sqlc/graph.sql.go +++ b/sqldb/sqlc/graph.sql.go @@ -397,6 +397,8 @@ SELECT cp1.message_flags AS policy_1_message_flags, cp1.channel_flags AS policy_1_channel_flags, cp1.signature AS policy_1_signature, + cp1.block_height AS policy_1_block_height, + cp1.disable_flags AS policy_1_disable_flags, -- Node 2 policy cp2.id AS policy_2_id, @@ -413,7 +415,9 @@ SELECT cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy_2_message_flags, cp2.channel_flags AS policy_2_channel_flags, - cp2.signature AS policy_2_signature + cp2.signature AS policy_2_signature, + cp2.block_height AS policy_2_block_height, + cp2.disable_flags AS policy_2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id JOIN graph_nodes n2 ON c.node_id_2 = n2.id @@ -448,6 +452,8 @@ type GetChannelByOutpointWithPoliciesRow struct { Policy1MessageFlags sql.NullInt16 Policy1ChannelFlags sql.NullInt16 Policy1Signature []byte + Policy1BlockHeight sql.NullInt64 + Policy1DisableFlags sql.NullInt16 Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 Policy2Version sql.NullInt16 @@ -463,6 +469,8 @@ type GetChannelByOutpointWithPoliciesRow struct { Policy2MessageFlags sql.NullInt16 Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte + Policy2BlockHeight sql.NullInt64 + Policy2DisableFlags sql.NullInt16 } func (q *Queries) GetChannelByOutpointWithPolicies(ctx context.Context, arg GetChannelByOutpointWithPoliciesParams) (GetChannelByOutpointWithPoliciesRow, error) { @@ -502,6 +510,8 @@ func (q *Queries) GetChannelByOutpointWithPolicies(ctx context.Context, arg GetC &i.Policy1MessageFlags, &i.Policy1ChannelFlags, &i.Policy1Signature, + &i.Policy1BlockHeight, + &i.Policy1DisableFlags, &i.Policy2ID, &i.Policy2NodeID, &i.Policy2Version, @@ -517,6 +527,8 @@ func (q *Queries) GetChannelByOutpointWithPolicies(ctx context.Context, arg GetC &i.Policy2MessageFlags, &i.Policy2ChannelFlags, &i.Policy2Signature, + &i.Policy2BlockHeight, + &i.Policy2DisableFlags, ) return i, err } @@ -577,6 +589,8 @@ SELECT cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Policy 2 cp2.id AS policy2_id, @@ -593,7 +607,9 @@ SELECT cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy_2_message_flags, cp2.channel_flags AS policy_2_channel_flags, - cp2.signature AS policy2_signature + cp2.signature AS policy2_signature, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -630,6 +646,8 @@ type GetChannelBySCIDWithPoliciesRow struct { Policy1MessageFlags sql.NullInt16 Policy1ChannelFlags sql.NullInt16 Policy1Signature []byte + Policy1BlockHeight sql.NullInt64 + Policy1DisableFlags sql.NullInt16 Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 Policy2Version sql.NullInt16 @@ -645,6 +663,8 @@ type GetChannelBySCIDWithPoliciesRow struct { Policy2MessageFlags sql.NullInt16 Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte + Policy2BlockHeight sql.NullInt64 + Policy2DisableFlags sql.NullInt16 } func (q *Queries) GetChannelBySCIDWithPolicies(ctx context.Context, arg GetChannelBySCIDWithPoliciesParams) (GetChannelBySCIDWithPoliciesRow, error) { @@ -698,6 +718,8 @@ func (q *Queries) GetChannelBySCIDWithPolicies(ctx context.Context, arg GetChann &i.Policy1MessageFlags, &i.Policy1ChannelFlags, &i.Policy1Signature, + &i.Policy1BlockHeight, + &i.Policy1DisableFlags, &i.Policy2ID, &i.Policy2NodeID, &i.Policy2Version, @@ -713,6 +735,8 @@ func (q *Queries) GetChannelBySCIDWithPolicies(ctx context.Context, arg GetChann &i.Policy2MessageFlags, &i.Policy2ChannelFlags, &i.Policy2Signature, + &i.Policy2BlockHeight, + &i.Policy2DisableFlags, ) return i, err } @@ -917,6 +941,8 @@ SELECT cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Policy 2 cp2.id AS policy2_id, @@ -933,7 +959,9 @@ SELECT cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy2_message_flags, cp2.channel_flags AS policy2_channel_flags, - cp2.signature AS policy2_signature + cp2.signature AS policy2_signature, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -966,6 +994,8 @@ type GetChannelsByIDsRow struct { Policy1MessageFlags sql.NullInt16 Policy1ChannelFlags sql.NullInt16 Policy1Signature []byte + Policy1BlockHeight sql.NullInt64 + Policy1DisableFlags sql.NullInt16 Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 Policy2Version sql.NullInt16 @@ -981,6 +1011,8 @@ type GetChannelsByIDsRow struct { Policy2MessageFlags sql.NullInt16 Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte + Policy2BlockHeight sql.NullInt64 + Policy2DisableFlags sql.NullInt16 } func (q *Queries) GetChannelsByIDs(ctx context.Context, ids []int64) ([]GetChannelsByIDsRow, error) { @@ -1038,6 +1070,8 @@ func (q *Queries) GetChannelsByIDs(ctx context.Context, ids []int64) ([]GetChann &i.Policy1MessageFlags, &i.Policy1ChannelFlags, &i.Policy1Signature, + &i.Policy1BlockHeight, + &i.Policy1DisableFlags, &i.Policy2ID, &i.Policy2NodeID, &i.Policy2Version, @@ -1053,6 +1087,8 @@ func (q *Queries) GetChannelsByIDs(ctx context.Context, ids []int64) ([]GetChann &i.Policy2MessageFlags, &i.Policy2ChannelFlags, &i.Policy2Signature, + &i.Policy2BlockHeight, + &i.Policy2DisableFlags, ); err != nil { return nil, err } @@ -1159,6 +1195,8 @@ SELECT cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Policy 2 (node_id_2) cp2.id AS policy2_id, @@ -1175,7 +1213,9 @@ SELECT cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy2_message_flags, cp2.channel_flags AS policy2_channel_flags, - cp2.signature AS policy2_signature + cp2.signature AS policy2_signature, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -1244,6 +1284,8 @@ type GetChannelsByPolicyLastUpdateRangeRow struct { Policy1MessageFlags sql.NullInt16 Policy1ChannelFlags sql.NullInt16 Policy1Signature []byte + Policy1BlockHeight sql.NullInt64 + Policy1DisableFlags sql.NullInt16 Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 Policy2Version sql.NullInt16 @@ -1259,6 +1301,8 @@ type GetChannelsByPolicyLastUpdateRangeRow struct { Policy2MessageFlags sql.NullInt16 Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte + Policy2BlockHeight sql.NullInt64 + Policy2DisableFlags sql.NullInt16 } func (q *Queries) GetChannelsByPolicyLastUpdateRange(ctx context.Context, arg GetChannelsByPolicyLastUpdateRangeParams) ([]GetChannelsByPolicyLastUpdateRangeRow, error) { @@ -1325,6 +1369,8 @@ func (q *Queries) GetChannelsByPolicyLastUpdateRange(ctx context.Context, arg Ge &i.Policy1MessageFlags, &i.Policy1ChannelFlags, &i.Policy1Signature, + &i.Policy1BlockHeight, + &i.Policy1DisableFlags, &i.Policy2ID, &i.Policy2NodeID, &i.Policy2Version, @@ -1340,6 +1386,8 @@ func (q *Queries) GetChannelsByPolicyLastUpdateRange(ctx context.Context, arg Ge &i.Policy2MessageFlags, &i.Policy2ChannelFlags, &i.Policy2Signature, + &i.Policy2BlockHeight, + &i.Policy2DisableFlags, ); err != nil { return nil, err } @@ -1440,6 +1488,8 @@ SELECT cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Policy 2 cp2.id AS policy2_id, @@ -1456,7 +1506,9 @@ SELECT cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy_2_message_flags, cp2.channel_flags AS policy_2_channel_flags, - cp2.signature AS policy2_signature + cp2.signature AS policy2_signature, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -1494,6 +1546,8 @@ type GetChannelsBySCIDWithPoliciesRow struct { Policy1MessageFlags sql.NullInt16 Policy1ChannelFlags sql.NullInt16 Policy1Signature []byte + Policy1BlockHeight sql.NullInt64 + Policy1DisableFlags sql.NullInt16 Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 Policy2Version sql.NullInt16 @@ -1509,6 +1563,8 @@ type GetChannelsBySCIDWithPoliciesRow struct { Policy2MessageFlags sql.NullInt16 Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte + Policy2BlockHeight sql.NullInt64 + Policy2DisableFlags sql.NullInt16 } func (q *Queries) GetChannelsBySCIDWithPolicies(ctx context.Context, arg GetChannelsBySCIDWithPoliciesParams) ([]GetChannelsBySCIDWithPoliciesRow, error) { @@ -1579,6 +1635,8 @@ func (q *Queries) GetChannelsBySCIDWithPolicies(ctx context.Context, arg GetChan &i.Policy1MessageFlags, &i.Policy1ChannelFlags, &i.Policy1Signature, + &i.Policy1BlockHeight, + &i.Policy1DisableFlags, &i.Policy2ID, &i.Policy2NodeID, &i.Policy2Version, @@ -1594,6 +1652,8 @@ func (q *Queries) GetChannelsBySCIDWithPolicies(ctx context.Context, arg GetChan &i.Policy2MessageFlags, &i.Policy2ChannelFlags, &i.Policy2Signature, + &i.Policy2BlockHeight, + &i.Policy2DisableFlags, ); err != nil { return nil, err } @@ -2826,6 +2886,8 @@ SELECT c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Policy 2 cp2.id AS policy2_id, @@ -2842,7 +2904,9 @@ SELECT c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy2_message_flags, cp2.channel_flags AS policy2_channel_flags, - cp2.signature AS policy2_signature + cp2.signature AS policy2_signature, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -2879,6 +2943,8 @@ type ListChannelsByNodeIDRow struct { Policy1MessageFlags sql.NullInt16 Policy1ChannelFlags sql.NullInt16 Policy1Signature []byte + Policy1BlockHeight sql.NullInt64 + Policy1DisableFlags sql.NullInt16 Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 Policy2Version sql.NullInt16 @@ -2894,6 +2960,8 @@ type ListChannelsByNodeIDRow struct { Policy2MessageFlags sql.NullInt16 Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte + Policy2BlockHeight sql.NullInt64 + Policy2DisableFlags sql.NullInt16 } func (q *Queries) ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNodeIDParams) ([]ListChannelsByNodeIDRow, error) { @@ -2939,6 +3007,8 @@ func (q *Queries) ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNo &i.Policy1MessageFlags, &i.Policy1ChannelFlags, &i.Policy1Signature, + &i.Policy1BlockHeight, + &i.Policy1DisableFlags, &i.Policy2ID, &i.Policy2NodeID, &i.Policy2Version, @@ -2954,6 +3024,8 @@ func (q *Queries) ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNo &i.Policy2MessageFlags, &i.Policy2ChannelFlags, &i.Policy2Signature, + &i.Policy2BlockHeight, + &i.Policy2DisableFlags, ); err != nil { return nil, err } @@ -2992,6 +3064,8 @@ SELECT c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Policy 2 cp2.id AS policy2_id, @@ -3008,7 +3082,9 @@ SELECT c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy2_message_flags, cp2.channel_flags AS policy2_channel_flags, - cp2.signature AS policy2_signature + cp2.signature AS policy2_signature, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -3047,6 +3123,8 @@ type ListChannelsForNodeIDsRow struct { Policy1MessageFlags sql.NullInt16 Policy1ChannelFlags sql.NullInt16 Policy1Signature []byte + Policy1BlockHeight sql.NullInt64 + Policy1DisableFlags sql.NullInt16 Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 Policy2Version sql.NullInt16 @@ -3062,6 +3140,8 @@ type ListChannelsForNodeIDsRow struct { Policy2MessageFlags sql.NullInt16 Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte + Policy2BlockHeight sql.NullInt64 + Policy2DisableFlags sql.NullInt16 } func (q *Queries) ListChannelsForNodeIDs(ctx context.Context, arg ListChannelsForNodeIDsParams) ([]ListChannelsForNodeIDsRow, error) { @@ -3126,6 +3206,8 @@ func (q *Queries) ListChannelsForNodeIDs(ctx context.Context, arg ListChannelsFo &i.Policy1MessageFlags, &i.Policy1ChannelFlags, &i.Policy1Signature, + &i.Policy1BlockHeight, + &i.Policy1DisableFlags, &i.Policy2ID, &i.Policy2NodeID, &i.Policy2Version, @@ -3141,6 +3223,8 @@ func (q *Queries) ListChannelsForNodeIDs(ctx context.Context, arg ListChannelsFo &i.Policy2MessageFlags, &i.Policy2ChannelFlags, &i.Policy2Signature, + &i.Policy2BlockHeight, + &i.Policy2DisableFlags, ); err != nil { return nil, err } @@ -3225,6 +3309,8 @@ SELECT cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Node 2 policy cp2.timelock AS policy_2_timelock, @@ -3236,7 +3322,9 @@ SELECT cp2.inbound_base_fee_msat AS policy2_inbound_base_fee_msat, cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy2_message_flags, - cp2.channel_flags AS policy2_channel_flags + cp2.channel_flags AS policy2_channel_flags, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -3272,6 +3360,8 @@ type ListChannelsWithPoliciesForCachePaginatedRow struct { Policy1InboundFeeRateMilliMsat sql.NullInt64 Policy1MessageFlags sql.NullInt16 Policy1ChannelFlags sql.NullInt16 + Policy1BlockHeight sql.NullInt64 + Policy1DisableFlags sql.NullInt16 Policy2Timelock sql.NullInt32 Policy2FeePpm sql.NullInt64 Policy2BaseFeeMsat sql.NullInt64 @@ -3282,6 +3372,8 @@ type ListChannelsWithPoliciesForCachePaginatedRow struct { Policy2InboundFeeRateMilliMsat sql.NullInt64 Policy2MessageFlags sql.NullInt16 Policy2ChannelFlags sql.NullInt16 + Policy2BlockHeight sql.NullInt64 + Policy2DisableFlags sql.NullInt16 } func (q *Queries) ListChannelsWithPoliciesForCachePaginated(ctx context.Context, arg ListChannelsWithPoliciesForCachePaginatedParams) ([]ListChannelsWithPoliciesForCachePaginatedRow, error) { @@ -3309,6 +3401,8 @@ func (q *Queries) ListChannelsWithPoliciesForCachePaginated(ctx context.Context, &i.Policy1InboundFeeRateMilliMsat, &i.Policy1MessageFlags, &i.Policy1ChannelFlags, + &i.Policy1BlockHeight, + &i.Policy1DisableFlags, &i.Policy2Timelock, &i.Policy2FeePpm, &i.Policy2BaseFeeMsat, @@ -3319,6 +3413,8 @@ func (q *Queries) ListChannelsWithPoliciesForCachePaginated(ctx context.Context, &i.Policy2InboundFeeRateMilliMsat, &i.Policy2MessageFlags, &i.Policy2ChannelFlags, + &i.Policy2BlockHeight, + &i.Policy2DisableFlags, ); err != nil { return nil, err } @@ -3356,6 +3452,8 @@ SELECT cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, cp1.signature AS policy_1_signature, -- Node 2 policy @@ -3373,7 +3471,9 @@ SELECT cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy2_message_flags, cp2.channel_flags AS policy2_channel_flags, - cp2.signature AS policy_2_signature + cp2.signature AS policy_2_signature, + cp2.block_height AS policy_2_block_height, + cp2.disable_flags AS policy_2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -3411,6 +3511,8 @@ type ListChannelsWithPoliciesPaginatedRow struct { Policy1InboundFeeRateMilliMsat sql.NullInt64 Policy1MessageFlags sql.NullInt16 Policy1ChannelFlags sql.NullInt16 + Policy1BlockHeight sql.NullInt64 + Policy1DisableFlags sql.NullInt16 Policy1Signature []byte Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 @@ -3427,6 +3529,8 @@ type ListChannelsWithPoliciesPaginatedRow struct { Policy2MessageFlags sql.NullInt16 Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte + Policy2BlockHeight sql.NullInt64 + Policy2DisableFlags sql.NullInt16 } func (q *Queries) ListChannelsWithPoliciesPaginated(ctx context.Context, arg ListChannelsWithPoliciesPaginatedParams) ([]ListChannelsWithPoliciesPaginatedRow, error) { @@ -3471,6 +3575,8 @@ func (q *Queries) ListChannelsWithPoliciesPaginated(ctx context.Context, arg Lis &i.Policy1InboundFeeRateMilliMsat, &i.Policy1MessageFlags, &i.Policy1ChannelFlags, + &i.Policy1BlockHeight, + &i.Policy1DisableFlags, &i.Policy1Signature, &i.Policy2ID, &i.Policy2NodeID, @@ -3487,6 +3593,8 @@ func (q *Queries) ListChannelsWithPoliciesPaginated(ctx context.Context, arg Lis &i.Policy2MessageFlags, &i.Policy2ChannelFlags, &i.Policy2Signature, + &i.Policy2BlockHeight, + &i.Policy2DisableFlags, ); err != nil { return nil, err } @@ -3674,9 +3782,9 @@ INSERT INTO graph_channel_policies ( base_fee_msat, min_htlc_msat, last_update, disabled, max_htlc_msat, inbound_base_fee_msat, inbound_fee_rate_milli_msat, message_flags, channel_flags, - signature + signature, block_height, disable_flags ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 ) ON CONFLICT (channel_id, node_id, version) -- Update the following fields if a conflict occurs on channel_id, @@ -3693,7 +3801,9 @@ ON CONFLICT (channel_id, node_id, version) inbound_fee_rate_milli_msat = EXCLUDED.inbound_fee_rate_milli_msat, message_flags = EXCLUDED.message_flags, channel_flags = EXCLUDED.channel_flags, - signature = EXCLUDED.signature + signature = EXCLUDED.signature, + block_height = EXCLUDED.block_height, + disable_flags = EXCLUDED.disable_flags WHERE EXCLUDED.last_update > graph_channel_policies.last_update RETURNING id ` @@ -3714,6 +3824,8 @@ type UpsertEdgePolicyParams struct { MessageFlags sql.NullInt16 ChannelFlags sql.NullInt16 Signature []byte + BlockHeight sql.NullInt64 + DisableFlags sql.NullInt16 } func (q *Queries) UpsertEdgePolicy(ctx context.Context, arg UpsertEdgePolicyParams) (int64, error) { @@ -3733,6 +3845,8 @@ func (q *Queries) UpsertEdgePolicy(ctx context.Context, arg UpsertEdgePolicyPara arg.MessageFlags, arg.ChannelFlags, arg.Signature, + arg.BlockHeight, + arg.DisableFlags, ) var id int64 err := row.Scan(&id) diff --git a/sqldb/sqlc/queries/graph.sql b/sqldb/sqlc/queries/graph.sql index 0ad71f878..e5c18c39c 100644 --- a/sqldb/sqlc/queries/graph.sql +++ b/sqldb/sqlc/queries/graph.sql @@ -407,6 +407,8 @@ SELECT cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Policy 2 cp2.id AS policy2_id, @@ -423,7 +425,9 @@ SELECT cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy_2_message_flags, cp2.channel_flags AS policy_2_channel_flags, - cp2.signature AS policy2_signature + cp2.signature AS policy2_signature, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -462,6 +466,8 @@ SELECT cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Policy 2 cp2.id AS policy2_id, @@ -478,7 +484,9 @@ SELECT cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy2_message_flags, cp2.channel_flags AS policy2_channel_flags, - cp2.signature AS policy2_signature + cp2.signature AS policy2_signature, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -511,6 +519,8 @@ SELECT cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Policy 2 (node_id_2) cp2.id AS policy2_id, @@ -527,7 +537,9 @@ SELECT cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy2_message_flags, cp2.channel_flags AS policy2_channel_flags, - cp2.signature AS policy2_signature + cp2.signature AS policy2_signature, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -590,6 +602,8 @@ SELECT cp1.message_flags AS policy_1_message_flags, cp1.channel_flags AS policy_1_channel_flags, cp1.signature AS policy_1_signature, + cp1.block_height AS policy_1_block_height, + cp1.disable_flags AS policy_1_disable_flags, -- Node 2 policy cp2.id AS policy_2_id, @@ -606,7 +620,9 @@ SELECT cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy_2_message_flags, cp2.channel_flags AS policy_2_channel_flags, - cp2.signature AS policy_2_signature + cp2.signature AS policy_2_signature, + cp2.block_height AS policy_2_block_height, + cp2.disable_flags AS policy_2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id JOIN graph_nodes n2 ON c.node_id_2 = n2.id @@ -647,6 +663,8 @@ SELECT sqlc.embed(c), cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Policy 2 cp2.id AS policy2_id, @@ -663,7 +681,9 @@ SELECT sqlc.embed(c), cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy2_message_flags, cp2.channel_flags AS policy2_channel_flags, - cp2.signature AS policy2_signature + cp2.signature AS policy2_signature, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -700,6 +720,8 @@ SELECT sqlc.embed(c), cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Policy 2 cp2.id AS policy2_id, @@ -716,7 +738,9 @@ SELECT sqlc.embed(c), cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy2_message_flags, cp2.channel_flags AS policy2_channel_flags, - cp2.signature AS policy2_signature + cp2.signature AS policy2_signature, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -765,6 +789,8 @@ SELECT cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, cp1.signature AS policy_1_signature, -- Node 2 policy @@ -782,7 +808,9 @@ SELECT cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy2_message_flags, cp2.channel_flags AS policy2_channel_flags, - cp2.signature AS policy_2_signature + cp2.signature AS policy_2_signature, + cp2.block_height AS policy_2_block_height, + cp2.disable_flags AS policy_2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -816,6 +844,8 @@ SELECT cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Node 2 policy cp2.timelock AS policy_2_timelock, @@ -827,7 +857,9 @@ SELECT cp2.inbound_base_fee_msat AS policy2_inbound_base_fee_msat, cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy2_message_flags, - cp2.channel_flags AS policy2_channel_flags + cp2.channel_flags AS policy2_channel_flags, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id @@ -900,9 +932,9 @@ INSERT INTO graph_channel_policies ( base_fee_msat, min_htlc_msat, last_update, disabled, max_htlc_msat, inbound_base_fee_msat, inbound_fee_rate_milli_msat, message_flags, channel_flags, - signature + signature, block_height, disable_flags ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 ) ON CONFLICT (channel_id, node_id, version) -- Update the following fields if a conflict occurs on channel_id, @@ -919,7 +951,9 @@ ON CONFLICT (channel_id, node_id, version) inbound_fee_rate_milli_msat = EXCLUDED.inbound_fee_rate_milli_msat, message_flags = EXCLUDED.message_flags, channel_flags = EXCLUDED.channel_flags, - signature = EXCLUDED.signature + signature = EXCLUDED.signature, + block_height = EXCLUDED.block_height, + disable_flags = EXCLUDED.disable_flags WHERE EXCLUDED.last_update > graph_channel_policies.last_update RETURNING id; @@ -952,6 +986,8 @@ SELECT cp1.message_flags AS policy1_message_flags, cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, + cp1.block_height AS policy1_block_height, + cp1.disable_flags AS policy1_disable_flags, -- Policy 2 cp2.id AS policy2_id, @@ -968,7 +1004,9 @@ SELECT cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, cp2.message_flags AS policy_2_message_flags, cp2.channel_flags AS policy_2_channel_flags, - cp2.signature AS policy2_signature + cp2.signature AS policy2_signature, + cp2.block_height AS policy2_block_height, + cp2.disable_flags AS policy2_disable_flags FROM graph_channels c JOIN graph_nodes n1 ON c.node_id_1 = n1.id From 3fe40c6540ed5f5220c68254a062f8c761c931d6 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 3 Feb 2026 11:19:29 +0200 Subject: [PATCH 44/55] graph/db: add v2 fields to ChannelEdgePolicy model Extend ChannelEdgePolicy to support v2 channel updates by adding: - Version field to track gossip protocol version (v1 or v2). - LastBlockHeight for v2's block-height-based timestamps. - SecondPeer flag to indicate which peer announced the policy in v2. - DisableFlags for v2-specific channel disable signaling. - ExtraSignedFields map for v2 extra signed TLV data. Add version-aware methods: - IsNode1() determines if the policy was announced by node_1, handling both v1 (via ChannelFlags direction bit) and v2 (via SecondPeer). - IsDisabled() checks disable status using ChannelFlags for v1 and DisableFlags for v2. - String() provides version-appropriate string representations. The new fields use zero values for v1 compatibility (Version defaults to GossipVersion1, LastBlockHeight to 0, SecondPeer to false). This lays the groundwork for v2 policy support; a subsequent commit will handle reading and writing these fields from/to the database. --- autopilot/prefattach_test.go | 2 ++ discovery/gossiper.go | 1 + graph/builder.go | 1 + graph/builder_test.go | 8 +++++ graph/db/graph_test.go | 6 ++++ graph/db/models/channel_edge_policy.go | 50 +++++++++++++++++++++++--- graph/notifications_test.go | 1 + lnrpc/devrpc/dev_server.go | 1 + lnrpc/invoicesrpc/addinvoice_test.go | 6 ++++ lnwire/channel_update_2.go | 18 ++++++++-- netann/chan_status_manager_test.go | 3 ++ routing/localchans/manager.go | 1 + routing/localchans/manager_test.go | 3 ++ routing/pathfind_test.go | 5 +++ routing/router_test.go | 5 +++ 15 files changed, 105 insertions(+), 6 deletions(-) diff --git a/autopilot/prefattach_test.go b/autopilot/prefattach_test.go index 30ec7ffc6..efddfb81e 100644 --- a/autopilot/prefattach_test.go +++ b/autopilot/prefattach_test.go @@ -512,6 +512,7 @@ func (d *testDBGraph) addRandChannel(node1, node2 *btcec.PublicKey, return nil, nil, err } edgePolicy := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: chanID.ToUint64(), LastUpdate: time.Now(), @@ -528,6 +529,7 @@ func (d *testDBGraph) addRandChannel(node1, node2 *btcec.PublicKey, return nil, nil, err } edgePolicy = &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: chanID.ToUint64(), LastUpdate: time.Now(), diff --git a/discovery/gossiper.go b/discovery/gossiper.go index 88ac2418b..3e72f8a1d 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -3423,6 +3423,7 @@ func (d *AuthenticatedGossiper) handleChanUpdate(ctx context.Context, // signs a different SCID than the database SCID, but since there will // only be a difference if AuthProof == nil, this is fine. update := &models.ChannelEdgePolicy{ + Version: upd.GossipVersion(), SigBytes: upd.Signature.ToSignatureBytes(), ChannelID: chanInfo.ChannelID, LastUpdate: timestamp, diff --git a/graph/builder.go b/graph/builder.go index 1f3309c04..10d0b2904 100644 --- a/graph/builder.go +++ b/graph/builder.go @@ -953,6 +953,7 @@ func (b *Builder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool { } update := &models.ChannelEdgePolicy{ + Version: msg.GossipVersion(), SigBytes: msg.Signature.ToSignatureBytes(), ChannelID: msg.ShortChannelID.ToUint64(), LastUpdate: time.Unix(int64(msg.Timestamp), 0), diff --git a/graph/builder_test.go b/graph/builder_test.go index 25b2d28b1..6929a4d49 100644 --- a/graph/builder_test.go +++ b/graph/builder_test.go @@ -166,6 +166,7 @@ func TestIgnoreChannelEdgePolicyForUnknownChannel(t *testing.T) { require.NoError(t, err) edgePolicy := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: edge.ChannelID, LastUpdate: testTime, @@ -1219,6 +1220,7 @@ func TestIsStaleEdgePolicy(t *testing.T) { // We'll also add two edge policies, one for each direction. edgePolicy := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: edge.ChannelID, LastUpdate: updateTimeStamp, @@ -1233,6 +1235,7 @@ func TestIsStaleEdgePolicy(t *testing.T) { } edgePolicy = &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: edge.ChannelID, LastUpdate: updateTimeStamp, @@ -1557,6 +1560,7 @@ func parseTestGraph(t *testing.T, useCache bool, path string) ( } edgePolicy := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), MessageFlags: lnwire.ChanUpdateMsgFlags( edge.MessageFlags, @@ -1939,7 +1943,9 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, channelFlags |= lnwire.ChanUpdateDisabled } + //nolint:ll edgePolicy := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), MessageFlags: msgFlags, ChannelFlags: channelFlags, @@ -1970,7 +1976,9 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, } channelFlags |= lnwire.ChanUpdateDirection + //nolint:ll edgePolicy := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), MessageFlags: msgFlags, ChannelFlags: channelFlags, diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 5148f63a8..f1acfe911 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -1044,6 +1044,7 @@ func createChannelEdge(node1, node2 *models.Node) (*models.ChannelEdgeInfo, ) edge1 := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: chanID, LastUpdate: nextUpdateTime(), @@ -1058,6 +1059,7 @@ func createChannelEdge(node1, node2 *models.Node) (*models.ChannelEdgeInfo, ExtraOpaqueData: []byte{1, 0}, } edge2 := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: chanID, LastUpdate: nextUpdateTime(), @@ -1442,6 +1444,7 @@ func randEdgePolicy(chanID uint64) *models.ChannelEdgePolicy { func copyEdgePolicy(p *models.ChannelEdgePolicy) *models.ChannelEdgePolicy { return &models.ChannelEdgePolicy{ + Version: p.Version, SigBytes: p.SigBytes, ChannelID: p.ChannelID, LastUpdate: p.LastUpdate, @@ -1459,6 +1462,7 @@ func copyEdgePolicy(p *models.ChannelEdgePolicy) *models.ChannelEdgePolicy { func newEdgePolicy(chanID uint64, updateTime int64) *models.ChannelEdgePolicy { return &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, ChannelID: chanID, LastUpdate: time.Unix(updateTime, 0), MessageFlags: 1, @@ -3454,6 +3458,7 @@ func TestFilterChannelRange(t *testing.T) { updateTime = time.Unix(updateTimeSeed, 0) err = graph.UpdateEdgePolicy( ctx, &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, ToNode: node.PubKeyBytes, ChannelFlags: chanFlags, ChannelID: chanID, @@ -4747,6 +4752,7 @@ func TestLightningNodeSigVerification(t *testing.T) { func TestComputeFee(t *testing.T) { var ( policy = models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, FeeBaseMSat: 10000, FeeProportionalMillionths: 30000, } diff --git a/graph/db/models/channel_edge_policy.go b/graph/db/models/channel_edge_policy.go index a2661ef6f..dd93c21b1 100644 --- a/graph/db/models/channel_edge_policy.go +++ b/graph/db/models/channel_edge_policy.go @@ -14,6 +14,10 @@ import ( // information concerning fees, and minimum time-lock information which is // utilized during path finding. type ChannelEdgePolicy struct { + // Version is the gossip version of the channel update that produced + // this policy. + Version lnwire.GossipVersion + // SigBytes is the raw bytes of the signature of the channel edge // policy. We'll only parse these if the caller needs to access the // signature for validation purposes. @@ -28,6 +32,14 @@ type ChannelEdgePolicy struct { // was received. LastUpdate time.Time + // LastBlockHeight is the block height that timestamps the last update + // for v2 channel updates. + LastBlockHeight uint32 + + // SecondPeer indicates whether this policy was announced by the second + // peer in the channel for v2 channel updates. + SecondPeer bool + // MessageFlags is a bitfield which indicates the presence of optional // fields (like max_htlc) in the policy. MessageFlags lnwire.ChanUpdateMsgFlags @@ -36,6 +48,10 @@ type ChannelEdgePolicy struct { // channel as well as the directed edge this update applies to. ChannelFlags lnwire.ChanUpdateChanFlags + // DisableFlags is a v2-specific bitfield which signals whether the + // channel is disabled for incoming or outgoing traffic. + DisableFlags lnwire.ChanUpdateDisableFlags + // TimeLockDelta is the number of blocks this node will subtract from // the expiry of an incoming HTLC. This value expresses the time buffer // the node would like to HTLC exchanges. @@ -77,11 +93,31 @@ type ChannelEdgePolicy struct { // and ensure we're able to make upgrades to the network in a forwards // compatible manner. ExtraOpaqueData lnwire.ExtraOpaqueData + + // ExtraSignedFields are the extra signed fields found in v2 channel + // updates. + ExtraSignedFields map[uint64][]byte +} + +// IsNode1 returns true if this policy was announced by the channel's node_1. +func (c *ChannelEdgePolicy) IsNode1() bool { + if c.Version == lnwire.GossipVersion1 { + return c.ChannelFlags&lnwire.ChanUpdateDirection == 0 + } + + return !c.SecondPeer } // IsDisabled determines whether the edge has the disabled bit set. +// +// NOTE: for v2 channel updates, we return true here only if both the incoming +// and outgoing disabled bits are set. func (c *ChannelEdgePolicy) IsDisabled() bool { - return c.ChannelFlags.IsDisabled() + if c.Version == lnwire.GossipVersion1 { + return c.ChannelFlags.IsDisabled() + } + + return !c.DisableFlags.IsEnabled() } // ComputeFee computes the fee to forward an HTLC of `amt` milli-satoshis over @@ -95,7 +131,13 @@ func (c *ChannelEdgePolicy) ComputeFee( // String returns a human-readable version of the channel edge policy. func (c *ChannelEdgePolicy) String() string { - return fmt.Sprintf("ChannelID=%v, MessageFlags=%v, ChannelFlags=%v, "+ - "LastUpdate=%v", c.ChannelID, c.MessageFlags, c.ChannelFlags, - c.LastUpdate) + if c.Version == lnwire.GossipVersion1 { + return fmt.Sprintf("ChannelID=%v, MessageFlags=%v, "+ + "ChannelFlags=%v, LastUpdate=%v", c.ChannelID, + c.MessageFlags, c.ChannelFlags, c.LastUpdate) + } + + return fmt.Sprintf("ChannelID=%v, Node1=%v, DisableFlags=%v, "+ + "BlockHeight=%v", c.ChannelID, !c.SecondPeer, + c.DisableFlags, c.LastBlockHeight) } diff --git a/graph/notifications_test.go b/graph/notifications_test.go index b086fc562..20e45026a 100644 --- a/graph/notifications_test.go +++ b/graph/notifications_test.go @@ -113,6 +113,7 @@ func randEdgePolicy(chanID *lnwire.ShortChannelID, } return &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: chanID.ToUint64(), LastUpdate: time.Unix(int64(prand.Int31()), 0), diff --git a/lnrpc/devrpc/dev_server.go b/lnrpc/devrpc/dev_server.go index 7dee1627a..3cecb36b5 100644 --- a/lnrpc/devrpc/dev_server.go +++ b/lnrpc/devrpc/dev_server.go @@ -304,6 +304,7 @@ func (s *Server) ImportGraph(ctx context.Context, makePolicy := func(rpcPolicy *lnrpc.RoutingPolicy) *models.ChannelEdgePolicy { //nolint:ll policy := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, ChannelID: rpcEdge.ChannelId, LastUpdate: time.Unix( int64(rpcPolicy.LastUpdate), 0, diff --git a/lnrpc/invoicesrpc/addinvoice_test.go b/lnrpc/invoicesrpc/addinvoice_test.go index ca83f8bab..104b2873d 100644 --- a/lnrpc/invoicesrpc/addinvoice_test.go +++ b/lnrpc/invoicesrpc/addinvoice_test.go @@ -317,7 +317,9 @@ var shouldIncludeChannelTestCases = []struct { return edge }(), + //nolint:ll &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, FeeBaseMSat: 1000, FeeProportionalMillionths: 20, TimeLockDelta: 13, @@ -364,7 +366,9 @@ var shouldIncludeChannelTestCases = []struct { ).Once().Return( &models.ChannelEdgeInfo{}, &models.ChannelEdgePolicy{}, + //nolint:ll &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, FeeBaseMSat: 1000, FeeProportionalMillionths: 20, TimeLockDelta: 13, @@ -409,7 +413,9 @@ var shouldIncludeChannelTestCases = []struct { ).Once().Return( &models.ChannelEdgeInfo{}, &models.ChannelEdgePolicy{}, + //nolint:ll &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, FeeBaseMSat: 1000, FeeProportionalMillionths: 20, TimeLockDelta: 13, diff --git a/lnwire/channel_update_2.go b/lnwire/channel_update_2.go index 8e82fe9d7..3fba2470d 100644 --- a/lnwire/channel_update_2.go +++ b/lnwire/channel_update_2.go @@ -380,9 +380,23 @@ func (c ChanUpdateDisableFlags) IsEnabled() bool { return c == 0 } -// String returns the bitfield flags as a string. +// String returns a human-readable representation of the disable flags. func (c ChanUpdateDisableFlags) String() string { - return fmt.Sprintf("%08b", c) + if c.IsEnabled() { + return "Enabled" + } + + incoming := c.IncomingDisabled() + outgoing := c.OutgoingDisabled() + + switch { + case incoming && outgoing: + return "Disabled(incoming&outgoing)" + case incoming: + return "Disabled(incoming)" + default: + return "Disabled(outgoing)" + } } // Record returns the tlv record for the disable flags. diff --git a/netann/chan_status_manager_test.go b/netann/chan_status_manager_test.go index 5265d317f..59ce324c6 100644 --- a/netann/chan_status_manager_test.go +++ b/netann/chan_status_manager_test.go @@ -116,12 +116,14 @@ func createEdgePolicies(t *testing.T, channel *channeldb.OpenChannel, return edgeInfo, &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, ChannelID: channel.ShortChanID().ToUint64(), ChannelFlags: dir1, LastUpdate: time.Now(), SigBytes: testSigBytes, }, &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, ChannelID: channel.ShortChanID().ToUint64(), ChannelFlags: dir2, LastUpdate: time.Now(), @@ -222,6 +224,7 @@ func (g *mockGraph) ApplyChannelUpdate(update *lnwire.ChannelUpdate1, timestamp := time.Unix(int64(update.Timestamp), 0) policy := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, ChannelID: update.ShortChannelID.ToUint64(), ChannelFlags: update.ChannelFlags, LastUpdate: timestamp, diff --git a/routing/localchans/manager.go b/routing/localchans/manager.go index cc86d82cc..6dd30b0ae 100644 --- a/routing/localchans/manager.go +++ b/routing/localchans/manager.go @@ -364,6 +364,7 @@ func (r *Manager) createEdge(channel *channeldb.OpenChannel, // be updated with the new values in the call to processChan below. timeLockDelta := uint16(r.DefaultRoutingPolicy.TimeLockDelta) edge := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, ChannelID: shortChanID.ToUint64(), LastUpdate: timestamp, TimeLockDelta: timeLockDelta, diff --git a/routing/localchans/manager_test.go b/routing/localchans/manager_test.go index 6196330bd..c48e61641 100644 --- a/routing/localchans/manager_test.go +++ b/routing/localchans/manager_test.go @@ -64,6 +64,7 @@ func TestManager(t *testing.T) { } currentPolicy := models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, MinHTLC: minHTLC, MessageFlags: lnwire.ChanUpdateRequiredMaxHtlc, } @@ -451,6 +452,7 @@ func TestCreateEdgeLower(t *testing.T) { require.NoError(t, err) expectedEdge := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, ChannelID: 8, LastUpdate: timestamp, TimeLockDelta: 7, @@ -542,6 +544,7 @@ func TestCreateEdgeHigher(t *testing.T) { require.NoError(t, err) expectedEdge := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, ChannelID: 8, LastUpdate: timestamp, TimeLockDelta: 7, diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index ed291a6aa..01f5d9849 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -389,6 +389,7 @@ func parseTestGraph(t *testing.T, useCache bool, path string) ( } edgePolicy := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), MessageFlags: lnwire.ChanUpdateMsgFlags(edge.MessageFlags), ChannelFlags: channelFlags, @@ -740,7 +741,9 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, channelFlags |= lnwire.ChanUpdateDisabled } + //nolint:ll edgePolicy := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), MessageFlags: msgFlags, ChannelFlags: channelFlags, @@ -772,7 +775,9 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, } channelFlags |= lnwire.ChanUpdateDirection + //nolint:ll edgePolicy := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), MessageFlags: msgFlags, ChannelFlags: channelFlags, diff --git a/routing/router_test.go b/routing/router_test.go index 6f9d2c3f3..12c4adb8f 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -2751,6 +2751,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { // We must add the edge policy to be able to use the edge for route // finding. edgePolicy := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: edge.ChannelID, LastUpdate: testTime, @@ -2766,6 +2767,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { // Create edge in the other direction as well. edgePolicy = &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: edge.ChannelID, LastUpdate: testTime, @@ -2832,6 +2834,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { require.NoError(t, ctx.graph.AddChannelEdge(ctxb, edge)) edgePolicy = &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: edge.ChannelID, LastUpdate: testTime, @@ -2846,6 +2849,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { require.NoError(t, ctx.graph.UpdateEdgePolicy(ctxb, edgePolicy)) edgePolicy = &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: edge.ChannelID, LastUpdate: testTime, @@ -2965,6 +2969,7 @@ func (m *mockGraphBuilder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool { } err := m.updateEdge(&models.ChannelEdgePolicy{ + Version: msg.GossipVersion(), SigBytes: msg.Signature.ToSignatureBytes(), ChannelID: msg.ShortChannelID.ToUint64(), LastUpdate: time.Unix(int64(msg.Timestamp), 0), From 9c7a12c0e05a7323a38ef1cd932f66647a033363 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 3 Feb 2026 11:45:27 +0200 Subject: [PATCH 45/55] graph/db: refactor CachedEdgePolicy to use explicit boolean fields Replace MessageFlags and ChannelFlags bitfields in CachedEdgePolicy with explicit boolean fields to improve clarity and support both v1 and v2 channel updates: - Replace MessageFlags with HasMaxHTLC boolean. - Replace ChannelFlags with IsNode1 and IsDisabled booleans. - Update NewCachedPolicy to extract these fields version-appropriately: - For v1: derive from MessageFlags and ChannelFlags bits. - For v2: derive from policy.SecondPeer and policy.DisableFlags. Update all call sites that used method calls IsNode1() and IsDisabled() to instead access the fields directly. This includes: - graph_cache.go: policy direction and disable checks - unified_edges.go: HasMaxHTLC and IsDisabled checks - Tests: policy construction and assertions This refactoring improves readability by making the cached policy's state explicit rather than encoded in bitfields, and enables seamless support for both gossip protocol versions. --- graph/db/graph_cache.go | 12 +++---- graph/db/graph_cache_test.go | 33 ++++++++++-------- graph/db/models/cached_edge_policy.go | 49 ++++++++++++++++----------- routing/unified_edges.go | 6 ++-- routing/unified_edges_test.go | 4 +-- 5 files changed, 59 insertions(+), 45 deletions(-) diff --git a/graph/db/graph_cache.go b/graph/db/graph_cache.go index 4a3a3b0f9..7cf101fac 100644 --- a/graph/db/graph_cache.go +++ b/graph/db/graph_cache.go @@ -142,8 +142,8 @@ func (c *GraphCache) AddChannel(info *models.CachedEdgeInfo, // Skip adding policies if both are disabled, as the channel is // currently unusable for routing. However, we still add the channel // structure above so that policy updates can later enable it. - if policy1 != nil && policy1.IsDisabled() && - policy2 != nil && policy2.IsDisabled() { + if policy1 != nil && policy1.IsDisabled && + policy2 != nil && policy2.IsDisabled { log.Debugf("Skipping policies for channel %v: both "+ "policies are disabled (channel structure still "+ @@ -156,14 +156,14 @@ func (c *GraphCache) AddChannel(info *models.CachedEdgeInfo, // of node 2 then we have the policy 1 as seen from node 1. if policy1 != nil { fromNode, toNode := info.NodeKey1Bytes, info.NodeKey2Bytes - if !policy1.IsNode1() { + if !policy1.IsNode1 { fromNode, toNode = toNode, fromNode } c.UpdatePolicy(policy1, fromNode, toNode) } if policy2 != nil { fromNode, toNode := info.NodeKey2Bytes, info.NodeKey1Bytes - if policy2.IsNode1() { + if policy2.IsNode1 { fromNode, toNode = toNode, fromNode } c.UpdatePolicy(policy2, fromNode, toNode) @@ -210,7 +210,7 @@ func (c *GraphCache) UpdatePolicy(policy *models.CachedEdgePolicy, fromNode, switch { // This is node 1, and it is edge 1, so this is the outgoing // policy for node 1. - case channel.IsNode1 && policy.IsNode1(): + case channel.IsNode1 && policy.IsNode1: channel.OutPolicySet = true policy.InboundFee.WhenSome(func(fee lnwire.Fee) { channel.InboundFee = fee @@ -218,7 +218,7 @@ func (c *GraphCache) UpdatePolicy(policy *models.CachedEdgePolicy, fromNode, // This is node 2, and it is edge 2, so this is the outgoing // policy for node 2. - case !channel.IsNode1 && !policy.IsNode1(): + case !channel.IsNode1 && !policy.IsNode1: channel.OutPolicySet = true policy.InboundFee.WhenSome(func(fee lnwire.Fee) { channel.InboundFee = fee diff --git a/graph/db/graph_cache_test.go b/graph/db/graph_cache_test.go index 89e3a7e87..3d5fba85d 100644 --- a/graph/db/graph_cache_test.go +++ b/graph/db/graph_cache_test.go @@ -33,9 +33,9 @@ func TestGraphCacheAddNode(t *testing.T) { runTest := func(nodeA, nodeB route.Vertex) { t.Helper() - channelFlagA, channelFlagB := 0, 1 + isNode1A, isNode1B := true, false if nodeA == pubKey2 { - channelFlagA, channelFlagB = 1, 0 + isNode1A, isNode1B = false, true } inboundFee := lnwire.Fee{ @@ -44,8 +44,9 @@ func TestGraphCacheAddNode(t *testing.T) { } outPolicy1 := &models.CachedEdgePolicy{ - ChannelID: 1000, - ChannelFlags: lnwire.ChanUpdateChanFlags(channelFlagA), + ChannelID: 1000, + IsNode1: isNode1A, + IsDisabled: false, ToNodePubKey: func() route.Vertex { return nodeB }, @@ -53,8 +54,9 @@ func TestGraphCacheAddNode(t *testing.T) { InboundFee: fn.Some(inboundFee), } inPolicy1 := &models.CachedEdgePolicy{ - ChannelID: 1000, - ChannelFlags: lnwire.ChanUpdateChanFlags(channelFlagB), + ChannelID: 1000, + IsNode1: isNode1B, + IsDisabled: false, ToNodePubKey: func() route.Vertex { return nodeA }, @@ -125,8 +127,9 @@ func assertCachedPolicyEqual(t *testing.T, original, cached *models.CachedEdgePolicy) { require.Equal(t, original.ChannelID, cached.ChannelID) - require.Equal(t, original.MessageFlags, cached.MessageFlags) - require.Equal(t, original.ChannelFlags, cached.ChannelFlags) + require.Equal(t, original.HasMaxHTLC, cached.HasMaxHTLC) + require.Equal(t, original.IsNode1, cached.IsNode1) + require.Equal(t, original.IsDisabled, cached.IsDisabled) require.Equal(t, original.TimeLockDelta, cached.TimeLockDelta) require.Equal(t, original.MinHTLC, cached.MinHTLC) require.Equal(t, original.MaxHTLC, cached.MaxHTLC) @@ -171,13 +174,14 @@ func TestGraphCacheDisabledPoliciesRegression(t *testing.T) { // Create two disabled policies. disabledPolicy1 := &models.CachedEdgePolicy{ - ChannelID: chanID, - ChannelFlags: lnwire.ChanUpdateDisabled, + ChannelID: chanID, + IsNode1: true, + IsDisabled: true, } disabledPolicy2 := &models.CachedEdgePolicy{ - ChannelID: chanID, - ChannelFlags: lnwire.ChanUpdateDisabled | - lnwire.ChanUpdateDirection, + ChannelID: chanID, + IsNode1: false, + IsDisabled: true, } // Add the channel with both policies disabled (simulating @@ -207,7 +211,8 @@ func TestGraphCacheDisabledPoliciesRegression(t *testing.T) { // Now simulate receiving a fresh update enabling one direction. enabledPolicy1 := &models.CachedEdgePolicy{ ChannelID: chanID, - ChannelFlags: 0, // NOT disabled anymore + IsNode1: true, + IsDisabled: false, TimeLockDelta: 40, MinHTLC: lnwire.MilliSatoshi(1000), } diff --git a/graph/db/models/cached_edge_policy.go b/graph/db/models/cached_edge_policy.go index 40b0d9212..90c8d56c3 100644 --- a/graph/db/models/cached_edge_policy.go +++ b/graph/db/models/cached_edge_policy.go @@ -20,13 +20,15 @@ type CachedEdgePolicy struct { // and the last 2 bytes are the output index for the channel. ChannelID uint64 - // MessageFlags is a bitfield which indicates the presence of optional - // fields (like max_htlc) in the policy. - MessageFlags lnwire.ChanUpdateMsgFlags + // HasMaxHTLC indicates whether the policy has a max HTLC value. + HasMaxHTLC bool - // ChannelFlags is a bitfield which signals the capabilities of the - // channel as well as the directed edge this update applies to. - ChannelFlags lnwire.ChanUpdateChanFlags + // IsNode1 indicates whether this policy was announced by the channel's + // node_1. + IsNode1 bool + + // IsDisabled indicates whether the policy disables forwarding. + IsDisabled bool // TimeLockDelta is the number of blocks this node will subtract from // the expiry of an incoming HTLC. This value expresses the time buffer @@ -75,24 +77,31 @@ func (c *CachedEdgePolicy) ComputeFee( return c.FeeBaseMSat + (amt*c.FeeProportionalMillionths)/feeRateParts } -// IsDisabled returns true if the channel is disabled in the direction from the -// advertising node. -func (c *CachedEdgePolicy) IsDisabled() bool { - return c.ChannelFlags&lnwire.ChanUpdateDisabled != 0 -} - -// IsNode1 returns true if this policy was announced by the channel's node_1 -// node. -func (c *CachedEdgePolicy) IsNode1() bool { - return c.ChannelFlags&lnwire.ChanUpdateDirection == 0 -} - // NewCachedPolicy turns a full policy into a minimal one that can be cached. func NewCachedPolicy(policy *ChannelEdgePolicy) *CachedEdgePolicy { + if policy.Version != lnwire.GossipVersion2 { + return &CachedEdgePolicy{ + ChannelID: policy.ChannelID, + HasMaxHTLC: policy.MessageFlags.HasMaxHtlc(), + IsDisabled: policy.ChannelFlags& + lnwire.ChanUpdateDisabled != 0, + IsNode1: policy.ChannelFlags& + lnwire.ChanUpdateDirection == 0, + TimeLockDelta: policy.TimeLockDelta, + MinHTLC: policy.MinHTLC, + MaxHTLC: policy.MaxHTLC, + FeeBaseMSat: policy.FeeBaseMSat, + FeeProportionalMillionths: policy. + FeeProportionalMillionths, + InboundFee: policy.InboundFee, + } + } + return &CachedEdgePolicy{ ChannelID: policy.ChannelID, - MessageFlags: policy.MessageFlags, - ChannelFlags: policy.ChannelFlags, + HasMaxHTLC: true, + IsNode1: !policy.SecondPeer, + IsDisabled: !policy.DisableFlags.IsEnabled(), TimeLockDelta: policy.TimeLockDelta, MinHTLC: policy.MinHTLC, MaxHTLC: policy.MaxHTLC, diff --git a/routing/unified_edges.go b/routing/unified_edges.go index 9b8f6c5c0..fda06aabf 100644 --- a/routing/unified_edges.go +++ b/routing/unified_edges.go @@ -188,7 +188,7 @@ func (u *unifiedEdge) amtInRange(amt lnwire.MilliSatoshi) bool { } // Skip channels for which this htlc is too large. - if u.policy.MessageFlags.HasMaxHtlc() && + if u.policy.HasMaxHTLC && amt > u.policy.MaxHTLC { log.Tracef("Exceeds policy's MaxHTLC: amt=%v, MaxHTLC=%v", @@ -376,7 +376,7 @@ func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi, } // For network channels, skip the disabled ones. - if edge.policy.IsDisabled() { + if edge.policy.IsDisabled { log.Debugf("Skipped edge %v due to it being disabled", edge.policy.ChannelID) continue @@ -385,7 +385,7 @@ func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi, // Track the maximal capacity for usable channels. If we don't // know the capacity, we fall back to MaxHTLC. capMsat := lnwire.NewMSatFromSatoshis(edge.capacity) - if capMsat == 0 && edge.policy.MessageFlags.HasMaxHtlc() { + if capMsat == 0 && edge.policy.HasMaxHTLC { log.Tracef("No capacity available for channel %v, "+ "using MaxHtlcMsat (%v) as a fallback.", edge.policy.ChannelID, edge.policy.MaxHTLC) diff --git a/routing/unified_edges_test.go b/routing/unified_edges_test.go index 8fc79031a..25c8e9220 100644 --- a/routing/unified_edges_test.go +++ b/routing/unified_edges_test.go @@ -30,7 +30,7 @@ func TestNodeEdgeUnifier(t *testing.T) { FeeProportionalMillionths: 100000, FeeBaseMSat: 30, TimeLockDelta: 60, - MessageFlags: lnwire.ChanUpdateRequiredMaxHtlc, + HasMaxHTLC: true, MaxHTLC: 5000, MinHTLC: 100, } @@ -39,7 +39,7 @@ func TestNodeEdgeUnifier(t *testing.T) { FeeProportionalMillionths: 190000, FeeBaseMSat: 10, TimeLockDelta: 40, - MessageFlags: lnwire.ChanUpdateRequiredMaxHtlc, + HasMaxHTLC: true, MaxHTLC: 4000, MinHTLC: 100, } From a73c45946ba5ad2071c59b8e1611e1ebdbe62b86 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 3 Feb 2026 12:14:59 +0200 Subject: [PATCH 46/55] multi: add ChanEdgePolicyFromWire constructor for ChannelEdgePolicy Add ChanEdgePolicyFromWire to construct ChannelEdgePolicy from channel update messages, centralizing v1/v2 field mapping. Update call sites to use the helper: - discovery/gossiper: handleChanUpdate - graph/builder: ApplyChannelUpdate - routing/router_test: ApplyChannelUpdate test helper This consolidates update-to-policy conversion logic across versions. --- discovery/gossiper.go | 20 ++++------- graph/builder.go | 20 ++++------- graph/db/models/channel_edge_policy.go | 50 ++++++++++++++++++++++++++ routing/router_test.go | 21 ++++------- 4 files changed, 69 insertions(+), 42 deletions(-) diff --git a/discovery/gossiper.go b/discovery/gossiper.go index 3e72f8a1d..d2375cb1d 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -3422,20 +3422,12 @@ func (d *AuthenticatedGossiper) handleChanUpdate(ctx context.Context, // different alias. This might mean that SigBytes is incorrect as it // signs a different SCID than the database SCID, but since there will // only be a difference if AuthProof == nil, this is fine. - update := &models.ChannelEdgePolicy{ - Version: upd.GossipVersion(), - SigBytes: upd.Signature.ToSignatureBytes(), - ChannelID: chanInfo.ChannelID, - LastUpdate: timestamp, - MessageFlags: upd.MessageFlags, - ChannelFlags: upd.ChannelFlags, - TimeLockDelta: upd.TimeLockDelta, - MinHTLC: upd.HtlcMinimumMsat, - MaxHTLC: upd.HtlcMaximumMsat, - FeeBaseMSat: lnwire.MilliSatoshi(upd.BaseFee), - FeeProportionalMillionths: lnwire.MilliSatoshi(upd.FeeRate), - InboundFee: upd.InboundFee.ValOpt(), - ExtraOpaqueData: upd.ExtraOpaqueData, + update, err := models.ChanEdgePolicyFromWire( + chanInfo.ChannelID, upd, + ) + if err != nil { + nMsg.err <- err + return nil, false } if err := d.cfg.Graph.UpdateEdge(ctx, update, ops...); err != nil { diff --git a/graph/builder.go b/graph/builder.go index 10d0b2904..fa8b0b4a9 100644 --- a/graph/builder.go +++ b/graph/builder.go @@ -952,20 +952,12 @@ func (b *Builder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool { return false } - update := &models.ChannelEdgePolicy{ - Version: msg.GossipVersion(), - SigBytes: msg.Signature.ToSignatureBytes(), - ChannelID: msg.ShortChannelID.ToUint64(), - LastUpdate: time.Unix(int64(msg.Timestamp), 0), - MessageFlags: msg.MessageFlags, - ChannelFlags: msg.ChannelFlags, - TimeLockDelta: msg.TimeLockDelta, - MinHTLC: msg.HtlcMinimumMsat, - MaxHTLC: msg.HtlcMaximumMsat, - FeeBaseMSat: lnwire.MilliSatoshi(msg.BaseFee), - FeeProportionalMillionths: lnwire.MilliSatoshi(msg.FeeRate), - InboundFee: msg.InboundFee.ValOpt(), - ExtraOpaqueData: msg.ExtraOpaqueData, + update, err := models.ChanEdgePolicyFromWire( + msg.ShortChannelID.ToUint64(), msg, + ) + if err != nil { + log.Errorf("Unable to parse channel update: %v", err) + return false } err = b.UpdateEdge(ctx, update) diff --git a/graph/db/models/channel_edge_policy.go b/graph/db/models/channel_edge_policy.go index dd93c21b1..067c7861a 100644 --- a/graph/db/models/channel_edge_policy.go +++ b/graph/db/models/channel_edge_policy.go @@ -99,6 +99,56 @@ type ChannelEdgePolicy struct { ExtraSignedFields map[uint64][]byte } +// ChanEdgePolicyFromWire constructs a ChannelEdgePolicy from a channel update +// message. +func ChanEdgePolicyFromWire(scid uint64, + update lnwire.ChannelUpdate) (*ChannelEdgePolicy, error) { + + switch upd := update.(type) { + case *lnwire.ChannelUpdate1: + //nolint:ll + return &ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, + SigBytes: upd.Signature.ToSignatureBytes(), + ChannelID: scid, + LastUpdate: time.Unix(int64(upd.Timestamp), 0), + MessageFlags: upd.MessageFlags, + ChannelFlags: upd.ChannelFlags, + TimeLockDelta: upd.TimeLockDelta, + MinHTLC: upd.HtlcMinimumMsat, + MaxHTLC: upd.HtlcMaximumMsat, + FeeBaseMSat: lnwire.MilliSatoshi(upd.BaseFee), + FeeProportionalMillionths: lnwire.MilliSatoshi(upd.FeeRate), + InboundFee: upd.InboundFee.ValOpt(), + ExtraOpaqueData: upd.ExtraOpaqueData, + }, nil + + case *lnwire.ChannelUpdate2: + return &ChannelEdgePolicy{ + Version: lnwire.GossipVersion2, + SigBytes: upd.Signature.Val.ToSignatureBytes(), + ChannelID: scid, + LastBlockHeight: upd.BlockHeight.Val, + SecondPeer: upd.SecondPeer.IsSome(), + DisableFlags: upd.DisabledFlags.Val, + TimeLockDelta: upd.CLTVExpiryDelta.Val, + MinHTLC: upd.HTLCMinimumMsat.Val, + MaxHTLC: upd.HTLCMaximumMsat.Val, + FeeBaseMSat: lnwire.MilliSatoshi( + upd.FeeBaseMsat.Val, + ), + FeeProportionalMillionths: lnwire.MilliSatoshi( + upd.FeeProportionalMillionths.Val, + ), + InboundFee: upd.InboundFee.ValOpt(), + ExtraSignedFields: upd.ExtraSignedFields, + }, nil + } + + return nil, fmt.Errorf("unknown channel update version: %v", + update.MsgType()) +} + // IsNode1 returns true if this policy was announced by the channel's node_1. func (c *ChannelEdgePolicy) IsNode1() bool { if c.Version == lnwire.GossipVersion1 { diff --git a/routing/router_test.go b/routing/router_test.go index 12c4adb8f..115c02c2c 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -2968,20 +2968,13 @@ func (m *mockGraphBuilder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool { return false } - err := m.updateEdge(&models.ChannelEdgePolicy{ - Version: msg.GossipVersion(), - SigBytes: msg.Signature.ToSignatureBytes(), - ChannelID: msg.ShortChannelID.ToUint64(), - LastUpdate: time.Unix(int64(msg.Timestamp), 0), - MessageFlags: msg.MessageFlags, - ChannelFlags: msg.ChannelFlags, - TimeLockDelta: msg.TimeLockDelta, - MinHTLC: msg.HtlcMinimumMsat, - MaxHTLC: msg.HtlcMaximumMsat, - FeeBaseMSat: lnwire.MilliSatoshi(msg.BaseFee), - FeeProportionalMillionths: lnwire.MilliSatoshi(msg.FeeRate), - ExtraOpaqueData: msg.ExtraOpaqueData, - }) + update, err := models.ChanEdgePolicyFromWire( + msg.ShortChannelID.ToUint64(), msg, + ) + if err != nil { + return false + } + err = m.updateEdge(update) return err == nil } From bd215b1a7d2f54f521658059a55b572153c577fb Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Fri, 6 Feb 2026 16:25:14 +0200 Subject: [PATCH 47/55] graph/db: set policy version for cache load Ensure policies loaded for graph cache reconstruction are tagged as v1. --- graph/db/sql_store.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 9a62f2f5d..bcb8245be 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -4822,6 +4822,7 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, case sqlc.ListChannelsWithPoliciesForCachePaginatedRow: if r.Policy1Timelock.Valid { policy1 = &sqlc.GraphChannelPolicy{ + Version: int16(lnwire.GossipVersion1), Timelock: r.Policy1Timelock.Int32, FeePpm: r.Policy1FeePpm.Int64, BaseFeeMsat: r.Policy1BaseFeeMsat.Int64, @@ -4838,6 +4839,7 @@ func extractChannelPolicies(row any) (*sqlc.GraphChannelPolicy, } if r.Policy2Timelock.Valid { policy2 = &sqlc.GraphChannelPolicy{ + Version: int16(lnwire.GossipVersion1), Timelock: r.Policy2Timelock.Int32, FeePpm: r.Policy2FeePpm.Int64, BaseFeeMsat: r.Policy2BaseFeeMsat.Int64, From 40e3ac964f08d2e6644071ba97308165c5a0639e Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 3 Feb 2026 12:33:25 +0200 Subject: [PATCH 48/55] graph/db: update policy builders to support v2 fields Update buildChanPolicy and related functions in both KV and SQL stores to properly construct ChannelEdgePolicy with version-specific fields: KVStore changes: - Reject non-v1 policies in updateEdgePolicy and serializeChanEdgePolicy since KV store only supports v1 gossip protocol. - Set Version to GossipVersion1 when deserializing policies from KV. SQLStore changes: - Add isNode1 parameter to buildChanPolicy functions to properly set SecondPeer field (v2 uses SecondPeer instead of ChannelFlags direction). - Extract Version from database and populate version-specific fields: - For v1: MessageFlags, ChannelFlags, LastUpdate, ExtraOpaqueData - For v2: DisableFlags, LastBlockHeight, ExtraSignedFields - Thread isNode1 through buildChanPolicyWithBatchData and buildCachedChanPolicies call sites. This enables the SQL store to read and reconstruct both v1 and v2 channel policies from the database with proper field mapping. --- graph/db/graph_test.go | 155 +++++++++++++++++++++++---------------- graph/db/kv_store.go | 11 ++- graph/db/sql_store.go | 160 ++++++++++++++++++++++++++--------------- 3 files changed, 204 insertions(+), 122 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index f1acfe911..9b923f662 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -1048,6 +1048,7 @@ func createChannelEdge(node1, node2 *models.Node) (*models.ChannelEdgeInfo, SigBytes: testSig.Serialize(), ChannelID: chanID, LastUpdate: nextUpdateTime(), + SecondPeer: false, MessageFlags: 1, ChannelFlags: 0, TimeLockDelta: 99, @@ -1062,6 +1063,7 @@ func createChannelEdge(node1, node2 *models.Node) (*models.ChannelEdgeInfo, Version: lnwire.GossipVersion1, SigBytes: testSig.Serialize(), ChannelID: chanID, + SecondPeer: true, LastUpdate: nextUpdateTime(), MessageFlags: 1, ChannelFlags: 1, @@ -1439,7 +1441,7 @@ func assertEdgeWithPolicyInCache(t *testing.T, g *ChannelGraph, func randEdgePolicy(chanID uint64) *models.ChannelEdgePolicy { update := prand.Int63() - return newEdgePolicy(chanID, update) + return newEdgePolicy(lnwire.GossipVersion1, chanID, update, true) } func copyEdgePolicy(p *models.ChannelEdgePolicy) *models.ChannelEdgePolicy { @@ -1448,8 +1450,11 @@ func copyEdgePolicy(p *models.ChannelEdgePolicy) *models.ChannelEdgePolicy { SigBytes: p.SigBytes, ChannelID: p.ChannelID, LastUpdate: p.LastUpdate, + LastBlockHeight: p.LastBlockHeight, + SecondPeer: p.SecondPeer, MessageFlags: p.MessageFlags, ChannelFlags: p.ChannelFlags, + DisableFlags: p.DisableFlags, TimeLockDelta: p.TimeLockDelta, MinHTLC: p.MinHTLC, MaxHTLC: p.MaxHTLC, @@ -1457,22 +1462,40 @@ func copyEdgePolicy(p *models.ChannelEdgePolicy) *models.ChannelEdgePolicy { FeeProportionalMillionths: p.FeeProportionalMillionths, ToNode: p.ToNode, ExtraOpaqueData: p.ExtraOpaqueData, + ExtraSignedFields: p.ExtraSignedFields, } } -func newEdgePolicy(chanID uint64, updateTime int64) *models.ChannelEdgePolicy { - return &models.ChannelEdgePolicy{ - Version: lnwire.GossipVersion1, +func newEdgePolicy(v lnwire.GossipVersion, chanID uint64, + updateTime int64, isNode1 bool) *models.ChannelEdgePolicy { + + policy := &models.ChannelEdgePolicy{ + Version: v, + SecondPeer: !isNode1, ChannelID: chanID, - LastUpdate: time.Unix(updateTime, 0), - MessageFlags: 1, - ChannelFlags: 0, TimeLockDelta: uint16(prand.Int63()), MinHTLC: lnwire.MilliSatoshi(prand.Int63()), MaxHTLC: lnwire.MilliSatoshi(prand.Int63()), FeeBaseMSat: lnwire.MilliSatoshi(prand.Int63()), FeeProportionalMillionths: lnwire.MilliSatoshi(prand.Int63()), } + + if v == lnwire.GossipVersion1 { + policy.LastUpdate = time.Unix(updateTime, 0) + policy.MessageFlags = 1 + if !isNode1 { + policy.ChannelFlags = lnwire.ChanUpdateDirection + } + policy.ExtraOpaqueData = []byte{1, 0} + } else { + policy.LastBlockHeight = nextBlockHeight() + policy.DisableFlags = 0 + policy.ExtraSignedFields = map[uint64][]byte{ + 100: {0x1, 0x2, 0x3}, + } + } + + return policy } // testAddEdgeProof tests the ability to add an edge proof to an existing edge. @@ -2358,7 +2381,8 @@ func TestChanUpdatesInHorizon(t *testing.T) { endTime = endTime.Add(time.Second * 10) edge1 := newEdgePolicy( - chanID.ToUint64(), edge1UpdateTime.Unix(), + lnwire.GossipVersion1, chanID.ToUint64(), + edge1UpdateTime.Unix(), true, ) edge1.ChannelFlags = 0 edge1.ToNode = node2.PubKeyBytes @@ -2368,7 +2392,8 @@ func TestChanUpdatesInHorizon(t *testing.T) { } edge2 := newEdgePolicy( - chanID.ToUint64(), edge2UpdateTime.Unix(), + lnwire.GossipVersion1, chanID.ToUint64(), + edge2UpdateTime.Unix(), false, ) edge2.ChannelFlags = 1 edge2.ToNode = node1.PubKeyBytes @@ -2821,7 +2846,9 @@ func TestChanUpdatesInHorizonBoundaryConditions(t *testing.T) { ) edge1 := newEdgePolicy( + lnwire.GossipVersion1, chanID.ToUint64(), updateTime.Unix(), + true, ) edge1.ChannelFlags = 0 edge1.ToNode = node2.PubKeyBytes @@ -2831,7 +2858,9 @@ func TestChanUpdatesInHorizonBoundaryConditions(t *testing.T) { ) edge2 := newEdgePolicy( + lnwire.GossipVersion1, chanID.ToUint64(), updateTime.Unix(), + false, ) edge2.ChannelFlags = 1 edge2.ToNode = node1.PubKeyBytes @@ -3668,16 +3697,20 @@ func TestFetchChanInfos(t *testing.T) { updateTime := endTime endTime = updateTime.Add(time.Second * 10) - edge1 := newEdgePolicy(chanID.ToUint64(), updateTime.Unix()) - edge1.ChannelFlags = 0 + edge1 := newEdgePolicy( + lnwire.GossipVersion1, chanID.ToUint64(), + updateTime.Unix(), true, + ) edge1.ToNode = node2.PubKeyBytes edge1.SigBytes = testSig.Serialize() if err := graph.UpdateEdgePolicy(ctx, edge1); err != nil { t.Fatalf("unable to update edge: %v", err) } - edge2 := newEdgePolicy(chanID.ToUint64(), updateTime.Unix()) - edge2.ChannelFlags = 1 + edge2 := newEdgePolicy( + lnwire.GossipVersion1, chanID.ToUint64(), + updateTime.Unix(), false, + ) edge2.ToNode = node1.PubKeyBytes edge2.SigBytes = testSig.Serialize() if err := graph.UpdateEdgePolicy(ctx, edge2); err != nil { @@ -3800,8 +3833,10 @@ func TestIncompleteChannelPolicies(t *testing.T) { // unknown. updateTime := time.Unix(1234, 0) - edgePolicy := newEdgePolicy(chanID.ToUint64(), updateTime.Unix()) - edgePolicy.ChannelFlags = 0 + edgePolicy := newEdgePolicy( + lnwire.GossipVersion1, chanID.ToUint64(), updateTime.Unix(), + true, + ) edgePolicy.ToNode = node2.PubKeyBytes edgePolicy.SigBytes = testSig.Serialize() if err := graph.UpdateEdgePolicy(ctx, edgePolicy); err != nil { @@ -3813,8 +3848,10 @@ func TestIncompleteChannelPolicies(t *testing.T) { // Create second policy and assert that both policies are reported // as present. - edgePolicy = newEdgePolicy(chanID.ToUint64(), updateTime.Unix()) - edgePolicy.ChannelFlags = 1 + edgePolicy = newEdgePolicy( + lnwire.GossipVersion1, chanID.ToUint64(), updateTime.Unix(), + false, + ) edgePolicy.ToNode = node1.PubKeyBytes edgePolicy.SigBytes = testSig.Serialize() if err := graph.UpdateEdgePolicy(ctx, edgePolicy); err != nil { @@ -4661,53 +4698,47 @@ func compareNodes(t *testing.T, a, b *models.Node) { require.Equal(t, a, b) } -// compareEdgePolicies is used to compare two ChannelEdgePolices using -// compareNodes, so as to exclude comparisons of the Nodes' Features struct. +// compareEdgePolicies compares two ChannelEdgePolicy values for semantic +// equality after normalizing version-specific/backend-specific differences. func compareEdgePolicies(a, b *models.ChannelEdgePolicy) error { - if a.ChannelID != b.ChannelID { - return fmt.Errorf("ChannelID doesn't match: expected %v, "+ - "got %v", a.ChannelID, b.ChannelID) + //nolint:ll + normalize := func(p *models.ChannelEdgePolicy) *models.ChannelEdgePolicy { + if p == nil { + return nil + } + + policy := copyEdgePolicy(p) + if len(policy.ExtraOpaqueData) == 0 { + policy.ExtraOpaqueData = nil + } + if len(policy.ExtraSignedFields) == 0 { + policy.ExtraSignedFields = nil + } + + switch policy.Version { + case lnwire.GossipVersion1: + // SecondPeer is v2-specific; derive canonical direction + // for v1. + policy.SecondPeer = !policy.IsNode1() + policy.LastBlockHeight = 0 + policy.DisableFlags = 0 + policy.ExtraSignedFields = nil + + case lnwire.GossipVersion2: + policy.LastUpdate = time.Time{} + policy.MessageFlags = 0 + policy.ChannelFlags = 0 + policy.ExtraOpaqueData = nil + } + + return policy } - if !reflect.DeepEqual(a.LastUpdate, b.LastUpdate) { - return fmt.Errorf("edge LastUpdate doesn't match: "+ - "expected %#v, got %#v", a.LastUpdate, b.LastUpdate) - } - if a.MessageFlags != b.MessageFlags { - return fmt.Errorf("MessageFlags doesn't match: expected %v, "+ - "got %v", a.MessageFlags, b.MessageFlags) - } - if a.ChannelFlags != b.ChannelFlags { - return fmt.Errorf("ChannelFlags doesn't match: expected %v, "+ - "got %v", a.ChannelFlags, b.ChannelFlags) - } - if a.TimeLockDelta != b.TimeLockDelta { - return fmt.Errorf("TimeLockDelta doesn't match: expected %v, "+ - "got %v", a.TimeLockDelta, b.TimeLockDelta) - } - if a.MinHTLC != b.MinHTLC { - return fmt.Errorf("MinHTLC doesn't match: expected %v, "+ - "got %v", a.MinHTLC, b.MinHTLC) - } - if a.MaxHTLC != b.MaxHTLC { - return fmt.Errorf("MaxHTLC doesn't match: expected %v, "+ - "got %v", a.MaxHTLC, b.MaxHTLC) - } - if a.FeeBaseMSat != b.FeeBaseMSat { - return fmt.Errorf("FeeBaseMSat doesn't match: expected %v, "+ - "got %v", a.FeeBaseMSat, b.FeeBaseMSat) - } - if a.FeeProportionalMillionths != b.FeeProportionalMillionths { - return fmt.Errorf("FeeProportionalMillionths doesn't match: "+ - "expected %v, got %v", a.FeeProportionalMillionths, - b.FeeProportionalMillionths) - } - if !bytes.Equal(a.ExtraOpaqueData, b.ExtraOpaqueData) { - return fmt.Errorf("extra data doesn't match: %v vs %v", - a.ExtraOpaqueData, b.ExtraOpaqueData) - } - if !bytes.Equal(a.ToNode[:], b.ToNode[:]) { - return fmt.Errorf("ToNode doesn't match: expected %x, got %x", - a.ToNode, b.ToNode) + + normalizedA := normalize(a) + normalizedB := normalize(b) + if !reflect.DeepEqual(normalizedA, normalizedB) { + return fmt.Errorf("expected %v, got %v", normalizedA, + normalizedB) } return nil diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index eb4c3b9a1..2bbfe89cd 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -3296,6 +3296,9 @@ func updateEdgePolicy(tx kvdb.RwTx, edge *models.ChannelEdgePolicy) ( route.Vertex, route.Vertex, bool, error) { var noVertex route.Vertex + if edge.Version != lnwire.GossipVersion1 { + return noVertex, noVertex, false, ErrVersionNotSupportedForKVDB + } edges := tx.ReadWriteBucket(edgeBucket) if edges == nil { @@ -5226,6 +5229,10 @@ func fetchChanEdgePolicies(edgeIndex kvdb.RBucket, edges kvdb.RBucket, func serializeChanEdgePolicy(w io.Writer, edge *models.ChannelEdgePolicy, to []byte) error { + if edge.Version != lnwire.GossipVersion1 { + return ErrVersionNotSupportedForKVDB + } + err := wire.WriteVarBytes(w, 0, edge.SigBytes) if err != nil { return err @@ -5314,7 +5321,9 @@ func deserializeChanEdgePolicy(r io.Reader) (*models.ChannelEdgePolicy, error) { func deserializeChanEdgePolicyRaw(r io.Reader) (*models.ChannelEdgePolicy, error) { - edge := &models.ChannelEdgePolicy{} + edge := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion1, + } var err error edge.SigBytes, err = wire.ReadVarBytes(r, 0, 80, "sig") diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index bcb8245be..4468ad305 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -3455,10 +3455,16 @@ func updateChanEdgePolicy(ctx context.Context, tx SQLQueries, var ( node1Pub, node2Pub route.Vertex - isNode1 bool chanIDB = channelIDToBytes(edge.ChannelID) + version = edge.Version ) + if !isKnownGossipVersion(version) { + return node1Pub, node2Pub, false, fmt.Errorf( + "unsupported gossip version: %d", version, + ) + } + // Check that this edge policy refers to a channel that we already // know of. We do this explicitly so that we can return the appropriate // ErrEdgeNotFound error if the channel doesn't exist, rather than @@ -3466,7 +3472,7 @@ func updateChanEdgePolicy(ctx context.Context, tx SQLQueries, dbChan, err := tx.GetChannelAndNodesBySCID( ctx, sqlc.GetChannelAndNodesBySCIDParams{ Scid: chanIDB, - Version: int16(lnwire.GossipVersion1), + Version: int16(version), }, ) if errors.Is(err, sql.ErrNoRows) { @@ -3480,7 +3486,7 @@ func updateChanEdgePolicy(ctx context.Context, tx SQLQueries, copy(node2Pub[:], dbChan.Node2PubKey) // Figure out which node this edge is from. - isNode1 = edge.ChannelFlags&lnwire.ChanUpdateDirection == 0 + isNode1 := edge.IsNode1() nodeID := dbChan.NodeID1 if !isNode1 { nodeID = dbChan.NodeID2 @@ -3495,31 +3501,40 @@ func updateChanEdgePolicy(ctx context.Context, tx SQLQueries, inboundBase = sqldb.SQLInt64(fee.BaseFee) }) - id, err := tx.UpsertEdgePolicy(ctx, sqlc.UpsertEdgePolicyParams{ - Version: int16(lnwire.GossipVersion1), - ChannelID: dbChan.ID, - NodeID: nodeID, - Timelock: int32(edge.TimeLockDelta), - FeePpm: int64(edge.FeeProportionalMillionths), - BaseFeeMsat: int64(edge.FeeBaseMSat), - MinHtlcMsat: int64(edge.MinHTLC), - LastUpdate: sqldb.SQLInt64(edge.LastUpdate.Unix()), - Disabled: sql.NullBool{ - Valid: true, - Bool: edge.IsDisabled(), - }, - MaxHtlcMsat: sql.NullInt64{ - Valid: edge.MessageFlags.HasMaxHtlc(), - Int64: int64(edge.MaxHTLC), - }, + params := sqlc.UpsertEdgePolicyParams{ + Version: int16(version), + ChannelID: dbChan.ID, + NodeID: nodeID, + Timelock: int32(edge.TimeLockDelta), + FeePpm: int64(edge.FeeProportionalMillionths), + BaseFeeMsat: int64(edge.FeeBaseMSat), + MinHtlcMsat: int64(edge.MinHTLC), MessageFlags: sqldb.SQLInt16(edge.MessageFlags), ChannelFlags: sqldb.SQLInt16(edge.ChannelFlags), InboundBaseFeeMsat: inboundBase, InboundFeeRateMilliMsat: inboundRate, Signature: edge.SigBytes, - BlockHeight: sql.NullInt64{}, - DisableFlags: sql.NullInt16{}, - }) + } + + if version == lnwire.GossipVersion1 { + params.LastUpdate = sqldb.SQLInt64(edge.LastUpdate.Unix()) + params.Disabled = sql.NullBool{ + Valid: true, + Bool: edge.IsDisabled(), + } + params.MaxHtlcMsat = sql.NullInt64{ + Valid: edge.MessageFlags.HasMaxHtlc(), + Int64: int64(edge.MaxHTLC), + } + } else { + params.BlockHeight = sqldb.SQLInt64( + int64(edge.LastBlockHeight), + ) + params.DisableFlags = sqldb.SQLInt16(edge.DisableFlags) + params.MaxHtlcMsat = sqldb.SQLInt64(int64(edge.MaxHTLC)) + } + + id, err := tx.UpsertEdgePolicy(ctx, params) if err != nil { return node1Pub, node2Pub, isNode1, fmt.Errorf("unable to upsert edge policy: %w", err) @@ -3527,10 +3542,13 @@ func updateChanEdgePolicy(ctx context.Context, tx SQLQueries, // Convert the flat extra opaque data into a map of TLV types to // values. - extra, err := marshalExtraOpaqueData(edge.ExtraOpaqueData) - if err != nil { - return node1Pub, node2Pub, false, fmt.Errorf("unable to "+ - "marshal extra opaque data: %w", err) + extra := edge.ExtraSignedFields + if version == lnwire.GossipVersion1 { + extra, err = marshalExtraOpaqueData(edge.ExtraOpaqueData) + if err != nil { + return node1Pub, node2Pub, false, fmt.Errorf("unable to "+ + "marshal extra opaque data: %w", err) + } } // Update the channel policy's extra signed fields. @@ -4713,14 +4731,14 @@ func getAndBuildChanPolicies(ctx context.Context, cfg *sqldb.QueryConfig, } pol1, err := buildChanPolicyWithBatchData( - dbPol1, channelID, node2, batchData, + true, dbPol1, channelID, node2, batchData, ) if err != nil { return nil, nil, fmt.Errorf("unable to build policy1: %w", err) } pol2, err := buildChanPolicyWithBatchData( - dbPol2, channelID, node1, batchData, + false, dbPol2, channelID, node1, batchData, ) if err != nil { return nil, nil, fmt.Errorf("unable to build policy2: %w", err) @@ -4738,7 +4756,9 @@ func buildCachedChanPolicies(dbPol1, dbPol2 *sqlc.GraphChannelPolicy, var p1, p2 *models.CachedEdgePolicy if dbPol1 != nil { - policy1, err := buildChanPolicy(*dbPol1, channelID, nil, node2) + policy1, err := buildChanPolicy( + true, *dbPol1, channelID, nil, node2, + ) if err != nil { return nil, nil, err } @@ -4746,7 +4766,9 @@ func buildCachedChanPolicies(dbPol1, dbPol2 *sqlc.GraphChannelPolicy, p1 = models.NewCachedPolicy(policy1) } if dbPol2 != nil { - policy2, err := buildChanPolicy(*dbPol2, channelID, nil, node1) + policy2, err := buildChanPolicy( + false, *dbPol2, channelID, nil, node1, + ) if err != nil { return nil, nil, err } @@ -4759,16 +4781,10 @@ func buildCachedChanPolicies(dbPol1, dbPol2 *sqlc.GraphChannelPolicy, // buildChanPolicy builds a models.ChannelEdgePolicy instance from the // provided sqlc.GraphChannelPolicy and other required information. -func buildChanPolicy(dbPolicy sqlc.GraphChannelPolicy, channelID uint64, - extras map[uint64][]byte, +func buildChanPolicy(isNode1 bool, dbPolicy sqlc.GraphChannelPolicy, + channelID uint64, extras map[uint64][]byte, toNode route.Vertex) (*models.ChannelEdgePolicy, error) { - recs, err := lnwire.CustomRecords(extras).Serialize() - if err != nil { - return nil, fmt.Errorf("unable to serialize extra signed "+ - "fields: %w", err) - } - var inboundFee fn.Option[lnwire.Fee] if dbPolicy.InboundFeeRateMilliMsat.Valid || dbPolicy.InboundBaseFeeMsat.Valid { @@ -4779,18 +4795,11 @@ func buildChanPolicy(dbPolicy sqlc.GraphChannelPolicy, channelID uint64, }) } - return &models.ChannelEdgePolicy{ - SigBytes: dbPolicy.Signature, - ChannelID: channelID, - LastUpdate: time.Unix( - dbPolicy.LastUpdate.Int64, 0, - ), - MessageFlags: sqldb.ExtractSqlInt16[lnwire.ChanUpdateMsgFlags]( - dbPolicy.MessageFlags, - ), - ChannelFlags: sqldb.ExtractSqlInt16[lnwire.ChanUpdateChanFlags]( - dbPolicy.ChannelFlags, - ), + p := &models.ChannelEdgePolicy{ + Version: lnwire.GossipVersion(dbPolicy.Version), + SigBytes: dbPolicy.Signature, + ChannelID: channelID, + SecondPeer: !isNode1, TimeLockDelta: uint16(dbPolicy.Timelock), MinHTLC: lnwire.MilliSatoshi( dbPolicy.MinHtlcMsat, @@ -4804,8 +4813,40 @@ func buildChanPolicy(dbPolicy sqlc.GraphChannelPolicy, channelID uint64, FeeProportionalMillionths: lnwire.MilliSatoshi(dbPolicy.FeePpm), ToNode: toNode, InboundFee: inboundFee, - ExtraOpaqueData: recs, - }, nil + } + + if p.Version == lnwire.GossipVersion1 { + recs, err := lnwire.CustomRecords(extras).Serialize() + if err != nil { + return nil, fmt.Errorf("unable to serialize extra "+ + "signed fields: %w", err) + } + + p.ExtraOpaqueData = recs + p.LastUpdate = time.Unix(dbPolicy.LastUpdate.Int64, 0) + //nolint:ll + p.MessageFlags = sqldb.ExtractSqlInt16[lnwire.ChanUpdateMsgFlags]( + dbPolicy.MessageFlags, + ) + //nolint:ll + p.ChannelFlags = sqldb.ExtractSqlInt16[lnwire.ChanUpdateChanFlags]( + dbPolicy.ChannelFlags, + ) + } else { + if dbPolicy.BlockHeight.Valid { + p.LastBlockHeight = uint32( + dbPolicy.BlockHeight.Int64, + ) + } + + //nolint:ll + p.DisableFlags = sqldb.ExtractSqlInt16[lnwire.ChanUpdateDisableFlags]( + dbPolicy.DisableFlags, + ) + p.ExtraSignedFields = extras + } + + return p, nil } // extractChannelPolicies extracts the sqlc.GraphChannelPolicy records from the give @@ -5517,14 +5558,14 @@ func buildChanPoliciesWithBatchData(dbPol1, dbPol2 *sqlc.GraphChannelPolicy, *models.ChannelEdgePolicy, error) { pol1, err := buildChanPolicyWithBatchData( - dbPol1, channelID, node2, batchData, + true, dbPol1, channelID, node2, batchData, ) if err != nil { return nil, nil, fmt.Errorf("unable to build policy1: %w", err) } pol2, err := buildChanPolicyWithBatchData( - dbPol2, channelID, node1, batchData, + false, dbPol2, channelID, node1, batchData, ) if err != nil { return nil, nil, fmt.Errorf("unable to build policy2: %w", err) @@ -5535,9 +5576,10 @@ func buildChanPoliciesWithBatchData(dbPol1, dbPol2 *sqlc.GraphChannelPolicy, // buildChanPolicyWithBatchData builds a models.ChannelEdgePolicy instance from // the provided sqlc.GraphChannelPolicy and the provided batchChannelData. -func buildChanPolicyWithBatchData(dbPol *sqlc.GraphChannelPolicy, - channelID uint64, toNode route.Vertex, - batchData *batchChannelData) (*models.ChannelEdgePolicy, error) { +func buildChanPolicyWithBatchData(isNode1 bool, + dbPol *sqlc.GraphChannelPolicy, channelID uint64, + toNode route.Vertex, batchData *batchChannelData) ( + *models.ChannelEdgePolicy, error) { if dbPol == nil { return nil, nil @@ -5550,7 +5592,7 @@ func buildChanPolicyWithBatchData(dbPol *sqlc.GraphChannelPolicy, dbPol1Extras = make(map[uint64][]byte) } - return buildChanPolicy(*dbPol, channelID, dbPol1Extras, toNode) + return buildChanPolicy(isNode1, *dbPol, channelID, dbPol1Extras, toNode) } // batchChannelData holds all the related data for a batch of channels. From 4a6213c35eea3a6370385c0e2139b2a38041ece7 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 3 Feb 2026 13:52:41 +0200 Subject: [PATCH 49/55] graph/db: version reject cache for policies Make the reject cache version-aware so v1 and v2 policy state can be cached independently per channel ID. Add helpers to store v1 timestamps or v2 block heights and thread the versioned cache key through KV/SQL store cache accesses. --- graph/db/kv_store.go | 22 ++++---- graph/db/reject_cache.go | 102 ++++++++++++++++++++++++++++++---- graph/db/reject_cache_test.go | 21 ++++--- graph/db/sql_store.go | 45 ++++++++------- 4 files changed, 141 insertions(+), 49 deletions(-) diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 2bbfe89cd..5e71ad700 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -1179,7 +1179,7 @@ func (c *KVStore) AddChannelEdge(ctx context.Context, case alreadyExists: return ErrEdgeAlreadyExist default: - c.rejectCache.remove(edge.ChannelID) + c.rejectCache.remove(lnwire.GossipVersion1, edge.ChannelID) c.chanCache.remove(edge.ChannelID) return nil } @@ -1302,7 +1302,7 @@ func (c *KVStore) HasChannelEdge( // We'll query the cache with the shared lock held to allow multiple // readers to access values in the cache concurrently if they exist. c.cacheMu.RLock() - if entry, ok := c.rejectCache.get(chanID); ok { + if entry, ok := c.rejectCache.get(lnwire.GossipVersion1, chanID); ok { c.cacheMu.RUnlock() upd1Time = time.Unix(entry.upd1Time, 0) upd2Time = time.Unix(entry.upd2Time, 0) @@ -1318,7 +1318,7 @@ func (c *KVStore) HasChannelEdge( // The item was not found with the shared lock, so we'll acquire the // exclusive lock and check the cache again in case another method added // the entry to the cache while no lock was held. - if entry, ok := c.rejectCache.get(chanID); ok { + if entry, ok := c.rejectCache.get(lnwire.GossipVersion1, chanID); ok { upd1Time = time.Unix(entry.upd1Time, 0) upd2Time = time.Unix(entry.upd2Time, 0) exists, isZombie = entry.flags.unpack() @@ -1385,7 +1385,7 @@ func (c *KVStore) HasChannelEdge( return time.Time{}, time.Time{}, exists, isZombie, err } - c.rejectCache.insert(chanID, rejectCacheEntry{ + c.rejectCache.insert(lnwire.GossipVersion1, chanID, rejectCacheEntry{ upd1Time: upd1Time.Unix(), upd2Time: upd2Time.Unix(), flags: packRejectFlags(exists, isZombie), @@ -1564,7 +1564,7 @@ func (c *KVStore) PruneGraph(spentOutputs []*wire.OutPoint, } for _, channel := range chansClosed { - c.rejectCache.remove(channel.ChannelID) + c.rejectCache.remove(lnwire.GossipVersion1, channel.ChannelID) c.chanCache.remove(channel.ChannelID) } @@ -1831,7 +1831,7 @@ func (c *KVStore) DisconnectBlockAtHeight(height uint32) ( } for _, channel := range removedChans { - c.rejectCache.remove(channel.ChannelID) + c.rejectCache.remove(lnwire.GossipVersion1, channel.ChannelID) c.chanCache.remove(channel.ChannelID) } @@ -1950,7 +1950,7 @@ func (c *KVStore) DeleteChannelEdges(v lnwire.GossipVersion, } for _, chanID := range chanIDs { - c.rejectCache.remove(chanID) + c.rejectCache.remove(lnwire.GossipVersion1, chanID) c.chanCache.remove(chanID) } @@ -3265,13 +3265,13 @@ func (c *KVStore) updateEdgeCache(e *models.ChannelEdgePolicy, // the entry with the updated timestamp for the direction that was just // written. If the edge doesn't exist, we'll load the cache entry lazily // during the next query for this edge. - if entry, ok := c.rejectCache.get(e.ChannelID); ok { + if entry, ok := c.rejectCache.get(lnwire.GossipVersion1, e.ChannelID); ok { if isUpdate1 { entry.upd1Time = e.LastUpdate.Unix() } else { entry.upd2Time = e.LastUpdate.Unix() } - c.rejectCache.insert(e.ChannelID, entry) + c.rejectCache.insert(lnwire.GossipVersion1, e.ChannelID, entry) } // If an entry for this channel is found in channel cache, we'll modify @@ -4185,7 +4185,7 @@ func (c *KVStore) MarkEdgeZombie(chanID uint64, return err } - c.rejectCache.remove(chanID) + c.rejectCache.remove(lnwire.GossipVersion1, chanID) c.chanCache.remove(chanID) return nil @@ -4254,7 +4254,7 @@ func (c *KVStore) markEdgeLiveUnsafe(tx kvdb.RwTx, chanID uint64) error { return err } - c.rejectCache.remove(chanID) + c.rejectCache.remove(lnwire.GossipVersion1, chanID) c.chanCache.remove(chanID) return nil diff --git a/graph/db/reject_cache.go b/graph/db/reject_cache.go index 2a2721928..bfe3c47ff 100644 --- a/graph/db/reject_cache.go +++ b/graph/db/reject_cache.go @@ -1,5 +1,11 @@ package graphdb +import ( + "time" + + "github.com/lightningnetwork/lnd/lnwire" +) + // rejectFlags is a compact representation of various metadata stored by the // reject cache about a particular channel. type rejectFlags uint8 @@ -41,9 +47,68 @@ func (f rejectFlags) unpack() (bool, bool) { // including the timestamps of its latest edge policies and whether or not the // channel exists in the graph. type rejectCacheEntry struct { + // upd{1,2}Time are Unix timestamps for v1 policies. upd1Time int64 upd2Time int64 - flags rejectFlags + + // upd{1,2}BlockHeight are the last known block heights for v2 + // policies. + upd1BlockHeight uint32 + upd2BlockHeight uint32 + + flags rejectFlags +} + +// newRejectCacheEntryV1 constructs a reject cache entry for v1 policies. +func newRejectCacheEntryV1(upd1, upd2 time.Time, exists, + isZombie bool) rejectCacheEntry { + + return rejectCacheEntry{ + upd1Time: upd1.Unix(), + upd2Time: upd2.Unix(), + flags: packRejectFlags(exists, isZombie), + } +} + +// newRejectCacheEntryV2 constructs a reject cache entry for v2 policies. +func newRejectCacheEntryV2(upd1, upd2 uint32, exists, + isZombie bool) rejectCacheEntry { + + return rejectCacheEntry{ + upd1BlockHeight: upd1, + upd2BlockHeight: upd2, + flags: packRejectFlags(exists, isZombie), + } +} + +// updateRejectCacheEntryV1 updates the cached v1 timestamps. +func updateRejectCacheEntryV1(entry *rejectCacheEntry, isUpdate1 bool, + lastUpdate time.Time) { + + if isUpdate1 { + entry.upd1Time = lastUpdate.Unix() + } else { + entry.upd2Time = lastUpdate.Unix() + } +} + +// updateRejectCacheEntryV2 updates the cached v2 block heights. +func updateRejectCacheEntryV2(entry *rejectCacheEntry, isUpdate1 bool, + blockHeight uint32) { + + if isUpdate1 { + entry.upd1BlockHeight = blockHeight + } else { + entry.upd2BlockHeight = blockHeight + } +} + +// rejectCacheKey uniquely identifies a channel entry in the reject cache by +// gossip version and channel ID. This allows v1 and v2 policy state for the +// same channel ID to be cached independently. +type rejectCacheKey struct { + version lnwire.GossipVersion + chanID uint64 } // rejectCache is an in-memory cache used to improve the performance of @@ -51,20 +116,25 @@ type rejectCacheEntry struct { // well as the most recent timestamps for each policy (if they exists). type rejectCache struct { n int - edges map[uint64]rejectCacheEntry + edges map[rejectCacheKey]rejectCacheEntry } // newRejectCache creates a new rejectCache with maximum capacity of n entries. func newRejectCache(n int) *rejectCache { return &rejectCache{ n: n, - edges: make(map[uint64]rejectCacheEntry, n), + edges: make(map[rejectCacheKey]rejectCacheEntry, n), } } // get returns the entry from the cache for chanid, if it exists. -func (c *rejectCache) get(chanid uint64) (rejectCacheEntry, bool) { - entry, ok := c.edges[chanid] +func (c *rejectCache) get(version lnwire.GossipVersion, chanid uint64) ( + rejectCacheEntry, bool) { + + entry, ok := c.edges[rejectCacheKey{ + version: version, + chanID: chanid, + }] return entry, ok } @@ -72,10 +142,17 @@ func (c *rejectCache) get(chanid uint64) (rejectCacheEntry, bool) { // exists, it will be replaced with the new entry. If the entry doesn't exists, // it will be inserted to the cache, performing a random eviction if the cache // is at capacity. -func (c *rejectCache) insert(chanid uint64, entry rejectCacheEntry) { +func (c *rejectCache) insert(version lnwire.GossipVersion, chanid uint64, + entry rejectCacheEntry) { + + key := rejectCacheKey{ + version: version, + chanID: chanid, + } + // If entry exists, replace it. - if _, ok := c.edges[chanid]; ok { - c.edges[chanid] = entry + if _, ok := c.edges[key]; ok { + c.edges[key] = entry return } @@ -86,10 +163,13 @@ func (c *rejectCache) insert(chanid uint64, entry rejectCacheEntry) { break } } - c.edges[chanid] = entry + c.edges[key] = entry } // remove deletes an entry for chanid from the cache, if it exists. -func (c *rejectCache) remove(chanid uint64) { - delete(c.edges, chanid) +func (c *rejectCache) remove(version lnwire.GossipVersion, chanid uint64) { + delete(c.edges, rejectCacheKey{ + version: version, + chanID: chanid, + }) } diff --git a/graph/db/reject_cache_test.go b/graph/db/reject_cache_test.go index f64c39c33..c526f487b 100644 --- a/graph/db/reject_cache_test.go +++ b/graph/db/reject_cache_test.go @@ -3,6 +3,8 @@ package graphdb import ( "reflect" "testing" + + "github.com/lightningnetwork/lnd/lnwire" ) // TestRejectCache checks the behavior of the rejectCache with respect to insertion, @@ -15,14 +17,14 @@ func TestRejectCache(t *testing.T) { // As a sanity check, assert that querying the empty cache does not // return an entry. - _, ok := c.get(0) + _, ok := c.get(lnwire.GossipVersion1, 0) if ok { t.Fatalf("reject cache should be empty") } // Now, fill up the cache entirely. for i := uint64(0); i < cacheSize; i++ { - c.insert(i, entryForInt(i)) + c.insert(lnwire.GossipVersion1, i, entryForInt(i)) } // Assert that the cache has all of the entries just inserted, since no @@ -30,7 +32,10 @@ func TestRejectCache(t *testing.T) { assertHasEntries(t, c, 0, cacheSize) // Now, insert a new element that causes the cache to evict an element. - c.insert(cacheSize, entryForInt(cacheSize)) + c.insert( + lnwire.GossipVersion1, cacheSize, + entryForInt(cacheSize), + ) // Assert that the cache has this last entry, as the cache should evict // some prior element and not the newly inserted one. @@ -40,7 +45,7 @@ func TestRejectCache(t *testing.T) { // elements. evicted := make(map[uint64]struct{}) for i := uint64(0); i < cacheSize+1; i++ { - _, ok := c.get(i) + _, ok := c.get(lnwire.GossipVersion1, i) if !ok { evicted[i] = struct{}{} } @@ -54,9 +59,9 @@ func TestRejectCache(t *testing.T) { // Remove the highest item which initially caused the eviction and // reinsert the element that was evicted prior. - c.remove(cacheSize) + c.remove(lnwire.GossipVersion1, cacheSize) for i := range evicted { - c.insert(i, entryForInt(i)) + c.insert(lnwire.GossipVersion1, i, entryForInt(i)) } // Since the removal created an extra slot, the last insertion should @@ -69,7 +74,7 @@ func TestRejectCache(t *testing.T) { // happening on inserts for existing cache items, we expect this to fail // with high probability. for i := uint64(0); i < cacheSize; i++ { - c.insert(i, entryForInt(i)) + c.insert(lnwire.GossipVersion1, i, entryForInt(i)) } assertHasEntries(t, c, 0, cacheSize) @@ -82,7 +87,7 @@ func assertHasEntries(t *testing.T, c *rejectCache, start, end uint64) { t.Helper() for i := start; i < end; i++ { - entry, ok := c.get(i) + entry, ok := c.get(lnwire.GossipVersion1, i) if !ok { t.Fatalf("reject cache should contain chan %d", i) } diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 4468ad305..d72c8537f 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -747,7 +747,7 @@ func (s *SQLStore) AddChannelEdge(ctx context.Context, case alreadyExists: return ErrEdgeAlreadyExist default: - s.rejectCache.remove(edge.ChannelID) + s.rejectCache.remove(edge.Version, edge.ChannelID) s.chanCache.remove(edge.ChannelID) return nil } @@ -862,13 +862,18 @@ func (s *SQLStore) updateEdgeCache(e *models.ChannelEdgePolicy, // the entry with the updated timestamp for the direction that was just // written. If the edge doesn't exist, we'll load the cache entry lazily // during the next query for this edge. - if entry, ok := s.rejectCache.get(e.ChannelID); ok { - if isUpdate1 { - entry.upd1Time = e.LastUpdate.Unix() - } else { - entry.upd2Time = e.LastUpdate.Unix() + if entry, ok := s.rejectCache.get(e.Version, e.ChannelID); ok { + switch e.Version { + case lnwire.GossipVersion1: + updateRejectCacheEntryV1( + &entry, isUpdate1, e.LastUpdate, + ) + case lnwire.GossipVersion2: + updateRejectCacheEntryV2( + &entry, isUpdate1, e.LastBlockHeight, + ) } - s.rejectCache.insert(e.ChannelID, entry) + s.rejectCache.insert(e.Version, e.ChannelID, entry) } // If an entry for this channel is found in channel cache, we'll modify @@ -1764,7 +1769,7 @@ func (s *SQLStore) MarkEdgeZombie(chanID uint64, "(channel_id=%d): %w", chanID, err) } - s.rejectCache.remove(chanID) + s.rejectCache.remove(lnwire.GossipVersion1, chanID) s.chanCache.remove(chanID) return nil @@ -1813,7 +1818,7 @@ func (s *SQLStore) MarkEdgeLive(chanID uint64) error { "(channel_id=%d): %w", chanID, err) } - s.rejectCache.remove(chanID) + s.rejectCache.remove(lnwire.GossipVersion1, chanID) s.chanCache.remove(chanID) return err @@ -1995,7 +2000,7 @@ func (s *SQLStore) DeleteChannelEdges(v lnwire.GossipVersion, } for _, chanID := range chanIDs { - s.rejectCache.remove(chanID) + s.rejectCache.remove(v, chanID) s.chanCache.remove(chanID) } @@ -2222,7 +2227,7 @@ func (s *SQLStore) HasChannelEdge(chanID uint64) (time.Time, time.Time, bool, // We'll query the cache with the shared lock held to allow multiple // readers to access values in the cache concurrently if they exist. s.cacheMu.RLock() - if entry, ok := s.rejectCache.get(chanID); ok { + if entry, ok := s.rejectCache.get(lnwire.GossipVersion1, chanID); ok { s.cacheMu.RUnlock() node1LastUpdate = time.Unix(entry.upd1Time, 0) node2LastUpdate = time.Unix(entry.upd2Time, 0) @@ -2238,7 +2243,7 @@ func (s *SQLStore) HasChannelEdge(chanID uint64) (time.Time, time.Time, bool, // The item was not found with the shared lock, so we'll acquire the // exclusive lock and check the cache again in case another method added // the entry to the cache while no lock was held. - if entry, ok := s.rejectCache.get(chanID); ok { + if entry, ok := s.rejectCache.get(lnwire.GossipVersion1, chanID); ok { node1LastUpdate = time.Unix(entry.upd1Time, 0) node2LastUpdate = time.Unix(entry.upd2Time, 0) exists, isZombie = entry.flags.unpack() @@ -2309,11 +2314,13 @@ func (s *SQLStore) HasChannelEdge(chanID uint64) (time.Time, time.Time, bool, fmt.Errorf("unable to fetch channel: %w", err) } - s.rejectCache.insert(chanID, rejectCacheEntry{ - upd1Time: node1LastUpdate.Unix(), - upd2Time: node2LastUpdate.Unix(), - flags: packRejectFlags(exists, isZombie), - }) + s.rejectCache.insert( + lnwire.GossipVersion1, chanID, + newRejectCacheEntryV1( + node1LastUpdate, node2LastUpdate, exists, + isZombie, + ), + ) return node1LastUpdate, node2LastUpdate, exists, isZombie, nil } @@ -2732,7 +2739,7 @@ func (s *SQLStore) PruneGraph(spentOutputs []*wire.OutPoint, } for _, channel := range closedChans { - s.rejectCache.remove(channel.ChannelID) + s.rejectCache.remove(channel.Version, channel.ChannelID) s.chanCache.remove(channel.ChannelID) } @@ -3001,7 +3008,7 @@ func (s *SQLStore) DisconnectBlockAtHeight(height uint32) ( s.cacheMu.Lock() for _, channel := range removedChans { - s.rejectCache.remove(channel.ChannelID) + s.rejectCache.remove(channel.Version, channel.ChannelID) s.chanCache.remove(channel.ChannelID) } s.cacheMu.Unlock() From 506ec037e2f2218c2b2760ad6d101fbffd5feb50 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 3 Feb 2026 14:33:13 +0200 Subject: [PATCH 50/55] graph/db: add versioned HasChannelEdge method Add a new HasChannelEdge method that takes a gossip version parameter and returns only existence and zombie status, without timestamp data. This supports both v1 and v2 gossip protocols. The original HasChannelEdge method is renamed to HasV1ChannelEdge to preserve v1-specific functionality for callers that need timestamp information. All call sites are updated accordingly. The SQL store implementation now handles both gossip versions, using timestamps for v1 and block heights for v2 policies, with proper reject cache support for both versions. --- graph/builder.go | 16 ++-- graph/builder_test.go | 22 ++--- graph/db/graph.go | 23 ++++- graph/db/graph_test.go | 19 +++-- graph/db/interfaces.go | 11 ++- graph/db/kv_store.go | 38 +++++++-- graph/db/reject_cache.go | 7 +- graph/db/sql_store.go | 176 ++++++++++++++++++++++++++++++++++++--- 8 files changed, 258 insertions(+), 54 deletions(-) diff --git a/graph/builder.go b/graph/builder.go index fa8b0b4a9..2cf4d1f34 100644 --- a/graph/builder.go +++ b/graph/builder.go @@ -1043,8 +1043,8 @@ func (b *Builder) addEdge(ctx context.Context, edge *models.ChannelEdgeInfo, // Prior to processing the announcement we first check if we // already know of this channel, if so, then we can exit early. - _, _, exists, isZombie, err := b.cfg.Graph.HasChannelEdge( - edge.ChannelID, + exists, isZombie, err := b.cfg.Graph.HasChannelEdge( + edge.Version, edge.ChannelID, ) if err != nil && !errors.Is(err, graphdb.ErrGraphNoEdgesFound) { return fmt.Errorf("unable to check for edge existence: %w", @@ -1145,7 +1145,7 @@ func (b *Builder) updateEdge(ctx context.Context, defer b.channelEdgeMtx.Unlock(policy.ChannelID) edge1Timestamp, edge2Timestamp, exists, isZombie, err := - b.cfg.Graph.HasChannelEdge(policy.ChannelID) + b.cfg.Graph.HasV1ChannelEdge(policy.ChannelID) if err != nil && !errors.Is(err, graphdb.ErrGraphNoEdgesFound) { return fmt.Errorf("unable to check for edge existence: %w", err) } @@ -1331,8 +1331,8 @@ func (b *Builder) IsPublicNode(node route.Vertex) (bool, error) { // // NOTE: This method is part of the ChannelGraphSource interface. func (b *Builder) IsKnownEdge(chanID lnwire.ShortChannelID) bool { - _, _, exists, isZombie, _ := b.cfg.Graph.HasChannelEdge( - chanID.ToUint64(), + exists, isZombie, _ := b.cfg.Graph.HasChannelEdge( + lnwire.GossipVersion1, chanID.ToUint64(), ) return exists || isZombie @@ -1343,7 +1343,9 @@ func (b *Builder) IsKnownEdge(chanID lnwire.ShortChannelID) bool { // // NOTE: This method is part of the ChannelGraphSource interface. func (b *Builder) IsZombieEdge(chanID lnwire.ShortChannelID) (bool, error) { - _, _, _, isZombie, err := b.cfg.Graph.HasChannelEdge(chanID.ToUint64()) + _, isZombie, err := b.cfg.Graph.HasChannelEdge( + lnwire.GossipVersion1, chanID.ToUint64(), + ) return isZombie, err } @@ -1356,7 +1358,7 @@ func (b *Builder) IsStaleEdgePolicy(chanID lnwire.ShortChannelID, timestamp time.Time, flags lnwire.ChanUpdateChanFlags) bool { edge1Timestamp, edge2Timestamp, exists, isZombie, err := - b.cfg.Graph.HasChannelEdge(chanID.ToUint64()) + b.cfg.Graph.HasV1ChannelEdge(chanID.ToUint64()) if err != nil { log.Debugf("Check stale edge policy got error: %v", err) return false diff --git a/graph/builder_test.go b/graph/builder_test.go index 6929a4d49..acab91ccf 100644 --- a/graph/builder_test.go +++ b/graph/builder_test.go @@ -319,7 +319,7 @@ func TestWakeUpOnStaleBranch(t *testing.T) { } // Check that the fundingTxs are in the graph db. - _, _, has, isZombie, err := ctx.graph.HasChannelEdge(chanID1) + has, isZombie, err := ctx.graph.HasChannelEdge(chanID1) if err != nil { t.Fatalf("error looking for edge: %v", chanID1) } @@ -330,7 +330,7 @@ func TestWakeUpOnStaleBranch(t *testing.T) { t.Fatal("edge was marked as zombie") } - _, _, has, isZombie, err = ctx.graph.HasChannelEdge(chanID2) + has, isZombie, err = ctx.graph.HasChannelEdge(chanID2) if err != nil { t.Fatalf("error looking for edge: %v", chanID2) } @@ -387,7 +387,7 @@ func TestWakeUpOnStaleBranch(t *testing.T) { // The channel with chanID2 should not be in the database anymore, // since it is not confirmed on the longest chain. chanID1 should // still be. - _, _, has, isZombie, err = ctx.graph.HasChannelEdge(chanID1) + has, isZombie, err = ctx.graph.HasChannelEdge(chanID1) require.NoError(t, err) if !has { @@ -397,7 +397,7 @@ func TestWakeUpOnStaleBranch(t *testing.T) { t.Fatal("edge was marked as zombie") } - _, _, has, isZombie, err = ctx.graph.HasChannelEdge(chanID2) + has, isZombie, err = ctx.graph.HasChannelEdge(chanID2) if err != nil { t.Fatalf("error looking for edge: %v", chanID2) } @@ -527,7 +527,7 @@ func TestDisconnectedBlocks(t *testing.T) { } // Check that the fundingTxs are in the graph db. - _, _, has, isZombie, err := ctx.graph.HasChannelEdge(chanID1) + has, isZombie, err := ctx.graph.HasChannelEdge(chanID1) if err != nil { t.Fatalf("error looking for edge: %v", chanID1) } @@ -538,7 +538,7 @@ func TestDisconnectedBlocks(t *testing.T) { t.Fatal("edge was marked as zombie") } - _, _, has, isZombie, err = ctx.graph.HasChannelEdge(chanID2) + has, isZombie, err = ctx.graph.HasChannelEdge(chanID2) if err != nil { t.Fatalf("error looking for edge: %v", chanID2) } @@ -580,7 +580,7 @@ func TestDisconnectedBlocks(t *testing.T) { // chanID2 should not be in the database anymore, since it is not // confirmed on the longest chain. chanID1 should still be. - _, _, has, isZombie, err = ctx.graph.HasChannelEdge(chanID1) + has, isZombie, err = ctx.graph.HasChannelEdge(chanID1) if err != nil { t.Fatalf("error looking for edge: %v", chanID1) } @@ -591,7 +591,7 @@ func TestDisconnectedBlocks(t *testing.T) { t.Fatal("edge was marked as zombie") } - _, _, has, isZombie, err = ctx.graph.HasChannelEdge(chanID2) + has, isZombie, err = ctx.graph.HasChannelEdge(chanID2) if err != nil { t.Fatalf("error looking for edge: %v", chanID2) } @@ -665,7 +665,7 @@ func TestChansClosedOfflinePruneGraph(t *testing.T) { } // The router should now be aware of the channel we created above. - _, _, hasChan, isZombie, err := ctx.graph.HasChannelEdge( + hasChan, isZombie, err := ctx.graph.HasChannelEdge( chanID1.ToUint64(), ) if err != nil { @@ -747,7 +747,7 @@ func TestChansClosedOfflinePruneGraph(t *testing.T) { // At this point, the channel that was pruned should no longer be known // by the router. - _, _, hasChan, isZombie, err = ctx.graph.HasChannelEdge( + hasChan, isZombie, err = ctx.graph.HasChannelEdge( chanID1.ToUint64(), ) if err != nil { @@ -1719,7 +1719,7 @@ func assertChannelsPruned(t *testing.T, graph *graphdb.VersionedGraph, for _, channel := range channels { _, shouldPrune := pruned[channel.ChannelID] - _, _, exists, isZombie, err := graph.HasChannelEdge( + exists, isZombie, err := graph.HasChannelEdge( channel.ChannelID, ) if err != nil { diff --git a/graph/db/graph.go b/graph/db/graph.go index 41396aceb..3aa860cc1 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -661,11 +661,18 @@ func (c *ChannelGraph) DisabledChannelIDs() ([]uint64, error) { return c.db.DisabledChannelIDs() } -// HasChannelEdge returns true if the database knows of a channel edge. -func (c *ChannelGraph) HasChannelEdge(chanID uint64) (time.Time, time.Time, - bool, bool, error) { +// HasV1ChannelEdge returns true if the database knows of a channel edge. +func (c *ChannelGraph) HasV1ChannelEdge(chanID uint64) (time.Time, + time.Time, bool, bool, error) { - return c.db.HasChannelEdge(chanID) + return c.db.HasV1ChannelEdge(chanID) +} + +// HasChannelEdge returns true if the database knows of a channel edge. +func (c *ChannelGraph) HasChannelEdge(v lnwire.GossipVersion, + chanID uint64) (bool, bool, error) { + + return c.db.HasChannelEdge(v, chanID) } // AddEdgeProof sets the proof of an existing edge in the graph database. @@ -889,6 +896,14 @@ func (c *VersionedGraph) DeleteChannelEdges(strictZombiePruning, return err } +// HasChannelEdge returns true if the database knows of a channel edge with the +// passed channel ID and this graph's gossip version, and false otherwise. If it +// is not found, then the zombie index is checked and its result is returned as +// the second boolean. +func (c *VersionedGraph) HasChannelEdge(chanID uint64) (bool, bool, error) { + return c.db.HasChannelEdge(c.v, chanID) +} + // 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) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 9b923f662..d1aff61e1 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -886,7 +886,9 @@ func TestDisconnectBlockAtHeight(t *testing.T) { } // The two first edges should be removed from the db. - _, _, has, isZombie, err := graph.HasChannelEdge(edgeInfo.ChannelID) + has, isZombie, err := graph.HasChannelEdge( + lnwire.GossipVersion1, edgeInfo.ChannelID, + ) require.NoError(t, err, "unable to query for edge") if has { t.Fatalf("edge1 was not pruned from the graph") @@ -894,7 +896,9 @@ func TestDisconnectBlockAtHeight(t *testing.T) { if isZombie { t.Fatal("reorged edge1 should not be marked as zombie") } - _, _, has, isZombie, err = graph.HasChannelEdge(edgeInfo2.ChannelID) + has, isZombie, err = graph.HasChannelEdge( + lnwire.GossipVersion1, edgeInfo2.ChannelID, + ) require.NoError(t, err, "unable to query for edge") if has { t.Fatalf("edge2 was not pruned from the graph") @@ -904,7 +908,9 @@ func TestDisconnectBlockAtHeight(t *testing.T) { } // Edge 3 should not be removed. - _, _, has, isZombie, err = graph.HasChannelEdge(edgeInfo3.ChannelID) + has, isZombie, err = graph.HasChannelEdge( + lnwire.GossipVersion1, edgeInfo3.ChannelID, + ) require.NoError(t, err, "unable to query for edge") if !has { t.Fatalf("edge3 was pruned from the graph") @@ -1129,7 +1135,9 @@ func TestEdgeInfoUpdates(t *testing.T) { // Check for existence of the edge within the database, it should be // found. - _, _, found, isZombie, err := graph.HasChannelEdge(chanID) + found, isZombie, err := graph.HasChannelEdge( + lnwire.GossipVersion1, chanID, + ) require.NoError(t, err, "unable to query for edge") if !found { t.Fatalf("graph should have of inserted edge") @@ -3317,7 +3325,8 @@ func TestStressTestChannelGraphAPI(t *testing.T) { return nil } - _, _, _, _, err := graph.HasChannelEdge( + _, _, err := graph.HasChannelEdge( + lnwire.GossipVersion1, channel.id.ToUint64(), ) diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index 24a48e4c7..42f63f932 100644 --- a/graph/db/interfaces.go +++ b/graph/db/interfaces.go @@ -197,13 +197,20 @@ type Store interface { //nolint:interfacebloat AddChannelEdge(ctx context.Context, edge *models.ChannelEdgeInfo, op ...batch.SchedulerOption) error - // HasChannelEdge returns true if the database knows of a channel edge + // HasV1ChannelEdge returns true if the database knows of a channel edge // with the passed channel ID, and false otherwise. If an edge with that // ID is found within the graph, then two time stamps representing the // last time the edge was updated for both directed edges are returned // along with the boolean. If it is not found, then the zombie index is // checked and its result is returned as the second boolean. - HasChannelEdge(chanID uint64) (time.Time, time.Time, bool, bool, + HasV1ChannelEdge(chanID uint64) (time.Time, time.Time, bool, bool, + error) + + // HasChannelEdge returns true if the database knows of a channel edge + // with the passed channel ID and gossip version, and false otherwise. + // If it is not found, then the zombie index is checked and its result + // is returned as the second boolean. + HasChannelEdge(v lnwire.GossipVersion, chanID uint64) (bool, bool, error) // DeleteChannelEdges removes edges with the given channel IDs from the diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 5e71ad700..58b227a22 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -1179,8 +1179,11 @@ func (c *KVStore) AddChannelEdge(ctx context.Context, case alreadyExists: return ErrEdgeAlreadyExist default: - c.rejectCache.remove(lnwire.GossipVersion1, edge.ChannelID) + c.rejectCache.remove( + lnwire.GossipVersion1, edge.ChannelID, + ) c.chanCache.remove(edge.ChannelID) + return nil } }, @@ -1283,13 +1286,13 @@ func (c *KVStore) addChannelEdge(tx kvdb.RwTx, return chanIndex.Put(b.Bytes(), chanKey[:]) } -// HasChannelEdge returns true if the database knows of a channel edge with the -// passed channel ID, and false otherwise. If an edge with that ID is found -// within the graph, then two time stamps representing the last time the edge -// was updated for both directed edges are returned along with the boolean. If -// it is not found, then the zombie index is checked and its result is returned -// as the second boolean. -func (c *KVStore) HasChannelEdge( +// HasV1ChannelEdge returns true if the database knows of a channel edge +// with the passed channel ID, and false otherwise. If an edge with that ID +// is found within the graph, then two time stamps representing the last time +// the edge was updated for both directed edges are returned along with the +// boolean. If it is not found, then the zombie index is checked and its +// result is returned as the second boolean. +func (c *KVStore) HasV1ChannelEdge( chanID uint64) (time.Time, time.Time, bool, bool, error) { var ( @@ -1394,6 +1397,22 @@ func (c *KVStore) HasChannelEdge( return upd1Time, upd2Time, exists, isZombie, nil } +// HasChannelEdge returns true if the database knows of a channel edge with the +// passed channel ID and gossip version, and false otherwise. If it is not +// found, then the zombie index is checked and its result is returned as the +// second boolean. +func (c *KVStore) HasChannelEdge(v lnwire.GossipVersion, + chanID uint64) (bool, bool, error) { + + if v != lnwire.GossipVersion1 { + return false, false, ErrVersionNotSupportedForKVDB + } + + _, _, exists, isZombie, err := c.HasV1ChannelEdge(chanID) + + return exists, isZombie, err +} + // AddEdgeProof sets the proof of an existing edge in the graph database. func (c *KVStore) AddEdgeProof(chanID lnwire.ShortChannelID, proof *models.ChannelAuthProof) error { @@ -3265,7 +3284,8 @@ func (c *KVStore) updateEdgeCache(e *models.ChannelEdgePolicy, // the entry with the updated timestamp for the direction that was just // written. If the edge doesn't exist, we'll load the cache entry lazily // during the next query for this edge. - if entry, ok := c.rejectCache.get(lnwire.GossipVersion1, e.ChannelID); ok { + entry, ok := c.rejectCache.get(lnwire.GossipVersion1, e.ChannelID) + if ok { if isUpdate1 { entry.upd1Time = e.LastUpdate.Unix() } else { diff --git a/graph/db/reject_cache.go b/graph/db/reject_cache.go index bfe3c47ff..f54463224 100644 --- a/graph/db/reject_cache.go +++ b/graph/db/reject_cache.go @@ -59,7 +59,6 @@ type rejectCacheEntry struct { flags rejectFlags } -// newRejectCacheEntryV1 constructs a reject cache entry for v1 policies. func newRejectCacheEntryV1(upd1, upd2 time.Time, exists, isZombie bool) rejectCacheEntry { @@ -70,7 +69,6 @@ func newRejectCacheEntryV1(upd1, upd2 time.Time, exists, } } -// newRejectCacheEntryV2 constructs a reject cache entry for v2 policies. func newRejectCacheEntryV2(upd1, upd2 uint32, exists, isZombie bool) rejectCacheEntry { @@ -81,7 +79,6 @@ func newRejectCacheEntryV2(upd1, upd2 uint32, exists, } } -// updateRejectCacheEntryV1 updates the cached v1 timestamps. func updateRejectCacheEntryV1(entry *rejectCacheEntry, isUpdate1 bool, lastUpdate time.Time) { @@ -92,7 +89,6 @@ func updateRejectCacheEntryV1(entry *rejectCacheEntry, isUpdate1 bool, } } -// updateRejectCacheEntryV2 updates the cached v2 block heights. func updateRejectCacheEntryV2(entry *rejectCacheEntry, isUpdate1 bool, blockHeight uint32) { @@ -104,8 +100,7 @@ func updateRejectCacheEntryV2(entry *rejectCacheEntry, isUpdate1 bool, } // rejectCacheKey uniquely identifies a channel entry in the reject cache by -// gossip version and channel ID. This allows v1 and v2 policy state for the -// same channel ID to be cached independently. +// gossip version and channel ID. type rejectCacheKey struct { version lnwire.GossipVersion chanID uint64 diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index d72c8537f..00be30f38 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -747,8 +747,11 @@ func (s *SQLStore) AddChannelEdge(ctx context.Context, case alreadyExists: return ErrEdgeAlreadyExist default: - s.rejectCache.remove(edge.Version, edge.ChannelID) + s.rejectCache.remove( + edge.Version, edge.ChannelID, + ) s.chanCache.remove(edge.ChannelID) + return nil } }, @@ -2204,15 +2207,15 @@ func (s *SQLStore) FetchChannelEdgesByOutpoint(v lnwire.GossipVersion, return edge, policy1, policy2, nil } -// HasChannelEdge returns true if the database knows of a channel edge with the -// passed channel ID, and false otherwise. If an edge with that ID is found -// within the graph, then two time stamps representing the last time the edge -// was updated for both directed edges are returned along with the boolean. If -// it is not found, then the zombie index is checked and its result is returned -// as the second boolean. +// HasV1ChannelEdge returns true if the database knows of a channel edge +// with the passed channel ID, and false otherwise. If an edge with that ID +// is found within the graph, then two time stamps representing the last time +// the edge was updated for both directed edges are returned along with the +// boolean. If it is not found, then the zombie index is checked and its +// result is returned as the second boolean. // // NOTE: part of the Store interface. -func (s *SQLStore) HasChannelEdge(chanID uint64) (time.Time, time.Time, bool, +func (s *SQLStore) HasV1ChannelEdge(chanID uint64) (time.Time, time.Time, bool, bool, error) { ctx := context.TODO() @@ -2325,6 +2328,158 @@ func (s *SQLStore) HasChannelEdge(chanID uint64) (time.Time, time.Time, bool, return node1LastUpdate, node2LastUpdate, exists, isZombie, nil } +// HasChannelEdge returns true if the database knows of a channel edge with the +// passed channel ID and gossip version, and false otherwise. If an edge with +// that ID is found within the graph, then the zombie index is checked and its +// result is returned as the second boolean. +// +// NOTE: part of the Store interface. +func (s *SQLStore) HasChannelEdge(v lnwire.GossipVersion, + chanID uint64) (bool, bool, error) { + + if !isKnownGossipVersion(v) { + return false, false, fmt.Errorf( + "unsupported gossip version: %d", v, + ) + } + + ctx := context.TODO() + + var ( + exists bool + isZombie bool + node1LastUpdate time.Time + node2LastUpdate time.Time + node1Block uint32 + node2Block uint32 + ) + + // We'll query the cache with the shared lock held to allow multiple + // readers to access values in the cache concurrently if they exist. + s.cacheMu.RLock() + if entry, ok := s.rejectCache.get(v, chanID); ok { + s.cacheMu.RUnlock() + exists, isZombie = entry.flags.unpack() + return exists, isZombie, nil + } + s.cacheMu.RUnlock() + + s.cacheMu.Lock() + defer s.cacheMu.Unlock() + + // The item was not found with the shared lock, so we'll acquire the + // exclusive lock and check the cache again in case another method added + // the entry to the cache while no lock was held. + if entry, ok := s.rejectCache.get(v, chanID); ok { + exists, isZombie = entry.flags.unpack() + return exists, isZombie, nil + } + + chanIDB := channelIDToBytes(chanID) + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + channel, err := db.GetChannelBySCID( + ctx, sqlc.GetChannelBySCIDParams{ + Scid: chanIDB, + Version: int16(v), + }, + ) + if errors.Is(err, sql.ErrNoRows) { + // Check if it is a zombie channel. + isZombie, err = db.IsZombieChannel( + ctx, sqlc.IsZombieChannelParams{ + Scid: chanIDB, + Version: int16(v), + }, + ) + if err != nil { + return fmt.Errorf("could not check if channel "+ + "is zombie: %w", err) + } + + return nil + } else if err != nil { + return fmt.Errorf("unable to fetch channel: %w", err) + } + + exists = true + + policy1, err := db.GetChannelPolicyByChannelAndNode( + ctx, sqlc.GetChannelPolicyByChannelAndNodeParams{ + Version: int16(v), + ChannelID: channel.ID, + NodeID: channel.NodeID1, + }, + ) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("unable to fetch channel policy: %w", + err) + } else if err == nil { + switch v { + case lnwire.GossipVersion1: + if policy1.LastUpdate.Valid { + node1LastUpdate = time.Unix( + policy1.LastUpdate.Int64, 0, + ) + } + case lnwire.GossipVersion2: + if policy1.BlockHeight.Valid { + node1Block = uint32( + policy1.BlockHeight.Int64, + ) + } + } + } + + policy2, err := db.GetChannelPolicyByChannelAndNode( + ctx, sqlc.GetChannelPolicyByChannelAndNodeParams{ + Version: int16(v), + ChannelID: channel.ID, + NodeID: channel.NodeID2, + }, + ) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("unable to fetch channel policy: %w", + err) + } else if err == nil { + switch v { + case lnwire.GossipVersion1: + if policy2.LastUpdate.Valid { + node2LastUpdate = time.Unix( + policy2.LastUpdate.Int64, 0, + ) + } + case lnwire.GossipVersion2: + if policy2.BlockHeight.Valid { + node2Block = uint32( + policy2.BlockHeight.Int64, + ) + } + } + } + + return nil + }, sqldb.NoOpReset) + if err != nil { + return false, false, + fmt.Errorf("unable to fetch channel: %w", err) + } + + var entry rejectCacheEntry + switch v { + case lnwire.GossipVersion1: + entry = newRejectCacheEntryV1( + node1LastUpdate, node2LastUpdate, exists, isZombie, + ) + case lnwire.GossipVersion2: + entry = newRejectCacheEntryV2( + node1Block, node2Block, exists, isZombie, + ) + } + s.rejectCache.insert(v, chanID, entry) + + return exists, isZombie, nil +} + // ChannelID attempt to lookup the 8-byte compact channel ID which maps to the // passed channel point (outpoint). If the passed channel doesn't exist within // the database, then ErrEdgeNotFound is returned. @@ -3553,8 +3708,9 @@ func updateChanEdgePolicy(ctx context.Context, tx SQLQueries, if version == lnwire.GossipVersion1 { extra, err = marshalExtraOpaqueData(edge.ExtraOpaqueData) if err != nil { - return node1Pub, node2Pub, false, fmt.Errorf("unable to "+ - "marshal extra opaque data: %w", err) + return node1Pub, node2Pub, false, fmt.Errorf( + "unable to marshal extra opaque data: %w", err, + ) } } From 0cfb9c9d2c45d43aeba0da5169ec5b31062d5f17 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 3 Feb 2026 15:19:38 +0200 Subject: [PATCH 51/55] graph/db: add gossip version parameter to ForEachChannel Update ForEachChannel to accept a gossip version parameter, allowing callers to specify which gossip version's channels should be iterated. This change prepares the graph database for supporting multiple gossip versions while maintaining backward compatibility. The KVStore implementation validates that only GossipVersion1 is requested, returning ErrVersionNotSupportedForKVDB for other versions. The SQLStore implementation validates known versions and passes the version through to the underlying paginated query. Update VersionedGraph to include a ForEachChannel method that automatically uses its configured gossip version, and update the DescribeGraph RPC handler to use the v1Graph instead of the global graphDB. --- graph/db/benchmark_test.go | 82 +++++++++++++++++++------------------- graph/db/graph.go | 15 +++++-- graph/db/graph_test.go | 19 +++++---- graph/db/interfaces.go | 9 +++-- graph/db/kv_store.go | 6 ++- graph/db/sql_store.go | 17 +++++--- rpcserver.go | 9 +++-- 7 files changed, 92 insertions(+), 65 deletions(-) diff --git a/graph/db/benchmark_test.go b/graph/db/benchmark_test.go index d7cc3fbc9..00cfeaf33 100644 --- a/graph/db/benchmark_test.go +++ b/graph/db/benchmark_test.go @@ -370,8 +370,8 @@ func TestPopulateDBs(t *testing.T) { numPolicies = 0 ) err := graph.ForEachChannel( - ctx, func(info *models.ChannelEdgeInfo, - policy, + ctx, lnwire.GossipVersion1, + func(info *models.ChannelEdgeInfo, policy, policy2 *models.ChannelEdgePolicy) error { numChans++ @@ -497,48 +497,49 @@ func syncGraph(t *testing.T, src, dest *ChannelGraph) { } var wgChans sync.WaitGroup - err = src.ForEachChannel(ctx, func(info *models.ChannelEdgeInfo, - policy1, policy2 *models.ChannelEdgePolicy) error { + err = src.ForEachChannel(ctx, lnwire.GossipVersion1, + func(info *models.ChannelEdgeInfo, + policy1, policy2 *models.ChannelEdgePolicy) error { - // Add each channel & policy. We do this in a goroutine to - // take advantage of batch processing. - wgChans.Add(1) - go func() { - defer wgChans.Done() + // Add each channel & policy. We do this in a goroutine + // to take advantage of batch processing. + wgChans.Add(1) + go func() { + defer wgChans.Done() - err := dest.AddChannelEdge( - ctx, info, batch.LazyAdd(), - ) - if !errors.Is(err, ErrEdgeAlreadyExist) { - require.NoError(t, err) - } - - if policy1 != nil { - err = dest.UpdateEdgePolicy( - ctx, policy1, batch.LazyAdd(), + err := dest.AddChannelEdge( + ctx, info, batch.LazyAdd(), ) - require.NoError(t, err) - } + if !errors.Is(err, ErrEdgeAlreadyExist) { + require.NoError(t, err) + } - if policy2 != nil { - err = dest.UpdateEdgePolicy( - ctx, policy2, batch.LazyAdd(), - ) - require.NoError(t, err) - } + if policy1 != nil { + err = dest.UpdateEdgePolicy( + ctx, policy1, batch.LazyAdd(), + ) + require.NoError(t, err) + } - mu.Lock() - total++ - chunk++ - s.Do(func() { - reportChanStats() - chunk = 0 - }) - mu.Unlock() - }() + if policy2 != nil { + err = dest.UpdateEdgePolicy( + ctx, policy2, batch.LazyAdd(), + ) + require.NoError(t, err) + } - return nil - }, func() {}) + mu.Lock() + total++ + chunk++ + s.Do(func() { + reportChanStats() + chunk = 0 + }) + mu.Unlock() + }() + + return nil + }, func() {}) require.NoError(t, err) wgChans.Wait() @@ -638,7 +639,8 @@ func BenchmarkGraphReadMethods(b *testing.B) { fn: func(b testing.TB, store Store) { //nolint:ll err := store.ForEachChannel( - ctx, func(_ *models.ChannelEdgeInfo, + ctx, lnwire.GossipVersion1, + func(_ *models.ChannelEdgeInfo, _ *models.ChannelEdgePolicy, _ *models.ChannelEdgePolicy) error { @@ -821,7 +823,7 @@ func BenchmarkFindOptimalSQLQueryConfig(b *testing.B) { //nolint:ll err = store.ForEachChannel( - ctx, + ctx, lnwire.GossipVersion1, func(_ *models.ChannelEdgeInfo, _, _ *models.ChannelEdgePolicy) error { diff --git a/graph/db/graph.go b/graph/db/graph.go index 3aa860cc1..d7398e045 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -642,10 +642,11 @@ func (c *ChannelGraph) IsPublicNode(pubKey [33]byte) (bool, error) { // ForEachChannel iterates through all channel edges stored within the graph. func (c *ChannelGraph) ForEachChannel(ctx context.Context, - cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, - *models.ChannelEdgePolicy) error, reset func()) error { + v lnwire.GossipVersion, cb func(*models.ChannelEdgeInfo, + *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error, + reset func()) error { - return c.db.ForEachChannel(ctx, cb, reset) + return c.db.ForEachChannel(ctx, v, cb, reset) } // ForEachChannelCacheable iterates through all channel edges for the cache. @@ -904,6 +905,14 @@ func (c *VersionedGraph) HasChannelEdge(chanID uint64) (bool, bool, error) { return c.db.HasChannelEdge(c.v, chanID) } +// ForEachChannel iterates through all channel edges stored within the graph. +func (c *VersionedGraph) ForEachChannel(ctx context.Context, + cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy) error, reset func()) error { + + return c.db.ForEachChannel(ctx, c.v, cb, reset) +} + // 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) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index d1aff61e1..6a13e3203 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -1219,7 +1219,8 @@ func TestEdgePolicyCRUD(t *testing.T) { // assert that the deserialized policies match the original // ones. err := graph.ForEachChannel( - ctx, func(info *models.ChannelEdgeInfo, + ctx, lnwire.GossipVersion1, + func(info *models.ChannelEdgeInfo, policy1 *models.ChannelEdgePolicy, policy2 *models.ChannelEdgePolicy) error { @@ -1703,13 +1704,14 @@ func TestGraphTraversal(t *testing.T) { // Iterate through all the known channels within the graph DB, once // again if the map is empty that indicates that all edges have // properly been reached. - err = graph.ForEachChannel(ctx, func(ei *models.ChannelEdgeInfo, - _ *models.ChannelEdgePolicy, - _ *models.ChannelEdgePolicy) error { + err = graph.ForEachChannel(ctx, lnwire.GossipVersion1, + func(ei *models.ChannelEdgeInfo, + _ *models.ChannelEdgePolicy, + _ *models.ChannelEdgePolicy) error { - delete(chanIndex, ei.ChannelID) - return nil - }, func() {}) + delete(chanIndex, ei.ChannelID) + return nil + }, func() {}) require.NoError(t, err) require.Len(t, chanIndex, 0) @@ -2005,7 +2007,8 @@ func assertPruneTip(t *testing.T, graph *ChannelGraph, func assertNumChans(t *testing.T, graph *ChannelGraph, n int) { numChans := 0 err := graph.ForEachChannel( - t.Context(), func(*models.ChannelEdgeInfo, + t.Context(), lnwire.GossipVersion1, + func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error { diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index 42f63f932..c650bd23b 100644 --- a/graph/db/interfaces.go +++ b/graph/db/interfaces.go @@ -162,9 +162,12 @@ type Store interface { //nolint:interfacebloat // NOTE: If an edge can't be found, or wasn't advertised, then a nil // pointer for that particular channel edge routing policy will be // passed into the callback. - ForEachChannel(ctx context.Context, cb func(*models.ChannelEdgeInfo, - *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error, - reset func()) error + // + // TODO(elle): add a cross-version iteration API and make this iterate + // over all versions. + ForEachChannel(ctx context.Context, v lnwire.GossipVersion, + cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy) error, reset func()) error // ForEachChannelCacheable iterates through all the channel edges stored // within the graph and invokes the passed callback for each edge. The diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 58b227a22..03eaf02c7 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -411,10 +411,14 @@ func (c *KVStore) AddrsForNode(ctx context.Context, v lnwire.GossipVersion, // NOTE: If an edge can't be found, or wasn't advertised, then a nil pointer // for that particular channel edge routing policy will be passed into the // callback. -func (c *KVStore) ForEachChannel(_ context.Context, +func (c *KVStore) ForEachChannel(_ context.Context, v lnwire.GossipVersion, cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error, reset func()) error { + if v != lnwire.GossipVersion1 { + return ErrVersionNotSupportedForKVDB + } + return forEachChannel(c.db, cb, reset) } diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 00be30f38..b66e2fe49 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -1610,11 +1610,16 @@ func (s *SQLStore) ForEachChannelCacheable(cb func(*models.CachedEdgeInfo, // // NOTE: part of the Store interface. func (s *SQLStore) ForEachChannel(ctx context.Context, - cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, - *models.ChannelEdgePolicy) error, reset func()) error { + v lnwire.GossipVersion, cb func(*models.ChannelEdgeInfo, + *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error, + reset func()) error { + + if !isKnownGossipVersion(v) { + return fmt.Errorf("unsupported gossip version: %d", v) + } return s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { - return forEachChannelWithPolicies(ctx, db, s.cfg, cb) + return forEachChannelWithPolicies(ctx, db, s.cfg, v, cb) }, reset) } @@ -5973,8 +5978,8 @@ func forEachNodePaginated(ctx context.Context, cfg *sqldb.QueryConfig, // forEachChannelWithPolicies executes a paginated query to process each channel // with policies in the graph. func forEachChannelWithPolicies(ctx context.Context, db SQLQueries, - cfg *SQLStoreConfig, processChannel func(*models.ChannelEdgeInfo, - *models.ChannelEdgePolicy, + cfg *SQLStoreConfig, v lnwire.GossipVersion, + processChannel func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error) error { type channelBatchIDs struct { @@ -5988,7 +5993,7 @@ func forEachChannelWithPolicies(ctx context.Context, db SQLQueries, return db.ListChannelsWithPoliciesPaginated( ctx, sqlc.ListChannelsWithPoliciesPaginatedParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), ID: lastID, Limit: limit, }, diff --git a/rpcserver.go b/rpcserver.go index 927635a84..564d8cc32 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -6853,10 +6853,11 @@ func (r *rpcServer) DescribeGraph(ctx context.Context, } } - // Obtain the pointer to the global singleton channel graph, this will - // provide a consistent view of the graph due to bolt db's - // transactional model. - graph := r.server.graphDB + // Obtain the pointer to the V1 channel graph. This will provide a + // consistent view of the graph due to bolt db's transactional model. + // + // TODO(elle): switch to a cross-version graph view when available. + graph := r.server.v1Graph // First iterate through all the known nodes (connected or unconnected // within the graph), collating their current state into the RPC From 677e6ea89c9a386db22eacb6d169761b369eeedd Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 3 Feb 2026 15:44:05 +0200 Subject: [PATCH 52/55] sqldb: use version-specific staleness checks in UpsertChannelPolicy Update the UpsertChannelPolicy query to apply different staleness checks based on gossip version. For v1 policies, continue checking last_update timestamps. For v2 policies, check block_height instead, using >= comparison to handle policies from the same block. The version-specific WHERE clause ensures that policy updates are only applied when they contain newer information according to the versioning scheme appropriate for that gossip version. --- sqldb/sqlc/graph.sql.go | 13 ++++++++++++- sqldb/sqlc/queries/graph.sql | 13 ++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index 1d569b18e..aa2a04671 100644 --- a/sqldb/sqlc/graph.sql.go +++ b/sqldb/sqlc/graph.sql.go @@ -3804,7 +3804,18 @@ ON CONFLICT (channel_id, node_id, version) signature = EXCLUDED.signature, block_height = EXCLUDED.block_height, disable_flags = EXCLUDED.disable_flags -WHERE EXCLUDED.last_update > graph_channel_policies.last_update +WHERE ( + EXCLUDED.version = 1 AND ( + graph_channel_policies.last_update IS NULL + OR EXCLUDED.last_update > graph_channel_policies.last_update + ) +) +OR ( + EXCLUDED.version = 2 AND ( + graph_channel_policies.block_height IS NULL + OR EXCLUDED.block_height >= graph_channel_policies.block_height + ) +) RETURNING id ` diff --git a/sqldb/sqlc/queries/graph.sql b/sqldb/sqlc/queries/graph.sql index e5c18c39c..a4a42a1e3 100644 --- a/sqldb/sqlc/queries/graph.sql +++ b/sqldb/sqlc/queries/graph.sql @@ -954,7 +954,18 @@ ON CONFLICT (channel_id, node_id, version) signature = EXCLUDED.signature, block_height = EXCLUDED.block_height, disable_flags = EXCLUDED.disable_flags -WHERE EXCLUDED.last_update > graph_channel_policies.last_update +WHERE ( + EXCLUDED.version = 1 AND ( + graph_channel_policies.last_update IS NULL + OR EXCLUDED.last_update > graph_channel_policies.last_update + ) +) +OR ( + EXCLUDED.version = 2 AND ( + graph_channel_policies.block_height IS NULL + OR EXCLUDED.block_height >= graph_channel_policies.block_height + ) +) RETURNING id; -- name: GetChannelPolicyByChannelAndNode :one From d948c5bfad3ca4a6e6de3f721c88e7c67feb08d6 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 3 Feb 2026 15:44:14 +0200 Subject: [PATCH 53/55] graph/db: convert testEdgePolicyCRUD to versioned test Convert testEdgePolicyCRUD to a versioned test that runs against both v1 and v2 gossip versions. Update the test to use version-specific edge creation helpers and to test version-specific fields and flag behavior (ChannelFlags/MessageFlags for v1, DisableFlags/ ExtraSignedFields for v2). --- graph/db/graph_test.go | 63 ++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 6a13e3203..3e71f5e6e 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -146,6 +146,10 @@ var versionedTests = []versionedTest{ name: "edge insertion deletion", test: testEdgeInsertionDeletion, }, + { + name: "edge policy crud", + test: testEdgePolicyCRUD, + }, { name: "partial node", test: testPartialNode, @@ -1182,18 +1186,28 @@ func TestEdgeInfoUpdates(t *testing.T) { assertEdgeInfoEqual(t, dbEdgeInfo, edgeInfo) } -// TestEdgePolicyCRUD tests basic CRUD operations for edge policies. -func TestEdgePolicyCRUD(t *testing.T) { +// testEdgePolicyCRUD tests basic CRUD operations for edge policies. +func testEdgePolicyCRUD(t *testing.T, v lnwire.GossipVersion) { t.Parallel() ctx := t.Context() - graph := MakeTestGraph(t) + graph := NewVersionedGraph(MakeTestGraph(t), v) - node1 := createTestVertex(t, lnwire.GossipVersion1) - node2 := createTestVertex(t, lnwire.GossipVersion1) + node1 := createTestVertex(t, v) + node2 := createTestVertex(t, v) // Create an edge. Don't add it to the DB yet. - edgeInfo, edge1, edge2 := createChannelEdge(node1, node2) + edgeInfo, shortChanID := createEdge( + v, 100, 1, 0, 0, node1, node2, + ) + chanID := shortChanID.ToUint64() + + edge1 := newEdgePolicy(v, chanID, nextUpdateTime().Unix(), true) + edge2 := newEdgePolicy(v, chanID, nextUpdateTime().Unix(), false) + edge1.ToNode = edgeInfo.NodeKey2Bytes + edge2.ToNode = edgeInfo.NodeKey1Bytes + edge1.SigBytes = testSig.Serialize() + edge2.SigBytes = testSig.Serialize() updateAndAssertPolicies := func() { // Make copies of the policies before calling UpdateEdgePolicy @@ -1202,8 +1216,14 @@ func TestEdgePolicyCRUD(t *testing.T) { edge1 := copyEdgePolicy(edge1) edge2 := copyEdgePolicy(edge2) - edge1.LastUpdate = nextUpdateTime() - edge2.LastUpdate = nextUpdateTime() + switch v { + case lnwire.GossipVersion1: + edge1.LastUpdate = nextUpdateTime() + edge2.LastUpdate = nextUpdateTime() + case lnwire.GossipVersion2: + edge1.LastBlockHeight = nextBlockHeight() + edge2.LastBlockHeight = nextBlockHeight() + } require.NoError(t, graph.UpdateEdgePolicy(ctx, edge1)) require.NoError(t, graph.UpdateEdgePolicy(ctx, edge2)) @@ -1219,7 +1239,7 @@ func TestEdgePolicyCRUD(t *testing.T) { // assert that the deserialized policies match the original // ones. err := graph.ForEachChannel( - ctx, lnwire.GossipVersion1, + ctx, func(info *models.ChannelEdgeInfo, policy1 *models.ChannelEdgePolicy, policy2 *models.ChannelEdgePolicy) error { @@ -1251,13 +1271,26 @@ func TestEdgePolicyCRUD(t *testing.T) { updateAndAssertPolicies() - // Update one of the edges to have ChannelFlags include a bit unknown - // to us. - edge1.ChannelFlags |= 1 << 6 + switch v { + case lnwire.GossipVersion1: + // Update one of the edges to have ChannelFlags include a bit + // unknown to us. + edge1.ChannelFlags |= 1 << 6 - // Update the other edge to have MessageFlags include a bit unknown to - // us. - edge2.MessageFlags |= 1 << 4 + // Update the other edge to have MessageFlags include a bit + // unknown to us. + edge2.MessageFlags |= 1 << 4 + + case lnwire.GossipVersion2: + // Update one of the edges to have DisableFlags include a bit + // unknown to us. + edge1.DisableFlags |= 1 << 6 + + // Update the other edge to have a modified extra signed field. + edge2.ExtraSignedFields = map[uint64][]byte{ + 200: {0x4, 0x5}, + } + } updateAndAssertPolicies() } From 1e35f258e413b3e7234a8ef34e8657626ab710f8 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 3 Feb 2026 16:04:07 +0200 Subject: [PATCH 54/55] graph/db: add gossip version parameter to ForEachNodeChannel Update ForEachNodeChannel to accept a gossip version parameter, allowing callers to specify which gossip version's channels should be iterated. This change mirrors the approach taken in ForEachChannel and prepares the graph database for supporting multiple gossip versions while maintaining backward compatibility. The Store interface is updated to include the version parameter, and both KVStore and SQLStore implementations are updated accordingly: - KVStore validates that only GossipVersion1 is requested, returning ErrVersionNotSupportedForKVDB for other versions. - SQLStore passes the version through to the underlying node query, enabling version-specific channel iteration. The ChannelGraph wrapper is updated to accept and pass through the version parameter. VersionedGraph gains a ForEachNodeChannel method that automatically uses its configured gossip version, providing a clean interface for version-specific operations. Update all call sites to explicitly pass lnwire.GossipVersion1, except for the local channel manager in server.go which now uses the v1Graph directly (matching the pattern used in other parts of the codebase). --- graph/builder.go | 2 +- graph/db/graph.go | 14 ++++++- graph/db/graph_test.go | 87 +++++++++++++++++------------------------- graph/db/interfaces.go | 5 ++- graph/db/kv_store.go | 7 +++- graph/db/sql_store.go | 16 ++++---- server.go | 3 +- 7 files changed, 68 insertions(+), 66 deletions(-) diff --git a/graph/builder.go b/graph/builder.go index 2cf4d1f34..9d3baebfe 100644 --- a/graph/builder.go +++ b/graph/builder.go @@ -1276,7 +1276,7 @@ func (b *Builder) ForAllOutgoingChannels(ctx context.Context, reset func()) error { return b.cfg.Graph.ForEachNodeChannel( - ctx, b.cfg.SelfNode, + ctx, lnwire.GossipVersion1, b.cfg.SelfNode, func(c *models.ChannelEdgeInfo, e *models.ChannelEdgePolicy, _ *models.ChannelEdgePolicy) error { diff --git a/graph/db/graph.go b/graph/db/graph.go index d7398e045..baee3eccb 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -597,11 +597,12 @@ func (c *ChannelGraph) ForEachSourceNodeChannel(ctx context.Context, // ForEachNodeChannel iterates through all channels of the given node. func (c *ChannelGraph) ForEachNodeChannel(ctx context.Context, - nodePub route.Vertex, cb func(*models.ChannelEdgeInfo, + v lnwire.GossipVersion, nodePub route.Vertex, + cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error, reset func()) error { - return c.db.ForEachNodeChannel(ctx, nodePub, cb, reset) + return c.db.ForEachNodeChannel(ctx, v, nodePub, cb, reset) } // ForEachNode iterates through all stored vertices/nodes in the graph. @@ -905,6 +906,15 @@ func (c *VersionedGraph) HasChannelEdge(chanID uint64) (bool, bool, error) { return c.db.HasChannelEdge(c.v, chanID) } +// ForEachNodeChannel iterates through all channels of the given node. +func (c *VersionedGraph) ForEachNodeChannel(ctx context.Context, + nodePub route.Vertex, cb func(*models.ChannelEdgeInfo, + *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy) error, reset func()) error { + + return c.db.ForEachNodeChannel(ctx, c.v, nodePub, cb, reset) +} + // ForEachChannel iterates through all channel edges stored within the graph. func (c *VersionedGraph) ForEachChannel(ctx context.Context, cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 3e71f5e6e..2edfc064a 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -150,6 +150,10 @@ var versionedTests = []versionedTest{ name: "edge policy crud", test: testEdgePolicyCRUD, }, + { + name: "incomplete channel policies", + test: testIncompleteChannelPolicies, + }, { name: "partial node", test: testPartialNode, @@ -1753,7 +1757,7 @@ func TestGraphTraversal(t *testing.T) { numNodeChans := 0 firstNode, secondNode := nodeList[0], nodeList[1] err = graph.ForEachNodeChannel( - ctx, firstNode.PubKeyBytes, + ctx, lnwire.GossipVersion1, firstNode.PubKeyBytes, func(_ *models.ChannelEdgeInfo, outEdge, inEdge *models.ChannelEdgePolicy) error { @@ -3810,32 +3814,26 @@ func TestFetchChanInfos(t *testing.T) { } } -// TestIncompleteChannelPolicies tests that a channel that only has a policy +// testIncompleteChannelPolicies tests that a channel that only has a policy // specified on one end is properly returned in ForEachChannel calls from // both sides. -func TestIncompleteChannelPolicies(t *testing.T) { +func testIncompleteChannelPolicies(t *testing.T, v lnwire.GossipVersion) { t.Parallel() ctx := t.Context() - graph := MakeTestGraph(t) + graph := NewVersionedGraph(MakeTestGraph(t), v) // Create two nodes. - node1 := createTestVertex(t, lnwire.GossipVersion1) - if err := graph.AddNode(ctx, node1); err != nil { - t.Fatalf("unable to add node: %v", err) - } - node2 := createTestVertex(t, lnwire.GossipVersion1) - if err := graph.AddNode(ctx, node2); err != nil { - t.Fatalf("unable to add node: %v", err) - } + node1 := createTestVertex(t, v) + require.NoError(t, graph.AddNode(ctx, node1)) + node2 := createTestVertex(t, v) + require.NoError(t, graph.AddNode(ctx, node2)) channel, chanID := createEdge( - lnwire.GossipVersion1, uint32(0), 0, 0, 0, node1, node2, + v, uint32(0), 0, 0, 0, node1, node2, ) - if err := graph.AddChannelEdge(ctx, channel); err != nil { - t.Fatalf("unable to create channel edge: %v", err) - } + require.NoError(t, graph.AddChannelEdge(ctx, channel)) // Ensure that channel is reported with unknown policies. checkPolicies := func(node *models.Node, expectedIn, @@ -3847,21 +3845,8 @@ func TestIncompleteChannelPolicies(t *testing.T) { func(_ *models.ChannelEdgeInfo, outEdge, inEdge *models.ChannelEdgePolicy) error { - if !expectedOut && outEdge != nil { - t.Fatalf("Expected no outgoing policy") - } - - if expectedOut && outEdge == nil { - t.Fatalf("Expected an outgoing policy") - } - - if !expectedIn && inEdge != nil { - t.Fatalf("Expected no incoming policy") - } - - if expectedIn && inEdge == nil { - t.Fatalf("Expected an incoming policy") - } + require.Equal(t, expectedOut, outEdge != nil) + require.Equal(t, expectedIn, inEdge != nil) calls++ @@ -3874,34 +3859,30 @@ func TestIncompleteChannelPolicies(t *testing.T) { checkPolicies(node2, false, false) + newTestEdgePolicy := func(isNode1 bool, + toNode route.Vertex) *models.ChannelEdgePolicy { + + policy := newEdgePolicy( + v, chanID.ToUint64(), nextUpdateTime().Unix(), isNode1, + ) + policy.ToNode = toNode + policy.SigBytes = testSig.Serialize() + + return policy + } + // Only create an edge policy for node1 and leave the policy for node2 // unknown. - updateTime := time.Unix(1234, 0) - - edgePolicy := newEdgePolicy( - lnwire.GossipVersion1, chanID.ToUint64(), updateTime.Unix(), - true, - ) - edgePolicy.ToNode = node2.PubKeyBytes - edgePolicy.SigBytes = testSig.Serialize() - if err := graph.UpdateEdgePolicy(ctx, edgePolicy); err != nil { - t.Fatalf("unable to update edge: %v", err) - } + edgePolicy := newTestEdgePolicy(true, node2.PubKeyBytes) + require.NoError(t, graph.UpdateEdgePolicy(ctx, edgePolicy)) checkPolicies(node1, false, true) checkPolicies(node2, true, false) // Create second policy and assert that both policies are reported // as present. - edgePolicy = newEdgePolicy( - lnwire.GossipVersion1, chanID.ToUint64(), updateTime.Unix(), - false, - ) - edgePolicy.ToNode = node1.PubKeyBytes - edgePolicy.SigBytes = testSig.Serialize() - if err := graph.UpdateEdgePolicy(ctx, edgePolicy); err != nil { - t.Fatalf("unable to update edge: %v", err) - } + edgePolicy = newTestEdgePolicy(false, node1.PubKeyBytes) + require.NoError(t, graph.UpdateEdgePolicy(ctx, edgePolicy)) checkPolicies(node1, true, true) checkPolicies(node2, true, true) @@ -5024,7 +5005,9 @@ func BenchmarkForEachChannel(b *testing.B) { return nil } - err := graph.ForEachNodeChannel(ctx, n, cb, func() {}) + err := graph.ForEachNodeChannel( + ctx, lnwire.GossipVersion1, n, cb, func() {}, + ) require.NoError(b, err) } } diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go index c650bd23b..0936386be 100644 --- a/graph/db/interfaces.go +++ b/graph/db/interfaces.go @@ -73,8 +73,9 @@ type Store interface { //nolint:interfacebloat // to the caller. // // Unknown policies are passed into the callback as nil values. - ForEachNodeChannel(ctx context.Context, nodePub route.Vertex, - cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + ForEachNodeChannel(ctx context.Context, v lnwire.GossipVersion, + nodePub route.Vertex, cb func(*models.ChannelEdgeInfo, + *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error, reset func()) error // ForEachNodeCached is similar to forEachNode, but it returns diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index 03eaf02c7..22c87f0ee 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -3690,10 +3690,15 @@ func nodeTraversal(tx kvdb.RTx, nodePub []byte, db kvdb.Backend, // halted with the error propagated back up to the caller. // // Unknown policies are passed into the callback as nil values. -func (c *KVStore) ForEachNodeChannel(_ context.Context, nodePub route.Vertex, +func (c *KVStore) ForEachNodeChannel(_ context.Context, + v lnwire.GossipVersion, nodePub route.Vertex, cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error, reset func()) error { + if v != lnwire.GossipVersion1 { + return ErrVersionNotSupportedForKVDB + } + return nodeTraversal( nil, nodePub[:], c.db, func(_ kvdb.RTx, info *models.ChannelEdgeInfo, policy, diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index b66e2fe49..44e2fd75b 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -913,7 +913,7 @@ func (s *SQLStore) ForEachSourceNodeChannel(ctx context.Context, } return forEachNodeChannel( - ctx, db, s.cfg, nodeID, + ctx, db, s.cfg, lnwire.GossipVersion1, nodeID, func(info *models.ChannelEdgeInfo, outPolicy *models.ChannelEdgePolicy, _ *models.ChannelEdgePolicy) error { @@ -1027,14 +1027,15 @@ func (s *SQLStore) ForEachNodeCacheable(ctx context.Context, // Unknown policies are passed into the callback as nil values. // // NOTE: part of the Store interface. -func (s *SQLStore) ForEachNodeChannel(ctx context.Context, nodePub route.Vertex, +func (s *SQLStore) ForEachNodeChannel(ctx context.Context, + v lnwire.GossipVersion, nodePub route.Vertex, cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error, reset func()) error { return s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { dbNode, err := db.GetNodeByPubKey( ctx, sqlc.GetNodeByPubKeyParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), PubKey: nodePub[:], }, ) @@ -1044,7 +1045,7 @@ func (s *SQLStore) ForEachNodeChannel(ctx context.Context, nodePub route.Vertex, return fmt.Errorf("unable to fetch node: %w", err) } - return forEachNodeChannel(ctx, db, s.cfg, dbNode.ID, cb) + return forEachNodeChannel(ctx, db, s.cfg, v, dbNode.ID, cb) }, reset) } @@ -3524,14 +3525,15 @@ func forEachNodeCacheable(ctx context.Context, cfg *sqldb.QueryConfig, // edge information, the outgoing policy and the incoming policy for the // channel and node combo. func forEachNodeChannel(ctx context.Context, db SQLQueries, - cfg *SQLStoreConfig, id int64, cb func(*models.ChannelEdgeInfo, + cfg *SQLStoreConfig, v lnwire.GossipVersion, id int64, + cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error) error { - // Get all the V1 channels for this node. + // Get all the channels for this node. rows, err := db.ListChannelsByNodeID( ctx, sqlc.ListChannelsByNodeIDParams{ - Version: int16(lnwire.GossipVersion1), + Version: int16(v), NodeID1: id, }, ) diff --git a/server.go b/server.go index 7ba74176a..ad97261ed 100644 --- a/server.go +++ b/server.go @@ -1131,7 +1131,8 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr, *models.ChannelEdgePolicy) error, reset func()) error { - return s.graphDB.ForEachNodeChannel(ctx, selfVertex, + return s.v1Graph.ForEachNodeChannel( + ctx, selfVertex, func(c *models.ChannelEdgeInfo, e *models.ChannelEdgePolicy, _ *models.ChannelEdgePolicy) error { From 4d4b42d0ea6f707125b0a12bd1bff8133895d58e Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 3 Feb 2026 16:31:29 +0200 Subject: [PATCH 55/55] docs: add release notes for gossip v2 policy support --- discovery/gossiper_test.go | 4 +++- docs/release-notes/release-notes-0.21.0.md | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/discovery/gossiper_test.go b/discovery/gossiper_test.go index b1231b0e3..74409196b 100644 --- a/discovery/gossiper_test.go +++ b/discovery/gossiper_test.go @@ -4448,7 +4448,9 @@ func TestRateLimitChannelUpdates(t *testing.T) { // our rebroadcast interval. rateLimitKeepAliveUpdate := *batch.chanUpdAnn1 rateLimitKeepAliveUpdate.Timestamp++ - require.NoError(t, signUpdate(remoteKeyPriv1, &rateLimitKeepAliveUpdate)) + require.NoError( + t, signUpdate(remoteKeyPriv1, &rateLimitKeepAliveUpdate), + ) assertRateLimit(&rateLimitKeepAliveUpdate, nodePeer1, true) keepAliveUpdate := *batch.chanUpdAnn1 diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index 1c74a245e..f81ba63dc 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -179,7 +179,8 @@ * 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) - [3](https://github.com/lightningnetwork/lnd/pull/10380). + [3](https://github.com/lightningnetwork/lnd/pull/10380) + [4](https://github.com/lightningnetwork/lnd/pull/10542). ## Code Health