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

handshake-org / hsd / 8452447998

27 Mar 2024 01:17PM UTC coverage: 16.226% (-52.4%) from 68.632%
8452447998

Pull #888

github

web-flow
Merge c40b9e40c into 0a4f24bdb
Pull Request #888: Wallet TX Count and time indexing

1001 of 12966 branches covered (7.72%)

Branch coverage included in aggregate %.

130 of 474 new or added lines in 11 files covered. (27.43%)

17522 existing lines in 124 files now uncovered.

6546 of 33547 relevant lines covered (19.51%)

37.41 hits per line

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

47.77
/lib/node/node.js
1
/*!
2
 * node.js - node object 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 EventEmitter = require('events');
1✔
11
const fs = require('bfile');
1✔
12
const Logger = require('blgr');
1✔
13
const Config = require('bcfg');
1✔
14
const secp256k1 = require('bcrypto/lib/secp256k1');
1✔
15
const Network = require('../protocol/network');
1✔
16
const WorkerPool = require('../workers/workerpool');
1✔
17
const {ownership} = require('../covenants/ownership');
1✔
18

19
/**
20
 * Node
21
 * Base class from which every other
22
 * Node-like object inherits.
23
 * @alias module:node.Node
24
 * @extends EventEmitter
25
 * @abstract
26
 */
27

28
class Node extends EventEmitter {
29
  /**
30
   * Create a node.
31
   * @constructor
32
   * @param {Object} options
33
   */
34

35
  constructor(module, config, file, options) {
36
    super();
31✔
37

38
    this.config = new Config(module, {
31✔
39
      suffix: 'network',
40
      fallback: 'main',
41
      alias: { 'n': 'network' }
42
    });
43

44
    this.config.inject(options);
31✔
45
    this.config.load(options);
31✔
46

47
    if (options.config)
31!
48
      this.config.open(config);
×
49

50
    this.network = Network.get(this.config.getSuffix());
31✔
51
    this.memory = this.config.bool('memory', true);
31✔
52
    this.identityKey = this.loadKey();
31✔
53
    this.startTime = -1;
31✔
54
    this.bound = [];
31✔
55
    this.plugins = Object.create(null);
31✔
56
    this.stack = [];
31✔
57

58
    this.logger = null;
31✔
59
    this.workers = null;
31✔
60

61
    this.spv = false;
31✔
62
    this.blocks = null;
31✔
63
    this.chain = null;
31✔
64
    this.fees = null;
31✔
65
    this.mempool = null;
31✔
66
    this.pool = null;
31✔
67
    this.miner = null;
31✔
68
    this.http = null;
31✔
69

70
    this._init(file);
31✔
71
  }
72

73
  /**
74
   * Initialize node.
75
   * @private
76
   * @param {Object} options
77
   */
78

79
  _init(file) {
80
    const config = this.config;
31✔
81

82
    let logger = new Logger();
31✔
83

84
    if (config.has('logger'))
31✔
85
      logger = config.obj('logger');
13✔
86

87
    logger.set({
31✔
88
      filename: !this.memory && config.bool('log-file')
63!
89
        ? config.location(file)
90
        : null,
91
      level: config.str('log-level'),
92
      console: config.bool('log-console'),
93
      shrink: config.bool('log-shrink'),
94
      maxFileSize: config.mb('log-max-file-size'),
95
      maxFiles: config.uint('log-max-files')
96
    });
97

98
    this.logger = logger.context('node');
31✔
99

100
    if (config.bool('ignore-forged')) {
31!
101
      if (this.network.type !== 'regtest')
×
102
        throw new Error('Forged claims are regtest-only.');
×
103
      ownership.ignore = true;
×
104
    }
105

106
    this.workers = new WorkerPool({
31✔
107
      enabled: config.bool('workers'),
108
      size: config.uint('workers-size'),
109
      timeout: config.uint('workers-timeout'),
110
      file: config.str('worker-file')
111
    });
112

113
    this.on('error', () => {});
31✔
114

115
    this.workers.on('spawn', (child) => {
31✔
UNCOV
116
      this.logger.info('Spawning worker process: %d.', child.id);
×
117
    });
118

119
    this.workers.on('exit', (code, child) => {
31✔
UNCOV
120
      this.logger.warning('Worker %d exited: %s.', child.id, code);
×
121
    });
122

123
    this.workers.on('log', (text, child) => {
31✔
124
      this.logger.debug('Worker %d says:', child.id);
×
125
      this.logger.debug(text);
×
126
    });
127

128
    this.workers.on('error', (err, child) => {
31✔
129
      if (child) {
×
130
        this.logger.error('Worker %d error: %s', child.id, err.message);
×
131
        return;
×
132
      }
133
      this.emit('error', err);
×
134
    });
135
  }
136

137
  /**
138
   * Ensure prefix directory.
139
   * @returns {Promise}
140
   */
141

142
  async ensure() {
UNCOV
143
    if (fs.unsupported)
×
144
      return undefined;
×
145

UNCOV
146
    if (this.memory)
×
UNCOV
147
      return undefined;
×
148

UNCOV
149
    if (this.blocks)
×
UNCOV
150
      await this.blocks.ensure();
×
151

UNCOV
152
    return fs.mkdirp(this.config.prefix);
×
153
  }
154

155
  /**
156
   * Create a file path using `prefix`.
157
   * @param {String} file
158
   * @returns {String}
159
   */
160

161
  location(name) {
162
    return this.config.location(name);
1✔
163
  }
164

165
  /**
166
   * Generate identity key.
167
   * @private
168
   * @returns {Buffer}
169
   */
170

171
  genKey() {
172
    if (this.network.identityKey)
31!
173
      return this.network.identityKey;
31✔
UNCOV
174
    return secp256k1.privateKeyGenerate();
×
175
  }
176

177
  /**
178
   * Load identity key.
179
   * @private
180
   * @returns {Buffer}
181
   */
182

183
  loadKey() {
184
    if (this.memory || fs.unsupported)
31✔
185
      return this.genKey();
30✔
186

187
    const filename = this.location('key');
1✔
188

189
    let key = this.config.buf('identity-key');
1✔
190
    let fresh = false;
1✔
191

192
    if (!key) {
1!
193
      try {
1✔
194
        const data = fs.readFileSync(filename, 'utf8');
1✔
UNCOV
195
        key = Buffer.from(data.trim(), 'hex');
×
196
      } catch (e) {
197
        if (e.code !== 'ENOENT')
1!
198
          throw e;
×
199
        key = this.genKey();
1✔
200
        fresh = true;
1✔
201
      }
202
    } else {
203
      fresh = true;
×
204
    }
205

206
    if (key.length !== 32 || !secp256k1.privateKeyVerify(key))
1!
207
      throw new Error('Invalid identity key.');
×
208

209
    if (fresh) {
1!
210
      // XXX Shouldn't be doing this.
211
      fs.mkdirpSync(this.config.prefix);
1✔
212
      fs.writeFileSync(filename, key.toString('hex') + '\n');
1✔
213
    }
214

215
    return key;
1✔
216
  }
217

218
  /**
219
   * Open node. Bind all events.
220
   * @private
221
   */
222

223
  async handlePreopen() {
UNCOV
224
    await this.logger.open();
×
UNCOV
225
    await this.workers.open();
×
226

UNCOV
227
    this._bind(this.network.time, 'offset', (offset) => {
×
UNCOV
228
      this.logger.info(
×
229
        'Time offset: %d (%d minutes).',
230
        offset, offset / 60 | 0);
231
    });
232

UNCOV
233
    this._bind(this.network.time, 'sample', (sample, total) => {
×
UNCOV
234
      this.logger.debug(
×
235
        'Added time data: samples=%d, offset=%d (%d minutes).',
236
        total, sample, sample / 60 | 0);
237
    });
238

UNCOV
239
    this._bind(this.network.time, 'mismatch', () => {
×
240
      this.logger.warning('Adjusted time mismatch!');
×
241
      this.logger.warning('Please make sure your system clock is correct!');
×
242
    });
243
  }
244

245
  /**
246
   * Open node.
247
   * @private
248
   */
249

250
  async handleOpen() {
UNCOV
251
    this.startTime = Date.now();
×
252

UNCOV
253
    if (!this.workers.enabled) {
×
UNCOV
254
      this.logger.warning('Warning: worker pool is disabled.');
×
UNCOV
255
      this.logger.warning('Verification will be slow.');
×
256
    }
257
  }
258

259
  /**
260
   * Open node. Bind all events.
261
   * @private
262
   */
263

264
  async handlePreclose() {
265
    ;
266
  }
267

268
  /**
269
   * Close node. Unbind all events.
270
   * @private
271
   */
272

273
  async handleClose() {
UNCOV
274
    for (const [obj, event, listener] of this.bound)
×
UNCOV
275
      obj.removeListener(event, listener);
×
276

UNCOV
277
    this.bound.length = 0;
×
UNCOV
278
    this.startTime = -1;
×
279

UNCOV
280
    await this.workers.close();
×
UNCOV
281
    await this.logger.close();
×
282
  }
283

284
  /**
285
   * Bind to an event on `obj`, save listener for removal.
286
   * @private
287
   * @param {EventEmitter} obj
288
   * @param {String} event
289
   * @param {Function} listener
290
   */
291

292
  _bind(obj, event, listener) {
UNCOV
293
    this.bound.push([obj, event, listener]);
×
UNCOV
294
    obj.on(event, listener);
×
295
  }
296

297
  /**
298
   * Emit and log an error.
299
   * @private
300
   * @param {Error} err
301
   */
302

303
  error(err) {
UNCOV
304
    this.logger.error(err);
×
UNCOV
305
    this.emit('error', err);
×
306
  }
307

308
  /**
309
   * Emit and log an abort error.
310
   * @private
311
   * @param {Error} err
312
   */
313

314
  abort(err) {
UNCOV
315
    this.logger.error(err);
×
UNCOV
316
    this.emit('abort', err);
×
317
  }
318

319
  /**
320
   * Get node uptime in seconds.
321
   * @returns {Number}
322
   */
323

324
  uptime() {
UNCOV
325
    if (this.startTime === -1)
×
326
      return 0;
×
327

UNCOV
328
    return Math.floor((Date.now() - this.startTime) / 1000);
×
329
  }
330

331
  /**
332
   * Attach a plugin.
333
   * @param {Object} plugin
334
   * @returns {Object} Plugin instance.
335
   */
336

337
  use(plugin) {
338
    assert(plugin, 'Plugin must be an object.');
21✔
339
    assert(typeof plugin.init === 'function', '`init` must be a function.');
21✔
340

341
    assert(!this.loaded, 'Cannot add plugin after node is loaded.');
21✔
342

343
    const instance = plugin.init(this);
21✔
344

345
    assert(!instance.open || typeof instance.open === 'function',
21✔
346
      '`open` must be a function.');
347
    assert(!instance.close || typeof instance.close === 'function',
21✔
348
      '`close` must be a function.');
349

350
    if (plugin.id) {
21!
351
      assert(typeof plugin.id === 'string', '`id` must be a string.');
21✔
352

353
      // Reserved names
354
      switch (plugin.id) {
21!
355
        case 'chain':
356
        case 'fees':
357
        case 'mempool':
358
        case 'miner':
359
        case 'pool':
360
        case 'rpc':
361
        case 'http':
362
        case 'ns':
363
        case 'rs':
364
          assert(false, `${plugin.id} is already added.`);
×
365
          break;
×
366
      }
367

368
      assert(!this.plugins[plugin.id], `${plugin.id} is already added.`);
21✔
369

370
      this.plugins[plugin.id] = instance;
21✔
371
    }
372

373
    this.stack.push(instance);
21✔
374

375
    if (typeof instance.on === 'function') {
21!
376
      instance.on('error', err => this.error(err));
21✔
377
      instance.on('abort', msg => this.abort(msg));
21✔
378
    }
379

380
    return instance;
21✔
381
  }
382

383
  /**
384
   * Test whether a plugin is available.
385
   * @param {String} name
386
   * @returns {Boolean}
387
   */
388

389
  has(name) {
UNCOV
390
    return this.plugins[name] != null;
×
391
  }
392

393
  /**
394
   * Get a plugin.
395
   * @param {String} name
396
   * @returns {Object|null}
397
   */
398

399
  get(name) {
400
    assert(typeof name === 'string', 'Plugin name must be a string.');
16✔
401

402
    // Reserved names.
403
    switch (name) {
16!
404
      case 'chain':
405
        assert(this.chain, 'chain is not loaded.');
×
406
        return this.chain;
×
407
      case 'fees':
408
        assert(this.fees, 'fees is not loaded.');
×
409
        return this.fees;
×
410
      case 'mempool':
411
        assert(this.mempool, 'mempool is not loaded.');
×
412
        return this.mempool;
×
413
      case 'miner':
414
        assert(this.miner, 'miner is not loaded.');
×
415
        return this.miner;
×
416
      case 'pool':
417
        assert(this.pool, 'pool is not loaded.');
×
418
        return this.pool;
×
419
      case 'rpc':
420
        assert(this.rpc, 'rpc is not loaded.');
×
421
        return this.rpc;
×
422
      case 'http':
423
        assert(this.http, 'http is not loaded.');
×
424
        return this.http;
×
425
      case 'rs':
426
        assert(this.rs, 'rs is not loaded.');
×
427
        return this.rs;
×
428
      case 'ns':
429
        assert(this.ns, 'ns is not loaded.');
×
430
        return this.ns;
×
431
    }
432

433
    return this.plugins[name] || null;
16!
434
  }
435

436
  /**
437
   * Require a plugin.
438
   * @param {String} name
439
   * @returns {Object}
440
   * @throws {Error} on onloaded plugin
441
   */
442

443
  require(name) {
444
    const plugin = this.get(name);
15✔
445
    assert(plugin, `${name} is not loaded.`);
15✔
446
    return plugin;
15✔
447
  }
448

449
  /**
450
   * Load plugins.
451
   * @private
452
   */
453

454
  loadPlugins() {
455
    const plugins = this.config.array('plugins', []);
31✔
456
    const loader = this.config.func('loader');
31✔
457

458
    for (let plugin of plugins) {
31✔
459
      if (typeof plugin === 'string') {
15!
460
        assert(loader, 'Must pass a loader function.');
×
461
        plugin = loader(plugin);
×
462
      }
463
      this.use(plugin);
15✔
464
    }
465
  }
466

467
  /**
468
   * Open plugins.
469
   * @private
470
   */
471

472
  async openPlugins() {
UNCOV
473
    for (const plugin of this.stack) {
×
UNCOV
474
      if (plugin.open)
×
UNCOV
475
        await plugin.open();
×
476
    }
477
  }
478

479
  /**
480
   * Close plugins.
481
   * @private
482
   */
483

484
  async closePlugins() {
UNCOV
485
    for (const plugin of this.stack) {
×
UNCOV
486
      if (plugin.close)
×
UNCOV
487
        await plugin.close();
×
488
    }
489
  }
490
}
491

492
/*
493
 * Expose
494
 */
495

496
module.exports = Node;
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