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

chainpoint / bitcoin-header-node / 142

pending completion
142

cron

travis-ci-com

bucko13
fix lint error

130 of 182 branches covered (71.43%)

Branch coverage included in aggregate %.

423 of 484 relevant lines covered (87.4%)

39.66 hits per line

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

83.59
/lib/headerindexer.js
1
'use strict'
2

3
const bdb = require('bdb')
1✔
4
const assert = require('bsert')
1✔
5
const bio = require('bufio')
1✔
6
const { Lock } = require('bmutex')
1✔
7
const { Indexer, Headers, ChainEntry, CoinView, util } = require('bcoin')
1✔
8
const { BlockMeta } = require('./records')
1✔
9
const layout = require('./layout')
1✔
10
const { getRemoteBlockEntries } = require('./util')
1✔
11
/**
12
 * FilterIndexer
13
 * @alias module:indexer.FilterIndexer
14
 * @extends Indexer
15
 */
16
class HeaderIndexer extends Indexer {
17
  /**
18
   * Create a indexer
19
   * @constructor
20
   * @param {Object} options
21
   */
22

23
  constructor(options) {
24
    super('headers', options)
7✔
25

26
    this.db = bdb.create(this.options)
7✔
27
    this.checkpoints = this.chain.options.checkpoints
7✔
28
    this.locker = new Lock()
7✔
29
    this.bound = []
7✔
30
    if (options) this.fromOptions(options)
7!
31
  }
32

33
  /**
34
   * Inject properties from object.
35
   * @private
36
   * @param {Object} options
37
   * @returns {HeaderIndexer options}
38
   */
39

40
  fromOptions(options) {
41
    if (!options.network) {
7✔
42
      // Without this, the base indexer defaults to mainnet if no
43
      // network is set even if chain has a different network
44
      this.network = this.chain.network
5✔
45
    }
46

47
    if (options.startHeight) {
7✔
48
      assert(typeof options.startHeight === 'number')
1✔
49
      this.startHeight = options.startHeight
1✔
50
    } else {
51
      // always need to initialize a startHeight so set to genesis if none is passed
52
      // this will get overriden later if one has been saved in the database already
53
      // or below if a startBlock is passed
54
      this.startHeight = 0
6✔
55
    }
56

57
    // start block will take precedence over startHeight
58
    if (options.startBlock) {
7✔
59
      assert(options.startBlock.length >= 2, 'Chain tip array must have two items to initiate block')
3✔
60

61
      // check that we have two buffers for chain tip
62
      for (let raw of options.startBlock) {
3✔
63
        assert(Buffer.isBuffer(raw), 'Chain tip items must be buffers')
6✔
64
      }
65

66
      // start entry is the last in the tip field since we need a previous block
67
      // to pass contextual chain checks
68
      const startEntry = ChainEntry.fromRaw(options.startBlock.slice(-1)[0])
3✔
69

70
      // once everything passes, we can set our values
71
      this.startBlock = options.startBlock
3✔
72
      this.startHeight = startEntry.height
3✔
73
    }
74

75
    return this
7✔
76
  }
77

78
  async open() {
79
    // ensure is normally run by `super`'s `open` method
80
    // but in this case we need to make sure that the required
81
    // direcetories exist before setStartBlock is run
82
    await this.ensure()
9✔
83

84
    // need to setStartBlock before anything else since
85
    // the super open method will also connect with the chain
86
    // which causes events to fire that the tip needs to be initialized for first
87
    await this.setStartBlock()
9✔
88
    await super.open()
7✔
89
    await this.initializeChain()
7✔
90
    this.logger.info('Indexer successfully loaded')
7✔
91
  }
92

93
  /**
94
   * Close the indexer, wait for the database to close,
95
   * unbind all events.
96
   * @returns {Promise}
97
   */
98

99
  async close() {
100
    await this.db.close()
5✔
101
    // removing listeners when close to avoid duplicated listeners if
102
    // the indexer is re-opened
103
    for (const [event, listener] of this.bound) this.chain.removeListener(event, listener)
15✔
104

105
    this.bound.length = 0
5✔
106
  }
107

108
  /**
109
   * Bind to chain events and save listeners for removal on close
110
   * @private
111
   */
112

113
  bind() {
114
    const listener = async (entry, block, view) => {
7✔
115
      const meta = new BlockMeta(entry.hash, entry.height)
293✔
116

117
      try {
293✔
118
        await this.sync(meta, block, view)
293✔
119
      } catch (e) {
120
        this.emit('error', e)
×
121
      }
122
    }
123

124
    for (const event of ['connect', 'disconnect', 'reset']) {
7✔
125
      this.bound.push([event, listener])
21✔
126
      this.chain.on(event, listener)
21✔
127
    }
128
  }
129

130
  /**
131
   * @private
132
   * Set custom starting entry for fast(er) sync
133
   */
134

135
  async setStartBlock() {
136
    const unlock = await this.locker.lock()
10✔
137
    try {
10✔
138
      // since this method is run before the `open` method where
139
      // there are other contextual checks that are done,
140
      // let's manually open the indexer db to initialize starting tip
141
      if (!this.db.loaded) await this.db.open()
10!
142
      this.start() // batch db write operations
10✔
143
      await this._setStartBlock()
10✔
144
      this.commit() // write to the db
8✔
145
    } finally {
146
      unlock()
10✔
147
      if (this.db.loaded) await this.db.close()
10!
148
    }
149
  }
150

151
  async _setStartBlock() {
152
    // first need to see if a start height was already saved in the db
153
    this.logger.debug('Checking database for existing starting entry')
10✔
154

155
    const startBlock = await this.getStartBlock()
10✔
156
    if (startBlock) {
10✔
157
      const startEntry = ChainEntry.fromRaw(startBlock[1])
2✔
158

159
      // perform some checks on the startEntry:
160
      // if a start height was passed as an option and doesn't match with
161
      // one saved in DB, throw an error
162
      if (this.startHeight && startEntry.height !== this.startHeight)
2!
163
        throw new Error(
×
164
          `Cannot retroactively change start height. Current start height is ${startEntry.height}. To change the start height delete indexer database otherwise remove start height config to use existing.`
165
        )
166
      else if (this.startHeight && this.startHeight === startEntry.height)
2!
167
        this.logger.debug(`Start height already set at block ${startEntry.height}.`)
2✔
168
      else this.logger.info(`Starting block for header chain initializing to ${startEntry.height}`)
×
169

170
      // if checks have completed, we can initialize the start block in the db
171
      await this.initStartBlock(startBlock)
2✔
172

173
      // set indexer's start block and start height for reference
174
      this.startHeight = startEntry.height
2✔
175
      this.startBlock = startBlock
2✔
176

177
      // if we had a startBlock then we're done and can return
178
      return
2✔
179
    }
180

181
    // if no custom startBlock or startHeight then we can skip and sync from genesis
182
    if (!this.startBlock && !this.startHeight) {
8✔
183
      this.startHeight = 0
4✔
184
      return
4✔
185
    }
186

187
    // validate the startHeight that has been set correctly
188
    // will throw on any validation errors and should end startup
189
    this.validateStartHeight(this.startHeight)
4✔
190

191
    // Next, if we have a startHeight but no startBlock, we can "cheat" by using an external api
192
    // to retrieve the block header information for mainnet and testnet blocks.
193
    // This is not a trustless approach but is easier to bootstrap.
194
    // startBlock will take precedence if one is set however and the chain won't be able to sync
195
    // if initialized with "fake" blocks
196
    if (this.startHeight && !this.startBlock) {
2!
197
      assert(
×
198
        this.network.type === 'main' || this.network.type === 'testnet',
×
199
        'Can only get starting block data for mainnet or testnet. Use `startBlock` \
200
        with raw block data instead for other networks'
201
      )
202

203
      const entries = await getRemoteBlockEntries(this.network.type, this.startHeight - 1, this.startHeight)
×
204

205
      this.logger.info('Setting custom start height at %d', this.startHeight)
×
206
      this.startBlock = entries
×
207
    }
208

209
    // Next, validate and init starting tip in db
210
    const tipEntries = await this.initStartBlock(this.startBlock)
2✔
211

212
    // get last two items for the prev and tip to index and set indexer state
213
    const [prev, tip] = tipEntries.slice(-2)
2✔
214

215
    // need to set the indexer state so that the tip can be properly read and set
216
    this.height = tip.height
2✔
217
    this.startHeight = tip.height
2✔
218

219
    // save the starting height in the database for re-starting later
220
    // this value is checked in getStartBlock which is called at the beginning of this method
221
    // TODO: would be nice if this was in records.js for reading and writing from buffer/database
222
    const bw = bio.write()
2✔
223
    bw.writeU32(this.startHeight)
2✔
224
    await this.db.put(layout.s.encode(), bw.render())
2✔
225

226
    // also need to add the entry to the header index if it doesn't exist
227
    // this will index the entry and set the tip
228
    if (!(await this.getHeader(prev.height))) await this.indexEntryBlock(prev)
2!
229
    if (!(await this.getHeader(tip.height))) await this.indexEntryBlock(tip)
2!
230

231
    // closing and re-opening the chain will reset the state
232
    // based on the custom starting tip
233
    await this.chain.close()
2✔
234
    await this.chain.open()
2✔
235
  }
236

237
  /*
238
   * @private
239
   * check the headers index database for an existing start block.
240
   * Needs the db to be open first
241
   * @returns {null|Buffer[]} - null if no start height set otherwise an array
242
   * of two items with the two starting entries
243
   */
244

245
  async getStartBlock() {
246
    const data = await this.db.get(layout.s.encode())
12✔
247

248
    // if no height is saved then return null
249
    if (!data) return null
12✔
250

251
    // convert data buffer to U32
252
    const buffReader = bio.read(data)
3✔
253
    const startHeight = buffReader.readU32()
3✔
254
    let startEntry = await this.getEntry(startHeight)
3✔
255
    assert(startEntry, `Could not find an entry in database for starting height ${startHeight}`)
3✔
256

257
    // Need to also get a prevEntry which is necessary for contextual checks of an entry
258
    const prevEntry = await this.getEntry(startHeight - 1)
3✔
259
    assert(prevEntry, `No entry in db for starting block's previous entry at: ${startHeight - 1}`)
3✔
260
    return [prevEntry.toRaw(), startEntry.toRaw()]
3✔
261
  }
262

263
  /**
264
   * @private
265
   * verify start block or height to confirm it passes the minimum threshold
266
   * MUST be called after this.network has been set
267
   * @param {ChainEntry | Number} entryOrHeight
268
   * @returns {void|Boolean} throws on any invalidations otherwise re
269
   */
270

271
  validateStartHeight(height) {
272
    assert(typeof height === 'number', 'Must pass a number as the start height to verify')
13✔
273

274
    const { lastCheckpoint } = this.network
13✔
275

276
    // cannot be genesis (this is default anyway though)
277
    assert(height >= 0, 'Custom start height must be a positive integer')
13✔
278

279
    // must be less than last checkpoint
280
    // must qualify as historical with at least one retarget interval occuring between height and lastCheckpoint
281
    if (lastCheckpoint)
13!
282
      assert(
13✔
283
        this.isHistorical(height),
284
        `Starting entry height ${height} is too high. Must be before the lastCheckpoint (${lastCheckpoint}) ` +
285
          `and a retargetting interval. Recommended max start height: ${this.getHistoricalPoint()}`
286
      )
287

288
    return true
10✔
289
  }
290

291
  /**
292
   * @private
293
   * initialize a startBlock by running some validations and adding it to the _chain_ db
294
   * This will validate the startBlock argument and add them to the chain db
295
   * @param {Buffer[]} startBlock - an array of at least two raw chain entries
296
   * @returns {ChainEntry[]} entry - promise that resolves to array of tip entries
297
   */
298

299
  async initStartBlock(startBlock) {
300
    // when chain is reset and the tip is not
301
    // the genesis block, chain will check to see if
302
    // it can find the previous block for the tip.
303
    // this means that for a custom start, we need two
304
    // entries: the tip to start from, and the previous entry
305
    assert(Array.isArray(startBlock) && startBlock.length >= 2, 'Need at least two blocks for custom start block')
4✔
306

307
    // need the chain db to be open so that we can set the tip there to match the indexer
308
    assert(this.chain.opened, 'Chain should be opened to set the header index tip')
4✔
309

310
    const tip = [] // store an array of serialized entries to return if everything is successful
4✔
311

312
    let entry, prev
313
    for (let raw of startBlock) {
4✔
314
      prev = entry ? entry : null
8✔
315
      try {
8✔
316
        // this will fail if serialization is wrong (i.e. not an entry buffer) or if data is not a Buffer
317
        entry = ChainEntry.fromRaw(raw)
8✔
318
      } catch (e) {
319
        if (e.type === 'EncodingError')
×
320
          throw new Error(
×
321
            'headerindexer: There was a problem deserializing data. Must pass a block or chain entry buffer to start fast sync.'
322
          )
323
        throw e
×
324
      }
325

326
      this.validateStartHeight(entry.height)
8✔
327

328
      // confirm that the starter tip is made up of incrementing blocks
329
      // i.e. prevBlock hash matches hash of previous block in array
330
      if (prev) {
8✔
331
        assert.equal(
4✔
332
          entry.prevBlock.toString('hex'),
333
          prev.hash.toString('hex'),
334
          `Entry's prevBlock doesn't match previous block hash (prev: ${prev.hash.toString(
335
            'hex'
336
          )}, tip: ${entry.prevBlock.toString('hex')})`
337
        )
338
      }
339

340
      // and then add the entries to the chaindb with reconnect
341
      // note that this won't update the chain object, just its db
342
      await this.addEntryToChain(entry)
8✔
343
      tip.push(entry)
8✔
344
    }
345
    return tip
4✔
346
  }
347

348
  /**
349
   * Initialize chain by comparing with an existing
350
   * Headers index if one exists
351
   * Because we only use an in-memory chain, we may need to initialize
352
   * the chain from saved state in the headers index if it's being persisted
353
   */
354

355
  async initializeChain() {
356
    const unlock = await this.locker.lock()
7✔
357
    try {
7✔
358
      await this._initializeChain()
7✔
359
    } finally {
360
      unlock()
7✔
361
    }
362
  }
363

364
  async _initializeChain() {
365
    const indexerHeight = await this.height
7✔
366

367
    // if everything is fresh, we can sync as normal
368
    if (!indexerHeight) return
7✔
369

370
    // get chain tip to compare w/ headers index
371
    let chainTip = await this.chain.db.getTip()
5✔
372

373
    // if there is no chainTip or chainTip is behind the headers height
374
    // then we need to rebuild the in-memory chain for contextual checks
375
    // and index management
376
    if (!chainTip || chainTip.height < indexerHeight) {
5✔
377
      this.logger.info('Chain state is behind header. Re-initializing...')
2✔
378

379
      // Need to set the starting entry to initialize the chain from.
380
      // Option 1) If tip is before historical point, then we will re-intialize chain from the startHeight
381
      // Option 2) If there's no lastCheckpoint (e.g. regtest), re-initialize from genesis
382
      // Option 3) When header tip is not historical, the chain still needs to be initialized to
383
      // start from first non-historical block for contextual checks (e.g. pow)
384
      let entry
385
      if (this.isHistorical(indexerHeight)) {
2!
386
        this.logger.debug(
×
387
          'Headers tip before last checkpoint. Re-initializing chain from start height: %d',
388
          this.startHeight
389
        )
390

391
        entry = await this.getEntry(this.startHeight)
×
392
      } else if (!this.network.lastCheckpoint) {
2!
393
        this.logger.info('Re-initializing chain db from genesis block')
×
394
        // since the genesis block will be hard-coded in, we actually will be initializing from block #1
395
        // but first run sanity check that we have a genesis block
396
        assert(this.network.genesisBlock, `Could not find a genesis block for ${this.network.type}`)
×
397
        entry = await this.getEntry(1)
×
398
      } else {
399
        // otherwise first entry in the chain should be the first "non-historical" block
400
        this.logger.info('Re-initializing chain from last historical block: %d', this.getHistoricalPoint())
2✔
401
        entry = await this.getHeader(this.getHistoricalPoint() + 1)
2✔
402
      }
403

404
      // add entries until chain is caught up to the header index
405
      while (entry && entry.height <= indexerHeight) {
2✔
406
        this.logger.spam('Re-indexing block entry %d to chain: %h', entry.height, entry.hash)
202✔
407
        await this.addEntryToChain(entry)
202✔
408
        // increment to the next entry
409
        entry = await this.getEntry(entry.height + 1)
202✔
410
      }
411

412
      // reset the chain once the db is loaded
413
      await this.chain.close()
2✔
414
      await this.chain.open()
2✔
415

416
      this.logger.info('ChainDB successfully re-initialized to headers tip.')
2✔
417
    }
418
  }
419

420
  /**
421
   * Add a block's transactions without a lock.
422
   * modified addBlock from parent class
423
   * @private
424
   * @param {BlockMeta} meta
425
   * @param {Block} block
426
   * @param {CoinView} view
427
   * @returns {Promise}
428
   */
429

430
  async _addBlock(meta, block, view) {
431
    // removed hasRaw check for block from parent class since we are in spv mode,
432
    // which we use for header node, we get merkleblocks which don't have
433
    // the `hasRaw` method and the check is for tx serialization anyway
434
    const start = util.bench()
293✔
435

436
    if (meta.height !== this.height + 1) throw new Error('Indexer: Can not add block.')
293!
437

438
    // Start the batch write.
439
    this.start()
293✔
440

441
    // Call the implemented indexer to add to
442
    // the batch write.
443
    await this.indexBlock(meta, block, view)
293✔
444

445
    // Sync the height to the new tip.
446
    const height = await this._setTip(meta)
293✔
447

448
    // Commit the write batch to disk.
449
    await this.commit()
293✔
450

451
    // Update height _after_ successful commit.
452
    this.height = height
293✔
453

454
    // Log the current indexer status.
455
    this.logStatus(start, block, meta)
293✔
456
  }
457

458
  /**
459
   * add header to index.
460
   * @private
461
   * @param {ChainEntry} entry for block to chain
462
   * @param {Block} block - Block to index
463
   * @param {CoinView} view - Coin View
464
   * @returns {Promise} returns promise
465
   */
466
  async indexBlock(meta, block) {
467
    const height = meta.height
299✔
468

469
    // save block header
470
    // if block is historical (i.e. older than last checkpoint w/ at least one retarget interval)
471
    // we can save the header. Otherwise need to save the
472
    // whole entry so the chain can be replayed from that point
473
    if (this.isHistorical(height)) {
299✔
474
      const header = Headers.fromBlock(block)
56✔
475
      this.put(layout.b.encode(height), header.toRaw())
56✔
476
    } else {
477
      const prev = await this.chain.getEntry(height - 1)
243✔
478
      const entry = ChainEntry.fromBlock(block, prev)
243✔
479
      this.put(layout.b.encode(height), entry.toRaw())
243✔
480
    }
481
  }
482

483
  /**
484
   * Remove header from index.
485
   * @private
486
   * @param {ChainEntry} entry
487
   * @param {Block} block
488
   * @param {CoinView} view
489
   */
490

491
  async unindexBlock(meta) {
492
    const height = meta.height
×
493

494
    this.del(layout.b.encode(height))
×
495
  }
496

497
  /**
498
   * locator code is mostly from bcoin's chain.getLocator
499
   * Calculate chain locator (an array of hashes).
500
   * Need this to override chain's getLocator to account for custom startBlock
501
   * which means we have no history earlier than that block which breaks
502
   * the normal getLocator
503
   * @param {Hash?} start - Height or hash to treat as the tip.
504
   * The current tip will be used if not present. Note that this can be a
505
   * non-existent hash, which is useful for headers-first locators.
506
   * @returns {Promise} - Returns {@link Hash}[].
507
   */
508

509
  async getLocator(start) {
510
    const unlock = await this.locker.lock()
15✔
511
    try {
15✔
512
      return await this._getLocator(start)
15✔
513
    } finally {
514
      unlock()
15✔
515
    }
516
  }
517

518
  /**
519
   * Calculate chain locator without a lock.
520
   * Last locator should be genesis _or_ startHeight
521
   * if there is one
522
   * @private
523
   * @param {Hash?} start
524
   * @returns {Hash[]} hashes - array of entry hashs
525
   */
526
  async _getLocator(start) {
527
    let entry
528
    if (start == null) {
15✔
529
      entry = await this.getEntry(this.height)
13✔
530
    } else {
531
      assert(Buffer.isBuffer(start))
2✔
532
      entry = await this.chain.getEntryByHash(start)
2✔
533
    }
534
    const hashes = []
15✔
535

536
    let main = await this.chain.isMainChain(entry)
15✔
537
    let hash = entry.hash
15✔
538
    let height = entry.height
15✔
539
    let step = 1
15✔
540

541
    hashes.push(hash)
15✔
542

543
    // in `Chain` version of getLocator this is just zero. But this will break if
544
    // we try and get an entry older than the historical point
545
    const end = this.startHeight
15✔
546

547
    while (height > end) {
15✔
548
      height -= step
163✔
549

550
      if (height < end) height = end
163✔
551

552
      if (hashes.length > 10) step *= 2
163✔
553

554
      if (main) {
163!
555
        // If we're on the main chain, we can
556
        // do a fast lookup of the hash.
557
        hash = await this.getHash(height)
163✔
558
        assert(hash)
163✔
559
      } else {
560
        const ancestor = await this.chain.getAncestor(entry, height)
×
561
        assert(ancestor)
×
562
        main = await this.chain.isMainChain(ancestor)
×
563
        hash = ancestor.hash
×
564
      }
565

566
      hashes.push(hash)
163✔
567
    }
568

569
    return hashes
15✔
570
  }
571

572
  /**
573
   * Get block header by height
574
   * @param {height} block height
575
   * @returns {Headers|null} block header
576
   */
577

578
  async getHeader(height) {
579
    assert(typeof height === 'number' && height >= 0, 'Must pass valid height to get header')
248✔
580
    const data = await this.db.get(layout.b.encode(height))
248✔
581
    if (!data) return null
248✔
582
    if (this.isHistorical(height)) return Headers.fromRaw(data)
236✔
583
    return ChainEntry.fromRaw(data)
225✔
584
  }
585

586
  /**
587
   * Get block entry by height or hash
588
   * Overwrites the parent method which only handles by hash
589
   * If passed a height then it can convert a header to entry for
590
   * historical blocks which don't have an entry available
591
   * @param {Number|Buffer} height or hash - block height or hash
592
   * @returns {Headers|null} block entry
593
   */
594
  async getEntry(heightOrHash) {
595
    // indexer checks the chain db first by default
596
    // we can use that first and use header indexer
597
    // if none is found in the chain db (since it is not persisted)
598
    const entry = await super.getEntry(heightOrHash)
241✔
599
    if (entry) return entry
241✔
600

601
    let header = await this.getHeader(heightOrHash)
207✔
602

603
    // return null if none exists
604
    if (!header) return null
207✔
605

606
    // if it is already a chainentry then we can return it
607
    if (ChainEntry.isChainEntry(header)) return header
200!
608
    let { height } = header
×
609

610
    if (!height) height = heightOrHash
×
611

612
    assert(typeof height === 'number')
×
613

614
    // otherwise convert to an entry by getting JSON w/ correct height
615
    // and adding a null chainwork (needed for entry initialization)
616
    header = header.getJSON(this.network.type, null, height)
×
617
    header.chainwork = '0'
×
618
    return ChainEntry.fromJSON(header)
×
619
  }
620

621
  /**
622
   * Test whether the entry is potentially an ancestor of a checkpoint.
623
   * This is adapted from the chain's "isHistorical"
624
   * but to account for custom startHeights. Historical in this case is shifted to be before
625
   * the last retarget before the lastCheckpoint since chain needs at least 1 retargeted entry
626
   * @param {ChainEntry} prev
627
   * @returns {Boolean}
628
   */
629

630
  isHistorical(height) {
631
    if (this.checkpoints) {
550!
632
      // in the case where there is no lastCheckpoint then we just set to zero
633
      const historicalPoint = this.getHistoricalPoint()
550✔
634
      if (height <= historicalPoint) return true
550✔
635
    }
636
    return false
473✔
637
  }
638

639
  getHistoricalPoint() {
640
    const {
641
      lastCheckpoint,
642
      pow: { retargetInterval }
643
    } = this.network
572✔
644
    // in the case where there is no lastCheckpoint then we just set to zero
645
    return lastCheckpoint ? lastCheckpoint - (lastCheckpoint % retargetInterval) : 0
572✔
646
  }
647

648
  /*
649
   * Simple utility to add an entry to the chain
650
   * with chaindb's 'reconnect'
651
   */
652
  async addEntryToChain(entry) {
653
    // `reconnect` needs a block. The AbstractBlock class
654
    // that Headers inherits from should be sufficient
655
    const block = Headers.fromHead(entry.toRaw())
210✔
656
    block.txs = []
210✔
657

658
    // chaindb's reconnect will make the updates to the
659
    // the chain state that we need to catch up
660
    await this.chain.db.reconnect(entry, block, new CoinView())
210✔
661
  }
662

663
  /*
664
   * Takes a ChainEntry and derives a block so that it can index
665
   * the block and set a new tip
666
   * @param {ChainEntry} entry - chain entry to index
667
   */
668
  async indexEntryBlock(entry) {
669
    this.logger.debug('Indexing entry block %d: %h', entry.height, entry.hash)
4✔
670
    const block = Headers.fromHead(entry.toRaw())
4✔
671
    await this.indexBlock(entry, block, new CoinView())
4✔
672
    const tip = BlockMeta.fromEntry(entry)
4✔
673
    await this._setTip(tip)
4✔
674
  }
675

676
  /**
677
   * Get the hash of a block by height. Note that this
678
   * will only return hashes in the main chain.
679
   * @param {Number} height
680
   * @returns {Promise} - Returns {@link Hash}.
681
   */
682

683
  async getHash(height) {
684
    if (Buffer.isBuffer(height)) return height
163!
685

686
    assert(typeof height === 'number')
163✔
687

688
    if (height < 0) return null
163!
689

690
    // NOTE: indexer has no cache
691
    // this.getHash is replacing functionality normally done by the chain
692
    // which does cacheing for performance improvement
693
    // this would be a good target for future optimization of the header chain
694

695
    // const entry = this.cacheHeight.get(height);
696

697
    // if (entry)
698
    //   return entry.hash;
699

700
    return this.db.get(layout.h.encode(height))
163✔
701
  }
702

703
  /**
704
   * Get index tip.
705
   * @param {Hash} hash
706
   * @returns {Promise}
707
   */
708

709
  async getTip() {
710
    let height = this.height
5✔
711
    assert(height, 'Cannot get headers tip until indexer has been initialized and synced')
5✔
712

713
    // in some instances when this has been run the indexer hasn't had a chance to
714
    // catch up to the chain and re-index a block, so we need to rollforward to the tip
715
    if (height < this.chain.height) {
5!
716
      await this._rollforward()
×
717
      height = this.height
×
718
    }
719

720
    const tip = await this.getHeader(height)
5✔
721

722
    if (!tip) throw new Error('Indexer: Tip not found!')
5!
723

724
    return tip
5✔
725
  }
726
}
727

728
module.exports = HeaderIndexer
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

© 2025 Coveralls, Inc