From 04564977d84cf6fd7dda39310f1ee86e363ae62f Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Fri, 27 Jun 2025 15:53:55 +0200 Subject: [PATCH 1/4] lncfg: add IsLocalNetwork helper It returns true for Signet and Regtest networks. --- chainreg/chainregistry.go | 4 ++-- lncfg/chain.go | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/chainreg/chainregistry.go b/chainreg/chainregistry.go index 6ee274d68..71e9c04a6 100644 --- a/chainreg/chainregistry.go +++ b/chainreg/chainregistry.go @@ -528,7 +528,7 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) { // On local test networks we usually don't have multiple // chain backend peers, so we can skip // the checkOutboundPeers test. - if cfg.Bitcoin.SimNet || cfg.Bitcoin.RegTest { + if cfg.Bitcoin.IsLocalNetwork() { return nil } @@ -651,7 +651,7 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) { // On local test networks we usually don't have multiple // chain backend peers, so we can skip // the checkOutboundPeers test. - if cfg.Bitcoin.SimNet || cfg.Bitcoin.RegTest { + if cfg.Bitcoin.IsLocalNetwork() { return nil } diff --git a/lncfg/chain.go b/lncfg/chain.go index cbcb9d0ee..3e6fdfdd8 100644 --- a/lncfg/chain.go +++ b/lncfg/chain.go @@ -52,3 +52,9 @@ func (c *Chain) Validate(minTimeLockDelta uint32, minDelay uint16) error { return nil } + +// IsLocalNetwork returns true if the chain is a local network, such as +// simnet or regtest. +func (c *Chain) IsLocalNetwork() bool { + return c.SimNet || c.RegTest +} From bca6c0d115506c057f2be85022363ac6576d285f Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Fri, 27 Jun 2025 15:56:18 +0200 Subject: [PATCH 2/4] server: use IsLocalNetwork check for DNS bootstrapper decision It should be ok to use IsLocalNetwork here since the chainreg.ChainDNSSeeds map does not have an entry for either simnet or regtest. --- server.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server.go b/server.go index 7e35c33ec..f6884a1fe 100644 --- a/server.go +++ b/server.go @@ -3070,9 +3070,11 @@ func initNetworkBootstrappers(s *server) ([]discovery.NetworkPeerBootstrapper, e } bootStrappers = append(bootStrappers, graphBootstrapper) - // If this isn't simnet mode, then one of our additional bootstrapping - // sources will be the set of running DNS seeds. - if !s.cfg.Bitcoin.SimNet { + // If this isn't using simnet or regtest mode, then one of our + // additional bootstrapping sources will be the set of running DNS + // seeds. + if !s.cfg.Bitcoin.IsLocalNetwork() { + //nolint:ll dnsSeeds, ok := chainreg.ChainDNSSeeds[*s.cfg.ActiveNetParams.GenesisHash] // If we have a set of DNS seeds for this chain, then we'll add From 339dd0c1a72745cd7ee0e6b3db24bb40d62c7805 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 29 Jun 2025 16:28:17 +0200 Subject: [PATCH 3/4] discovery: introduce hashAccumulator interface Create an abstract hashAccumulator interface for the Channel graph bootstrapper so that we can later introduce a deterministic accumulator to be used during testing. --- discovery/bootstrapper.go | 88 ++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 16 deletions(-) diff --git a/discovery/bootstrapper.go b/discovery/bootstrapper.go index 6263db3f7..81576df3d 100644 --- a/discovery/bootstrapper.go +++ b/discovery/bootstrapper.go @@ -18,6 +18,7 @@ import ( "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/tor" "github.com/miekg/dns" ) @@ -121,11 +122,10 @@ func shuffleBootstrappers(candidates []NetworkPeerBootstrapper) []NetworkPeerBoo type ChannelGraphBootstrapper struct { chanGraph autopilot.ChannelGraph - // hashAccumulator is a set of 32 random bytes that are read upon the - // creation of the channel graph bootstrapper. We use this value to - // randomly select nodes within the known graph to connect to. After - // each selection, we rotate the accumulator by hashing it with itself. - hashAccumulator [32]byte + // hashAccumulator is used to determine which nodes to use for + // bootstrapping. It allows us to potentially introduce some randomness + // into the selection process. + hashAccumulator hashAccumulator tried map[autopilot.NodeID]struct{} } @@ -138,18 +138,20 @@ var _ NetworkPeerBootstrapper = (*ChannelGraphBootstrapper)(nil) // backed by an active autopilot.ChannelGraph instance. This type of network // peer bootstrapper will use the authenticated nodes within the known channel // graph to bootstrap connections. -func NewGraphBootstrapper(cg autopilot.ChannelGraph) (NetworkPeerBootstrapper, error) { +func NewGraphBootstrapper(cg autopilot.ChannelGraph) (NetworkPeerBootstrapper, + error) { - c := &ChannelGraphBootstrapper{ - chanGraph: cg, - tried: make(map[autopilot.NodeID]struct{}), + hashAccumulator, err := newRandomHashAccumulator() + if err != nil { + return nil, fmt.Errorf("unable to create hash accumulator: %w", + err) } - if _, err := rand.Read(c.hashAccumulator[:]); err != nil { - return nil, err - } - - return c, nil + return &ChannelGraphBootstrapper{ + chanGraph: cg, + tried: make(map[autopilot.NodeID]struct{}), + hashAccumulator: hashAccumulator, + }, nil } // SampleNodeAddrs uniformly samples a set of specified address from the @@ -199,7 +201,7 @@ func (c *ChannelGraphBootstrapper) SampleNodeAddrs(_ context.Context, // it's 50/50. If it isn't less, than then we'll // continue forward. nodePubKeyBytes := node.PubKey() - if bytes.Compare(c.hashAccumulator[:], nodePubKeyBytes[1:]) > 0 { + if c.hashAccumulator.skipNode(nodePubKeyBytes) { return nil } @@ -259,7 +261,7 @@ func (c *ChannelGraphBootstrapper) SampleNodeAddrs(_ context.Context, tries++ // We'll now rotate our hash accumulator one value forwards. - c.hashAccumulator = sha256.Sum256(c.hashAccumulator[:]) + c.hashAccumulator.rotate() // If this attempt didn't yield any addresses, then we'll exit // early. @@ -546,3 +548,57 @@ search: func (d *DNSSeedBootstrapper) Name() string { return fmt.Sprintf("BOLT-0010 DNS Seed: %v", d.dnsSeeds) } + +// hashAccumulator is an interface that defines the methods required for +// a hash accumulator used to sample nodes from the channel graph. +type hashAccumulator interface { + // rotate rotates the hash accumulator value. + rotate() + + // skipNode returns true if the node with the given public key + // should be skipped based on the current hash accumulator state. + skipNode(pubKey route.Vertex) bool +} + +// randomHashAccumulator is an implementation of the hashAccumulator +// interface that uses a random hash to sample nodes from the channel graph. +type randomHashAccumulator struct { + hash [32]byte +} + +// A compile time assertion to ensure that randomHashAccumulator meets the +// hashAccumulator interface. +var _ hashAccumulator = (*randomHashAccumulator)(nil) + +// newRandomHashAccumulator returns a new instance of a randomHashAccumulator. +// This accumulator is used to randomly sample nodes from the channel graph. +func newRandomHashAccumulator() (*randomHashAccumulator, error) { + var r randomHashAccumulator + + if _, err := rand.Read(r.hash[:]); err != nil { + return nil, fmt.Errorf("unable to read random bytes: %w", err) + } + + return &r, nil +} + +// rotate rotates the hash accumulator by hashing the current value +// with itself. This ensures that we have a new random value to compare +// against when we sample nodes from the channel graph. +// +// NOTE: this is part of the hashAccumulator interface. +func (r *randomHashAccumulator) rotate() { + r.hash = sha256.Sum256(r.hash[:]) +} + +// skipNode returns true if the node with the given public key should be skipped +// based on the current hash accumulator state. It will return false for the +// pub key if it is lexicographically less than our current accumulator value. +// It does so by comparing the current hash accumulator value with the passed +// byte slice. When comparing, we skip the first byte as it's 50/50 between 02 +// and 03 for compressed pub keys. +// +// NOTE: this is part of the hashAccumulator interface. +func (r *randomHashAccumulator) skipNode(pub route.Vertex) bool { + return bytes.Compare(r.hash[:], pub[1:]) > 0 +} From 37d639064206cb89b50c0f3387179926f9c83a68 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 29 Jun 2025 16:33:44 +0200 Subject: [PATCH 4/4] discovery: use a no-op hash accumluator for local networks If LND is running on a local network, then use deterministic sampling so that we can have deterministic peer bootstrapping. --- discovery/bootstrapper.go | 51 +++++++++++++++++++--- docs/release-notes/release-notes-0.20.0.md | 4 +- server.go | 4 +- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/discovery/bootstrapper.go b/discovery/bootstrapper.go index 81576df3d..dbfdbc7d2 100644 --- a/discovery/bootstrapper.go +++ b/discovery/bootstrapper.go @@ -138,13 +138,26 @@ var _ NetworkPeerBootstrapper = (*ChannelGraphBootstrapper)(nil) // backed by an active autopilot.ChannelGraph instance. This type of network // peer bootstrapper will use the authenticated nodes within the known channel // graph to bootstrap connections. -func NewGraphBootstrapper(cg autopilot.ChannelGraph) (NetworkPeerBootstrapper, - error) { +func NewGraphBootstrapper(cg autopilot.ChannelGraph, + deterministicSampling bool) (NetworkPeerBootstrapper, error) { - hashAccumulator, err := newRandomHashAccumulator() - if err != nil { - return nil, fmt.Errorf("unable to create hash accumulator: %w", - err) + var ( + hashAccumulator hashAccumulator + err error + ) + if deterministicSampling { + // If we're using deterministic sampling, then we'll use a + // no-op hash accumulator that will always return false for + // skipNode. + hashAccumulator = newNoOpHashAccumulator() + } else { + // Otherwise, we'll use a random hash accumulator to sample + // nodes from the channel graph. + hashAccumulator, err = newRandomHashAccumulator() + if err != nil { + return nil, fmt.Errorf("unable to create hash "+ + "accumulator: %w", err) + } } return &ChannelGraphBootstrapper{ @@ -602,3 +615,29 @@ func (r *randomHashAccumulator) rotate() { func (r *randomHashAccumulator) skipNode(pub route.Vertex) bool { return bytes.Compare(r.hash[:], pub[1:]) > 0 } + +// noOpHashAccumulator is a no-op implementation of the hashAccumulator +// interface. This is used when we want deterministic behavior and don't +// want to sample nodes randomly from the channel graph. +type noOpHashAccumulator struct{} + +// newNoOpHashAccumulator returns a new instance of a noOpHashAccumulator. +func newNoOpHashAccumulator() *noOpHashAccumulator { + return &noOpHashAccumulator{} +} + +// rotate is a no-op for the noOpHashAccumulator. +// +// NOTE: this is part of the hashAccumulator interface. +func (*noOpHashAccumulator) rotate() {} + +// skipNode always returns false, meaning that no nodes will be skipped. +// +// NOTE: this is part of the hashAccumulator interface. +func (*noOpHashAccumulator) skipNode(route.Vertex) bool { + return false +} + +// A compile-time assertion to ensure that noOpHashAccumulator meets the +// hashAccumulator interface. +var _ hashAccumulator = (*noOpHashAccumulator)(nil) diff --git a/docs/release-notes/release-notes-0.20.0.md b/docs/release-notes/release-notes-0.20.0.md index 29c8ba532..d0e163c45 100644 --- a/docs/release-notes/release-notes-0.20.0.md +++ b/docs/release-notes/release-notes-0.20.0.md @@ -144,7 +144,9 @@ reader of a payment request. disabling has now been [removed](https://github.com/lightningnetwork/lnd/pull/9967) meaning that any test network scripts that rely on bootstrapping being disabled will need to - explicitly define the `--nobootstrap` flag. + explicitly define the `--nobootstrap` flag. Bootstrapping will now also be + [deterministic](https://github.com/lightningnetwork/lnd/pull/10003) on local + test networks so that bootstrapping behaviour can be tested for. ## Database diff --git a/server.go b/server.go index f6884a1fe..a0089ea38 100644 --- a/server.go +++ b/server.go @@ -3064,7 +3064,9 @@ func initNetworkBootstrappers(s *server) ([]discovery.NetworkPeerBootstrapper, e // this can be used by default if we've already partially seeded the // network. chanGraph := autopilot.ChannelGraphFromDatabase(s.graphDB) - graphBootstrapper, err := discovery.NewGraphBootstrapper(chanGraph) + graphBootstrapper, err := discovery.NewGraphBootstrapper( + chanGraph, s.cfg.Bitcoin.IsLocalNetwork(), + ) if err != nil { return nil, err }