lnd/graph/db/options.go
Elle Mouton 497d479440
graph/db: version NodeUpdatesInHorizon and ChanUpdatesInHorizon
Replace the (startTime, endTime time.Time) parameters on
NodeUpdatesInHorizon and ChanUpdatesInHorizon with
(v GossipVersion, r NodeUpdateRange/ChanUpdateRange). The range
types enforce version-correct bounds at the type level: v1 uses unix
timestamps, v2 will use block heights.

The KV store rejects non-v1 versions since it only stores v1 data.
The SQL store dispatches to version-specific helpers
(nodeUpdatesInHorizonV1, chanUpdatesInHorizonV1); the v2
block-height paths return an error for now and will be wired up in
follow-up commits.

VersionedGraph wrappers supply the version from the embedded field,
so callers only pass the range.
2026-03-31 09:14:49 +02:00

379 lines
11 KiB
Go

package graphdb
import (
"fmt"
"time"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/lnwire"
)
const (
// DefaultRejectCacheSize is the default number of rejectCacheEntries to
// cache for use in the rejection cache of incoming gossip traffic. This
// produces a cache size of around 1MB.
DefaultRejectCacheSize = 50000
// DefaultChannelCacheSize is the default number of ChannelEdges cached
// in order to reply to gossip queries. This produces a cache size of
// around 40MB.
DefaultChannelCacheSize = 20000
// 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
// graph, so we choose a number that is slightly higher.
DefaultPreAllocCacheNumNodes = 15000
)
// IteratorOption is a functional option used to change the per-call
// configuration for iterators.
type IteratorOption func(*iterConfig)
// iterConfig holds the configuration for graph operations.
type iterConfig struct {
// chanUpdateIterBatchSize is the batch size to use when reading out
// channel updates to send a peer a backlog.
chanUpdateIterBatchSize int
// nodeUpdateIterBatchSize is the batch size to use when reading out
// node updates to send to a peer backlog.
nodeUpdateIterBatchSize int
// iterPublicNodes is used to make an iterator that only iterates over
// public nodes.
iterPublicNodes bool
}
// ChanUpdateRange describes a range for channel updates. Only one of the time
// or height ranges should be set depending on the gossip version.
type ChanUpdateRange struct {
// StartTime is the inclusive lower time bound (v1 gossip only).
StartTime fn.Option[time.Time]
// EndTime is the exclusive upper time bound (v1 gossip only).
EndTime fn.Option[time.Time]
// StartHeight is the inclusive lower block-height bound (v2 gossip
// only).
StartHeight fn.Option[uint32]
// EndHeight is the exclusive upper block-height bound (v2 gossip
// only).
EndHeight fn.Option[uint32]
}
// validateForVersion checks that the range fields are consistent with the
// given gossip version: v1 requires time bounds, v2 requires block-height
// bounds, and mixing the two is rejected.
func (r ChanUpdateRange) validateForVersion(v lnwire.GossipVersion) error {
var (
hasStartTime = r.StartTime.IsSome()
hasEndTime = r.EndTime.IsSome()
hasTimeRange = hasStartTime || hasEndTime
hasStartHeight = r.StartHeight.IsSome()
hasEndHeight = r.EndHeight.IsSome()
hasBlockRange = hasStartHeight || hasEndHeight
)
if hasTimeRange && hasBlockRange {
return fmt.Errorf("chan update range has both time and block " +
"ranges")
}
switch v {
case gossipV1:
if hasBlockRange {
return fmt.Errorf("v1 chan update range must use time")
}
if !hasTimeRange {
return fmt.Errorf("v1 chan update range missing time")
}
if !hasStartTime || !hasEndTime {
return fmt.Errorf("v1 chan update range " +
"missing time bounds")
}
start := r.StartTime.UnwrapOr(time.Time{})
end := r.EndTime.UnwrapOr(time.Time{})
if start.After(end) {
return fmt.Errorf("v1 chan update range: " +
"start time after end time")
}
case gossipV2:
if hasTimeRange {
return fmt.Errorf("v2 chan update range must use " +
"blocks")
}
if !hasBlockRange {
return fmt.Errorf("v2 chan update range missing " +
"block range")
}
if !hasStartHeight || !hasEndHeight {
return fmt.Errorf("v2 chan update range " +
"missing block bounds")
}
start := r.StartHeight.UnwrapOr(0)
end := r.EndHeight.UnwrapOr(0)
if start > end {
return fmt.Errorf("v2 chan update range: " +
"start height after end height")
}
default:
return fmt.Errorf("unknown gossip version: %v", v)
}
return nil
}
// NodeUpdateRange describes a range for node updates. Only one of the time or
// height ranges should be set depending on the gossip version.
type NodeUpdateRange struct {
// StartTime is the inclusive lower time bound (v1 gossip only).
StartTime fn.Option[time.Time]
// EndTime is the exclusive upper time bound (v1 gossip only).
EndTime fn.Option[time.Time]
// StartHeight is the inclusive lower block-height bound (v2 gossip
// only).
StartHeight fn.Option[uint32]
// EndHeight is the exclusive upper block-height bound (v2 gossip
// only).
EndHeight fn.Option[uint32]
}
// validateForVersion checks that the range fields are consistent with the
// given gossip version: v1 requires time bounds, v2 requires block-height
// bounds, and mixing the two is rejected.
func (r NodeUpdateRange) validateForVersion(v lnwire.GossipVersion) error {
var (
hasStartTime = r.StartTime.IsSome()
hasEndTime = r.EndTime.IsSome()
hasStartHeight = r.StartHeight.IsSome()
hasEndHeight = r.EndHeight.IsSome()
hasTimeRange = hasStartTime || hasEndTime
hasBlockRange = hasStartHeight || hasEndHeight
)
if hasTimeRange && hasBlockRange {
return fmt.Errorf("node update range has both " +
"time and block ranges")
}
switch v {
case gossipV1:
if hasBlockRange {
return fmt.Errorf("v1 node update range must use time")
}
if !hasTimeRange {
return fmt.Errorf("v1 node update range missing time")
}
if !hasStartTime || !hasEndTime {
return fmt.Errorf("v1 node update range missing " +
"time bounds")
}
start := r.StartTime.UnwrapOr(time.Time{})
end := r.EndTime.UnwrapOr(time.Time{})
if start.After(end) {
return fmt.Errorf("v1 node update range: start time " +
"after end time")
}
case gossipV2:
if hasTimeRange {
return fmt.Errorf("v2 node update range must use " +
"height")
}
if !hasBlockRange {
return fmt.Errorf("v2 node update range missing height")
}
if !hasStartHeight || !hasEndHeight {
return fmt.Errorf("v2 node update range missing " +
"height bounds")
}
start := r.StartHeight.UnwrapOr(0)
end := r.EndHeight.UnwrapOr(0)
if start > end {
return fmt.Errorf("v2 node update range: start " +
"height after end height")
}
default:
return fmt.Errorf("unknown gossip version: %d", v)
}
return nil
}
// defaultIteratorConfig returns the default configuration.
func defaultIteratorConfig() *iterConfig {
return &iterConfig{
chanUpdateIterBatchSize: 1_000,
nodeUpdateIterBatchSize: 1_000,
}
}
// WithChanUpdateIterBatchSize sets the batch size for channel update
// iterators.
func WithChanUpdateIterBatchSize(size int) IteratorOption {
return func(cfg *iterConfig) {
if size > 0 {
cfg.chanUpdateIterBatchSize = size
}
}
}
// WithNodeUpdateIterBatchSize set the batch size for node ann iterators.
func WithNodeUpdateIterBatchSize(size int) IteratorOption {
return func(cfg *iterConfig) {
if size > 0 {
cfg.nodeUpdateIterBatchSize = size
}
}
}
// WithIterPublicNodesOnly is used to create an iterator that only iterates over
// public nodes.
func WithIterPublicNodesOnly() IteratorOption {
return func(cfg *iterConfig) {
cfg.iterPublicNodes = true
}
}
// chanGraphOptions holds parameters for tuning and customizing the
// ChannelGraph.
type chanGraphOptions struct {
// useGraphCache denotes whether the in-memory graph cache should be
// used or a fallback version that uses the underlying database for
// path finding.
useGraphCache bool
// preAllocCacheNumNodes is the number of nodes we expect to be in the
// graph cache, so we can pre-allocate the map accordingly.
preAllocCacheNumNodes int
// asyncGraphCachePopulation indicates whether the graph cache
// should be populated asynchronously or if the Start method should
// block until the cache is fully populated. This is true by
// default.
asyncGraphCachePopulation bool
}
// defaultChanGraphOptions returns a new chanGraphOptions instance populated
// with default values.
func defaultChanGraphOptions() *chanGraphOptions {
return &chanGraphOptions{
useGraphCache: true,
asyncGraphCachePopulation: true,
preAllocCacheNumNodes: DefaultPreAllocCacheNumNodes,
}
}
// ChanGraphOption describes the signature of a functional option that can be
// used to customize a ChannelGraph instance.
type ChanGraphOption func(*chanGraphOptions)
// WithUseGraphCache sets whether the in-memory graph cache should be used.
func WithUseGraphCache(use bool) ChanGraphOption {
return func(o *chanGraphOptions) {
o.useGraphCache = use
}
}
// WithPreAllocCacheNumNodes sets the number of nodes we expect to be in the
// graph cache, so we can pre-allocate the map accordingly.
func WithPreAllocCacheNumNodes(n int) ChanGraphOption {
return func(o *chanGraphOptions) {
o.preAllocCacheNumNodes = n
}
}
// WithAsyncGraphCachePopulation sets whether the graph cache should be
// populated asynchronously or if the Start method should block until the
// cache is fully populated.
func WithAsyncGraphCachePopulation(async bool) ChanGraphOption {
return func(o *chanGraphOptions) {
o.asyncGraphCachePopulation = async
}
}
// WithSyncGraphCachePopulation will cause the ChannelGraph to block
// until the graph cache is fully populated before returning from the Start
// method. This is useful for tests that need to ensure the graph cache is
// fully populated before proceeding with further operations.
func WithSyncGraphCachePopulation() ChanGraphOption {
return func(o *chanGraphOptions) {
o.asyncGraphCachePopulation = false
}
}
// StoreOptions holds parameters for tuning and customizing a graph DB.
type StoreOptions struct {
// RejectCacheSize is the maximum number of rejectCacheEntries to hold
// in the rejection cache.
RejectCacheSize int
// ChannelCacheSize is the maximum number of ChannelEdges to hold in the
// channel cache.
ChannelCacheSize int
// BatchCommitInterval is the maximum duration the batch schedulers will
// wait before attempting to commit a pending set of updates.
BatchCommitInterval time.Duration
// NoMigration specifies that underlying backend was opened in read-only
// mode and migrations shouldn't be performed. This can be useful for
// applications that use the channeldb package as a library.
NoMigration bool
}
// DefaultOptions returns a StoreOptions populated with default values.
func DefaultOptions() *StoreOptions {
return &StoreOptions{
RejectCacheSize: DefaultRejectCacheSize,
ChannelCacheSize: DefaultChannelCacheSize,
NoMigration: false,
}
}
// StoreOptionModifier is a function signature for modifying the default
// StoreOptions.
type StoreOptionModifier func(*StoreOptions)
// WithRejectCacheSize sets the RejectCacheSize to n.
func WithRejectCacheSize(n int) StoreOptionModifier {
return func(o *StoreOptions) {
o.RejectCacheSize = n
}
}
// WithChannelCacheSize sets the ChannelCacheSize to n.
func WithChannelCacheSize(n int) StoreOptionModifier {
return func(o *StoreOptions) {
o.ChannelCacheSize = n
}
}
// WithBatchCommitInterval sets the batch commit interval for the interval batch
// schedulers.
func WithBatchCommitInterval(interval time.Duration) StoreOptionModifier {
return func(o *StoreOptions) {
o.BatchCommitInterval = interval
}
}