onionmessage: add LRU cache to SCID resolver
What changed, and why it matters
This commit adds a small in-memory cache to speed up repeated lookups of which node public key belongs to a given short channel ID (SCID). It is a performance optimization, not a security fix. There is no evidence in the commit or supplied references that it addresses a vulnerability.
No security action required. Treat as a normal performance/refactoring change. If reviewing for defense in depth, verify that the LRU cache library (github.com/lightninglabs/neutrino/cache/lru) is thread-safe and that cached public keys are immutable, but the commit itself does not introduce a known security issue.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces an LRU cache (default 1000 entries) inside onionmessage.GraphNodeResolver to avoid repeated graph database queries when resolving SCIDs to remote node public keys during onion message forwarding. It also adds a constructor NewGraphNodeResolver and updates server.go to use it. The cache stores 33-byte compressed public keys keyed by uint64 SCID. No input validation, cryptographic, or authorization logic is changed; behavior on cache miss remains identical to the previous database lookup path.
Changed components
onionmessage/resolver.goserver.goInspect captured patch +76 / −9
diff --git a/onionmessage/resolver.go b/onionmessage/resolver.go
index b930d96..278f2a6 100644
--- a/onionmessage/resolver.go
+++ b/onionmessage/resolver.go
@@ -5,22 +5,83 @@ import (
"encoding/hex"
"github.com/btcsuite/btcd/btcec/v2"
+ "github.com/lightninglabs/neutrino/cache/lru"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/lnwire"
)
+const (
+ // defaultSCIDCacheSize is the default number of SCID to pubkey mappings
+ // to cache. This is relatively small since onion message forwarding via
+ // SCID is expected to be infrequent compared to forwarding via explicit
+ // node ID.
+ defaultSCIDCacheSize = 1000
+)
+
+// cachedPubKey is a wrapper around a compressed public key that implements the
+// cache.Value interface required by the LRU cache.
+type cachedPubKey struct {
+ pubKeyBytes [33]byte
+}
+
+// Size returns the "size" of an entry. We return 1 as we just want to limit
+// the total number of entries rather than do accurate size accounting.
+func (c *cachedPubKey) Size() (uint64, error) {
+ return 1, nil
+}
+
+// GraphNodeResolver resolves node public keys from short channel IDs using the
+// channel graph. It maintains an LRU cache to avoid repeated database lookups
+// for frequently used SCIDs.
type GraphNodeResolver struct {
- Graph *graphdb.ChannelGraph
- OurPub *btcec.PublicKey
+ graph *graphdb.ChannelGraph
+ ourPub *btcec.PublicKey
+
+ // scidCache is an LRU cache mapping SCID (as uint64) to the remote
+ // node's compressed public key bytes.
+ scidCache *lru.Cache[uint64, *cachedPubKey]
+}
+
+// NewGraphNodeResolver creates a new GraphNodeResolver with the given channel
+// graph and our node's public key. It initializes an LRU cache for SCID
+// lookups.
+func NewGraphNodeResolver(graph *graphdb.ChannelGraph,
+ ourPub *btcec.PublicKey) *GraphNodeResolver {
+
+ return &GraphNodeResolver{
+ graph: graph,
+ ourPub: ourPub,
+ scidCache: lru.NewCache[uint64, *cachedPubKey](
+ defaultSCIDCacheSize,
+ ),
+ }
}
// RemotePubFromSCID resolves a node public key from a short channel ID.
func (r *GraphNodeResolver) RemotePubFromSCID(ctx context.Context,
scid lnwire.ShortChannelID) (*btcec.PublicKey, error) {
- log.Tracef("Resolving node public key for SCID %v", scid)
+ scidInt := scid.ToUint64()
+
+ // Check the cache first.
+ if cached, err := r.scidCache.Get(scidInt); err == nil {
+ pubKey, parseErr := btcec.ParsePubKey(cached.pubKeyBytes[:])
+ if parseErr == nil {
+ log.Tracef("Resolved SCID %v from cache to node %s",
+ scid,
+ hex.EncodeToString(cached.pubKeyBytes[:]))
+
+ return pubKey, nil
+ }
- edge, _, _, err := r.Graph.FetchChannelEdgesByID(ctx, scid.ToUint64())
+ // Cache contained invalid data, fall through to DB lookup.
+ log.Debugf("Invalid cached pubkey for SCID %v: %v",
+ scid, parseErr)
+ }
+
+ log.Tracef("Resolving node public key for SCID %v from graph", scid)
+
+ edge, _, _, err := r.graph.FetchChannelEdgesByID(ctx, scid.ToUint64())
if err != nil {
log.Debugf("Failed to fetch channel edges for SCID %v: %v",
scid, err)
@@ -29,7 +90,7 @@ func (r *GraphNodeResolver) RemotePubFromSCID(ctx context.Context,
}
otherNodeKeyBytes, err := edge.OtherNodeKeyBytes(
- r.OurPub.SerializeCompressed(),
+ r.ourPub.SerializeCompressed(),
)
if err != nil {
log.Debugf("Failed to get other node key for SCID %v: %v",
@@ -46,6 +107,13 @@ func (r *GraphNodeResolver) RemotePubFromSCID(ctx context.Context,
return nil, err
}
+ // Cache the result for future lookups. We ignore the return values as
+ // caching is best-effort and a failure just means the next lookup will
+ // hit the database again.
+ _, _ = r.scidCache.Put(scidInt, &cachedPubKey{
+ pubKeyBytes: otherNodeKeyBytes,
+ })
+
log.Tracef("Resolved SCID %v to node %s", scid,
hex.EncodeToString(pubKey.SerializeCompressed()))
diff --git a/server.go b/server.go
index 54dc6dd..0359dc7 100644
--- a/server.go
+++ b/server.go
@@ -2374,10 +2374,9 @@ func (s *server) Start(ctx context.Context) error {
// spawn per-peer actors for handling onion messages. Skip if
// onion messaging is disabled via config.
if !s.cfg.ProtocolOptions.NoOnionMessages() {
- resolver := &onionmessage.GraphNodeResolver{
- Graph: s.graphDB,
- OurPub: s.identityECDH.PubKey(),
- }
+ resolver := onionmessage.NewGraphNodeResolver(
+ s.graphDB, s.identityECDH.PubKey(),
+ )
s.onionActorFactory = onionmessage.NewOnionActorFactory(
s.sphinxOnionMsg, resolver, s,
s.onionMessageServer,
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.