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

handshake-org / hsd / 5762308034

pending completion
5762308034

push

github

nodech
Merge PR #834 from 'nodech/soft-fork-times'

7479 of 12752 branches covered (58.65%)

Branch coverage included in aggregate %.

23904 of 32971 relevant lines covered (72.5%)

31617.2 hits per line

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

92.09
/lib/net/hostlist.js
1
/*!
2
 * hostlist.js - address management for hsd
3
 * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
4
 * https://github.com/handshake-org/hsd
5
 */
6

7
'use strict';
8

9
const assert = require('bsert');
1✔
10
const path = require('path');
1✔
11
const fs = require('bfile');
1✔
12
const IP = require('binet');
1✔
13
const bio = require('bufio');
1✔
14
const Logger = require('blgr');
1✔
15
const Hash256 = require('bcrypto/lib/hash256');
1✔
16
const List = require('blst');
1✔
17
const rng = require('bcrypto/lib/random');
1✔
18
const secp256k1 = require('bcrypto/lib/secp256k1');
1✔
19
const {lookup} = require('./lookup');
1✔
20
const util = require('../utils/util');
1✔
21
const Network = require('../protocol/network');
1✔
22
const NetAddress = require('./netaddress');
1✔
23
const common = require('./common');
1✔
24
const seeds = require('./seeds');
1✔
25

26
/**
27
 * Stochastic address manager based on bitcoin addrman.
28
 *
29
 * Design goals:
30
 *  * Keep the address tables in-memory, and asynchronously dump the entire
31
 *    table to hosts.json.
32
 *  * Make sure no (localized) attacker can fill the entire table with his
33
 *    nodes/addresses.
34
 *
35
 * To that end:
36
 *  * Addresses are organized into buckets that can each store up
37
 *    to 64 entries (maxEntries).
38
 *    * Addresses to which our node has not successfully connected go into
39
 *      1024 "fresh" buckets (maxFreshBuckets).
40
 *      * Based on the address range of the source of information
41
 *        64 buckets are selected at random.
42
 *      * The actual bucket is chosen from one of these, based on the range in
43
 *        which the address itself is located.
44
 *      * The position in the bucket is chosen based on the full address.
45
 *      * One single address can occur in up to 8 different buckets to increase
46
 *        selection chances for addresses that are seen frequently. The chance
47
 *        for increasing this multiplicity decreases exponentially.
48
 *      * When adding a new address to an occupied position of a bucket, it
49
 *        will not replace the existing entry unless that address is also stored
50
 *        in another bucket or it doesn't meet one of several quality criteria
51
 *        (see isStale for exact criteria).
52
 *    * Addresses of nodes that are known to be accessible go into 256 "tried"
53
 *      buckets.
54
 *      * Each address range selects at random 8 of these buckets.
55
 *      * The actual bucket is chosen from one of these, based on the full
56
 *        address.
57
 *    * Bucket selection is based on cryptographic hashing,
58
 *      using a randomly-generated 256-bit key, which should not be observable
59
 *      by adversaries (key).
60
 */
61

62
/**
63
 * Host List
64
 * @alias module:net.HostList
65
 */
66

67
class HostList {
68
  /**
69
   * Create a host list.
70
   * @constructor
71
   * @param {Object} options
72
   */
73

74
  constructor(options) {
75
    this.options = new HostListOptions(options);
165✔
76
    this.network = this.options.network;
165✔
77
    this.logger = this.options.logger.context('hostlist');
165✔
78
    this.address = this.options.address;
165✔
79
    this.brontide = this.options.brontide;
165✔
80
    this.resolve = this.options.resolve;
165✔
81
    this.random = this.options.random;
165✔
82

83
    this.key = rng.randomBytes(32);
165✔
84
    this.hash = new Hash256();
165✔
85
    this.hashbuf = Buffer.alloc(4);
165✔
86
    this.portbuf = Buffer.alloc(2);
165✔
87

88
    this.dnsSeeds = [];
165✔
89
    this.dnsNodes = [];
165✔
90

91
    this.map = new Map();
165✔
92
    this.fresh = [];
165✔
93
    this.totalFresh = 0;
165✔
94
    this.used = [];
165✔
95
    this.totalUsed = 0;
165✔
96
    this.nodes = [];
165✔
97
    this.local = new Map();
165✔
98
    this.banned = new Map();
165✔
99

100
    this.maxFreshBuckets = 1024;
165✔
101
    this.maxUsedBuckets = 256;
165✔
102
    this.maxEntries = 64;
165✔
103

104
    this.timer = null;
165✔
105
    this.needsFlush = false;
165✔
106
    this.flushing = false;
165✔
107
    this.added = false;
165✔
108

109
    this.init();
165✔
110
  }
111

112
  /**
113
   * Initialize list.
114
   * @private
115
   */
116

117
  init() {
118
    for (let i = 0; i < this.maxFreshBuckets; i++)
165✔
119
      this.fresh.push(new Map());
168,960✔
120

121
    for (let i = 0; i < this.maxUsedBuckets; i++)
165✔
122
      this.used.push(new List());
42,240✔
123
  }
124

125
  /**
126
   * Initialize list.
127
   * @private
128
   */
129

130
  initAdd() {
131
    if (this.added)
29✔
132
      return;
1✔
133

134
    const options = this.options;
28✔
135
    const scores = HostList.scores;
28✔
136

137
    this.setSeeds(options.seeds);
28✔
138
    this.setNodes(options.nodes);
28✔
139

140
    this.pushLocal(this.address, scores.MANUAL);
28✔
141
    this.pushLocal(this.brontide, scores.MANUAL);
28✔
142
    this.addLocal(options.host, options.port, scores.BIND);
28✔
143

144
    const hosts = IP.getPublic();
28✔
145
    const port = this.address.port;
28✔
146

147
    for (const host of hosts)
28✔
148
      this.addLocal(host, port, scores.IF);
2✔
149

150
    this.added = true;
28✔
151
  }
152

153
  /**
154
   * Open hostlist and read hosts file.
155
   * @method
156
   * @returns {Promise}
157
   */
158

159
  async open() {
160
    this.initAdd();
27✔
161

162
    try {
27✔
163
      await this.loadFile();
27✔
164
    } catch (e) {
165
      this.logger.warning('Hosts deserialization failed.');
×
166
      this.logger.error(e);
×
167
    }
168

169
    if (this.size() === 0)
27✔
170
      this.injectSeeds();
13✔
171

172
    await this.discoverNodes();
27✔
173

174
    this.start();
27✔
175
  }
176

177
  /**
178
   * Close hostlist.
179
   * @method
180
   * @returns {Promise}
181
   */
182

183
  async close() {
184
    this.stop();
27✔
185
    await this.flush();
27✔
186
    this.reset();
27✔
187
  }
188

189
  /**
190
   * Start flush interval.
191
   */
192

193
  start() {
194
    if (this.options.memory)
27✔
195
      return;
25✔
196

197
    if (!this.options.filename)
2!
198
      return;
×
199

200
    assert(this.timer == null);
2✔
201
    this.timer = setInterval(() => this.flush(), this.options.flushInterval);
2✔
202
  }
203

204
  /**
205
   * Stop flush interval.
206
   */
207

208
  stop() {
209
    if (this.options.memory)
27✔
210
      return;
25✔
211

212
    if (!this.options.filename)
2!
213
      return;
×
214

215
    assert(this.timer != null);
2✔
216
    clearInterval(this.timer);
2✔
217
    this.timer = null;
2✔
218
  }
219

220
  /**
221
   * Read and initialize from hosts file.
222
   * @method
223
   * @returns {Promise}
224
   */
225

226
  injectSeeds() {
227
    const nodes = seeds.get(this.network.type);
13✔
228

229
    for (const node of nodes) {
13✔
230
      const addr = NetAddress.fromHostname(node, this.network);
201✔
231

232
      if (this.map.has(addr.hostname))
201!
233
        continue;
×
234

235
      if (!addr.isRoutable())
201!
236
        continue;
×
237

238
      if (!this.options.onion && addr.isOnion())
201!
239
        continue;
×
240

241
      if (this.options.brontideOnly && !addr.hasKey())
201!
242
        continue;
×
243

244
      if (addr.port === 0)
201!
245
        continue;
×
246

247
      this.add(addr);
201✔
248
    }
249
  }
250

251
  /**
252
   * Read and initialize from hosts file.
253
   * @method
254
   * @returns {Promise}
255
   */
256

257
  async loadFile() {
258
    const filename = this.options.filename;
27✔
259

260
    if (fs.unsupported)
27!
261
      return;
×
262

263
    if (this.options.memory)
27✔
264
      return;
25✔
265

266
    if (!filename)
2!
267
      return;
×
268

269
    let data;
270
    try {
2✔
271
      data = await fs.readFile(filename, 'utf8');
2✔
272
    } catch (e) {
273
      if (e.code === 'ENOENT')
1!
274
        return;
1✔
275
      throw e;
×
276
    }
277

278
    const json = JSON.parse(data);
1✔
279

280
    this.fromJSON(json);
1✔
281
  }
282

283
  /**
284
   * Flush addrs to hosts file.
285
   * @method
286
   * @returns {Promise}
287
   */
288

289
  async flush() {
290
    const filename = this.options.filename;
27✔
291

292
    if (fs.unsupported)
27!
293
      return;
×
294

295
    if (this.options.memory)
27✔
296
      return;
25✔
297

298
    if (!filename)
2!
299
      return;
×
300

301
    if (!this.needsFlush)
2✔
302
      return;
1✔
303

304
    if (this.flushing)
1!
305
      return;
×
306

307
    this.needsFlush = false;
1✔
308

309
    this.logger.debug('Writing hosts to %s.', filename);
1✔
310

311
    const json = this.toJSON();
1✔
312
    const data = JSON.stringify(json);
1✔
313

314
    this.flushing = true;
1✔
315

316
    try {
1✔
317
      await fs.writeFile(filename, data, 'utf8');
1✔
318
    } catch (e) {
319
      this.logger.warning('Writing hosts failed.');
×
320
      this.logger.error(e);
×
321
    }
322

323
    this.flushing = false;
1✔
324
  }
325

326
  /**
327
   * Get list size.
328
   * @returns {Number}
329
   */
330

331
  size() {
332
    return this.totalFresh + this.totalUsed;
66✔
333
  }
334

335
  /**
336
   * Test whether the host list is full.
337
   * @returns {Boolean}
338
   */
339

340
  isFull() {
341
    return this.totalFresh >= this.maxFreshBuckets * this.maxEntries;
×
342
  }
343

344
  /**
345
   * Reset host list.
346
   */
347

348
  reset() {
349
    this.map.clear();
31✔
350

351
    for (const bucket of this.fresh)
31✔
352
      bucket.clear();
31,744✔
353

354
    for (const bucket of this.used)
31✔
355
      bucket.reset();
7,936✔
356

357
    this.totalFresh = 0;
31✔
358
    this.totalUsed = 0;
31✔
359

360
    this.nodes.length = 0;
31✔
361
  }
362

363
  /**
364
   * Mark a peer as banned.
365
   * @param {String} host
366
   */
367

368
  ban(host) {
369
    this.banned.set(host, util.now());
10✔
370
  }
371

372
  /**
373
   * Unban host.
374
   * @param {String} host
375
   */
376

377
  unban(host) {
378
    this.banned.delete(host);
3✔
379
  }
380

381
  /**
382
   * Clear banned hosts.
383
   */
384

385
  clearBanned() {
386
    this.banned.clear();
1✔
387
  }
388

389
  /**
390
   * Test whether the host is banned.
391
   * @param {String} host
392
   * @returns {Boolean}
393
   */
394

395
  isBanned(host) {
396
    const time = this.banned.get(host);
50✔
397

398
    if (time == null)
50✔
399
      return false;
44✔
400

401
    if (util.now() > time + this.options.banTime) {
6✔
402
      this.banned.delete(host);
1✔
403
      return false;
1✔
404
    }
405

406
    return true;
5✔
407
  }
408

409
  /**
410
   * Allocate a new host.
411
   * @returns {HostEntry}
412
   */
413

414
  getHost() {
415
    let buckets = null;
33✔
416

417
    if (this.totalFresh > 0)
33✔
418
      buckets = this.fresh;
31✔
419

420
    if (this.totalUsed > 0) {
33✔
421
      if (this.totalFresh === 0 || this.random(2) === 0)
1!
422
        buckets = this.used;
1✔
423
    }
424

425
    if (!buckets)
33✔
426
      return null;
1✔
427

428
    const now = this.network.now();
32✔
429

430
    let factor = 1;
32✔
431

432
    for (;;) {
32✔
433
      const i = this.random(buckets.length);
1,747✔
434
      const bucket = buckets[i];
1,747✔
435

436
      if (bucket.size === 0)
1,747✔
437
        continue;
1,715✔
438

439
      let index = this.random(bucket.size);
32✔
440
      let entry;
441

442
      if (buckets === this.used) {
32✔
443
        entry = bucket.head;
1✔
444
        while (index--)
1✔
445
          entry = entry.next;
93✔
446
      } else {
447
        for (entry of bucket.values()) {
31✔
448
          if (index === 0)
69✔
449
            break;
31✔
450
          index -= 1;
38✔
451
        }
452
      }
453

454
      const num = this.random(1 << 30);
32✔
455

456
      if (num < factor * entry.chance(now) * (1 << 30))
32!
457
        return entry;
32✔
458

459
      factor *= 1.2;
×
460
    }
461
  }
462

463
  /**
464
   * Get fresh bucket for host.
465
   * @private
466
   * @param {HostEntry} entry
467
   * @param {NetAddress?} src
468
   * @returns {Map}
469
   */
470

471
  freshBucket(entry, src) {
472
    const addr = entry.addr;
294✔
473

474
    if (!src)
294✔
475
      src = entry.src;
1✔
476

477
    this.hash.init();
294✔
478
    this.hash.update(this.key);
294✔
479
    this.hash.update(addr.getGroup());
294✔
480
    this.hash.update(src.getGroup());
294✔
481

482
    const hash1 = this.hash.final();
294✔
483
    const hash32 = bio.readU32(hash1, 0) % 64;
294✔
484

485
    bio.writeU32(this.hashbuf, hash32, 0);
294✔
486

487
    this.hash.init();
294✔
488
    this.hash.update(this.key);
294✔
489
    this.hash.update(src.getGroup());
294✔
490
    this.hash.update(this.hashbuf);
294✔
491

492
    const hash2 = this.hash.final();
294✔
493
    const hash = bio.readU32(hash2, 0);
294✔
494
    const index = hash % this.fresh.length;
294✔
495

496
    return this.fresh[index];
294✔
497
  }
498

499
  /**
500
   * Get used bucket for host.
501
   * @private
502
   * @param {HostEntry} entry
503
   * @returns {List}
504
   */
505

506
  usedBucket(entry) {
507
    const addr = entry.addr;
14✔
508

509
    bio.writeU16(this.portbuf, addr.port, 0);
14✔
510

511
    this.hash.init();
14✔
512
    this.hash.update(this.key);
14✔
513
    this.hash.update(addr.raw);
14✔
514
    this.hash.update(this.portbuf);
14✔
515
    this.hash.update(addr.key);
14✔
516

517
    const hash1 = this.hash.final();
14✔
518
    const hash32 = bio.readU32(hash1, 0) % 8;
14✔
519

520
    bio.writeU32(this.hashbuf, hash32, 0);
14✔
521

522
    this.hash.init();
14✔
523
    this.hash.update(this.key);
14✔
524
    this.hash.update(addr.getGroup());
14✔
525
    this.hash.update(this.hashbuf);
14✔
526

527
    const hash2 = this.hash.final();
14✔
528
    const hash = bio.readU32(hash2, 0);
14✔
529
    const index = hash % this.used.length;
14✔
530

531
    return this.used[index];
14✔
532
  }
533

534
  /**
535
   * Add host to host list.
536
   * @param {NetAddress} addr
537
   * @param {NetAddress?} src
538
   * @returns {Boolean}
539
   */
540

541
  add(addr, src) {
542
    assert(addr.port !== 0);
383✔
543

544
    let entry = this.map.get(addr.hostname);
383✔
545

546
    if (entry) {
383✔
547
      let penalty = 2 * 60 * 60;
23✔
548
      let interval = 24 * 60 * 60;
23✔
549

550
      // No source means we're inserting
551
      // this ourselves. No penalty.
552
      if (!src)
23✔
553
        penalty = 0;
1✔
554

555
      // Update services.
556
      entry.addr.services |= addr.services;
23✔
557
      entry.addr.services >>>= 0;
23✔
558

559
      // Online?
560
      const now = this.network.now();
23✔
561
      if (now - addr.time < 24 * 60 * 60)
23✔
562
        interval = 60 * 60;
20✔
563

564
      // Periodically update time.
565
      if (entry.addr.time < addr.time - interval - penalty) {
23✔
566
        entry.addr.time = addr.time;
4✔
567
        this.needsFlush = true;
4✔
568
      }
569

570
      // Do not update if no new
571
      // information is present.
572
      if (entry.addr.time && addr.time <= entry.addr.time)
23✔
573
        return false;
5✔
574

575
      // Do not update if the entry was
576
      // already in the "used" table.
577
      if (entry.used)
18✔
578
        return false;
3✔
579

580
      assert(entry.refCount > 0);
15✔
581

582
      // Do not update if the max
583
      // reference count is reached.
584
      if (entry.refCount === HostList.MAX_REFS)
15✔
585
        return false;
1✔
586

587
      assert(entry.refCount < HostList.MAX_REFS);
14✔
588

589
      // Stochastic test: previous refCount
590
      // N: 2^N times harder to increase it.
591
      let factor = 1;
14✔
592
      for (let i = 0; i < entry.refCount; i++)
14✔
593
        factor *= 2;
56✔
594

595
      if (this.random(factor) !== 0)
14!
596
        return false;
×
597
    } else {
598
      if (!src)
360✔
599
        src = this.address;
244✔
600

601
      entry = new HostEntry(addr, src);
360✔
602

603
      this.totalFresh += 1;
360✔
604
    }
605

606
    const bucket = this.freshBucket(entry, src);
374✔
607

608
    if (bucket.has(entry.key()))
374✔
609
      return false;
1✔
610

611
    if (bucket.size >= this.maxEntries)
373✔
612
      this.evictFresh(bucket);
1✔
613

614
    bucket.set(entry.key(), entry);
373✔
615
    entry.refCount += 1;
373✔
616

617
    this.map.set(entry.key(), entry);
373✔
618
    this.needsFlush = true;
373✔
619

620
    return true;
373✔
621
  }
622

623
  /**
624
   * Evict a host from fresh bucket.
625
   * @param {Map} bucket
626
   */
627

628
  evictFresh(bucket) {
629
    let old = null;
9✔
630

631
    for (const entry of bucket.values()) {
9✔
632
      if (this.isStale(entry)) {
39✔
633
        bucket.delete(entry.key());
2✔
634

635
        if (--entry.refCount === 0) {
2✔
636
          this.map.delete(entry.key());
1✔
637
          this.totalFresh -= 1;
1✔
638
        }
639

640
        continue;
2✔
641
      }
642

643
      if (!old) {
37✔
644
        old = entry;
8✔
645
        continue;
8✔
646
      }
647

648
      if (entry.addr.time < old.addr.time)
29!
649
        old = entry;
29✔
650
    }
651

652
    if (!old)
9✔
653
      return;
1✔
654

655
    bucket.delete(old.key());
8✔
656

657
    if (--old.refCount === 0) {
8✔
658
      this.map.delete(old.key());
7✔
659
      this.totalFresh -= 1;
7✔
660
    }
661
  }
662

663
  /**
664
   * Test whether a host is evictable.
665
   * @param {HostEntry} entry
666
   * @returns {Boolean}
667
   */
668

669
  isStale(entry) {
670
    const now = this.network.now();
73✔
671

672
    if (entry.lastAttempt && entry.lastAttempt >= now - 60)
73✔
673
      return false;
1✔
674

675
    if (entry.addr.time > now + 10 * 60)
72✔
676
      return true;
2✔
677

678
    if (entry.addr.time === 0)
70✔
679
      return true;
4✔
680

681
    if (now - entry.addr.time > HostList.HORIZON_DAYS * 24 * 60 * 60)
66✔
682
      return true;
1✔
683

684
    if (entry.lastSuccess === 0 && entry.attempts >= HostList.RETRIES)
65✔
685
      return true;
3✔
686

687
    if (now - entry.lastSuccess > HostList.MIN_FAIL_DAYS * 24 * 60 * 60) {
62!
688
      if (entry.attempts >= HostList.MAX_FAILURES)
62✔
689
        return true;
1✔
690
    }
691

692
    return false;
61✔
693
  }
694

695
  /**
696
   * Remove host from host list.
697
   * @param {String} hostname
698
   * @returns {NetAddress}
699
   */
700

701
  remove(hostname) {
702
    const entry = this.map.get(hostname);
6✔
703

704
    if (!entry)
6✔
705
      return null;
2✔
706

707
    if (entry.used) {
4✔
708
      let head = entry;
2✔
709

710
      assert(entry.refCount === 0);
2✔
711

712
      while (head.prev)
2✔
713
        head = head.prev;
×
714

715
      for (const bucket of this.used) {
2✔
716
        if (bucket.head === head) {
244✔
717
          bucket.remove(entry);
2✔
718
          this.totalUsed -= 1;
2✔
719
          head = null;
2✔
720
          break;
2✔
721
        }
722
      }
723

724
      assert(!head);
2✔
725
    } else {
726
      for (const bucket of this.fresh) {
2✔
727
        if (bucket.delete(entry.key()))
2,048✔
728
          entry.refCount -= 1;
2✔
729
      }
730

731
      this.totalFresh -= 1;
2✔
732
      assert(entry.refCount === 0);
2✔
733
    }
734

735
    this.map.delete(entry.key());
4✔
736

737
    return entry.addr;
4✔
738
  }
739

740
  /**
741
   * Mark host as failed.
742
   * @param {String} hostname
743
   */
744

745
  markAttempt(hostname) {
746
    const entry = this.map.get(hostname);
41✔
747
    const now = this.network.now();
41✔
748

749
    if (!entry)
41✔
750
      return;
3✔
751

752
    entry.attempts += 1;
38✔
753
    entry.lastAttempt = now;
38✔
754
  }
755

756
  /**
757
   * Mark host as successfully connected.
758
   * @param {String} hostname
759
   */
760

761
  markSuccess(hostname) {
762
    const entry = this.map.get(hostname);
12✔
763
    const now = this.network.now();
12✔
764

765
    if (!entry)
12✔
766
      return;
1✔
767

768
    if (now - entry.addr.time > 20 * 60)
11✔
769
      entry.addr.time = now;
1✔
770
  }
771

772
  /**
773
   * Mark host as successfully ack'd.
774
   * @param {String} hostname
775
   * @param {Number} services
776
   */
777

778
  markAck(hostname, services) {
779
    const entry = this.map.get(hostname);
15✔
780

781
    if (!entry)
15✔
782
      return;
1✔
783

784
    const now = this.network.now();
14✔
785

786
    entry.addr.services |= services;
14✔
787
    entry.addr.services >>>= 0;
14✔
788

789
    entry.lastSuccess = now;
14✔
790
    entry.lastAttempt = now;
14✔
791
    entry.attempts = 0;
14✔
792

793
    if (entry.used)
14✔
794
      return;
1✔
795

796
    assert(entry.refCount > 0);
13✔
797

798
    // Remove from fresh.
799
    let old = null;
13✔
800
    for (const bucket of this.fresh) {
13✔
801
      if (bucket.delete(entry.key())) {
13,312✔
802
        entry.refCount -= 1;
20✔
803
        old = bucket;
20✔
804
      }
805
    }
806

807
    assert(old);
13✔
808
    assert(entry.refCount === 0);
13✔
809
    this.totalFresh -= 1;
13✔
810

811
    // Find room in used bucket.
812
    const bucket = this.usedBucket(entry);
13✔
813

814
    if (bucket.size < this.maxEntries) {
13✔
815
      entry.used = true;
12✔
816
      bucket.push(entry);
12✔
817
      this.totalUsed += 1;
12✔
818
      return;
12✔
819
    }
820

821
    // No room. Evict.
822
    const evicted = this.evictUsed(bucket);
1✔
823

824
    let fresh = this.freshBucket(evicted);
1✔
825

826
    // Move to entry's old bucket if no room.
827
    if (fresh.size >= this.maxEntries)
1!
828
      fresh = old;
×
829

830
    // Swap to evicted's used bucket.
831
    entry.used = true;
1✔
832
    bucket.replace(evicted, entry);
1✔
833

834
    // Move evicted to fresh bucket.
835
    evicted.used = false;
1✔
836
    fresh.set(evicted.key(), evicted);
1✔
837
    assert(evicted.refCount === 0);
1✔
838
    evicted.refCount += 1;
1✔
839
    this.totalFresh += 1;
1✔
840
  }
841

842
  /**
843
   * Pick used for eviction.
844
   * @param {List} bucket
845
   */
846

847
  evictUsed(bucket) {
848
    let old = bucket.head;
1✔
849

850
    for (let entry = bucket.head; entry; entry = entry.next) {
1✔
851
      if (entry.addr.time < old.addr.time)
64✔
852
        old = entry;
63✔
853
    }
854

855
    return old;
1✔
856
  }
857

858
  /**
859
   * Convert address list to array.
860
   * @returns {NetAddress[]}
861
   */
862

863
  toArray() {
864
    const items = [];
10✔
865
    const out = [];
10✔
866

867
    for (const entry of this.map.values())
10✔
868
      items.push(entry);
22✔
869

870
    for (let i = 0; i < items.length && out.length < 2500; i++) {
10✔
871
      const j = this.random(items.length - i);
22✔
872

873
      [items[i], items[i + j]] = [items[i + j], items[i]];
22✔
874

875
      const entry = items[i];
22✔
876

877
      if (!this.isStale(entry))
22✔
878
        out.push(entry.addr);
20✔
879
    }
880

881
    return out;
10✔
882
  }
883

884
  /**
885
   * Add a preferred seed.
886
   * @param {String} host
887
   */
888

889
  addSeed(host) {
890
    const ip = IP.fromHostname(host);
35✔
891

892
    if (ip.type === IP.types.DNS) {
35✔
893
      // Defer for resolution.
894
      this.dnsSeeds.push(ip);
27✔
895
      return null;
27✔
896
    }
897

898
    if (ip.port === 0)
8✔
899
      ip.port = ip.key ? this.network.brontidePort : this.network.port;
6✔
900

901
    const addr = NetAddress.fromHost(ip.host, ip.port, ip.key, this.network);
8✔
902

903
    this.add(addr);
8✔
904

905
    return addr;
8✔
906
  }
907

908
  /**
909
   * Add a priority node.
910
   * @param {String} host
911
   * @returns {NetAddress}
912
   */
913

914
  addNode(host) {
915
    const ip = IP.fromHostname(host);
28✔
916

917
    if (ip.type === IP.types.DNS) {
28✔
918
      // Defer for resolution.
919
      this.dnsNodes.push(ip);
4✔
920
      return null;
4✔
921
    }
922

923
    if (ip.port === 0)
24✔
924
      ip.port = ip.key ? this.network.brontidePort : this.network.port;
19✔
925

926
    const addr = NetAddress.fromHost(ip.host, ip.port, ip.key, this.network);
24✔
927

928
    this.nodes.push(addr);
24✔
929
    this.add(addr);
24✔
930

931
    return addr;
24✔
932
  }
933

934
  /**
935
   * Remove a priority node.
936
   * @param {String} host
937
   * @returns {Boolean}
938
   */
939

940
  removeNode(host) {
941
    const addr = IP.fromHostname(host);
5✔
942

943
    if (addr.port === 0)
5✔
944
      addr.port = addr.key ? this.network.brontidePort : this.network.port;
2✔
945

946
    for (let i = 0; i < this.nodes.length; i++) {
5✔
947
      const node = this.nodes[i];
10✔
948

949
      if (node.host !== addr.host)
10✔
950
        continue;
5✔
951

952
      if (node.port !== addr.port)
5✔
953
        continue;
1✔
954

955
      this.nodes.splice(i, 1);
4✔
956

957
      return true;
4✔
958
    }
959

960
    return false;
1✔
961
  }
962

963
  /**
964
   * Set initial seeds.
965
   * @param {String[]} seeds
966
   */
967

968
  setSeeds(seeds) {
969
    this.dnsSeeds.length = 0;
29✔
970

971
    for (const host of seeds)
29✔
972
      this.addSeed(host);
29✔
973
  }
974

975
  /**
976
   * Set priority nodes.
977
   * @param {String[]} nodes
978
   */
979

980
  setNodes(nodes) {
981
    this.dnsNodes.length = 0;
29✔
982
    this.nodes.length = 0;
29✔
983

984
    for (const host of nodes)
29✔
985
      this.addNode(host);
18✔
986
  }
987

988
  /**
989
   * Add a local address.
990
   * @param {String} host
991
   * @param {Number} port
992
   * @param {Number} score
993
   * @returns {Boolean}
994
   */
995

996
  addLocal(host, port, score) {
997
    const addr = NetAddress.fromHost(host, port, null, this.network);
39✔
998
    addr.services = this.options.services;
39✔
999
    return this.pushLocal(addr, score);
39✔
1000
  }
1001

1002
  /**
1003
   * Add a local address.
1004
   * @param {NetAddress} addr
1005
   * @param {Number} score
1006
   * @returns {Boolean}
1007
   */
1008

1009
  pushLocal(addr, score) {
1010
    if (!addr.isRoutable())
162✔
1011
      return false;
80✔
1012

1013
    if (this.local.has(addr.hostname))
82✔
1014
      return false;
1✔
1015

1016
    const local = new LocalAddress(addr, score);
81✔
1017

1018
    this.local.set(addr.hostname, local);
81✔
1019

1020
    return true;
81✔
1021
  }
1022

1023
  /**
1024
   * Get local address based on reachability.
1025
   * @param {NetAddress?} src
1026
   * @returns {NetAddress}
1027
   */
1028

1029
  getLocal(src) {
1030
    let bestReach = -1;
42✔
1031
    let bestScore = -1;
42✔
1032
    let bestDest = null;
42✔
1033

1034
    if (!src) {
42✔
1035
      for (const dest of this.local.values()) {
17✔
1036
        // Disable everything except MANUAL
1037
        if (dest.type < HostList.scores.UPNP)
9✔
1038
          continue;
4✔
1039

1040
        if (dest.addr.hasKey())
5✔
1041
          continue;
2✔
1042

1043
        if (dest.score > bestScore) {
3!
1044
          bestScore = dest.score;
3✔
1045
          bestDest = dest.addr;
3✔
1046
        }
1047
      }
1048

1049
      return bestDest;
17✔
1050
    }
1051

1052
    for (const dest of this.local.values()) {
25✔
1053
      // Disable everything except MANUAL
1054
      if (dest.type < HostList.scores.UPNP)
66✔
1055
        continue;
12✔
1056

1057
      if (dest.addr.hasKey())
54✔
1058
        continue;
4✔
1059

1060
      const reach = src.getReachability(dest.addr);
50✔
1061

1062
      if (reach < bestReach)
50✔
1063
        continue;
12✔
1064

1065
      if (reach > bestReach || dest.score > bestScore) {
38✔
1066
        bestReach = reach;
25✔
1067
        bestScore = dest.score;
25✔
1068
        bestDest = dest.addr;
25✔
1069
      }
1070
    }
1071

1072
    if (bestDest)
25✔
1073
      bestDest.time = this.network.now();
22✔
1074

1075
    return bestDest;
25✔
1076
  }
1077

1078
  /**
1079
   * Mark local address as seen during a handshake.
1080
   * @param {NetAddress} addr
1081
   * @returns {Boolean}
1082
   */
1083

1084
  markLocal(addr) {
1085
    const local = this.local.get(addr.hostname);
2✔
1086

1087
    if (!local)
2✔
1088
      return false;
1✔
1089

1090
    local.score += 1;
1✔
1091

1092
    return true;
1✔
1093
  }
1094

1095
  /**
1096
   * Discover hosts from seeds.
1097
   * @method
1098
   * @returns {Promise}
1099
   */
1100

1101
  async discoverSeeds() {
1102
    const jobs = [];
1✔
1103

1104
    for (const seed of this.dnsSeeds)
1✔
1105
      jobs.push(this.populateSeed(seed));
2✔
1106

1107
    return Promise.all(jobs);
1✔
1108
  }
1109

1110
  /**
1111
   * Discover hosts from nodes.
1112
   * @method
1113
   * @returns {Promise}
1114
   */
1115

1116
  async discoverNodes() {
1117
    const jobs = [];
28✔
1118

1119
    for (const node of this.dnsNodes)
28✔
1120
      jobs.push(this.populateNode(node));
2✔
1121

1122
    return Promise.all(jobs);
28✔
1123
  }
1124

1125
  /**
1126
   * Lookup node's domain.
1127
   * @method
1128
   * @param {Object} addr
1129
   * @returns {Promise}
1130
   */
1131

1132
  async populateNode(addr) {
1133
    const addrs = await this.populate(addr);
4✔
1134

1135
    if (addrs.length === 0)
4✔
1136
      return;
1✔
1137

1138
    this.nodes.push(addrs[0]);
3✔
1139
    this.add(addrs[0]);
3✔
1140
  }
1141

1142
  /**
1143
   * Populate from seed.
1144
   * @method
1145
   * @param {Object} seed
1146
   * @returns {Promise}
1147
   */
1148

1149
  async populateSeed(seed) {
1150
    const addrs = await this.populate(seed);
3✔
1151

1152
    for (const addr of addrs)
3✔
1153
      this.add(addr);
7✔
1154
  }
1155

1156
  /**
1157
   * Lookup hosts from dns host.
1158
   * @method
1159
   * @param {Object} target
1160
   * @returns {Promise}
1161
   */
1162

1163
  async populate(target) {
1164
    const addrs = [];
10✔
1165

1166
    assert(target.type === IP.types.DNS, 'Resolved host passed.');
10✔
1167

1168
    this.logger.info('Resolving host: %s.', target.host);
9✔
1169

1170
    let hosts;
1171
    try {
9✔
1172
      hosts = await this.resolve(target.host);
9✔
1173
    } catch (e) {
1174
      this.logger.error(e);
1✔
1175
      return addrs;
1✔
1176
    }
1177

1178
    for (const host of hosts) {
8✔
1179
      const addr =
1180
        NetAddress.fromHost(host, this.network.port, null, this.network);
17✔
1181
      addrs.push(addr);
17✔
1182
    }
1183

1184
    return addrs;
8✔
1185
  }
1186

1187
  /**
1188
   * Convert host list to json-friendly object.
1189
   * @returns {Object}
1190
   */
1191

1192
  toJSON() {
1193
    const addrs = [];
2✔
1194
    const fresh = [];
2✔
1195
    const used = [];
2✔
1196

1197
    for (const entry of this.map.values())
2✔
1198
      addrs.push(entry.toJSON());
40✔
1199

1200
    for (const bucket of this.fresh) {
2✔
1201
      const keys = [];
2,048✔
1202
      for (const key of bucket.keys())
2,048✔
1203
        keys.push(key);
20✔
1204
      fresh.push(keys);
2,048✔
1205
    }
1206

1207
    for (const bucket of this.used) {
2✔
1208
      const keys = [];
512✔
1209
      for (let entry = bucket.head; entry; entry = entry.next)
512✔
1210
        keys.push(entry.key());
20✔
1211
      used.push(keys);
512✔
1212
    }
1213

1214
    return {
2✔
1215
      version: HostList.VERSION,
1216
      network: this.network.type,
1217
      magic: this.network.magic,
1218
      key: this.key.toString('hex'),
1219
      addrs: addrs,
1220
      fresh: fresh,
1221
      used: used
1222
    };
1223
  }
1224

1225
  /**
1226
   * Inject properties from json object.
1227
   * @private
1228
   * @param {Object} json
1229
   * @returns {HostList}
1230
   */
1231

1232
  fromJSON(json) {
1233
    const sources = new Map();
2✔
1234
    const map = new Map();
2✔
1235
    const fresh = [];
2✔
1236
    const used = [];
2✔
1237

1238
    let totalFresh = 0;
2✔
1239
    let totalUsed = 0;
2✔
1240

1241
    assert(json && typeof json === 'object');
2✔
1242

1243
    assert(!json.network || json.network === this.network.type,
2✔
1244
      'Network mismatch.');
1245

1246
    assert(json.magic === this.network.magic, 'Magic mismatch.');
2✔
1247

1248
    if (json.version < 4) {
2!
1249
      // Migrate to v4.
1250
      for (const item of json.addrs) {
×
1251
        const entry = HostEntry.fromJSON(item, this.network);
×
1252
        const {addr, src} = entry;
×
1253
        const time = addr.time;
×
1254

1255
        if (!entry.lastSuccess)
×
1256
          continue;
×
1257

1258
        this.add(addr, src);
×
1259
        this.markAttempt(addr.hostname);
×
1260
        this.markSuccess(addr.hostname);
×
1261
        this.markAck(addr.hostname, 0);
×
1262

1263
        const e = this.map.get(addr.hostname);
×
1264

1265
        if (e) {
×
1266
          e.attempts = entry.attempts;
×
1267
          e.lastSuccess = entry.lastSuccess;
×
1268
          e.lastAttempt = entry.lastAttempt;
×
1269
          e.addr.time = time;
×
1270
        }
1271
      }
1272

1273
      this.injectSeeds();
×
1274

1275
      return this;
×
1276
    }
1277

1278
    assert(json.version === HostList.VERSION,
2✔
1279
      'Bad address serialization version.');
1280

1281
    assert(typeof json.key === 'string');
2✔
1282
    assert(Array.isArray(json.addrs));
2✔
1283

1284
    const key = Buffer.from(json.key, 'hex');
2✔
1285

1286
    assert(key.length === 32);
2✔
1287

1288
    for (const addr of json.addrs) {
2✔
1289
      const entry = HostEntry.fromJSON(addr, this.network);
40✔
1290

1291
      let src = sources.get(entry.src.hostname);
40✔
1292

1293
      // Save some memory.
1294
      if (!src) {
40!
1295
        src = entry.src;
40✔
1296
        sources.set(src.hostname, src);
40✔
1297
      }
1298

1299
      entry.src = src;
40✔
1300

1301
      map.set(entry.key(), entry);
40✔
1302
    }
1303

1304
    assert(Array.isArray(json.fresh));
2✔
1305
    assert(json.fresh.length <= this.maxFreshBuckets,
2✔
1306
      'Buckets mismatch.');
1307

1308
    for (const keys of json.fresh) {
2✔
1309
      const bucket = new Map();
2,048✔
1310

1311
      for (const key of keys) {
2,048✔
1312
        const entry = map.get(key);
20✔
1313
        assert(entry);
20✔
1314
        if (entry.refCount === 0)
20!
1315
          totalFresh += 1;
20✔
1316
        entry.refCount += 1;
20✔
1317
        bucket.set(key, entry);
20✔
1318
      }
1319

1320
      assert(bucket.size <= this.maxEntries,
2,048✔
1321
        'Bucket size mismatch.');
1322

1323
      fresh.push(bucket);
2,048✔
1324
    }
1325

1326
    assert(fresh.length === this.fresh.length,
2✔
1327
      'Buckets mismatch.');
1328

1329
    assert(Array.isArray(json.used));
2✔
1330
    assert(json.used.length <= this.maxUsedBuckets,
2✔
1331
      'Buckets mismatch.');
1332

1333
    for (const keys of json.used) {
2✔
1334
      const bucket = new List();
512✔
1335

1336
      for (const key of keys) {
512✔
1337
        const entry = map.get(key);
20✔
1338
        assert(entry);
20✔
1339
        assert(entry.refCount === 0);
20✔
1340
        assert(!entry.used);
20✔
1341
        entry.used = true;
20✔
1342
        totalUsed += 1;
20✔
1343
        bucket.push(entry);
20✔
1344
      }
1345

1346
      assert(bucket.size <= this.maxEntries,
512✔
1347
        'Bucket size mismatch.');
1348

1349
      used.push(bucket);
512✔
1350
    }
1351

1352
    assert(used.length === this.used.length,
2✔
1353
      'Buckets mismatch.');
1354

1355
    for (const entry of map.values())
2✔
1356
      assert(entry.used || entry.refCount > 0);
40✔
1357

1358
    this.key = key;
2✔
1359
    this.map = map;
2✔
1360
    this.fresh = fresh;
2✔
1361
    this.totalFresh = totalFresh;
2✔
1362
    this.used = used;
2✔
1363
    this.totalUsed = totalUsed;
2✔
1364

1365
    return this;
2✔
1366
  }
1367

1368
  /**
1369
   * Instantiate host list from json object.
1370
   * @param {Object} options
1371
   * @param {Object} json
1372
   * @returns {HostList}
1373
   */
1374

1375
  static fromJSON(options, json) {
1376
    return new this(options).fromJSON(json);
1✔
1377
  }
1378
}
1379

1380
/**
1381
 * Number of days before considering
1382
 * an address stale.
1383
 * @const {Number}
1384
 * @default
1385
 */
1386

1387
HostList.HORIZON_DAYS = 30;
1✔
1388

1389
/**
1390
 * Number of retries (without success)
1391
 * before considering an address stale.
1392
 * @const {Number}
1393
 * @default
1394
 */
1395

1396
HostList.RETRIES = 3;
1✔
1397

1398
/**
1399
 * Number of days after reaching
1400
 * MAX_FAILURES to consider an
1401
 * address stale.
1402
 * @const {Number}
1403
 * @default
1404
 */
1405

1406
HostList.MIN_FAIL_DAYS = 7;
1✔
1407

1408
/**
1409
 * Maximum number of failures
1410
 * allowed before considering
1411
 * an address stale.
1412
 * @const {Number}
1413
 * @default
1414
 */
1415

1416
HostList.MAX_FAILURES = 10;
1✔
1417

1418
/**
1419
 * Maximum number of references
1420
 * in fresh buckets.
1421
 * @const {Number}
1422
 * @default
1423
 */
1424

1425
HostList.MAX_REFS = 8;
1✔
1426

1427
/**
1428
 * Serialization version.
1429
 * @const {Number}
1430
 * @default
1431
 */
1432

1433
HostList.VERSION = 4;
1✔
1434

1435
/**
1436
 * Local address scores.
1437
 * @enum {Number}
1438
 * @default
1439
 */
1440

1441
HostList.scores = {
1✔
1442
  NONE: 0,
1443
  IF: 1,
1444
  BIND: 2,
1445
  DNS: 3,
1446
  UPNP: 4,
1447
  MANUAL: 5,
1448
  MAX: 6
1449
};
1450

1451
/**
1452
 * Host Entry
1453
 * @alias module:net.HostEntry
1454
 * @property {NetAddress} addr - host address.
1455
 * @property {NetAddress} src - the first address we discovered this entry
1456
 *                              from.
1457
 * @property {Boolean} used - is it in the used set.
1458
 * @property {Number} refCount - Reference count in new buckets.
1459
 * @property {Number} attempts - connection attempts since last successful one.
1460
 * @property {Number} lastSuccess - last success timestamp.
1461
 * @property {Number} lastAttempt - last attempt timestamp.
1462
 */
1463

1464
class HostEntry {
1465
  /**
1466
   * Create a host entry.
1467
   * @constructor
1468
   * @param {NetAddress} addr
1469
   * @param {NetAddress} src
1470
   */
1471

1472
  constructor(addr, src) {
1473
    this.addr = addr || new NetAddress();
715✔
1474
    this.src = src || new NetAddress();
715✔
1475
    this.prev = null;
715✔
1476
    this.next = null;
715✔
1477
    this.used = false;
715✔
1478
    this.refCount = 0;
715✔
1479
    this.attempts = 0;
715✔
1480
    this.lastSuccess = 0;
715✔
1481
    this.lastAttempt = 0;
715✔
1482

1483
    if (addr)
715✔
1484
      this.fromOptions(addr, src);
675✔
1485
  }
1486

1487
  /**
1488
   * Inject properties from options.
1489
   * @private
1490
   * @param {NetAddress} addr
1491
   * @param {NetAddress} src
1492
   * @returns {HostEntry}
1493
   */
1494

1495
  fromOptions(addr, src) {
1496
    assert(addr instanceof NetAddress);
675✔
1497
    assert(src instanceof NetAddress);
675✔
1498
    this.addr = addr;
675✔
1499
    this.src = src;
675✔
1500
    return this;
675✔
1501
  }
1502

1503
  /**
1504
   * Instantiate host entry from options.
1505
   * @param {NetAddress} addr
1506
   * @param {NetAddress} src
1507
   * @returns {HostEntry}
1508
   */
1509

1510
  static fromOptions(addr, src) {
1511
    return new this().fromOptions(addr, src);
×
1512
  }
1513

1514
  /**
1515
   * Get key suitable for a hash table (hostname).
1516
   * @returns {String}
1517
   */
1518

1519
  key() {
1520
    return this.addr.hostname;
18,003✔
1521
  }
1522

1523
  /**
1524
   * Get host priority.
1525
   * @param {Number} now
1526
   * @returns {Number}
1527
   */
1528

1529
  chance(now) {
1530
    let c = 1;
32✔
1531

1532
    if (now - this.lastAttempt < 60 * 10)
32!
1533
      c *= 0.01;
×
1534

1535
    c *= Math.pow(0.66, Math.min(this.attempts, 8));
32✔
1536

1537
    return c;
32✔
1538
  }
1539

1540
  /**
1541
   * Inspect host address.
1542
   * @returns {Object}
1543
   */
1544

1545
  inspect() {
1546
    return {
×
1547
      addr: this.addr,
1548
      src: this.src,
1549
      used: this.used,
1550
      refCount: this.refCount,
1551
      attempts: this.attempts,
1552
      lastSuccess: util.date(this.lastSuccess),
1553
      lastAttempt: util.date(this.lastAttempt)
1554
    };
1555
  }
1556

1557
  /**
1558
   * Convert host entry to json-friendly object.
1559
   * @returns {Object}
1560
   */
1561

1562
  toJSON() {
1563
    return {
40✔
1564
      addr: this.addr.hostname,
1565
      src: this.src.hostname,
1566
      services: this.addr.services.toString(2),
1567
      time: this.addr.time,
1568
      attempts: this.attempts,
1569
      lastSuccess: this.lastSuccess,
1570
      lastAttempt: this.lastAttempt
1571
    };
1572
  }
1573

1574
  /**
1575
   * Inject properties from json object.
1576
   * @private
1577
   * @param {Object} json
1578
   * @param {Network} network
1579
   * @returns {HostEntry}
1580
   */
1581

1582
  fromJSON(json, network) {
1583
    assert(json && typeof json === 'object');
40✔
1584
    assert(typeof json.addr === 'string');
40✔
1585
    assert(typeof json.src === 'string');
40✔
1586

1587
    this.addr.fromHostname(json.addr, network);
40✔
1588

1589
    if (json.services != null) {
40!
1590
      assert(typeof json.services === 'string');
40✔
1591
      assert(json.services.length > 0);
40✔
1592
      assert(json.services.length <= 32);
40✔
1593
      const services = parseInt(json.services, 2);
40✔
1594
      assert((services >>> 0) === services);
40✔
1595
      this.addr.services = services;
40✔
1596
    }
1597

1598
    if (json.time != null) {
40!
1599
      assert(Number.isSafeInteger(json.time));
40✔
1600
      assert(json.time >= 0);
40✔
1601
      this.addr.time = json.time;
40✔
1602
    }
1603

1604
    if (json.src != null) {
40!
1605
      assert(typeof json.src === 'string');
40✔
1606
      this.src.fromHostname(json.src, network);
40✔
1607
    }
1608

1609
    if (json.attempts != null) {
40!
1610
      assert((json.attempts >>> 0) === json.attempts);
40✔
1611
      this.attempts = json.attempts;
40✔
1612
    }
1613

1614
    if (json.lastSuccess != null) {
40!
1615
      assert(Number.isSafeInteger(json.lastSuccess));
40✔
1616
      assert(json.lastSuccess >= 0);
40✔
1617
      this.lastSuccess = json.lastSuccess;
40✔
1618
    }
1619

1620
    if (json.lastAttempt != null) {
40!
1621
      assert(Number.isSafeInteger(json.lastAttempt));
40✔
1622
      assert(json.lastAttempt >= 0);
40✔
1623
      this.lastAttempt = json.lastAttempt;
40✔
1624
    }
1625

1626
    return this;
40✔
1627
  }
1628

1629
  /**
1630
   * Instantiate host entry from json object.
1631
   * @param {Object} json
1632
   * @param {Network} network
1633
   * @returns {HostEntry}
1634
   */
1635

1636
  static fromJSON(json, network) {
1637
    return new this().fromJSON(json, network);
40✔
1638
  }
1639
}
1640

1641
/**
1642
 * Local Address
1643
 * @alias module:net.LocalAddress
1644
 */
1645

1646
class LocalAddress {
1647
  /**
1648
   * Create a local address.
1649
   * @constructor
1650
   * @param {NetAddress} addr
1651
   * @param {Number?} score
1652
   */
1653

1654
  constructor(addr, score) {
1655
    this.addr = addr;
81✔
1656
    this.type = score || 0;
81✔
1657
    this.score = score || 0;
81✔
1658
  }
1659
}
1660

1661
/**
1662
 * Host List Options
1663
 * @alias module:net.HostListOptions
1664
 */
1665

1666
class HostListOptions {
1667
  /**
1668
   * Create host list options.
1669
   * @constructor
1670
   * @param {Object?} options
1671
   */
1672

1673
  constructor(options) {
1674
    this.network = Network.primary;
165✔
1675
    this.logger = Logger.global;
165✔
1676
    this.resolve = lookup;
165✔
1677
    this.host = '0.0.0.0';
165✔
1678
    this.port = this.network.port;
165✔
1679
    this.services = common.LOCAL_SERVICES;
165✔
1680
    this.onion = false;
165✔
1681
    this.brontideOnly = false;
165✔
1682
    this.banTime = common.BAN_TIME;
165✔
1683
    this.random = random;
165✔
1684

1685
    this.address = new NetAddress();
165✔
1686
    this.address.services = this.services;
165✔
1687
    this.address.time = this.network.now();
165✔
1688

1689
    this.brontide = new NetAddress();
165✔
1690
    this.brontide.services = this.services;
165✔
1691
    this.brontide.time = this.network.now();
165✔
1692

1693
    this.seeds = this.network.seeds;
165✔
1694
    this.nodes = [];
165✔
1695

1696
    this.prefix = null;
165✔
1697
    this.filename = null;
165✔
1698
    this.memory = true;
165✔
1699
    this.flushInterval = 120000;
165✔
1700

1701
    if (options)
165✔
1702
      this.fromOptions(options);
106✔
1703
  }
1704

1705
  /**
1706
   * Inject properties from options.
1707
   * @private
1708
   * @param {Object} options
1709
   */
1710

1711
  fromOptions(options) {
1712
    assert(options, 'Options are required.');
106✔
1713

1714
    if (options.network != null) {
106✔
1715
      this.network = Network.get(options.network);
102✔
1716
      this.seeds = this.network.seeds;
102✔
1717
      this.address.port = this.network.port;
102✔
1718
      this.brontide.port = this.network.brontidePort;
102✔
1719
      this.port = this.network.port;
102✔
1720
    }
1721

1722
    if (options.logger != null) {
106✔
1723
      assert(typeof options.logger === 'object');
94✔
1724
      this.logger = options.logger;
94✔
1725
    }
1726

1727
    if (options.resolve != null) {
106✔
1728
      assert(typeof options.resolve === 'function');
94✔
1729
      this.resolve = options.resolve;
94✔
1730
    }
1731

1732
    if (options.banTime != null) {
106✔
1733
      assert(options.banTime >= 0);
94✔
1734
      this.banTime = options.banTime;
94✔
1735
    }
1736

1737
    if (options.seeds) {
106✔
1738
      assert(Array.isArray(options.seeds));
95✔
1739
      this.seeds = options.seeds;
95✔
1740
    }
1741

1742
    if (options.nodes) {
106✔
1743
      assert(Array.isArray(options.nodes));
95✔
1744
      this.nodes = options.nodes;
95✔
1745
    }
1746

1747
    if (options.host != null)
106✔
1748
      this.host = IP.normalize(options.host);
95✔
1749

1750
    if (options.port != null) {
106✔
1751
      assert((options.port & 0xffff) === options.port);
95✔
1752
      this.port = options.port;
95✔
1753
    }
1754

1755
    if (options.publicHost != null) {
106✔
1756
      assert(typeof options.publicHost === 'string');
95✔
1757
      this.address.setHost(options.publicHost);
95✔
1758
      this.brontide.setHost(options.publicHost);
95✔
1759
    }
1760

1761
    if (options.publicPort != null) {
106✔
1762
      assert((options.publicPort & 0xffff) === options.publicPort);
95✔
1763
      this.address.setPort(options.publicPort);
95✔
1764
    }
1765

1766
    if (options.publicBrontidePort != null) {
106✔
1767
      assert((options.publicBrontidePort & 0xffff)
95✔
1768
             === options.publicBrontidePort);
1769
      this.brontide.setPort(options.publicBrontidePort);
95✔
1770
    }
1771

1772
    if (options.identityKey) {
106✔
1773
      assert(Buffer.isBuffer(options.identityKey),
95✔
1774
        'Identity key must be a buffer.');
1775
      assert(secp256k1.privateKeyVerify(options.identityKey),
95✔
1776
        'Invalid identity key.');
1777
      this.brontide.setKey(secp256k1.publicKeyCreate(options.identityKey));
95✔
1778
    }
1779

1780
    if (options.services != null) {
106✔
1781
      assert(typeof options.services === 'number');
95✔
1782
      this.services = options.services;
95✔
1783
    }
1784

1785
    if (options.onion != null) {
106✔
1786
      assert(typeof options.onion === 'boolean');
94✔
1787
      this.onion = options.onion;
94✔
1788
    }
1789

1790
    if (options.brontideOnly != null) {
106✔
1791
      assert(typeof options.brontideOnly === 'boolean');
94✔
1792
      this.brontideOnly = options.brontideOnly;
94✔
1793
    }
1794

1795
    if (options.memory != null) {
106✔
1796
      assert(typeof options.memory === 'boolean');
96✔
1797
      this.memory = options.memory;
96✔
1798
    }
1799

1800
    if (options.prefix != null) {
106✔
1801
      assert(typeof options.prefix === 'string');
96✔
1802
      this.prefix = options.prefix;
96✔
1803
      this.filename = path.join(this.prefix, 'hosts.json');
96✔
1804
    }
1805

1806
    if (options.filename != null) {
106✔
1807
      assert(typeof options.filename === 'string');
1✔
1808
      this.filename = options.filename;
1✔
1809
    }
1810

1811
    if (options.flushInterval != null) {
106✔
1812
      assert(options.flushInterval >= 0);
1✔
1813
      this.flushInterval = options.flushInterval;
1✔
1814
    }
1815

1816
    if (options.random != null) {
106!
1817
      assert(typeof options.random === 'function');
×
1818
      this.random = options.random;;
×
1819
    }
1820

1821
    this.address.time = this.network.now();
106✔
1822
    this.address.services = this.services;
106✔
1823

1824
    this.brontide.time = this.network.now();
106✔
1825
    this.brontide.services = this.services;
106✔
1826

1827
    return this;
106✔
1828
  }
1829
}
1830

1831
/*
1832
 * Helpers
1833
 */
1834

1835
function random(max) {
1836
  // Fast insecure randomness (a la bitcoin).
1837
  return Math.floor(Math.random() * max);
1,833✔
1838
}
1839

1840
/*
1841
 * Expose
1842
 */
1843

1844
exports = HostList;
1✔
1845
exports.HostEntry = HostEntry;
1✔
1846

1847
module.exports = exports;
1✔
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

© 2026 Coveralls, Inc