Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/discovery"
"github.com/lightningnetwork/lnd/funding"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/htlcswitch/hodl"
"github.com/lightningnetwork/lnd/input"
Expand Down Expand Up @@ -675,8 +676,9 @@ func DefaultConfig() Config {
Sig: lncfg.DefaultSigWorkers,
},
Caches: &lncfg.Caches{
RejectCacheSize: channeldb.DefaultRejectCacheSize,
ChannelCacheSize: channeldb.DefaultChannelCacheSize,
RejectCacheSize: channeldb.DefaultRejectCacheSize,
ChannelCacheSize: channeldb.DefaultChannelCacheSize,
PublicNodeCacheSize: graphdb.DefaultPublicNodeCacheSize,
},
Prometheus: lncfg.DefaultPrometheus(),
Watchtower: lncfg.DefaultWatchtowerCfg(defaultTowerDir),
Expand Down
1 change: 1 addition & 0 deletions config_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,7 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
graphDBOptions := []graphdb.StoreOptionModifier{
graphdb.WithRejectCacheSize(cfg.Caches.RejectCacheSize),
graphdb.WithChannelCacheSize(cfg.Caches.ChannelCacheSize),
graphdb.WithPublicNodeCacheSize(cfg.Caches.PublicNodeCacheSize),
graphdb.WithBatchCommitInterval(cfg.DB.BatchCommitInterval),
}

Expand Down
7 changes: 7 additions & 0 deletions docs/release-notes/release-notes-0.20.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@
to use `UNION ALL` instead of `OR` conditions in the `WHERE` clause, improving
performance when checking for public nodes especially in large graphs when using `SQL` backends.

* [Add caching for](https://github.com/lightningnetwork/lnd/pull/10363)
`IsPublicNode` query which speedup calls to check for nodes visibility status.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can also add this cache for the KV store, it shouldn't be a big lift but at least it makes the config value more general, because right now kv db node runners would also think they have this feature available. So either we add clear comments that this is a sql feature or we also add it to the kv store which probably isn't a big change and would improve the performance there as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, will aim for it in a followup but if it's done before this get marged that will be fine too. For now I'll update the note.

This reduces the amount of time lnd needs to query the db to determine if a node
is public or not. Also added a new config `caches.public-node-cache-size` which
controls the max number of entries that the cache can accommodate.
**NOTE:** this improvement is only available for SQL backends.

## Deprecations

# Technical and Architectural Updates
Expand Down
59 changes: 59 additions & 0 deletions graph/db/graph_sql_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//go:build test_db_postgres || test_db_sqlite

package graphdb

import (
"testing"

"github.com/stretchr/testify/require"
)

// TestNodeIsPublicCache verifies that once a node is observed as public, it
// remains public on cache hit even if it later has zero public channels in the
// DB.
//
// NOTE: Once a pubkey is leaked, we don't invalidate the key from the cache
// even if it has no public channels anymore. This is because once leaked as
// public, the node cannot undo the state.
func TestNodeIsPublicCache(t *testing.T) {
t.Parallel()
ctx := t.Context()

graph := MakeTestGraph(t)

alice := createTestVertex(t)
bob := createTestVertex(t)
carol := createTestVertex(t)

require.NoError(t, graph.SetSourceNode(ctx, alice))

alice.LastUpdate = nextUpdateTime()
bob.LastUpdate = nextUpdateTime()
carol.LastUpdate = nextUpdateTime()

require.NoError(t, graph.AddNode(ctx, alice))
require.NoError(t, graph.AddNode(ctx, bob))
require.NoError(t, graph.AddNode(ctx, carol))

// Carol has no public channels, so she should be private.
isPublic, err := graph.IsPublicNode(carol.PubKeyBytes)
require.NoError(t, err)
require.False(t, isPublic)

// Add a public edge so Alice becomes public and is cached as such.
edge, _ := createEdge(10, 0, 0, 0, alice, bob)
require.NoError(t, graph.AddChannelEdge(ctx, &edge))

isPublic, err = graph.IsPublicNode(alice.PubKeyBytes)
require.NoError(t, err)
require.True(t, isPublic)

// Delete Alice's only public edge. Since we're using a public node
// cache, Alice is still treated as public until she is evicted from
// the cache.
require.NoError(t, graph.DeleteChannelEdges(false, true, edge.ChannelID))

isPublic, err = graph.IsPublicNode(alice.PubKeyBytes)
require.NoError(t, err)
require.True(t, isPublic)
}
9 changes: 9 additions & 0 deletions graph/db/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3925,6 +3925,15 @@ func TestNodeIsPublic(t *testing.T) {
// participant to replicate real-world scenarios (private edges being in
// some graphs but not others, etc.).
aliceGraph := MakeTestGraph(t)

// SQL store caches public nodes and once a node is cached as public, it
// stays public until eviction/restart. This test asserts
// public<->private transitions, so it doesn't apply to SQL.
if _, ok := aliceGraph.V1Store.(*SQLStore); ok {
Copy link
Collaborator

@ziggie1984 ziggie1984 Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes let's update the testcase to reflect this new behavior, so we do not special case it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I'd be adding cache for kvdb, I will leave this as is for now and remove/update this test once that is done so as not to block this PR from getting in.

t.Skip("Skipping test because SQL backend uses public node " +
"cache, public status is sticky until eviction")
}

aliceNode := createTestVertex(t)
if err := aliceGraph.SetSourceNode(ctx, aliceNode); err != nil {
t.Fatalf("unable to set source node: %v", err)
Expand Down
23 changes: 20 additions & 3 deletions graph/db/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ const (
// around 40MB.
DefaultChannelCacheSize = 20000

// DefaultPublicNodeCacheSize is the default number of node public
// status entries to cache. With 15k entries, this produces a cache of
// around 1-1.5MB (including map overhead and LRU bookkeeping).
DefaultPublicNodeCacheSize = 15000

// DefaultPreAllocCacheNumNodes is the default number of channels we
// assume for mainnet for pre-allocating the graph cache. As of
// September 2021, there currently are 14k nodes in a strictly pruned
Expand Down Expand Up @@ -125,6 +130,10 @@ type StoreOptions struct {
// channel cache.
ChannelCacheSize int

// PublicNodeCacheSize is the maximum number of node public status
// entries to hold in the cache.
PublicNodeCacheSize int

// BatchCommitInterval is the maximum duration the batch schedulers will
// wait before attempting to commit a pending set of updates.
BatchCommitInterval time.Duration
Expand All @@ -138,9 +147,10 @@ type StoreOptions struct {
// DefaultOptions returns a StoreOptions populated with default values.
func DefaultOptions() *StoreOptions {
return &StoreOptions{
RejectCacheSize: DefaultRejectCacheSize,
ChannelCacheSize: DefaultChannelCacheSize,
NoMigration: false,
RejectCacheSize: DefaultRejectCacheSize,
ChannelCacheSize: DefaultChannelCacheSize,
PublicNodeCacheSize: DefaultPublicNodeCacheSize,
NoMigration: false,
}
}

Expand Down Expand Up @@ -169,3 +179,10 @@ func WithBatchCommitInterval(interval time.Duration) StoreOptionModifier {
o.BatchCommitInterval = interval
}
}

// WithPublicNodeCacheSize sets the PublicNodeCacheSize to n.
func WithPublicNodeCacheSize(n int) StoreOptionModifier {
return func(o *StoreOptions) {
o.PublicNodeCacheSize = n
}
}
40 changes: 38 additions & 2 deletions graph/db/sql_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/neutrino/cache"
"github.com/lightninglabs/neutrino/cache/lru"
"github.com/lightningnetwork/lnd/aliasmgr"
"github.com/lightningnetwork/lnd/batch"
"github.com/lightningnetwork/lnd/fn/v2"
Expand Down Expand Up @@ -176,13 +178,25 @@ type SQLStore struct {
rejectCache *rejectCache
chanCache *channelCache

publicNodeCache *lru.Cache[[33]byte, *cachedPublicNode]

chanScheduler batch.Scheduler[SQLQueries]
nodeScheduler batch.Scheduler[SQLQueries]

srcNodes map[lnwire.GossipVersion]*srcNodeInfo
srcNodeMu sync.Mutex
}

// cachedPublicNode represents a value that can be stored in an LRU cache. It
// has the Size() method which the lru cache requires.
type cachedPublicNode struct{}

// Size returns the size of the cache entry. We return 1 as we just want to
// limit the number of entries rather than their actual memory size.
func (c *cachedPublicNode) Size() (uint64, error) {
return 1, nil
}

// A compile-time assertion to ensure that SQLStore implements the V1Store
// interface.
var _ V1Store = (*SQLStore)(nil)
Expand Down Expand Up @@ -217,7 +231,10 @@ func NewSQLStore(cfg *SQLStoreConfig, db BatchedSQLQueries,
db: db,
rejectCache: newRejectCache(opts.RejectCacheSize),
chanCache: newChannelCache(opts.ChannelCacheSize),
srcNodes: make(map[lnwire.GossipVersion]*srcNodeInfo),
publicNodeCache: lru.NewCache[[33]byte, *cachedPublicNode](
uint64(opts.PublicNodeCacheSize),
),
srcNodes: make(map[lnwire.GossipVersion]*srcNodeInfo),
}

s.chanScheduler = batch.NewTimeScheduler(
Expand Down Expand Up @@ -2292,8 +2309,19 @@ func (s *SQLStore) ChannelID(chanPoint *wire.OutPoint) (uint64, error) {
func (s *SQLStore) IsPublicNode(pubKey [33]byte) (bool, error) {
ctx := context.TODO()

// Check the cache first and return early if there is a hit.
cached, err := s.publicNodeCache.Get(pubKey)
if err == nil && cached != nil {
return true, nil
}

// Log any error other than NotFound.
if err != nil && !errors.Is(err, cache.ErrElementNotFound) {
log.Warnf("Unable to check cache if node is public: %v", err)
}

var isPublic bool
err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
err = s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
var err error
isPublic, err = db.IsPublicV1Node(ctx, pubKey[:])

Expand All @@ -2304,6 +2332,14 @@ func (s *SQLStore) IsPublicNode(pubKey [33]byte) (bool, error) {
"public: %w", err)
}

// Store the result in cache only if the node is public.
if isPublic {
_, err = s.publicNodeCache.Put(pubKey, &cachedPublicNode{})
if err != nil {
log.Warnf("Unable to store node info in cache: %v", err)
}
}

return isPublic, nil
}

Expand Down
14 changes: 14 additions & 0 deletions lncfg/caches.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const (
// channeldb's channel cache. This amounts to roughly 2 MB when full.
MinChannelCacheSize = 1000

// MinPublicNodeCacheSize is a floor on the maximum capacity allowed for
// public node cache. This amount is roughly 500 KB when full.
MinPublicNodeCacheSize = 5000

// DefaultRPCGraphCacheDuration is the default interval that the RPC
// response to DescribeGraph should be cached for.
DefaultRPCGraphCacheDuration = time.Minute
Expand All @@ -37,6 +41,11 @@ type Caches struct {
// RPCGraphCacheDuration is used to control the flush interval of the
// channel graph cache.
RPCGraphCacheDuration time.Duration `long:"rpc-graph-cache-duration" description:"The period of time expressed as a duration (1s, 1m, 1h, etc) that the RPC response to DescribeGraph should be cached for."`

// PublicNodeCacheSize is the maximum number of entries stored in lnd's
// public node cache, which is used to speed up checks for nodes
// visibility. Memory usage is roughly 100b per entry.
PublicNodeCacheSize int `long:"public-node-cache-size" description:"Maximum number of entries contained in the public node cache, which is used to speed up checks for nodes visibility. Each entry requires roughly 100 bytes."`
}

// Validate checks the Caches configuration for values that are too small to be
Expand All @@ -50,6 +59,11 @@ func (c *Caches) Validate() error {
return fmt.Errorf("channel cache size %d is less than min: %d",
c.ChannelCacheSize, MinChannelCacheSize)
}
if c.PublicNodeCacheSize < MinPublicNodeCacheSize {
return fmt.Errorf("public node cache size %d is less than "+
"min: %d", c.PublicNodeCacheSize,
MinPublicNodeCacheSize)
}

return nil
}
Expand Down
4 changes: 4 additions & 0 deletions sample-lnd.conf
Original file line number Diff line number Diff line change
Expand Up @@ -1388,6 +1388,10 @@
; roughly 2Kb.
; caches.channel-cache-size=20000

; Maximum number of entries contained in the public node cache, which is used to
; speed up checks for nodes visibility. Each entry requires roughly 100 bytes.
; caches.public-node-cache-size=15000

; The duration that the response to DescribeGraph should be cached for. Setting
; the value to zero disables the cache.
; Default:
Expand Down
Loading