Skip to content

Commit cd7fa63

Browse files
authored
Merge pull request #10003 from ellemouton/fixPeerBootstrappingFlake
discovery: deterministic bootstrapping for local test networks
2 parents 500808f + 37d6390 commit cd7fa63

File tree

5 files changed

+130
-23
lines changed

5 files changed

+130
-23
lines changed

chainreg/chainregistry.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) {
528528
// On local test networks we usually don't have multiple
529529
// chain backend peers, so we can skip
530530
// the checkOutboundPeers test.
531-
if cfg.Bitcoin.SimNet || cfg.Bitcoin.RegTest {
531+
if cfg.Bitcoin.IsLocalNetwork() {
532532
return nil
533533
}
534534

@@ -651,7 +651,7 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) {
651651
// On local test networks we usually don't have multiple
652652
// chain backend peers, so we can skip
653653
// the checkOutboundPeers test.
654-
if cfg.Bitcoin.SimNet || cfg.Bitcoin.RegTest {
654+
if cfg.Bitcoin.IsLocalNetwork() {
655655
return nil
656656
}
657657

discovery/bootstrapper.go

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/lightningnetwork/lnd/autopilot"
1919
"github.com/lightningnetwork/lnd/lnutils"
2020
"github.com/lightningnetwork/lnd/lnwire"
21+
"github.com/lightningnetwork/lnd/routing/route"
2122
"github.com/lightningnetwork/lnd/tor"
2223
"github.com/miekg/dns"
2324
)
@@ -121,11 +122,10 @@ func shuffleBootstrappers(candidates []NetworkPeerBootstrapper) []NetworkPeerBoo
121122
type ChannelGraphBootstrapper struct {
122123
chanGraph autopilot.ChannelGraph
123124

124-
// hashAccumulator is a set of 32 random bytes that are read upon the
125-
// creation of the channel graph bootstrapper. We use this value to
126-
// randomly select nodes within the known graph to connect to. After
127-
// each selection, we rotate the accumulator by hashing it with itself.
128-
hashAccumulator [32]byte
125+
// hashAccumulator is used to determine which nodes to use for
126+
// bootstrapping. It allows us to potentially introduce some randomness
127+
// into the selection process.
128+
hashAccumulator hashAccumulator
129129

130130
tried map[autopilot.NodeID]struct{}
131131
}
@@ -138,18 +138,33 @@ var _ NetworkPeerBootstrapper = (*ChannelGraphBootstrapper)(nil)
138138
// backed by an active autopilot.ChannelGraph instance. This type of network
139139
// peer bootstrapper will use the authenticated nodes within the known channel
140140
// graph to bootstrap connections.
141-
func NewGraphBootstrapper(cg autopilot.ChannelGraph) (NetworkPeerBootstrapper, error) {
141+
func NewGraphBootstrapper(cg autopilot.ChannelGraph,
142+
deterministicSampling bool) (NetworkPeerBootstrapper, error) {
142143

143-
c := &ChannelGraphBootstrapper{
144-
chanGraph: cg,
145-
tried: make(map[autopilot.NodeID]struct{}),
146-
}
147-
148-
if _, err := rand.Read(c.hashAccumulator[:]); err != nil {
149-
return nil, err
144+
var (
145+
hashAccumulator hashAccumulator
146+
err error
147+
)
148+
if deterministicSampling {
149+
// If we're using deterministic sampling, then we'll use a
150+
// no-op hash accumulator that will always return false for
151+
// skipNode.
152+
hashAccumulator = newNoOpHashAccumulator()
153+
} else {
154+
// Otherwise, we'll use a random hash accumulator to sample
155+
// nodes from the channel graph.
156+
hashAccumulator, err = newRandomHashAccumulator()
157+
if err != nil {
158+
return nil, fmt.Errorf("unable to create hash "+
159+
"accumulator: %w", err)
160+
}
150161
}
151162

152-
return c, nil
163+
return &ChannelGraphBootstrapper{
164+
chanGraph: cg,
165+
tried: make(map[autopilot.NodeID]struct{}),
166+
hashAccumulator: hashAccumulator,
167+
}, nil
153168
}
154169

155170
// SampleNodeAddrs uniformly samples a set of specified address from the
@@ -199,7 +214,7 @@ func (c *ChannelGraphBootstrapper) SampleNodeAddrs(_ context.Context,
199214
// it's 50/50. If it isn't less, than then we'll
200215
// continue forward.
201216
nodePubKeyBytes := node.PubKey()
202-
if bytes.Compare(c.hashAccumulator[:], nodePubKeyBytes[1:]) > 0 {
217+
if c.hashAccumulator.skipNode(nodePubKeyBytes) {
203218
return nil
204219
}
205220

@@ -259,7 +274,7 @@ func (c *ChannelGraphBootstrapper) SampleNodeAddrs(_ context.Context,
259274
tries++
260275

261276
// We'll now rotate our hash accumulator one value forwards.
262-
c.hashAccumulator = sha256.Sum256(c.hashAccumulator[:])
277+
c.hashAccumulator.rotate()
263278

264279
// If this attempt didn't yield any addresses, then we'll exit
265280
// early.
@@ -546,3 +561,83 @@ search:
546561
func (d *DNSSeedBootstrapper) Name() string {
547562
return fmt.Sprintf("BOLT-0010 DNS Seed: %v", d.dnsSeeds)
548563
}
564+
565+
// hashAccumulator is an interface that defines the methods required for
566+
// a hash accumulator used to sample nodes from the channel graph.
567+
type hashAccumulator interface {
568+
// rotate rotates the hash accumulator value.
569+
rotate()
570+
571+
// skipNode returns true if the node with the given public key
572+
// should be skipped based on the current hash accumulator state.
573+
skipNode(pubKey route.Vertex) bool
574+
}
575+
576+
// randomHashAccumulator is an implementation of the hashAccumulator
577+
// interface that uses a random hash to sample nodes from the channel graph.
578+
type randomHashAccumulator struct {
579+
hash [32]byte
580+
}
581+
582+
// A compile time assertion to ensure that randomHashAccumulator meets the
583+
// hashAccumulator interface.
584+
var _ hashAccumulator = (*randomHashAccumulator)(nil)
585+
586+
// newRandomHashAccumulator returns a new instance of a randomHashAccumulator.
587+
// This accumulator is used to randomly sample nodes from the channel graph.
588+
func newRandomHashAccumulator() (*randomHashAccumulator, error) {
589+
var r randomHashAccumulator
590+
591+
if _, err := rand.Read(r.hash[:]); err != nil {
592+
return nil, fmt.Errorf("unable to read random bytes: %w", err)
593+
}
594+
595+
return &r, nil
596+
}
597+
598+
// rotate rotates the hash accumulator by hashing the current value
599+
// with itself. This ensures that we have a new random value to compare
600+
// against when we sample nodes from the channel graph.
601+
//
602+
// NOTE: this is part of the hashAccumulator interface.
603+
func (r *randomHashAccumulator) rotate() {
604+
r.hash = sha256.Sum256(r.hash[:])
605+
}
606+
607+
// skipNode returns true if the node with the given public key should be skipped
608+
// based on the current hash accumulator state. It will return false for the
609+
// pub key if it is lexicographically less than our current accumulator value.
610+
// It does so by comparing the current hash accumulator value with the passed
611+
// byte slice. When comparing, we skip the first byte as it's 50/50 between 02
612+
// and 03 for compressed pub keys.
613+
//
614+
// NOTE: this is part of the hashAccumulator interface.
615+
func (r *randomHashAccumulator) skipNode(pub route.Vertex) bool {
616+
return bytes.Compare(r.hash[:], pub[1:]) > 0
617+
}
618+
619+
// noOpHashAccumulator is a no-op implementation of the hashAccumulator
620+
// interface. This is used when we want deterministic behavior and don't
621+
// want to sample nodes randomly from the channel graph.
622+
type noOpHashAccumulator struct{}
623+
624+
// newNoOpHashAccumulator returns a new instance of a noOpHashAccumulator.
625+
func newNoOpHashAccumulator() *noOpHashAccumulator {
626+
return &noOpHashAccumulator{}
627+
}
628+
629+
// rotate is a no-op for the noOpHashAccumulator.
630+
//
631+
// NOTE: this is part of the hashAccumulator interface.
632+
func (*noOpHashAccumulator) rotate() {}
633+
634+
// skipNode always returns false, meaning that no nodes will be skipped.
635+
//
636+
// NOTE: this is part of the hashAccumulator interface.
637+
func (*noOpHashAccumulator) skipNode(route.Vertex) bool {
638+
return false
639+
}
640+
641+
// A compile-time assertion to ensure that noOpHashAccumulator meets the
642+
// hashAccumulator interface.
643+
var _ hashAccumulator = (*noOpHashAccumulator)(nil)

docs/release-notes/release-notes-0.20.0.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,9 @@ reader of a payment request.
144144
disabling has now been
145145
[removed](https://github.com/lightningnetwork/lnd/pull/9967) meaning that any
146146
test network scripts that rely on bootstrapping being disabled will need to
147-
explicitly define the `--nobootstrap` flag.
147+
explicitly define the `--nobootstrap` flag. Bootstrapping will now also be
148+
[deterministic](https://github.com/lightningnetwork/lnd/pull/10003) on local
149+
test networks so that bootstrapping behaviour can be tested for.
148150

149151
## Database
150152

lncfg/chain.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,9 @@ func (c *Chain) Validate(minTimeLockDelta uint32, minDelay uint16) error {
5252

5353
return nil
5454
}
55+
56+
// IsLocalNetwork returns true if the chain is a local network, such as
57+
// simnet or regtest.
58+
func (c *Chain) IsLocalNetwork() bool {
59+
return c.SimNet || c.RegTest
60+
}

server.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3064,15 +3064,19 @@ func initNetworkBootstrappers(s *server) ([]discovery.NetworkPeerBootstrapper, e
30643064
// this can be used by default if we've already partially seeded the
30653065
// network.
30663066
chanGraph := autopilot.ChannelGraphFromDatabase(s.graphDB)
3067-
graphBootstrapper, err := discovery.NewGraphBootstrapper(chanGraph)
3067+
graphBootstrapper, err := discovery.NewGraphBootstrapper(
3068+
chanGraph, s.cfg.Bitcoin.IsLocalNetwork(),
3069+
)
30683070
if err != nil {
30693071
return nil, err
30703072
}
30713073
bootStrappers = append(bootStrappers, graphBootstrapper)
30723074

3073-
// If this isn't simnet mode, then one of our additional bootstrapping
3074-
// sources will be the set of running DNS seeds.
3075-
if !s.cfg.Bitcoin.SimNet {
3075+
// If this isn't using simnet or regtest mode, then one of our
3076+
// additional bootstrapping sources will be the set of running DNS
3077+
// seeds.
3078+
if !s.cfg.Bitcoin.IsLocalNetwork() {
3079+
//nolint:ll
30763080
dnsSeeds, ok := chainreg.ChainDNSSeeds[*s.cfg.ActiveNetParams.GenesisHash]
30773081

30783082
// If we have a set of DNS seeds for this chain, then we'll add

0 commit comments

Comments
 (0)