From 575766c5f4590af441244ec788bad4ac60da53e9 Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 18 Nov 2025 23:35:02 +0100 Subject: [PATCH 1/3] graph: fix graph cache population for channels with both policies disabled Fix a bug where channels with both policies disabled were not added to the graph cache during startup. When a policy update later re-enabled one of the directions, the update would succeed in the database but fail to update the graph cache (since the channel structure was never added), preventing the channel from being used for routing. --- graph/db/graph_cache.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/graph/db/graph_cache.go b/graph/db/graph_cache.go index a691361a2..593483747 100644 --- a/graph/db/graph_cache.go +++ b/graph/db/graph_cache.go @@ -121,13 +121,9 @@ func (c *GraphCache) AddChannel(info *models.CachedEdgeInfo, return } - if policy1 != nil && policy1.IsDisabled() && - policy2 != nil && policy2.IsDisabled() { - - return - } - - // Create the edge entry for both nodes. + // Create the edge entry for both nodes. We always add the channel + // structure to the cache, even if both policies are currently disabled, + // so that later policy updates can find and update the channel entry. c.mtx.Lock() c.updateOrAddEdge(info.NodeKey1Bytes, &DirectedChannel{ ChannelID: info.ChannelID, @@ -143,6 +139,19 @@ func (c *GraphCache) AddChannel(info *models.CachedEdgeInfo, }) c.mtx.Unlock() + // 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() { + + log.Debugf("Skipping policies for channel %v: both "+ + "policies are disabled (channel structure still "+ + "cached for future updates)", info.ChannelID) + + return + } + // The policy's node is always the to_node. So if policy 1 has to_node // of node 2 then we have the policy 1 as seen from node 1. if policy1 != nil { From 5225b9bbbc3f52c33024ef55c9e431a66d69cd30 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 19 Nov 2025 10:32:04 +0100 Subject: [PATCH 2/3] graph: add regression test for the fixed behaviour --- graph/db/graph_cache_test.go | 110 +++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/graph/db/graph_cache_test.go b/graph/db/graph_cache_test.go index 43c35862e..89e3a7e87 100644 --- a/graph/db/graph_cache_test.go +++ b/graph/db/graph_cache_test.go @@ -139,3 +139,113 @@ func assertCachedPolicyEqual(t *testing.T, original, require.Equal(t, original.ToNodePubKey(), cached.ToNodePubKey()) } } + +// TestGraphCacheDisabledPoliciesRegression is a regression test for the bug +// where channels with both policies disabled were not added to the graph cache +// during population, preventing future policy updates from working. +// +// The bug flow was: +// 1. Channel with both policies disabled exists in DB. +// 2. populateCache skips adding it to graph cache entirely. +// 3. Later, a policy update arrives enabling one direction. +// 4. UpdateEdgePolicy updates the DB successfully. +// 5. UpdateEdgePolicy tries to update graph cache but channel not found. +// 6. Channel never becomes usable for routing. +func TestGraphCacheDisabledPoliciesRegression(t *testing.T) { + t.Parallel() + + // Create a simple cache instance. + cache := NewGraphCache(10) + + // Simulate a channel with both policies disabled. + chanID := uint64(12345) + node1 := pubKey1 + node2 := pubKey2 + + edgeInfo := &models.CachedEdgeInfo{ + ChannelID: chanID, + NodeKey1Bytes: node1, + NodeKey2Bytes: node2, + Capacity: 1000000, + } + + // Create two disabled policies. + disabledPolicy1 := &models.CachedEdgePolicy{ + ChannelID: chanID, + ChannelFlags: lnwire.ChanUpdateDisabled, + } + disabledPolicy2 := &models.CachedEdgePolicy{ + ChannelID: chanID, + ChannelFlags: lnwire.ChanUpdateDisabled | + lnwire.ChanUpdateDirection, + } + + // Add the channel with both policies disabled (simulating + // populateCache). + cache.AddChannel(edgeInfo, disabledPolicy1, disabledPolicy2) + + // Verify the channel structure was added to cache. + var foundChannels []*DirectedChannel + err := cache.ForEachChannel(node1, func(c *DirectedChannel) error { + if c.ChannelID == chanID { + foundChannels = append(foundChannels, c) + } + + return nil + }) + require.NoError(t, err) + require.Len(t, foundChannels, 1, + "channel structure should be in cache even when both "+ + "policies are disabled") + + // Verify policies were NOT added (both disabled). + require.False(t, foundChannels[0].OutPolicySet, + "disabled outgoing policy should not be set in cache") + require.Nil(t, foundChannels[0].InPolicy, + "disabled incoming policy should not be set in cache") + + // Now simulate receiving a fresh update enabling one direction. + enabledPolicy1 := &models.CachedEdgePolicy{ + ChannelID: chanID, + ChannelFlags: 0, // NOT disabled anymore + TimeLockDelta: 40, + MinHTLC: lnwire.MilliSatoshi(1000), + } + + // Update the policy (simulating what UpdateEdgePolicy does). + cache.UpdatePolicy(enabledPolicy1, node1, node2) + + // Verify the policy update succeeded. Before the fix, UpdatePolicy + // would log "Channel not found in graph cache" and return early, + // so the policy would never be added. + foundChannels = nil + err = cache.ForEachChannel(node1, func(c *DirectedChannel) error { + if c.ChannelID == chanID { + foundChannels = append(foundChannels, c) + } + + return nil + }) + require.NoError(t, err) + require.Len(t, foundChannels, 1) + + // The policy should now be set. + require.True(t, foundChannels[0].OutPolicySet, + "REGRESSION: policy update should work even for channels that "+ + "had both policies disabled initially") + + // Verify we can also see it from node2's perspective. + foundChannels = nil + err = cache.ForEachChannel(node2, func(c *DirectedChannel) error { + if c.ChannelID == chanID { + foundChannels = append(foundChannels, c) + } + + return nil + }) + require.NoError(t, err) + require.Len(t, foundChannels, 1) + require.NotNil(t, foundChannels[0].InPolicy, + "incoming policy should be set after policy update") + require.Equal(t, uint16(40), foundChannels[0].InPolicy.TimeLockDelta) +} From 4f40d45d06c095beb26da425b3f31ffe9b6df078 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 19 Nov 2025 10:11:40 +0100 Subject: [PATCH 3/3] docs: add release-notes for 20.1 --- docs/release-notes/release-notes-0.20.1.md | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/release-notes/release-notes-0.20.1.md diff --git a/docs/release-notes/release-notes-0.20.1.md b/docs/release-notes/release-notes-0.20.1.md new file mode 100644 index 000000000..1963db61b --- /dev/null +++ b/docs/release-notes/release-notes-0.20.1.md @@ -0,0 +1,61 @@ +# Release Notes +- [Bug Fixes](#bug-fixes) +- [New Features](#new-features) + - [Functional Enhancements](#functional-enhancements) + - [RPC Additions](#rpc-additions) + - [lncli Additions](#lncli-additions) +- [Improvements](#improvements) + - [Functional Updates](#functional-updates) + - [RPC Updates](#rpc-updates) + - [lncli Updates](#lncli-updates) + - [Breaking Changes](#breaking-changes) + - [Performance Improvements](#performance-improvements) + - [Deprecations](#deprecations) +- [Technical and Architectural Updates](#technical-and-architectural-updates) + - [BOLT Spec Updates](#bolt-spec-updates) + - [Testing](#testing) + - [Database](#database) + - [Code Health](#code-health) + - [Tooling and Documentation](#tooling-and-documentation) +- [Contributors (Alphabetical Order)](#contributors) + +# Bug Fixes + +* Fix bug where channels with both [policies disabled at startup could never + be used for routing](https://github.com/lightningnetwork/lnd/pull/10378) + +# New Features + +## Functional Enhancements + +## RPC Additions + +## lncli Additions + +# Improvements +## Functional Updates + +## RPC Updates + +## lncli Updates + +## Breaking Changes + +## Performance Improvements + +## Deprecations + +# Technical and Architectural Updates +## BOLT Spec Updates + +## Testing + +## Database + +## Code Health + +## Tooling and Documentation + +# Contributors (Alphabetical Order) + +* Ziggie