• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 14328255003

08 Apr 2025 08:00AM UTC coverage: 58.638%. First build
14328255003

Pull #9685

github

web-flow
Merge 6f5a23891 into ac052988c
Pull Request #9685: discovery: thread contexts through syncer, sync manager and reliable sender

47 of 58 new or added lines in 6 files covered. (81.03%)

97175 of 165721 relevant lines covered (58.64%)

1.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

1.03
/discovery/bootstrapper.go
1
package discovery
2

3
import (
4
        "bytes"
5
        "context"
6
        "crypto/rand"
7
        "crypto/sha256"
8
        "errors"
9
        "fmt"
10
        prand "math/rand"
11
        "net"
12
        "strconv"
13
        "strings"
14
        "time"
15

16
        "github.com/btcsuite/btcd/btcec/v2"
17
        "github.com/btcsuite/btcd/btcutil/bech32"
18
        "github.com/lightningnetwork/lnd/autopilot"
19
        "github.com/lightningnetwork/lnd/lnutils"
20
        "github.com/lightningnetwork/lnd/lnwire"
21
        "github.com/lightningnetwork/lnd/tor"
22
        "github.com/miekg/dns"
23
)
24

25
func init() {
3✔
26
        prand.Seed(time.Now().Unix())
3✔
27
}
3✔
28

29
// NetworkPeerBootstrapper is an interface that represents an initial peer
30
// bootstrap mechanism. This interface is to be used to bootstrap a new peer to
31
// the connection by providing it with the pubkey+address of a set of existing
32
// peers on the network. Several bootstrap mechanisms can be implemented such
33
// as DNS, in channel graph, DHT's, etc.
34
type NetworkPeerBootstrapper interface {
35
        // SampleNodeAddrs uniformly samples a set of specified address from
36
        // the network peer bootstrapper source. The num addrs field passed in
37
        // denotes how many valid peer addresses to return. The passed set of
38
        // node nodes allows the caller to ignore a set of nodes perhaps
39
        // because they already have connections established.
40
        SampleNodeAddrs(ctx context.Context, numAddrs uint32,
41
                ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress,
42
                error)
43

44
        // Name returns a human readable string which names the concrete
45
        // implementation of the NetworkPeerBootstrapper.
46
        Name() string
47
}
48

49
// MultiSourceBootstrap attempts to utilize a set of NetworkPeerBootstrapper
50
// passed in to return the target (numAddrs) number of peer addresses that can
51
// be used to bootstrap a peer just joining the Lightning Network. Each
52
// bootstrapper will be queried successively until the target amount is met. If
53
// the ignore map is populated, then the bootstrappers will be instructed to
54
// skip those nodes.
55
func MultiSourceBootstrap(ctx context.Context,
56
        ignore map[autopilot.NodeID]struct{}, numAddrs uint32,
57
        bootstrappers ...NetworkPeerBootstrapper) ([]*lnwire.NetAddress, error) {
×
58

×
59
        // We'll randomly shuffle our bootstrappers before querying them in
×
60
        // order to avoid from querying the same bootstrapper method over and
×
61
        // over, as some of these might tend to provide better/worse results
×
62
        // than others.
×
63
        bootstrappers = shuffleBootstrappers(bootstrappers)
×
64

×
65
        var addrs []*lnwire.NetAddress
×
66
        for _, bootstrapper := range bootstrappers {
×
67
                // If we already have enough addresses, then we can exit early
×
68
                // w/o querying the additional bootstrappers.
×
69
                if uint32(len(addrs)) >= numAddrs {
×
70
                        break
×
71
                }
72

73
                log.Infof("Attempting to bootstrap with: %v", bootstrapper.Name())
×
74

×
75
                // If we still need additional addresses, then we'll compute
×
76
                // the number of address remaining that we need to fetch.
×
77
                numAddrsLeft := numAddrs - uint32(len(addrs))
×
78
                log.Tracef("Querying for %v addresses", numAddrsLeft)
×
NEW
79
                netAddrs, err := bootstrapper.SampleNodeAddrs(
×
NEW
80
                        ctx, numAddrsLeft, ignore,
×
NEW
81
                )
×
82
                if err != nil {
×
83
                        // If we encounter an error with a bootstrapper, then
×
84
                        // we'll continue on to the next available
×
85
                        // bootstrapper.
×
86
                        log.Errorf("Unable to query bootstrapper %v: %v",
×
87
                                bootstrapper.Name(), err)
×
88
                        continue
×
89
                }
90

91
                addrs = append(addrs, netAddrs...)
×
92
        }
93

94
        if len(addrs) == 0 {
×
95
                return nil, errors.New("no addresses found")
×
96
        }
×
97

98
        log.Infof("Obtained %v addrs to bootstrap network with", len(addrs))
×
99

×
100
        return addrs, nil
×
101
}
102

103
// shuffleBootstrappers shuffles the set of bootstrappers in order to avoid
104
// querying the same bootstrapper over and over. To shuffle the set of
105
// candidates, we use a version of the Fisher–Yates shuffle algorithm.
106
func shuffleBootstrappers(candidates []NetworkPeerBootstrapper) []NetworkPeerBootstrapper {
×
107
        shuffled := make([]NetworkPeerBootstrapper, len(candidates))
×
108
        perm := prand.Perm(len(candidates))
×
109

×
110
        for i, v := range perm {
×
111
                shuffled[v] = candidates[i]
×
112
        }
×
113

114
        return shuffled
×
115
}
116

117
// ChannelGraphBootstrapper is an implementation of the NetworkPeerBootstrapper
118
// which attempts to retrieve advertised peers directly from the active channel
119
// graph. This instance requires a backing autopilot.ChannelGraph instance in
120
// order to operate properly.
121
type ChannelGraphBootstrapper struct {
122
        chanGraph autopilot.ChannelGraph
123

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
129

130
        tried map[autopilot.NodeID]struct{}
131
}
132

133
// A compile time assertion to ensure that ChannelGraphBootstrapper meets the
134
// NetworkPeerBootstrapper interface.
135
var _ NetworkPeerBootstrapper = (*ChannelGraphBootstrapper)(nil)
136

137
// NewGraphBootstrapper returns a new instance of a ChannelGraphBootstrapper
138
// backed by an active autopilot.ChannelGraph instance. This type of network
139
// peer bootstrapper will use the authenticated nodes within the known channel
140
// graph to bootstrap connections.
141
func NewGraphBootstrapper(cg autopilot.ChannelGraph) (NetworkPeerBootstrapper, error) {
×
142

×
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
×
150
        }
×
151

152
        return c, nil
×
153
}
154

155
// SampleNodeAddrs uniformly samples a set of specified address from the
156
// network peer bootstrapper source. The num addrs field passed in denotes how
157
// many valid peer addresses to return.
158
//
159
// NOTE: Part of the NetworkPeerBootstrapper interface.
160
func (c *ChannelGraphBootstrapper) SampleNodeAddrs(_ context.Context,
161
        numAddrs uint32,
162
        ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress, error) {
×
163

×
164
        // We'll merge the ignore map with our currently selected map in order
×
165
        // to ensure we don't return any duplicate nodes.
×
166
        for n := range ignore {
×
167
                log.Tracef("Ignored node %x for bootstrapping", n)
×
168
                c.tried[n] = struct{}{}
×
169
        }
×
170

171
        // In order to bootstrap, we'll iterate all the nodes in the channel
172
        // graph, accumulating nodes until either we go through all active
173
        // nodes, or we reach our limit. We ensure that we meet the randomly
174
        // sample constraint as we maintain an xor accumulator to ensure we
175
        // randomly sample nodes independent of the iteration of the channel
176
        // graph.
177
        sampleAddrs := func() ([]*lnwire.NetAddress, error) {
×
178
                var (
×
179
                        a []*lnwire.NetAddress
×
180

×
181
                        // We'll create a special error so we can return early
×
182
                        // and abort the transaction once we find a match.
×
183
                        errFound = fmt.Errorf("found node")
×
184
                )
×
185

×
186
                err := c.chanGraph.ForEachNode(func(node autopilot.Node) error {
×
187
                        nID := autopilot.NodeID(node.PubKey())
×
188
                        if _, ok := c.tried[nID]; ok {
×
189
                                return nil
×
190
                        }
×
191

192
                        // We'll select the first node we come across who's
193
                        // public key is less than our current accumulator
194
                        // value. When comparing, we skip the first byte as
195
                        // it's 50/50. If it isn't less, than then we'll
196
                        // continue forward.
197
                        nodePubKeyBytes := node.PubKey()
×
198
                        if bytes.Compare(c.hashAccumulator[:], nodePubKeyBytes[1:]) > 0 {
×
199
                                return nil
×
200
                        }
×
201

202
                        for _, nodeAddr := range node.Addrs() {
×
203
                                // If we haven't yet reached our limit, then
×
204
                                // we'll copy over the details of this node
×
205
                                // into the set of addresses to be returned.
×
206
                                switch nodeAddr.(type) {
×
207
                                case *net.TCPAddr, *tor.OnionAddr:
×
208
                                default:
×
209
                                        // If this isn't a valid address
×
210
                                        // supported by the protocol, then we'll
×
211
                                        // skip this node.
×
212
                                        return nil
×
213
                                }
214

215
                                nodePub, err := btcec.ParsePubKey(
×
216
                                        nodePubKeyBytes[:],
×
217
                                )
×
218
                                if err != nil {
×
219
                                        return err
×
220
                                }
×
221

222
                                // At this point, we've found an eligible node,
223
                                // so we'll return early with our shibboleth
224
                                // error.
225
                                a = append(a, &lnwire.NetAddress{
×
226
                                        IdentityKey: nodePub,
×
227
                                        Address:     nodeAddr,
×
228
                                })
×
229
                        }
230

231
                        c.tried[nID] = struct{}{}
×
232

×
233
                        return errFound
×
234
                })
235
                if err != nil && err != errFound {
×
236
                        return nil, err
×
237
                }
×
238

239
                return a, nil
×
240
        }
241

242
        // We'll loop and sample new addresses from the graph source until
243
        // we've reached our target number of outbound connections or we hit 50
244
        // attempts, which ever comes first.
245
        var (
×
246
                addrs []*lnwire.NetAddress
×
247
                tries uint32
×
248
        )
×
249
        for tries < 30 && uint32(len(addrs)) < numAddrs {
×
250
                sampleAddrs, err := sampleAddrs()
×
251
                if err != nil {
×
252
                        return nil, err
×
253
                }
×
254

255
                tries++
×
256

×
257
                // We'll now rotate our hash accumulator one value forwards.
×
258
                c.hashAccumulator = sha256.Sum256(c.hashAccumulator[:])
×
259

×
260
                // If this attempt didn't yield any addresses, then we'll exit
×
261
                // early.
×
262
                if len(sampleAddrs) == 0 {
×
263
                        continue
×
264
                }
265

266
                addrs = append(addrs, sampleAddrs...)
×
267
        }
268

269
        log.Tracef("Ending hash accumulator state: %x", c.hashAccumulator)
×
270

×
271
        return addrs, nil
×
272
}
273

274
// Name returns a human readable string which names the concrete implementation
275
// of the NetworkPeerBootstrapper.
276
//
277
// NOTE: Part of the NetworkPeerBootstrapper interface.
278
func (c *ChannelGraphBootstrapper) Name() string {
×
279
        return "Authenticated Channel Graph"
×
280
}
×
281

282
// DNSSeedBootstrapper as an implementation of the NetworkPeerBootstrapper
283
// interface which implements peer bootstrapping via a special DNS seed as
284
// defined in BOLT-0010. For further details concerning Lightning's current DNS
285
// boot strapping protocol, see this link:
286
//   - https://github.com/lightningnetwork/lightning-rfc/blob/master/10-dns-bootstrap.md
287
type DNSSeedBootstrapper struct {
288
        // dnsSeeds is an array of two tuples we'll use for bootstrapping. The
289
        // first item in the tuple is the primary host we'll use to attempt the
290
        // SRV lookup we require. If we're unable to receive a response over
291
        // UDP, then we'll fall back to manual TCP resolution. The second item
292
        // in the tuple is a special A record that we'll query in order to
293
        // receive the IP address of the current authoritative DNS server for
294
        // the network seed.
295
        dnsSeeds [][2]string
296
        net      tor.Net
297

298
        // timeout is the maximum amount of time a dial will wait for a connect to
299
        // complete.
300
        timeout time.Duration
301
}
302

303
// A compile time assertion to ensure that DNSSeedBootstrapper meets the
304
// NetworkPeerjBootstrapper interface.
305
var _ NetworkPeerBootstrapper = (*ChannelGraphBootstrapper)(nil)
306

307
// NewDNSSeedBootstrapper returns a new instance of the DNSSeedBootstrapper.
308
// The set of passed seeds should point to DNS servers that properly implement
309
// Lightning's DNS peer bootstrapping protocol as defined in BOLT-0010. The set
310
// of passed DNS seeds should come in pairs, with the second host name to be
311
// used as a fallback for manual TCP resolution in the case of an error
312
// receiving the UDP response. The second host should return a single A record
313
// with the IP address of the authoritative name server.
314
func NewDNSSeedBootstrapper(
315
        seeds [][2]string, net tor.Net,
316
        timeout time.Duration) NetworkPeerBootstrapper {
×
317

×
318
        return &DNSSeedBootstrapper{dnsSeeds: seeds, net: net, timeout: timeout}
×
319
}
×
320

321
// fallBackSRVLookup attempts to manually query for SRV records we need to
322
// properly bootstrap. We do this by querying the special record at the "soa."
323
// sub-domain of supporting DNS servers. The returned IP address will be the IP
324
// address of the authoritative DNS server. Once we have this IP address, we'll
325
// connect manually over TCP to request the SRV record. This is necessary as
326
// the records we return are currently too large for a class of resolvers,
327
// causing them to be filtered out. The targetEndPoint is the original end
328
// point that was meant to be hit.
329
func (d *DNSSeedBootstrapper) fallBackSRVLookup(soaShim string,
330
        targetEndPoint string) ([]*net.SRV, error) {
×
331

×
332
        log.Tracef("Attempting to query fallback DNS seed")
×
333

×
334
        // First, we'll lookup the IP address of the server that will act as
×
335
        // our shim.
×
336
        addrs, err := d.net.LookupHost(soaShim)
×
337
        if err != nil {
×
338
                return nil, err
×
339
        }
×
340

341
        // Once we have the IP address, we'll establish a TCP connection using
342
        // port 53.
343
        dnsServer := net.JoinHostPort(addrs[0], "53")
×
344
        conn, err := d.net.Dial("tcp", dnsServer, d.timeout)
×
345
        if err != nil {
×
346
                return nil, err
×
347
        }
×
348

349
        dnsHost := fmt.Sprintf("_nodes._tcp.%v.", targetEndPoint)
×
350
        dnsConn := &dns.Conn{Conn: conn}
×
351
        defer dnsConn.Close()
×
352

×
353
        // With the connection established, we'll craft our SRV query, write
×
354
        // toe request, then wait for the server to give our response.
×
355
        msg := new(dns.Msg)
×
356
        msg.SetQuestion(dnsHost, dns.TypeSRV)
×
357
        if err := dnsConn.WriteMsg(msg); err != nil {
×
358
                return nil, err
×
359
        }
×
360
        resp, err := dnsConn.ReadMsg()
×
361
        if err != nil {
×
362
                return nil, err
×
363
        }
×
364

365
        // If the message response code was not the success code, fail.
366
        if resp.Rcode != dns.RcodeSuccess {
×
367
                return nil, fmt.Errorf("unsuccessful SRV request, "+
×
368
                        "received: %v", resp.Rcode)
×
369
        }
×
370

371
        // Retrieve the RR(s) of the Answer section, and convert to the format
372
        // that net.LookupSRV would normally return.
373
        var rrs []*net.SRV
×
374
        for _, rr := range resp.Answer {
×
375
                srv := rr.(*dns.SRV)
×
376
                rrs = append(rrs, &net.SRV{
×
377
                        Target:   srv.Target,
×
378
                        Port:     srv.Port,
×
379
                        Priority: srv.Priority,
×
380
                        Weight:   srv.Weight,
×
381
                })
×
382
        }
×
383

384
        return rrs, nil
×
385
}
386

387
// SampleNodeAddrs uniformly samples a set of specified address from the
388
// network peer bootstrapper source. The num addrs field passed in denotes how
389
// many valid peer addresses to return. The set of DNS seeds are used
390
// successively to retrieve eligible target nodes.
391
func (d *DNSSeedBootstrapper) SampleNodeAddrs(_ context.Context,
392
        numAddrs uint32,
393
        ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress, error) {
×
394

×
395
        var netAddrs []*lnwire.NetAddress
×
396

×
397
        // We'll try all the registered DNS seeds, exiting early if one of them
×
398
        // gives us all the peers we need.
×
399
        //
×
400
        // TODO(roasbeef): should combine results from both
×
401
search:
×
402
        for _, dnsSeedTuple := range d.dnsSeeds {
×
403
                // We'll first query the seed with an SRV record so we can
×
404
                // obtain a random sample of the encoded public keys of nodes.
×
405
                // We use the lndLookupSRV function for this task.
×
406
                primarySeed := dnsSeedTuple[0]
×
407
                _, addrs, err := d.net.LookupSRV(
×
408
                        "nodes", "tcp", primarySeed, d.timeout,
×
409
                )
×
410
                if err != nil {
×
411
                        log.Tracef("Unable to lookup SRV records via "+
×
412
                                "primary seed (%v): %v", primarySeed, err)
×
413

×
414
                        log.Trace("Falling back to secondary")
×
415

×
416
                        // If the host of the secondary seed is blank, then
×
417
                        // we'll bail here as we can't proceed.
×
418
                        if dnsSeedTuple[1] == "" {
×
419
                                log.Tracef("DNS seed %v has no secondary, "+
×
420
                                        "skipping fallback", primarySeed)
×
421
                                continue
×
422
                        }
423

424
                        // If we get an error when trying to query via the
425
                        // primary seed, we'll fallback to the secondary seed
426
                        // before concluding failure.
427
                        soaShim := dnsSeedTuple[1]
×
428
                        addrs, err = d.fallBackSRVLookup(
×
429
                                soaShim, primarySeed,
×
430
                        )
×
431
                        if err != nil {
×
432
                                log.Tracef("Unable to query fall "+
×
433
                                        "back dns seed (%v): %v", soaShim, err)
×
434
                                continue
×
435
                        }
436

437
                        log.Tracef("Successfully queried fallback DNS seed")
×
438
                }
439

440
                log.Tracef("Retrieved SRV records from dns seed: %v",
×
441
                        lnutils.SpewLogClosure(addrs))
×
442

×
443
                // Next, we'll need to issue an A record request for each of
×
444
                // the nodes, skipping it if nothing comes back.
×
445
                for _, nodeSrv := range addrs {
×
446
                        if uint32(len(netAddrs)) >= numAddrs {
×
447
                                break search
×
448
                        }
449

450
                        // With the SRV target obtained, we'll now perform
451
                        // another query to obtain the IP address for the
452
                        // matching bech32 encoded node key. We use the
453
                        // lndLookup function for this task.
454
                        bechNodeHost := nodeSrv.Target
×
455
                        addrs, err := d.net.LookupHost(bechNodeHost)
×
456
                        if err != nil {
×
457
                                return nil, err
×
458
                        }
×
459

460
                        if len(addrs) == 0 {
×
461
                                log.Tracef("No addresses for %v, skipping",
×
462
                                        bechNodeHost)
×
463
                                continue
×
464
                        }
465

466
                        log.Tracef("Attempting to convert: %v", bechNodeHost)
×
467

×
468
                        // If the host isn't correctly formatted, then we'll
×
469
                        // skip it.
×
470
                        if len(bechNodeHost) == 0 ||
×
471
                                !strings.Contains(bechNodeHost, ".") {
×
472

×
473
                                continue
×
474
                        }
475

476
                        // If we have a set of valid addresses, then we'll need
477
                        // to parse the public key from the original bech32
478
                        // encoded string.
479
                        bechNode := strings.Split(bechNodeHost, ".")
×
480
                        _, nodeBytes5Bits, err := bech32.Decode(bechNode[0])
×
481
                        if err != nil {
×
482
                                return nil, err
×
483
                        }
×
484

485
                        // Once we have the bech32 decoded pubkey, we'll need
486
                        // to convert the 5-bit word grouping into our regular
487
                        // 8-bit word grouping so we can convert it into a
488
                        // public key.
489
                        nodeBytes, err := bech32.ConvertBits(
×
490
                                nodeBytes5Bits, 5, 8, false,
×
491
                        )
×
492
                        if err != nil {
×
493
                                return nil, err
×
494
                        }
×
495
                        nodeKey, err := btcec.ParsePubKey(nodeBytes)
×
496
                        if err != nil {
×
497
                                return nil, err
×
498
                        }
×
499

500
                        // If we have an ignore list, and this node is in the
501
                        // ignore list, then we'll go to the next candidate.
502
                        if ignore != nil {
×
503
                                nID := autopilot.NewNodeID(nodeKey)
×
504
                                if _, ok := ignore[nID]; ok {
×
505
                                        continue
×
506
                                }
507
                        }
508

509
                        // Finally we'll convert the host:port peer to a proper
510
                        // TCP address to use within the lnwire.NetAddress. We
511
                        // don't need to use the lndResolveTCP function here
512
                        // because we already have the host:port peer.
513
                        addr := net.JoinHostPort(
×
514
                                addrs[0],
×
515
                                strconv.FormatUint(uint64(nodeSrv.Port), 10),
×
516
                        )
×
517
                        tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
×
518
                        if err != nil {
×
519
                                return nil, err
×
520
                        }
×
521

522
                        // Finally, with all the information parsed, we'll
523
                        // return this fully valid address as a connection
524
                        // attempt.
525
                        lnAddr := &lnwire.NetAddress{
×
526
                                IdentityKey: nodeKey,
×
527
                                Address:     tcpAddr,
×
528
                        }
×
529

×
530
                        log.Tracef("Obtained %v as valid reachable "+
×
531
                                "node", lnAddr)
×
532

×
533
                        netAddrs = append(netAddrs, lnAddr)
×
534
                }
535
        }
536

537
        return netAddrs, nil
×
538
}
539

540
// Name returns a human readable string which names the concrete
541
// implementation of the NetworkPeerBootstrapper.
542
func (d *DNSSeedBootstrapper) Name() string {
×
543
        return fmt.Sprintf("BOLT-0010 DNS Seed: %v", d.dnsSeeds)
×
544
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc