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.
This commit is contained in:
Elle Mouton committed 2026-02-13 07:21:24 +02:00
1 parent 40e3ac964f
commit 4a6213c35e
4 files changed
+141 -49

No files matched your search

+11 -11
View File
@@ -1179,7 +1179,7 @@ func (c *KVStore) AddChannelEdge(ctx context.Context,
case alreadyExists: case alreadyExists:
return ErrEdgeAlreadyExist return ErrEdgeAlreadyExist
default: default:
c.rejectCache.remove(edge.ChannelID) c.rejectCache.remove(lnwire.GossipVersion1, edge.ChannelID)
c.chanCache.remove(edge.ChannelID) c.chanCache.remove(edge.ChannelID)
return nil return nil
} }
@@ -1302,7 +1302,7 @@ func (c *KVStore) HasChannelEdge(
// We'll query the cache with the shared lock held to allow multiple // We'll query the cache with the shared lock held to allow multiple
// readers to access values in the cache concurrently if they exist. // readers to access values in the cache concurrently if they exist.
c.cacheMu.RLock() c.cacheMu.RLock()
if entry, ok := c.rejectCache.get(chanID); ok { if entry, ok := c.rejectCache.get(lnwire.GossipVersion1, chanID); ok {
c.cacheMu.RUnlock() c.cacheMu.RUnlock()
upd1Time = time.Unix(entry.upd1Time, 0) upd1Time = time.Unix(entry.upd1Time, 0)
upd2Time = time.Unix(entry.upd2Time, 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 // 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 // exclusive lock and check the cache again in case another method added
// the entry to the cache while no lock was held. // 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) upd1Time = time.Unix(entry.upd1Time, 0)
upd2Time = time.Unix(entry.upd2Time, 0) upd2Time = time.Unix(entry.upd2Time, 0)
exists, isZombie = entry.flags.unpack() exists, isZombie = entry.flags.unpack()
@@ -1385,7 +1385,7 @@ func (c *KVStore) HasChannelEdge(
return time.Time{}, time.Time{}, exists, isZombie, err return time.Time{}, time.Time{}, exists, isZombie, err
} }
c.rejectCache.insert(chanID, rejectCacheEntry{ c.rejectCache.insert(lnwire.GossipVersion1, chanID, rejectCacheEntry{
upd1Time: upd1Time.Unix(), upd1Time: upd1Time.Unix(),
upd2Time: upd2Time.Unix(), upd2Time: upd2Time.Unix(),
flags: packRejectFlags(exists, isZombie), flags: packRejectFlags(exists, isZombie),
@@ -1564,7 +1564,7 @@ func (c *KVStore) PruneGraph(spentOutputs []*wire.OutPoint,
} }
for _, channel := range chansClosed { for _, channel := range chansClosed {
c.rejectCache.remove(channel.ChannelID) c.rejectCache.remove(lnwire.GossipVersion1, channel.ChannelID)
c.chanCache.remove(channel.ChannelID) c.chanCache.remove(channel.ChannelID)
} }
@@ -1831,7 +1831,7 @@ func (c *KVStore) DisconnectBlockAtHeight(height uint32) (
} }
for _, channel := range removedChans { for _, channel := range removedChans {
c.rejectCache.remove(channel.ChannelID) c.rejectCache.remove(lnwire.GossipVersion1, channel.ChannelID)
c.chanCache.remove(channel.ChannelID) c.chanCache.remove(channel.ChannelID)
} }
@@ -1950,7 +1950,7 @@ func (c *KVStore) DeleteChannelEdges(v lnwire.GossipVersion,
} }
for _, chanID := range chanIDs { for _, chanID := range chanIDs {
c.rejectCache.remove(chanID) c.rejectCache.remove(lnwire.GossipVersion1, chanID)
c.chanCache.remove(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 // 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 // written. If the edge doesn't exist, we'll load the cache entry lazily
// during the next query for this edge. // 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 { if isUpdate1 {
entry.upd1Time = e.LastUpdate.Unix() entry.upd1Time = e.LastUpdate.Unix()
} else { } else {
entry.upd2Time = e.LastUpdate.Unix() 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 // 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 return err
} }
c.rejectCache.remove(chanID) c.rejectCache.remove(lnwire.GossipVersion1, chanID)
c.chanCache.remove(chanID) c.chanCache.remove(chanID)
return nil return nil
@@ -4254,7 +4254,7 @@ func (c *KVStore) markEdgeLiveUnsafe(tx kvdb.RwTx, chanID uint64) error {
return err return err
} }
c.rejectCache.remove(chanID) c.rejectCache.remove(lnwire.GossipVersion1, chanID)
c.chanCache.remove(chanID) c.chanCache.remove(chanID)
return nil return nil
+91 -11
View File
@@ -1,5 +1,11 @@
package graphdb package graphdb
import (
"time"
"github.com/lightningnetwork/lnd/lnwire"
)
// rejectFlags is a compact representation of various metadata stored by the // rejectFlags is a compact representation of various metadata stored by the
// reject cache about a particular channel. // reject cache about a particular channel.
type rejectFlags uint8 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 // including the timestamps of its latest edge policies and whether or not the
// channel exists in the graph. // channel exists in the graph.
type rejectCacheEntry struct { type rejectCacheEntry struct {
// upd{1,2}Time are Unix timestamps for v1 policies.
upd1Time int64 upd1Time int64
upd2Time 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 // 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). // well as the most recent timestamps for each policy (if they exists).
type rejectCache struct { type rejectCache struct {
n int n int
edges map[uint64]rejectCacheEntry edges map[rejectCacheKey]rejectCacheEntry
} }
// newRejectCache creates a new rejectCache with maximum capacity of n entries. // newRejectCache creates a new rejectCache with maximum capacity of n entries.
func newRejectCache(n int) *rejectCache { func newRejectCache(n int) *rejectCache {
return &rejectCache{ return &rejectCache{
n: n, 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. // get returns the entry from the cache for chanid, if it exists.
func (c *rejectCache) get(chanid uint64) (rejectCacheEntry, bool) { func (c *rejectCache) get(version lnwire.GossipVersion, chanid uint64) (
entry, ok := c.edges[chanid] rejectCacheEntry, bool) {
entry, ok := c.edges[rejectCacheKey{
version: version,
chanID: chanid,
}]
return entry, ok 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, // 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 // it will be inserted to the cache, performing a random eviction if the cache
// is at capacity. // 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 entry exists, replace it.
if _, ok := c.edges[chanid]; ok { if _, ok := c.edges[key]; ok {
c.edges[chanid] = entry c.edges[key] = entry
return return
} }
@@ -86,10 +163,13 @@ func (c *rejectCache) insert(chanid uint64, entry rejectCacheEntry) {
break break
} }
} }
c.edges[chanid] = entry c.edges[key] = entry
} }
// remove deletes an entry for chanid from the cache, if it exists. // remove deletes an entry for chanid from the cache, if it exists.
func (c *rejectCache) remove(chanid uint64) { func (c *rejectCache) remove(version lnwire.GossipVersion, chanid uint64) {
delete(c.edges, chanid) delete(c.edges, rejectCacheKey{
version: version,
chanID: chanid,
})
} }
+13 -8
View File
@@ -3,6 +3,8 @@ package graphdb
import ( import (
"reflect" "reflect"
"testing" "testing"
"github.com/lightningnetwork/lnd/lnwire"
) )
// TestRejectCache checks the behavior of the rejectCache with respect to insertion, // 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 // As a sanity check, assert that querying the empty cache does not
// return an entry. // return an entry.
_, ok := c.get(0) _, ok := c.get(lnwire.GossipVersion1, 0)
if ok { if ok {
t.Fatalf("reject cache should be empty") t.Fatalf("reject cache should be empty")
} }
// Now, fill up the cache entirely. // Now, fill up the cache entirely.
for i := uint64(0); i < cacheSize; i++ { 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 // 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) assertHasEntries(t, c, 0, cacheSize)
// Now, insert a new element that causes the cache to evict an element. // 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 // Assert that the cache has this last entry, as the cache should evict
// some prior element and not the newly inserted one. // some prior element and not the newly inserted one.
@@ -40,7 +45,7 @@ func TestRejectCache(t *testing.T) {
// elements. // elements.
evicted := make(map[uint64]struct{}) evicted := make(map[uint64]struct{})
for i := uint64(0); i < cacheSize+1; i++ { for i := uint64(0); i < cacheSize+1; i++ {
_, ok := c.get(i) _, ok := c.get(lnwire.GossipVersion1, i)
if !ok { if !ok {
evicted[i] = struct{}{} evicted[i] = struct{}{}
} }
@@ -54,9 +59,9 @@ func TestRejectCache(t *testing.T) {
// Remove the highest item which initially caused the eviction and // Remove the highest item which initially caused the eviction and
// reinsert the element that was evicted prior. // reinsert the element that was evicted prior.
c.remove(cacheSize) c.remove(lnwire.GossipVersion1, cacheSize)
for i := range evicted { 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 // 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 // happening on inserts for existing cache items, we expect this to fail
// with high probability. // with high probability.
for i := uint64(0); i < cacheSize; i++ { for i := uint64(0); i < cacheSize; i++ {
c.insert(i, entryForInt(i)) c.insert(lnwire.GossipVersion1, i, entryForInt(i))
} }
assertHasEntries(t, c, 0, cacheSize) assertHasEntries(t, c, 0, cacheSize)
@@ -82,7 +87,7 @@ func assertHasEntries(t *testing.T, c *rejectCache, start, end uint64) {
t.Helper() t.Helper()
for i := start; i < end; i++ { for i := start; i < end; i++ {
entry, ok := c.get(i) entry, ok := c.get(lnwire.GossipVersion1, i)
if !ok { if !ok {
t.Fatalf("reject cache should contain chan %d", i) t.Fatalf("reject cache should contain chan %d", i)
} }
+26 -19
View File
@@ -747,7 +747,7 @@ func (s *SQLStore) AddChannelEdge(ctx context.Context,
case alreadyExists: case alreadyExists:
return ErrEdgeAlreadyExist return ErrEdgeAlreadyExist
default: default:
s.rejectCache.remove(edge.ChannelID) s.rejectCache.remove(edge.Version, edge.ChannelID)
s.chanCache.remove(edge.ChannelID) s.chanCache.remove(edge.ChannelID)
return nil 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 // 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 // written. If the edge doesn't exist, we'll load the cache entry lazily
// during the next query for this edge. // during the next query for this edge.
if entry, ok := s.rejectCache.get(e.ChannelID); ok { if entry, ok := s.rejectCache.get(e.Version, e.ChannelID); ok {
if isUpdate1 { switch e.Version {
entry.upd1Time = e.LastUpdate.Unix() case lnwire.GossipVersion1:
} else { updateRejectCacheEntryV1(
entry.upd2Time = e.LastUpdate.Unix() &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 // 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) "(channel_id=%d): %w", chanID, err)
} }
s.rejectCache.remove(chanID) s.rejectCache.remove(lnwire.GossipVersion1, chanID)
s.chanCache.remove(chanID) s.chanCache.remove(chanID)
return nil return nil
@@ -1813,7 +1818,7 @@ func (s *SQLStore) MarkEdgeLive(chanID uint64) error {
"(channel_id=%d): %w", chanID, err) "(channel_id=%d): %w", chanID, err)
} }
s.rejectCache.remove(chanID) s.rejectCache.remove(lnwire.GossipVersion1, chanID)
s.chanCache.remove(chanID) s.chanCache.remove(chanID)
return err return err
@@ -1995,7 +2000,7 @@ func (s *SQLStore) DeleteChannelEdges(v lnwire.GossipVersion,
} }
for _, chanID := range chanIDs { for _, chanID := range chanIDs {
s.rejectCache.remove(chanID) s.rejectCache.remove(v, chanID)
s.chanCache.remove(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 // We'll query the cache with the shared lock held to allow multiple
// readers to access values in the cache concurrently if they exist. // readers to access values in the cache concurrently if they exist.
s.cacheMu.RLock() s.cacheMu.RLock()
if entry, ok := s.rejectCache.get(chanID); ok { if entry, ok := s.rejectCache.get(lnwire.GossipVersion1, chanID); ok {
s.cacheMu.RUnlock() s.cacheMu.RUnlock()
node1LastUpdate = time.Unix(entry.upd1Time, 0) node1LastUpdate = time.Unix(entry.upd1Time, 0)
node2LastUpdate = time.Unix(entry.upd2Time, 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 // 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 // exclusive lock and check the cache again in case another method added
// the entry to the cache while no lock was held. // 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) node1LastUpdate = time.Unix(entry.upd1Time, 0)
node2LastUpdate = time.Unix(entry.upd2Time, 0) node2LastUpdate = time.Unix(entry.upd2Time, 0)
exists, isZombie = entry.flags.unpack() 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) fmt.Errorf("unable to fetch channel: %w", err)
} }
s.rejectCache.insert(chanID, rejectCacheEntry{ s.rejectCache.insert(
upd1Time: node1LastUpdate.Unix(), lnwire.GossipVersion1, chanID,
upd2Time: node2LastUpdate.Unix(), newRejectCacheEntryV1(
flags: packRejectFlags(exists, isZombie), node1LastUpdate, node2LastUpdate, exists,
}) isZombie,
),
)
return node1LastUpdate, node2LastUpdate, exists, isZombie, nil return node1LastUpdate, node2LastUpdate, exists, isZombie, nil
} }
@@ -2732,7 +2739,7 @@ func (s *SQLStore) PruneGraph(spentOutputs []*wire.OutPoint,
} }
for _, channel := range closedChans { for _, channel := range closedChans {
s.rejectCache.remove(channel.ChannelID) s.rejectCache.remove(channel.Version, channel.ChannelID)
s.chanCache.remove(channel.ChannelID) s.chanCache.remove(channel.ChannelID)
} }
@@ -3001,7 +3008,7 @@ func (s *SQLStore) DisconnectBlockAtHeight(height uint32) (
s.cacheMu.Lock() s.cacheMu.Lock()
for _, channel := range removedChans { for _, channel := range removedChans {
s.rejectCache.remove(channel.ChannelID) s.rejectCache.remove(channel.Version, channel.ChannelID)
s.chanCache.remove(channel.ChannelID) s.chanCache.remove(channel.ChannelID)
} }
s.cacheMu.Unlock() s.cacheMu.Unlock()