lnd/graph/db/graph_cache_test.go
Elle Mouton 9c7a12c0e0
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.
2026-02-13 07:21:24 +02:00

256 lines
7.2 KiB
Go

package graphdb
import (
"encoding/hex"
"testing"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)
var (
pubKey1Bytes, _ = hex.DecodeString(
"0248f5cba4c6da2e4c9e01e81d1404dfac0cbaf3ee934a4fc117d2ea9a64" +
"22c91d",
)
pubKey2Bytes, _ = hex.DecodeString(
"038155ba86a8d3b23c806c855097ca5c9fa0f87621f1e7a7d2835ad057f6" +
"f4484f",
)
pubKey1, _ = route.NewVertexFromBytes(pubKey1Bytes)
pubKey2, _ = route.NewVertexFromBytes(pubKey2Bytes)
)
// TestGraphCacheAddNode tests that a channel going from node A to node B can be
// cached correctly, independent of the direction we add the channel as.
func TestGraphCacheAddNode(t *testing.T) {
t.Parallel()
runTest := func(nodeA, nodeB route.Vertex) {
t.Helper()
isNode1A, isNode1B := true, false
if nodeA == pubKey2 {
isNode1A, isNode1B = false, true
}
inboundFee := lnwire.Fee{
BaseFee: 10,
FeeRate: 20,
}
outPolicy1 := &models.CachedEdgePolicy{
ChannelID: 1000,
IsNode1: isNode1A,
IsDisabled: false,
ToNodePubKey: func() route.Vertex {
return nodeB
},
// Define an inbound fee.
InboundFee: fn.Some(inboundFee),
}
inPolicy1 := &models.CachedEdgePolicy{
ChannelID: 1000,
IsNode1: isNode1B,
IsDisabled: false,
ToNodePubKey: func() route.Vertex {
return nodeA
},
}
cache := NewGraphCache(10)
cache.AddNodeFeatures(nodeA, lnwire.EmptyFeatureVector())
cache.AddChannel(&models.CachedEdgeInfo{
ChannelID: 1000,
// Those are direction independent!
NodeKey1Bytes: pubKey1,
NodeKey2Bytes: pubKey2,
Capacity: 500,
}, outPolicy1, inPolicy1)
var fromChannels, toChannels []*DirectedChannel
_ = cache.ForEachChannel(nodeA, func(c *DirectedChannel) error {
fromChannels = append(fromChannels, c)
return nil
})
_ = cache.ForEachChannel(nodeB, func(c *DirectedChannel) error {
toChannels = append(toChannels, c)
return nil
})
require.Len(t, fromChannels, 1)
require.Len(t, toChannels, 1)
require.Equal(t, outPolicy1 != nil, fromChannels[0].OutPolicySet)
assertCachedPolicyEqual(t, inPolicy1, fromChannels[0].InPolicy)
require.Equal(t, inPolicy1 != nil, toChannels[0].OutPolicySet)
assertCachedPolicyEqual(t, outPolicy1, toChannels[0].InPolicy)
// Now that we've inserted two nodes into the graph, check that
// we'll recover the same set of channels during forEachNode.
nodes := make(map[route.Vertex]struct{})
chans := make(map[uint64]struct{})
_ = cache.ForEachNode(func(node route.Vertex,
edges map[uint64]*DirectedChannel) error {
nodes[node] = struct{}{}
for chanID, directedChannel := range edges {
chans[chanID] = struct{}{}
if node == nodeA {
require.NotZero(
t, directedChannel.InboundFee,
)
} else {
require.Zero(
t, directedChannel.InboundFee,
)
}
}
return nil
})
require.Len(t, nodes, 2)
require.Len(t, chans, 1)
}
runTest(pubKey1, pubKey2)
runTest(pubKey2, pubKey1)
}
func assertCachedPolicyEqual(t *testing.T, original,
cached *models.CachedEdgePolicy) {
require.Equal(t, original.ChannelID, cached.ChannelID)
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)
require.Equal(t, original.FeeBaseMSat, cached.FeeBaseMSat)
require.Equal(
t, original.FeeProportionalMillionths,
cached.FeeProportionalMillionths,
)
if original.ToNodePubKey != nil {
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,
IsNode1: true,
IsDisabled: true,
}
disabledPolicy2 := &models.CachedEdgePolicy{
ChannelID: chanID,
IsNode1: false,
IsDisabled: true,
}
// 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,
IsNode1: true,
IsDisabled: false,
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)
}