@@ -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
121122type 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:
546561func (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 )
0 commit comments