/*!
 * chain.js - blockchain management for hsd
 * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
 * https://github.com/handshake-org/hsd
 */
'use strict';
const assert = require('bsert');
const path = require('path');
const AsyncEmitter = require('bevent');
const Logger = require('blgr');
const {Lock} = require('bmutex');
const LRU = require('blru');
const {BufferMap, BufferSet} = require('buffer-map');
const Network = require('../protocol/network');
const ChainDB = require('./chaindb');
const common = require('./common');
const consensus = require('../protocol/consensus');
const rules = require('../covenants/rules');
const NameState = require('../covenants/namestate');
const util = require('../utils/util');
const ChainEntry = require('./chainentry');
const CoinView = require('../coins/coinview');
const Script = require('../script/script');
const {VerifyError} = require('../protocol/errors');
const {OwnershipProof} = require('../covenants/ownership');
const AirdropProof = require('../primitives/airdropproof');
const {CriticalError} = require('../errors');
const thresholdStates = common.thresholdStates;
const scanActions = common.scanActions;
const {states} = NameState;
const {
  VERIFY_COVENANTS_HARDENED,
  VERIFY_COVENANTS_LOCKUP
} = rules.nameFlags;
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').LockFlags} LockFlags */
/** @typedef {import('@handshake-org/bfilter').BloomFilter} BloomFilter */
/** @typedef {import('../primitives/block')} Block */
/** @typedef {import('../primitives/tx')} TX */
/** @typedef {import('../primitives/txmeta')} TXMeta */
/** @typedef {import('../primitives/outpoint')} Outpoint */
/** @typedef {import('../primitives/coin')} Coin */
/** @typedef {import('../primitives/address')} Address */
/** @typedef {import('../coins/coinentry')} CoinEntry */
/**
 * Blockchain
 * @alias module:blockchain.Chain
 * @property {ChainDB} db
 * @property {ChainEntry?} tip
 * @property {Number} height
 * @property {DeploymentState} state
 */
class Chain extends AsyncEmitter {
  /**
   * Create a blockchain.
   * @constructor
   * @param {Object} options
   */
  constructor(options) {
    super();
    this.opened = false;
    this.options = new ChainOptions(options);
    this.network = this.options.network;
    this.logger = this.options.logger.context('chain');
    this.workers = this.options.workers;
    this.db = new ChainDB(this.options);
    this.locker = new Lock(true, BufferMap);
    this.invalid = new LRU(5000, null, BufferMap);
    this.state = new DeploymentState(this.network.genesis.hash);
    this.tip = new ChainEntry();
    this.height = -1;
    this.synced = false;
    this.orphanMap = new BufferMap();
    this.orphanPrev = new BufferMap();
  }
  /**
   * Open the chain, wait for the database to load.
   * @returns {Promise<void>}
   */
  async open() {
    assert(!this.opened, 'Chain is already open.');
    this.opened = true;
    this.logger.info('Chain is loading.');
    if (this.options.checkpoints)
      this.logger.info('Checkpoints are enabled.');
    await this.db.open();
    const tip = await this.db.getTip();
    assert(tip);
    this.tip = tip;
    this.height = tip.height;
    this.logger.info('Chain Height: %d', tip.height);
    this.logger.memory();
    const state = await this.getDeploymentState();
    this.setDeploymentState(state);
    if (!this.options.spv) {
      const sync = await this.tryCompact();
      if (sync)
        await this.syncTree();
    }
    this.logger.memory();
    this.emit('tip', tip);
    this.maybeSync();
  }
  /**
   * Close the chain, wait for the database to close.
   * @returns {Promise<void>}
   */
  async close() {
    assert(this.opened, 'Chain is not open.');
    this.opened = false;
    return this.db.close();
  }
  /**
   * Get compaction heights.
   * @returns {Promise<Object>}
   */
  async getCompactionHeights() {
    if (this.options.spv)
      return null;
    const {keepBlocks} = this.network.block;
    const {compactionHeight} = await this.db.getTreeState();
    const {compactTreeInitInterval} = this.options;
    const compactFrom = compactionHeight + keepBlocks + compactTreeInitInterval;
    return {
      compactionHeight,
      compactFrom
    };
  }
  /**
   * Check if we need to compact tree data.
   * @returns {Promise<Boolean>} - Should we sync
   */
  async tryCompact() {
    if (this.options.spv)
      return false;
    if (!this.options.compactTreeOnInit)
      return true;
    const {txStart} = this.network;
    const {keepBlocks} = this.network.block;
    const startFrom = txStart + keepBlocks;
    if (this.height <= startFrom)
      return true;
    const {compactFrom} = await this.getCompactionHeights();
    if (compactFrom > this.height) {
      this.logger.debug(
        `Tree will compact when restarted after height ${compactFrom}.`);
      return true;
    }
    // Compact tree calls syncTree so we don't want to rerun it.
    await this.compactTree();
    return false;
  }
  /**
   * Sync tree state.
   */
  async syncTree() {
    this.logger.info('Synchronizing Tree with block history...');
    // Current state of the tree, loaded from chain database and
    // injected in chainDB.open(). It should be in the most
    // recently-committed state, which should have been at the last
    // tree interval. We might also need to recover from a
    // failed compactTree() operation. Either way, there might have been
    // new blocks added to the chain since then.
    const currentRoot = this.db.treeRoot();
    // We store commit height for the tree in the tree state.
    // commitHeight is the height of the block that committed tree root.
    // Note that the block at commitHeight has different tree root.
    const treeState = await this.db.getTreeState();
    const {commitHeight} = treeState;
    // sanity check
    if (commitHeight < this.height) {
      const entry = await this.db.getEntryByHeight(commitHeight + 1);
      assert(entry.treeRoot.equals(treeState.treeRoot));
      assert(entry.treeRoot.equals(currentRoot));
    }
    // Replay all blocks since the last tree interval to rebuild
    // the `txn` which is the in-memory delta between tree interval commitments.
    for (let height = commitHeight + 1; height <= this.height; height++) {
      const entry = await this.db.getEntryByHeight(height);
      assert(entry);
      const block = await this.db.getBlock(entry.hash);
      assert(block);
      const state = await this.readDeploymentState(entry);
      assert(state);
      const view = new CoinView();
      for (const tx of block.txs)
        await this.verifyCovenants(tx, view, height, state.nameFlags);
      // If the chain replay crosses a tree interval, it will commit
      // and write to disk in saveNames(), resetting the `txn` like usual.
      await this.db.saveNames(view, entry, false);
    }
    this.logger.info('Synchronized Tree Root: %x.', this.db.txn.rootHash());
  }
  /**
   * Perform all necessary contextual verification on a block.
   * @private
   * @param {Block} block
   * @param {ChainEntry} prev
   * @param {Number} flags
   * @returns {Promise<Array>} - [CoinView, DeploymentState]
   */
  async verifyContext(block, prev, flags) {
    // Initial non-contextual verification.
    const state = await this.verify(block, prev, flags);
    // Skip everything if we're in SPV mode.
    if (this.options.spv) {
      const view = new CoinView();
      return [view, state];
    }
    // Skip everything if we're using checkpoints.
    if (this.isHistorical(prev)) {
      const view = await this.updateInputs(block, prev, state);
      return [view, state];
    }
    // Verify scripts, spend and add coins.
    const view = await this.verifyInputs(block, prev, state);
    return [view, state];
  }
  /**
   * Perform all necessary contextual verification
   * on a block, without POW check.
   * @param {Block} block
   * @returns {Promise<Array>} - [CoinView, DeploymentState]
   */
  async verifyBlock(block) {
    const unlock = await this.locker.lock();
    try {
      return await this._verifyBlock(block);
    } finally {
      unlock();
    }
  }
  /**
   * Perform all necessary contextual verification
   * on a block, without POW check (no lock).
   * @private
   * @param {Block} block
   * @returns {Promise<Array>} - [CoinView, DeploymentState]
   */
  async _verifyBlock(block) {
    const flags = common.DEFAULT_FLAGS & ~common.flags.VERIFY_POW;
    return this.verifyContext(block, this.tip, flags);
  }
  /**
   * Test whether the hash is in the main chain.
   * @param {Hash} hash
   * @returns {Promise<Boolean>}
   */
  isMainHash(hash) {
    return this.db.isMainHash(hash);
  }
  /**
   * Test whether the entry is in the main chain.
   * @param {ChainEntry} entry
   * @returns {Promise<Boolean>}
   */
  isMainChain(entry) {
    return this.db.isMainChain(entry);
  }
  /**
   * Get ancestor by `height`.
   * @param {ChainEntry} entry
   * @param {Number} height
   * @returns {Promise<ChainEntry?>}
   */
  getAncestor(entry, height) {
    return this.db.getAncestor(entry, height);
  }
  /**
   * Get previous entry.
   * @param {ChainEntry} entry
   * @returns {Promise<ChainEntry?>}
   */
  getPrevious(entry) {
    return this.db.getPrevious(entry);
  }
  /**
   * Get previous cached entry.
   * @param {ChainEntry} entry
   * @returns {ChainEntry?}
   */
  getPrevCache(entry) {
    return this.db.getPrevCache(entry);
  }
  /**
   * Get next entry.
   * @param {ChainEntry} entry
   * @returns {Promise<ChainEntry?>}
   */
  getNext(entry) {
    return this.db.getNext(entry);
  }
  /**
   * Get next entry.
   * @param {ChainEntry} entry
   * @returns {Promise<ChainEntry?>}
   */
  getNextEntry(entry) {
    return this.db.getNextEntry(entry);
  }
  /**
   * Calculate median time past.
   * @param {ChainEntry} prev
   * @returns {Promise<Number>}
   */
  async getMedianTime(prev) {
    const timespan = consensus.MEDIAN_TIMESPAN;
    const median = [];
    let entry = prev;
    for (let i = 0; i < timespan && entry; i++) {
      median.push(entry.time);
      const cache = this.getPrevCache(entry);
      if (cache)
        entry = cache;
      else
        entry = await this.getPrevious(entry);
    }
    median.sort(cmp);
    return median[median.length >>> 1];
  }
  /**
   * Test whether the entry is potentially
   * an ancestor of a checkpoint.
   * @param {ChainEntry} prev
   * @returns {Boolean}
   */
  isHistorical(prev) {
    if (this.options.checkpoints) {
      if (prev.height + 1 <= this.network.lastCheckpoint)
        return true;
    }
    return false;
  }
  /**
   * Test whether the height is potentially
   * an ancestor of a checkpoint.
   * @param {Number} height
   * @returns {Boolean}
   */
  isHistoricalHeight(height) {
    if (this.options.checkpoints) {
      if (height <= this.network.lastCheckpoint)
        return true;
    }
    return false;
  }
  /**
   * Contextual verification for a block, including
   * version deployments (IsSuperMajority), versionbits,
   * coinbase height, finality checks.
   * @private
   * @param {Block} block
   * @param {ChainEntry} prev
   * @param {Number} flags
   * @returns {Promise<DeploymentState>}
   */
  async verify(block, prev, flags) {
    assert(typeof flags === 'number');
    // Extra sanity check.
    if (!block.prevBlock.equals(prev.hash))
      throw new VerifyError(block, 'invalid', 'bad-prevblk', 0);
    // Verify a checkpoint if there is one.
    const hash = block.hash();
    if (!this.verifyCheckpoint(prev, hash)) {
      throw new VerifyError(block,
        'checkpoint',
        'checkpoint mismatch',
        100);
    }
    // Skip everything when using checkpoints.
    // We can do this safely because every
    // block in between each checkpoint was
    // validated outside in the header chain.
    if (this.isHistorical(prev)) {
      // Check merkle root.
      if (flags & common.flags.VERIFY_BODY) {
        assert(typeof block.createMerkleRoot === 'function');
        const root = block.createMerkleRoot();
        if (!block.merkleRoot.equals(root)) {
          throw new VerifyError(block,
            'invalid',
            'bad-txnmrklroot',
            100,
            true);
        }
        const witnessRoot = block.createWitnessRoot();
        if (!block.witnessRoot.equals(witnessRoot)) {
          throw new VerifyError(block,
            'invalid',
            'bad-witnessroot',
            100,
            true);
        }
        flags &= ~common.flags.VERIFY_BODY;
      }
    }
    // Non-contextual checks.
    if (flags & common.flags.VERIFY_BODY) {
      const [valid, reason, score] = block.checkBody();
      if (!valid)
        throw new VerifyError(block, 'invalid', reason, score, true);
    }
    // Check name DoS limits.
    const set = new BufferSet();
    let opens = 0;
    let updates = 0;
    let renewals = 0;
    for (let i = 0; i < block.txs.length; i++) {
      const tx = block.txs[i];
      opens += rules.countOpens(tx);
      if (opens > consensus.MAX_BLOCK_OPENS) {
        throw new VerifyError(block,
          'invalid',
          'bad-blk-opens',
          100);
      }
      updates += rules.countUpdates(tx);
      if (updates > consensus.MAX_BLOCK_UPDATES) {
        throw new VerifyError(block,
          'invalid',
          'bad-blk-updates',
          100);
      }
      renewals += rules.countRenewals(tx);
      if (renewals > consensus.MAX_BLOCK_RENEWALS) {
        throw new VerifyError(block,
          'invalid',
          'bad-blk-renewals',
          100);
      }
      // Certain covenants can only be used once per name per block
      if (rules.hasNames(tx, set)) {
        throw new VerifyError(block,
          'invalid',
          'bad-blk-names',
          100);
      }
      rules.addNames(tx, set);
    }
    // Ensure the POW is what we expect.
    const bits = await this.getTarget(block.time, prev);
    if (block.bits !== bits) {
      this.logger.debug(
        'Bad diffbits: 0x%s != 0x%s',
        util.hex32(block.bits),
        util.hex32(bits));
      throw new VerifyError(block,
        'invalid',
        'bad-diffbits',
        100);
    }
    // Ensure the timestamp is correct.
    const mtp = await this.getMedianTime(prev);
    if (block.time <= mtp) {
      throw new VerifyError(block,
        'invalid',
        'time-too-old',
        0);
    }
    // Check timestamp against adj-time+2hours.
    // If this fails we may be able to accept
    // the block later.
    if (block.time > this.network.now() + 2 * 60 * 60) {
      throw new VerifyError(block,
        'invalid',
        'time-too-new',
        0,
        true);
    }
    // Skip all blocks in spv mode once
    // we've verified the network target.
    if (this.options.spv)
      return this.state;
    // Calculate height of current block.
    const height = prev.height + 1;
    // Get the new deployment state.
    const state = await this.getDeployments(block.time, prev);
    // Transactions must be finalized with
    // regards to nSequence and nLockTime.
    for (let i = 1; i < block.txs.length; i++) {
      const tx = block.txs[i];
      if (!tx.isFinal(height, mtp)) {
        throw new VerifyError(block,
          'invalid',
          'bad-txns-nonfinal',
          10);
      }
    }
    // Make sure the height contained
    // in the coinbase is correct.
    if (block.getCoinbaseHeight() !== height) {
      throw new VerifyError(block,
        'invalid',
        'bad-cb-height',
        100);
    }
    const cb = block.txs[0];
    for (let i = 1; i < cb.inputs.length; i++) {
      const {witness} = cb.inputs[i];
      if (witness.items.length !== 1) {
        throw new VerifyError(block,
          'invalid',
          'bad-witness-size',
          100);
      }
      if (i >= cb.outputs.length) {
        throw new VerifyError(block,
          'invalid',
          'bad-output',
          100);
      }
      const output = cb.outputs[i];
      // Airdrop proof.
      if (!output.covenant.isClaim()) {
        // Disable airdrop claims if airstop is activated
        if (state.hasAirstop) {
          throw new VerifyError(block,
            'invalid',
            'bad-airdrop-disabled',
            100);
        }
        let proof;
        try {
          proof = AirdropProof.decode(witness.items[0]);
        } catch (e) {
          throw new VerifyError(block,
            'invalid',
            'bad-airdrop-format',
            100);
        }
        if (!proof.isSane()) {
          throw new VerifyError(block,
            'invalid',
            'bad-airdrop-sanity',
            100);
        }
        if (prev.height + 1 >= this.network.goosigStop) {
          const key = proof.getKey();
          if (!key) {
            throw new VerifyError(block,
              'invalid',
              'bad-airdrop-proof',
              100);
          }
          if (key.isGoo()) {
            throw new VerifyError(block,
              'invalid',
              'bad-goosig-disabled',
              100);
          }
        }
        // Note: GooSig RSA 1024 is possible to
        // crack as well, but in order to make
        // it safe we would need to include a
        // commitment to the key size (bad).
        // We may have to just disallow <2048
        // bit for mainnet.
        if (state.hasHardening()) {
          if (proof.isWeak()) {
            throw new VerifyError(block,
              'invalid',
              'bad-airdrop-sanity',
              10);
          }
        }
        continue;
      }
      // DNSSEC ownership proof.
      let proof;
      try {
        proof = OwnershipProof.decode(witness.items[0]);
      } catch (e) {
        throw new VerifyError(block,
          'invalid',
          'bad-dnssec-format',
          100);
      }
      // Verify times.
      if (!proof.verifyTimes(prev.time)) {
        throw new VerifyError(block,
          'invalid',
          'bad-dnssec-times',
          10);
      }
    }
    return state;
  }
  /**
   * Check all deployments on a chain.
   * @param {Number} time
   * @param {ChainEntry} prev
   * @returns {Promise<DeploymentState>}
   */
  async getDeployments(time, prev) {
    const deployments = this.network.deployments;
    const state = new DeploymentState(prev.hash);
    // Disable RSA-1024.
    if (await this.isActive(prev, deployments.hardening))
      state.nameFlags |= rules.nameFlags.VERIFY_COVENANTS_HARDENED;
    // Disable ICANN, TOP100 and CUSTOM TLDs from getting auctioned.
    if (await this.isActive(prev, deployments.icannlockup))
      state.nameFlags |= rules.nameFlags.VERIFY_COVENANTS_LOCKUP;
    // Disable airdrop claims.
    if (await this.isActive(prev, deployments.airstop))
      state.hasAirstop = true;
    return state;
  }
  /**
   * Set a new deployment state.
   * @param {DeploymentState} state
   */
  setDeploymentState(state) {
    if (this.options.checkpoints && this.height < this.network.lastCheckpoint) {
      this.state = state;
      return;
    }
    if (!this.state.hasHardening() && state.hasHardening())
      this.logger.warning('RSA hardening has been activated.');
    if (this.height === this.network.deflationHeight)
      this.logger.warning('Name claim deflation has been activated.');
    if (!this.state.hasICANNLockup() && state.hasICANNLockup())
      this.logger.warning('ICANN lockup has been activated.');
    if (!this.state.hasAirstop && state.hasAirstop)
      this.logger.warning('Airdrop claims has been disabled.');
    this.state = state;
  }
  /**
   * Spend and update inputs (checkpoints only).
   * @private
   * @param {Block} block
   * @param {ChainEntry} prev
   * @param {DeploymentState} state
   * @returns {Promise<CoinView>}
   */
  async updateInputs(block, prev, state) {
    const view = new CoinView();
    const height = prev.height + 1;
    assert(block.treeRoot.equals(this.db.treeRoot()));
    for (let i = 0; i < block.txs.length; i++) {
      const tx = block.txs[i];
      if (i === 0) {
        assert(view.bits.spend(this.db.field, tx));
      } else {
        assert(await view.spendInputs(this.db, tx),
          'BUG: Spent inputs in historical data!');
      }
      await this.verifyCovenants(tx, view, height, state.nameFlags);
      view.addTX(tx, height);
    }
    return view;
  }
  /**
   * Check block transactions for all things pertaining
   * to inputs. This function is important because it is
   * what actually fills the coins into the block. This
   * function will check the block reward, the sigops,
   * the tx values, and execute and verify the scripts (it
   * will attempt to do this on the worker pool). If
   * `checkpoints` is enabled, it will skip verification
   * for historical data.
   * @private
   * @see TX#verifyInputs
   * @see TX#verify
   * @param {Block} block
   * @param {ChainEntry} prev
   * @param {DeploymentState} state
   * @returns {Promise<CoinView>}
   */
  async verifyInputs(block, prev, state) {
    const network = this.network;
    const view = new CoinView();
    const height = prev.height + 1;
    const interval = network.halvingInterval;
    let sigops = 0;
    let reward = 0;
    // Check the name tree root.
    if (!block.treeRoot.equals(this.db.treeRoot())) {
      throw new VerifyError(block,
        'invalid',
        'bad-tree-root',
        100);
    }
    // Check all transactions
    for (let i = 0; i < block.txs.length; i++) {
      const tx = block.txs[i];
      // Ensure tx is not double spending an output.
      if (i === 0) {
        if (!view.bits.spend(this.db.field, tx)) {
          throw new VerifyError(block,
            'invalid',
            'bad-txns-bits-missingorspent',
            100);
        }
      } else {
        if (!await view.spendInputs(this.db, tx)) {
          throw new VerifyError(block,
            'invalid',
            'bad-txns-inputs-missingorspent',
            100);
        }
        // Verify sequence locks.
        const valid = await this.verifyLocks(prev, tx, view, state.lockFlags);
        if (!valid) {
          throw new VerifyError(block,
            'invalid',
            'bad-txns-nonfinal',
            100);
        }
      }
      // Count sigops.
      sigops += tx.getSigops(view);
      if (sigops > consensus.MAX_BLOCK_SIGOPS) {
        throw new VerifyError(block,
          'invalid',
          'bad-blk-sigops',
          100);
      }
      // Contextual sanity checks.
      const [fee, reason, score] = tx.checkInputs(view, height, network);
      if (fee === -1) {
        throw new VerifyError(block,
          'invalid',
          reason,
          score);
      }
      reward += fee;
      if (reward > consensus.MAX_MONEY) {
        throw new VerifyError(block,
          'invalid',
          'bad-cb-amount',
          100);
      }
      // Verify covenants.
      await this.verifyCovenants(tx, view, height, state.nameFlags);
      // Add new coins.
      view.addTX(tx, height);
    }
    // Make sure the miner isn't trying to conjure more coins.
    reward += consensus.getReward(height, interval);
    if (block.getClaimed() > reward) {
      throw new VerifyError(block,
        'invalid',
        'bad-cb-amount',
        0);
    }
    // Push onto verification queue.
    const jobs = [];
    for (let i = 0; i < block.txs.length; i++) {
      const tx = block.txs[i];
      jobs.push(tx.verifyAsync(view, state.flags, this.workers));
    }
    // Verify all txs in parallel.
    const results = await Promise.all(jobs);
    for (const result of results) {
      if (!result) {
        throw new VerifyError(block,
          'invalid',
          'mandatory-script-verify-flag-failed',
          100);
      }
    }
    return view;
  }
  /**
   * Get main chain height for hash.
   * @param {Hash} hash
   * @returns {Promise<Number>}
   */
  async getMainHeight(hash) {
    const entry = await this.db.getEntry(hash);
    if (!entry)
      return -1;
    // Must be the current chain.
    if (!await this.db.isMainChain(entry))
      return -1;
    return entry.height;
  }
  /**
   * Verify a renewal.
   * @param {Hash} hash
   * @param {Number} height
   * @returns {Promise<Boolean>}
   */
  async verifyRenewal(hash, height) {
    assert(Buffer.isBuffer(hash));
    assert((height >>> 0) === height);
    // Cannot renew yet.
    if (height < this.network.names.renewalMaturity)
      return true;
    // We require renewals to commit to a block
    // within the past 6 months, to prove that
    // the user still owns the key. This prevents
    // people from presigning thousands of years
    // worth of renewals. The block must be at
    // least 400 blocks back to prevent the
    // possibility of a reorg invalidating the
    // covenant.
    const entry = await this.db.getEntry(hash);
    if (!entry)
      return false;
    // Must be the current chain.
    if (!await this.db.isMainChain(entry))
      return false;
    // Make sure it's a mature block (unlikely to be reorgd).
    if (entry.height > height - this.network.names.renewalMaturity)
      return false;
    // Block committed to must be
    // no older than a 6 months.
    if (entry.height < height - this.network.names.renewalPeriod)
      return false;
    return true;
  }
  /**
   * Verify covenants.
   * @param {TX} tx
   * @param {CoinView} view
   * @param {Number} height
   * @param {Number} nameFlags
   */
  async verifyCovenants(tx, view, height, nameFlags) {
    assert(tx);
    assert(view instanceof CoinView);
    assert((height >>> 0) === height);
    assert(typeof nameFlags === 'number');
    const {types} = rules;
    const network = this.network;
    for (let i = 0; i < tx.outputs.length; i++) {
      const output = tx.outputs[i];
      const {covenant} = output;
      if (!covenant.isName())
        continue;
      // BID and REDEEM covenants to do not update NameState.
      // Therefore, if we are still inside checkpoints we can simply
      // assume these covenants are valid without checking anything,
      // or even getting and decoding the NameState from the tree.
      // We could skip checks for ALL covenant types under checkpoints,
      // but since the other types modify the NameState we still
      // need to get the data, and the checks themselves are cheap.
      if (this.isHistoricalHeight(height)) {
        if (covenant.isBid() || covenant.isRedeem())
          continue;
      }
      const nameHash = covenant.getHash(0);
      const start = covenant.getU32(1);
      const ns = await view.getNameState(this.db, nameHash);
      if (ns.isNull()) {
        if (!covenant.isClaim() && !covenant.isOpen())
          throw new Error('Database inconsistency.');
        const name = covenant.get(2);
        ns.set(name, height);
      }
      // Check for name expiration/revocation.
      // Note that claimed names never expire
      // before the reservation period ends.
      // However, they _can_ be revoked.
      ns.maybeExpire(height, network);
      // Calculate the current state.
      const state = ns.state(height, network);
      // none -> claim
      if (covenant.isClaim()) {
        const flags = covenant.getU8(3);
        const weak = (flags & 1) !== 0;
        // Claims can be re-redeemed any time
        // before registration. This is required
        // in order for our emergency soft-forks
        // to truly behave as _soft_ forks. Once
        // re-redeemed, the locktime resets and
        // they re-enter the LOCKED state. Note
        // that a newer claim invalidates the
        // old output by committing to a higher
        // height (will fail with nonlocal).
        const valid = state === states.OPENING
                   || state === states.LOCKED
                   || (state === states.CLOSED && !ns.registered);
        if (!valid) {
          throw new VerifyError(tx,
            'invalid',
            'bad-claim-state',
            100);
        }
        // Can only claim reserved names.
        // Once a reserved name is revoked,
        // it is no longer claimable.
        if (ns.expired || !rules.isReserved(nameHash, height, network)) {
          throw new VerifyError(tx,
            'invalid',
            'bad-claim-notreserved',
            100);
        }
        // Once the fork is active, we reject
        // any weak algorithms (i.e. RSA-1024).
        // Any future emergency soft-forks should
        // also be included below this check.
        if ((nameFlags & VERIFY_COVENANTS_HARDENED) && weak) {
          throw new VerifyError(tx,
            'invalid',
            'bad-claim-algorithm',
            100);
        }
        // Check commitment hash.
        const block = covenant.getHash(4);
        const claimed = await this.getMainHeight(block);
        // Implicitly checks for `-1`.
        if (claimed !== covenant.getU32(5)) {
          throw new VerifyError(tx,
            'invalid',
            'bad-claim-commit-height',
            100);
        }
        // Implicitly disallows the genesis block.
        if (claimed <= ns.claimed) {
          throw new VerifyError(tx,
            'invalid',
            'bad-claim-commit-hash',
            100);
        }
        assert(claimed >= 1);
        // Handle inflation-fixing soft-fork.
        if (height >= network.deflationHeight) {
          const {claimFrequency} = network.names;
          // Require claim height to be 1 on
          // initial claims. This makes some
          // non-contextual verification easier.
          if (ns.owner.isNull()) {
            if (claimed !== 1) {
              throw new VerifyError(tx,
                'invalid',
                'bad-claim-height',
                0);
            }
          }
          // Limit the frequency of re-claims.
          if (!ns.owner.isNull() && height < ns.height + claimFrequency) {
            throw new VerifyError(tx,
              'invalid',
              'bad-claim-frequency',
              0);
          }
          // Allow replacement, but require the
          // same fee, which is then miner-burned.
          if (!ns.owner.isNull()) {
            const coin = await this.getCoin(ns.owner.hash, ns.owner.index);
            if (!coin || output.value !== coin.value) {
              throw new VerifyError(tx,
                'invalid',
                'bad-claim-value',
                0);
            }
          }
        }
        ns.setHeight(height);
        ns.setRenewal(height);
        ns.setClaimed(claimed);
        ns.setValue(0);
        ns.setOwner(tx.outpoint(i));
        ns.setHighest(0);
        ns.setWeak(weak);
        continue;
      }
      assert(!tx.isCoinbase());
      // none/redeem/open -> open
      if (covenant.isOpen()) {
        if (state !== states.OPENING) {
          throw new VerifyError(tx,
            'invalid',
            'bad-open-state',
            100);
        }
        // Only one open transaction can ever exist.
        if (ns.height !== height) {
          throw new VerifyError(tx,
            'invalid',
            'bad-open-multiple',
            100);
        }
        // Cannot bid on a reserved name.
        if (!ns.expired && rules.isReserved(nameHash, height, network)) {
          throw new VerifyError(tx,
            'invalid',
            'bad-open-reserved',
            100);
        }
        // Make sure locked up names are not opened if ICANN LOCKUP has
        // activated.
        const isLockUpActive = nameFlags & VERIFY_COVENANTS_LOCKUP;
        if (isLockUpActive && rules.isLockedUp(nameHash, height, network)) {
          throw new VerifyError(tx,
            'invalid',
            'bad-open-lockedup',
            100);
        }
        // On mainnet, names are released on a
        // weekly basis for the first year.
        if (!rules.hasRollout(nameHash, height, network)) {
          throw new VerifyError(tx,
            'invalid',
            'bad-open-rollout',
            100);
        }
        continue;
      }
      // none/redeem/open -> bid
      if (covenant.isBid()) {
        if (state !== states.BIDDING) {
          throw new VerifyError(tx,
            'invalid',
            'bad-bid-state',
            100);
        }
        if (start !== ns.height) {
          throw new VerifyError(tx,
            'invalid',
            'bad-bid-height',
            100);
        }
        continue;
      }
      assert(i < tx.inputs.length);
      const {prevout} = tx.inputs[i];
      switch (covenant.type) {
        // bid -> reveal
        case types.REVEAL: {
          if (start !== ns.height) {
            throw new VerifyError(tx,
              'invalid',
              'bad-reveal-nonlocal',
              100);
          }
          // Early reveals? No.
          if (state !== states.REVEAL) {
            throw new VerifyError(tx,
              'invalid',
              'bad-reveal-state',
              100);
          }
          if (ns.owner.isNull() || output.value > ns.highest) {
            ns.setValue(ns.highest);
            ns.setOwner(tx.outpoint(i));
            ns.setHighest(output.value);
          } else if (output.value > ns.value) {
            ns.setValue(output.value);
          }
          break;
        }
        // reveal -> redeem
        case types.REDEEM: {
          if (start !== ns.height) {
            throw new VerifyError(tx,
              'invalid',
              'bad-redeem-nonlocal',
              100);
          }
          // Allow participants to get their
          // money out, even in a revoked state.
          if (state < states.CLOSED) {
            throw new VerifyError(tx,
              'invalid',
              'bad-redeem-state',
              100);
          }
          // Must be the loser in order
          // to redeem the money now.
          if (prevout.equals(ns.owner)) {
            throw new VerifyError(tx,
              'invalid',
              'bad-redeem-owner',
              100);
          }
          break;
        }
        // claim/reveal -> register
        case types.REGISTER: {
          if (start !== ns.height) {
            throw new VerifyError(tx,
              'invalid',
              'bad-register-nonlocal',
              100);
          }
          if (state !== states.CLOSED) {
            throw new VerifyError(tx,
              'invalid',
              'bad-register-state',
              100);
          }
          const data = covenant.get(2);
          const hash = covenant.getHash(3);
          // Verify block hash for renewal.
          if (!await this.verifyRenewal(hash, height)) {
            throw new VerifyError(tx,
              'invalid',
              'bad-register-renewal',
              100);
          }
          // Must be the winner in
          // order to redeem the name.
          if (!prevout.equals(ns.owner)) {
            throw new VerifyError(tx,
              'invalid',
              'bad-register-owner',
              100);
          }
          // Must match the second highest bid.
          if (output.value !== ns.value) {
            throw new VerifyError(tx,
              'invalid',
              'bad-register-value',
              100);
          }
          // For claimed names: if the keys used in
          // the proof were somehow compromised, the
          // name becomes locked until the reservation
          // period ends. Note that this is the same
          // code path that can be used for emergency
          // soft-forks in the case that a large name
          // registrar's keys are compromised.
          if (ns.isClaimable(height, network)) {
            // Soft-fork #1 (RSA hardening).
            if ((nameFlags & VERIFY_COVENANTS_HARDENED) && ns.weak) {
              throw new VerifyError(tx,
                'invalid',
                'bad-register-state',
                100);
            }
            // Emergency soft-forks go here.
            // Use only to prevent sky from falling.
            //
            // A vision for an emergency soft-fork:
            //
            // 1. A list of compromised DNSKEYs are collected
            //    out of band.
            // 2. The chain is scanned on first boot in order
            //    to find proofs which are vulnerable. The
            //    relevant names are marked as such.
            //    - Pruned nodes and nodes without witness
            //      data will unfortunately need to re-sync.
            // 3. Any proof published before the flag day
            //    is also marked in this way if it contains
            //    a vulnerable key.
            // 4. At soft-fork activation, the "vulnerable"
            //    check will take place here. This function
            //    should return true for any name that was
            //    redeemed with a vulnerable key.
            //
            // To future generations:
            // PUT THE VULNERABLE KEY CHECK HERE!
          }
          ns.setRegistered(true);
          ns.setOwner(tx.outpoint(i));
          if (data.length > 0)
            ns.setData(data);
          ns.setRenewal(height);
          break;
        }
        // update/renew/register/finalize -> update
        case types.UPDATE: {
          if (start !== ns.height) {
            throw new VerifyError(tx,
              'invalid',
              'bad-update-nonlocal',
              100);
          }
          if (state !== states.CLOSED) {
            throw new VerifyError(tx,
              'invalid',
              'bad-update-state',
              100);
          }
          const data = covenant.get(2);
          ns.setOwner(tx.outpoint(i));
          if (data.length > 0)
            ns.setData(data);
          ns.setTransfer(0);
          break;
        }
        // update/renew/register/finalize -> renew
        case types.RENEW: {
          if (start !== ns.height) {
            throw new VerifyError(tx,
              'invalid',
              'bad-renewal-nonlocal',
              100);
          }
          if (state !== states.CLOSED) {
            throw new VerifyError(tx,
              'invalid',
              'bad-renewal-state',
              100);
          }
          const hash = covenant.getHash(2);
          if (height < ns.renewal + network.names.treeInterval) {
            throw new VerifyError(tx,
              'invalid',
              'bad-renewal-premature',
              100);
          }
          if (!await this.verifyRenewal(hash, height)) {
            throw new VerifyError(tx,
              'invalid',
              'bad-renewal',
              100);
          }
          ns.setOwner(tx.outpoint(i));
          ns.setTransfer(0);
          ns.setRenewal(height);
          ns.setRenewals(ns.renewals + 1);
          break;
        }
        // update/renew/register/finalize -> transfer
        case types.TRANSFER: {
          if (start !== ns.height) {
            throw new VerifyError(tx,
              'invalid',
              'bad-transfer-nonlocal',
              100);
          }
          if (state !== states.CLOSED) {
            throw new VerifyError(tx,
              'invalid',
              'bad-transfer-state',
              100);
          }
          ns.setOwner(tx.outpoint(i));
          assert(ns.transfer === 0);
          ns.setTransfer(height);
          break;
        }
        // transfer -> finalize
        case types.FINALIZE: {
          if (start !== ns.height) {
            throw new VerifyError(tx,
              'invalid',
              'bad-finalize-nonlocal',
              100);
          }
          if (state !== states.CLOSED) {
            throw new VerifyError(tx,
              'invalid',
              'bad-finalize-state',
              100);
          }
          assert(ns.transfer !== 0);
          assert(network.names.transferLockup >= network.names.treeInterval);
          if (height < ns.transfer + network.names.transferLockup) {
            throw new VerifyError(tx,
              'invalid',
              'bad-finalize-maturity',
              100);
          }
          const flags = covenant.getU8(3);
          const weak = (flags & 1) !== 0;
          const claimed = covenant.getU32(4);
          const renewals = covenant.getU32(5);
          const hash = covenant.getHash(6);
          if (weak !== ns.weak
              || claimed !== ns.claimed
              || renewals !== ns.renewals) {
            throw new VerifyError(tx,
              'invalid',
              'bad-finalize-statetransfer',
              100);
          }
          if (!await this.verifyRenewal(hash, height)) {
            throw new VerifyError(tx,
              'invalid',
              'bad-finalize-renewal',
              100);
          }
          ns.setOwner(tx.outpoint(i));
          ns.setTransfer(0);
          ns.setRenewal(height);
          ns.setRenewals(ns.renewals + 1);
          break;
        }
        // register/update/renew/transfer/finalize -> revoke
        case types.REVOKE: {
          if (start !== ns.height) {
            throw new VerifyError(tx,
              'invalid',
              'bad-revoke-nonlocal',
              100);
          }
          if (state !== states.CLOSED) {
            throw new VerifyError(tx,
              'invalid',
              'bad-revoke-state',
              100);
          }
          assert(ns.revoked === 0);
          ns.setRevoked(height);
          ns.setTransfer(0);
          ns.setData(null);
          break;
        }
        default: {
          assert.fail('Invalid covenant type.');
          break;
        }
      }
    }
    return;
  }
  /**
   * Find the block at which a fork ocurred.
   * @private
   * @param {ChainEntry} fork - The current chain.
   * @param {ChainEntry} longer - The competing chain.
   * @returns {Promise<ChainEntry>}
   */
  async findFork(fork, longer) {
    while (!fork.hash.equals(longer.hash)) {
      while (longer.height > fork.height) {
        longer = await this.getPrevious(longer);
        if (!longer)
          throw new Error('No previous entry for new tip.');
      }
      if (fork.hash.equals(longer.hash))
        return fork;
      fork = await this.getPrevious(fork);
      if (!fork)
        throw new Error('No previous entry for old tip.');
    }
    return fork;
  }
  /**
   * Reorganize the blockchain (connect and disconnect inputs).
   * Called when a competing chain with a higher chainwork
   * is received.
   * @private
   * @param {ChainEntry} competitor - The competing chain's tip.
   * @returns {Promise<ChainEntry>} - Fork block.
   */
  async reorganize(competitor) {
    const tip = this.tip;
    const fork = await this.findFork(tip, competitor);
    assert(fork, 'No free space or data corruption.');
    // Blocks to disconnect.
    const disconnect = [];
    let entry = tip;
    while (!entry.hash.equals(fork.hash)) {
      disconnect.push(entry);
      entry = await this.getPrevious(entry);
      assert(entry);
    }
    // Blocks to connect.
    const connect = [];
    entry = competitor;
    while (!entry.hash.equals(fork.hash)) {
      connect.push(entry);
      entry = await this.getPrevious(entry);
      assert(entry);
    }
    // Disconnect blocks/txs.
    for (let i = 0; i < disconnect.length; i++) {
      const entry = disconnect[i];
      await this.disconnect(entry);
    }
    // Connect blocks/txs.
    // We don't want to connect the new tip here.
    // That will be done outside in setBestChain.
    for (let i = connect.length - 1; i >= 1; i--) {
      const entry = connect[i];
      try {
        await this.reconnect(entry);
      } catch (err) {
        if (err.type === 'VerifyError') {
          if (!err.malleated) {
            while (i--)
              this.setInvalid(connect[i].hash);
          }
          if (this.tip.chainwork.lte(tip.chainwork))
            await this.unreorganize(fork, tip);
        }
        throw err;
      }
    }
    this.logger.warning(
      'Chain reorganization: old=%x(%d) new=%x(%d)',
      tip.hash,
      tip.height,
      competitor.hash,
      competitor.height
    );
    await this.emitAsync('reorganize', tip, competitor, fork);
    return fork;
  }
  /**
   * Revert a failed reorganization.
   * @private
   * @param {ChainEntry} fork - The common ancestor.
   * @param {ChainEntry} last - The previous valid tip.
   * @returns {Promise}
   */
  async unreorganize(fork, last) {
    const tip = this.tip;
    // Blocks to disconnect.
    const disconnect = [];
    let entry = tip;
    while (!entry.hash.equals(fork.hash)) {
      disconnect.push(entry);
      entry = await this.getPrevious(entry);
      assert(entry);
    }
    // Blocks to connect.
    const connect = [];
    entry = last;
    while (!entry.hash.equals(fork.hash)) {
      connect.push(entry);
      entry = await this.getPrevious(entry);
      assert(entry);
    }
    // Disconnect blocks/txs.
    for (let i = 0; i < disconnect.length; i++) {
      const entry = disconnect[i];
      await this.disconnect(entry);
    }
    // Connect blocks/txs.
    for (let i = connect.length - 1; i >= 0; i--) {
      const entry = connect[i];
      await this.reconnect(entry);
    }
    this.logger.warning(
      'Chain un-reorganization: old=%x(%d) new=%x(%d)',
      tip.hash,
      tip.height,
      last.hash,
      last.height
    );
    // Treat as a reorganize event.
    await this.emitAsync('reorganize', tip, last, fork);
  }
  /**
   * Reorganize the blockchain for SPV. This
   * will reset the chain to the fork block.
   * @private
   * @param {ChainEntry} competitor - The competing chain's tip.
   * @returns {Promise}
   */
  async reorganizeSPV(competitor) {
    const tip = this.tip;
    const fork = await this.findFork(tip, competitor);
    assert(fork, 'No free space or data corruption.');
    // Buffer disconnected blocks.
    const disconnect = [];
    let entry = tip;
    while (!entry.hash.equals(fork.hash)) {
      disconnect.push(entry);
      entry = await this.getPrevious(entry);
      assert(entry);
    }
    // Reset the main chain back
    // to the fork block, causing
    // us to redownload the blocks
    // on the new main chain.
    await this._reset(fork.hash, false);
    // Emit disconnection events now that
    // the chain has successfully reset.
    for (const entry of disconnect) {
      const headers = entry.toHeaders();
      const view = new CoinView();
      await this.emitAsync('disconnect', entry, headers, view);
    }
    this.logger.warning(
      'SPV reorganization: old=%x(%d) new=%x(%d)',
      tip.hash,
      tip.height,
      competitor.hash,
      competitor.height
    );
    this.logger.warning(
      'Chain replay from height %d necessary.',
      fork.height);
    return this.emitAsync('reorganize', tip, competitor, fork);
  }
  /**
   * Disconnect an entry from the chain (updates the tip).
   * @param {ChainEntry} entry
   * @returns {Promise}
   */
  async disconnect(entry) {
    let block = await this.getBlock(entry.hash);
    if (!block) {
      if (!this.options.spv)
        throw new Error('Block not found.');
      block = entry.toHeaders();
    }
    const prev = await this.getPrevious(entry);
    const view = await this.db.disconnect(entry, block);
    assert(prev);
    this.tip = prev;
    this.height = prev.height;
    this.emit('tip', prev);
    return this.emitAsync('disconnect', entry, block, view);
  }
  /**
   * Reconnect an entry to the chain (updates the tip).
   * This will do contextual-verification on the block
   * (necessary because we cannot validate the inputs
   * in alternate chains when they come in).
   * @param {ChainEntry} entry
   * @returns {Promise}
   */
  async reconnect(entry) {
    const flags = common.flags.VERIFY_NONE;
    let block = await this.getBlock(entry.hash);
    if (!block) {
      if (!this.options.spv)
        throw new Error('Block not found.');
      block = entry.toHeaders();
    }
    const prev = await this.getPrevious(entry);
    assert(prev);
    let view, state;
    try {
      [view, state] = await this.verifyContext(block, prev, flags);
    } catch (err) {
      if (err.type === 'VerifyError') {
        if (!err.malleated)
          this.setInvalid(entry.hash);
        this.logger.warning(
          'Tried to reconnect invalid block: %x (%d).',
          entry.hash, entry.height);
      }
      throw err;
    }
    await this.db.reconnect(entry, block, view);
    this.tip = entry;
    this.height = entry.height;
    this.setDeploymentState(state);
    this.emit('tip', entry);
    this.emit('reconnect', entry, block);
    if ((entry.height % this.network.names.treeInterval) === 0)
      this.emit('tree commit', this.db.tree.rootHash(), entry, block);
    return this.emitAsync('connect', entry, block, view);
  }
  /**
   * Set the best chain. This is called on every incoming
   * block with greater chainwork than the current tip.
   * @private
   * @param {ChainEntry} entry
   * @param {Block} block
   * @param {ChainEntry} prev
   * @param {Number} flags
   * @returns {Promise}
   */
  async setBestChain(entry, block, prev, flags) {
    const tip = this.tip;
    let fork = null;
    // A higher fork has arrived.
    // Time to reorganize the chain.
    if (!entry.prevBlock.equals(this.tip.hash)) {
      try {
        // Do as much verification
        // as we can before reorganizing.
        await this.verify(block, prev, flags);
      } catch (err) {
        if (err.type === 'VerifyError') {
          if (!err.malleated)
            this.setInvalid(entry.hash);
          this.logger.warning(
            'Tried to connect invalid block: %x (%d).',
            entry.hash, entry.height);
        }
        throw err;
      }
      this.logger.warning('WARNING: Reorganizing chain.');
      // In spv-mode, we reset the
      // chain and redownload the blocks.
      if (this.options.spv)
        return this.reorganizeSPV(entry);
      fork = await this.reorganize(entry);
    }
    // Warn of unknown versionbits.
    if (entry.hasUnknown(this.network)) {
      this.logger.warning(
        'Unknown version bits in block %d: %s.',
        entry.height, util.hex32(entry.version));
    }
    // Otherwise, everything is in order.
    // Do "contextual" verification on our block
    // now that we're certain its previous
    // block is in the chain.
    let view, state;
    try {
      [view, state] = await this.verifyContext(block, prev, flags);
    } catch (err) {
      if (err.type === 'VerifyError') {
        if (!err.malleated)
          this.setInvalid(entry.hash);
        this.logger.warning(
          'Tried to connect invalid block: %x (%d).',
          entry.hash, entry.height);
        if (fork && this.tip.chainwork.lte(tip.chainwork))
          await this.unreorganize(fork, tip);
      }
      throw err;
    }
    // Save block and connect inputs.
    try {
      await this.db.save(entry, block, view);
    } catch (e) {
      const error = new CriticalError(e.message);
      this.emit('abort', error);
      throw error;
    }
    // Expose the new state.
    this.tip = entry;
    this.height = entry.height;
    this.setDeploymentState(state);
    this.emit('tip', entry);
    this.emit('block', block, entry);
    if ((entry.height % this.network.names.treeInterval) === 0)
      this.emit('tree commit', this.db.tree.rootHash(), entry, block);
    return this.emitAsync('connect', entry, block, view);
  }
  /**
   * Save block on an alternate chain.
   * @private
   * @param {ChainEntry} entry
   * @param {Block} block
   * @param {ChainEntry} prev
   * @param {Number} flags
   * @returns {Promise}
   */
  async saveAlternate(entry, block, prev, flags) {
    // Do not accept forked chain older than the
    // last checkpoint.
    if (this.options.checkpoints) {
      if (prev.height + 1 < this.network.lastCheckpoint)
        throw new VerifyError(block,
          'checkpoint',
          'bad-fork-prior-to-checkpoint',
          100);
    }
    try {
      // Do as much verification
      // as we can before saving.
      await this.verify(block, prev, flags);
    } catch (err) {
      if (err.type === 'VerifyError') {
        if (!err.malleated)
          this.setInvalid(entry.hash);
        this.logger.warning(
          'Invalid block on alternate chain: %x (%d).',
          entry.hash, entry.height);
      }
      throw err;
    }
    // Warn of unknown versionbits.
    if (entry.hasUnknown(this.network)) {
      this.logger.warning(
        'Unknown version bits in block %d: %s.',
        entry.height, util.hex32(entry.version));
    }
    try {
      await this.db.save(entry, block);
    } catch (e) {
      const error = new CriticalError(e.message);
      this.emit('abort', error);
      throw error;
    }
    this.logger.warning('Heads up: Competing chain at height %d:'
      + ' tip-height=%d competitor-height=%d'
      + ' tip-hash=%x competitor-hash=%x'
      + ' tip-chainwork=%s competitor-chainwork=%s'
      + ' chainwork-diff=%s',
      entry.height,
      this.tip.height,
      entry.height,
      this.tip.hash,
      entry.hash,
      this.tip.chainwork.toString(),
      entry.chainwork.toString(),
      this.tip.chainwork.sub(entry.chainwork).toString());
    // Emit as a "competitor" block.
    this.emit('competitor', block, entry);
  }
  /**
   * Reset the chain to the desired block. This
   * is useful for replaying the blockchain download
   * for SPV.
   * @param {Hash|Number} block
   * @returns {Promise}
   */
  async reset(block) {
    const unlock = await this.locker.lock();
    try {
      return await this._reset(block, false);
    } finally {
      unlock();
    }
  }
  /**
   * Reset the chain to the desired block without a lock.
   * @private
   * @param {Hash|Number} block
   * @param {Boolean} silent - don't emit reset.
   * @returns {Promise}
   */
  async _reset(block, silent = false) {
    const tip = await this.db.reset(block);
    // Reset state.
    this.tip = tip;
    this.height = tip.height;
    this.synced = false;
    const state = await this.getDeploymentState();
    this.setDeploymentState(state);
    this.emit('tip', tip);
    if (!silent)
      await this.emitAsync('reset', tip);
    // Reset the orphan map completely. There may
    // have been some orphans on a forked chain we
    // no longer need.
    this.purgeOrphans();
    this.maybeSync();
  }
  /**
   * Reset the chain to a height or hash. Useful for replaying
   * the blockchain download for SPV.
   * @param {Hash|Number} block - hash/height
   * @returns {Promise}
   */
  async replay(block) {
    const unlock = await this.locker.lock();
    try {
      return await this._replay(block, true);
    } finally {
      unlock();
    }
  }
  /**
   * Reset the chain without a lock.
   * @private
   * @param {Hash|Number} block - hash/height
   * @param {Boolean} silent
   * @returns {Promise}
   */
  async _replay(block, silent) {
    const entry = await this.getEntry(block);
    if (!entry)
      throw new Error('Block not found.');
    if (!await this.isMainChain(entry))
      throw new Error('Cannot reset on alternate chain.');
    if (entry.isGenesis()) {
      await this._reset(entry.hash, silent);
      return;
    }
    await this._reset(entry.prevBlock, silent);
  }
  /**
   * Invalidate block.
   * @param {Hash} hash
   * @returns {Promise}
   */
  async invalidate(hash) {
    const unlock = await this.locker.lock();
    try {
      return await this._invalidate(hash);
    } finally {
      unlock();
    }
  }
  /**
   * Invalidate block (no lock).
   * @param {Hash} hash
   * @returns {Promise}
   */
  async _invalidate(hash) {
    await this._replay(hash, false);
    this.setInvalid(hash);
  }
  /**
   * Retroactively prune the database.
   * @returns {Promise<Boolean>}
   */
  async prune() {
    const unlock = await this.locker.lock();
    try {
      return await this.db.prune();
    } finally {
      unlock();
    }
  }
  /**
   * Compact the Urkel Tree.
   * Removes all historical state and all data not
   * linked directly to the provided root node hash.
   * @returns {Promise}
   */
  async compactTree() {
    if (this.options.spv)
      return;
    if (this.height < this.network.block.keepBlocks)
      throw new Error('Chain is too short to compact tree.');
    const unlock = await this.locker.lock();
    this.logger.info('Compacting Urkel Tree...');
    // To support chain reorgs of limited depth we compact the tree
    // to some commitment point in recent history, then rebuild it from there
    // back up to the current chain tip. In order to support pruning nodes,
    // all blocks above this depth must be available on disk.
    // This actually further reduces the ability for a pruning node to recover
    // from a deep reorg. On mainnet, `keepBlocks` is 288. A normal pruning
    // node can recover from a reorg up to that depth. Compacting the tree
    // potentially reduces that depth to 288 - 36 = 252. A reorg deeper than
    // that will result in a `MissingNodeError` thrown by Urkel inside
    // chain.saveNames() as it tries to restore a deleted state.
    // Oldest block available to a pruning node.
    const oldestBlock = this.height - this.network.block.keepBlocks;
    const {treeInterval} = this.network.names;
    // Distance from that block to the start of the oldest tree interval.
    const toNextInterval = (treeInterval - (oldestBlock % treeInterval))
      % treeInterval;
    // Get the oldest Urkel Tree root state a pruning node can recover from.
    const oldestTreeIntervalStart = oldestBlock + toNextInterval + 1;
    const entry = await this.db.getEntryByHeight(oldestTreeIntervalStart);
    try {
      // TODO: For RPC calls, If compaction fails while compacting
      // and we never hit syncTree, we need to shut down the node
      // so on restart chain can recover.
      // Error can also happen in syncTree, but that means the DB
      // is done for. (because restart would just retry syncTree.)
      // It's fine on open, open throwing would just stop the node.
      // Rewind Urkel Tree and delete all historical state.
      this.emit('tree compact start', entry.treeRoot, entry);
      await this.db.compactTree(entry);
      await this.syncTree();
      this.emit('tree compact end', entry.treeRoot, entry);
    } catch(e) {
      const error = new CriticalError(e.message);
      this.emit('abort', error);
      throw error;
    } finally {
      unlock();
    }
  }
  /**
   * Reconstruct the Urkel Tree.
   * @returns {Promise}
   */
  async reconstructTree() {
    if (this.options.spv)
      return;
    if (this.options.prune)
      throw new Error('Cannot reconstruct tree in pruned mode.');
    const unlock = await this.locker.lock();
    const treeState = await this.db.getTreeState();
    if (treeState.compactionHeight === 0)
      throw new Error('Nothing to reconstruct.');
    // Compact all the way to the first block and
    // let the syncTree do its job.
    const entry = await this.db.getEntryByHeight(1);
    try {
      this.emit('tree reconstruct start');
      await this.db.compactTree(entry);
      await this.syncTree();
      this.emit('tree reconstruct end');
    } finally {
      unlock();
    }
  }
  /**
   * Scan the blockchain for transactions containing specified address hashes.
   * @param {Hash|Number} start - Block hash or height to start at.
   * @param {BloomFilter} filter - Bloomfilter containing tx and address hashes.
   * @param {Function} iter - Iterator.
   * @returns {Promise<void>}
   */
  async scan(start, filter, iter) {
    const unlock = await this.locker.lock();
    try {
      return await this.db.scan(start, filter, iter);
    } finally {
      unlock();
    }
  }
  /** @typedef {import('./common').ScanAction} ScanAction */
  /**
   * @callback ScanInteractiveIterCB
   * @param {ChainEntry} entry
   * @param {TX[]} txs
   * @returns {Promise<ScanAction>}
   */
  /**
   * Interactive scan the blockchain for transactions containing specified
   * address hashes. Allows repeat and abort.
   * @param {Hash|Number} start - Block hash or height to start at.
   * @param {BloomFilter} filter - Starting bloom filter containing tx,
   * address and name hashes.
   * @param {ScanInteractiveIterCB} iter - Iterator.
   * @param {Boolean} [fullLock=false]
   * @returns {Promise<void>}
   */
  async scanInteractive(start, filter, iter, fullLock = false) {
    if (fullLock) {
      const unlock = await this.locker.lock();
      try {
        // We lock the whole chain, no longer lock per block scan.
        return await this._scanInteractive(start, filter, iter, false);
      } catch (e) {
        this.logger.debug('Scan(interactive) errored. Error: %s', e.message);
        throw e;
      } finally {
        unlock();
      }
    }
    return this._scanInteractive(start, filter, iter, true);
  }
  /**
   * Interactive scan the blockchain for transactions containing specified
   * address hashes. Allows repeat and abort.
   * @param {Hash|Number} start - Block hash or height to start at.
   * @param {BloomFilter} filter - Starting bloom filter containing tx,
   * address and name hashes.
   * @param {ScanInteractiveIterCB} iter - Iterator.
   * @param {Boolean} [lockPerScan=true] - if we should lock per block scan.
   * @returns {Promise<void>}
   */
  async _scanInteractive(start, filter, iter, lockPerScan = true) {
    if (start == null)
      start = this.network.genesis.hash;
    if (typeof start === 'number')
      this.logger.info('Scanning(interactive) from height %d.', start);
    else
      this.logger.info('Scanning(interactive) from block %x.', start);
    let hash = start;
    while (hash != null) {
      let unlock;
      if (lockPerScan)
        unlock = await this.locker.lock();
      try {
        const {entry, txs} = await this.db.scanBlock(hash, filter);
        const action = await iter(entry, txs);
        if (!action || typeof action !== 'object')
          throw new Error('Did not get proper action');
        switch (action.type) {
          case scanActions.REPEAT: {
            break;
          }
          case scanActions.REPEAT_SET: {
            // try again with updated filter.
            filter = action.filter;
            break;
          }
          case scanActions.REPEAT_ADD: {
            if (!filter)
              throw new Error('No filter set.');
            for (const chunk of action.chunks)
              filter.add(chunk);
            break;
          }
          case scanActions.NEXT: {
            const next = await this.getNext(entry);
            hash = next && next.hash;
            break;
          }
          case scanActions.ABORT: {
            this.logger.info('Scan(interactive) aborted at %x (%d).',
              entry.hash, entry.height);
            throw new Error('scan request aborted.');
          }
          default:
            this.logger.debug('Scan(interactive) aborting. Unknown action: %d',
              action.type);
            throw new Error('Unknown action.');
        }
      } catch (e) {
        this.logger.debug('Scan(interactive) errored. Error: %s', e.message);
        throw e;
      } finally {
        if (lockPerScan)
          unlock();
      }
    }
  }
  /**
   * Add a block to the chain, perform all necessary verification.
   * @param {Block} block
   * @param {Number?} flags
   * @param {Number?} id
   * @returns {Promise<ChainEntry?>}
   */
  async add(block, flags, id) {
    const hash = block.hash();
    const unlock = await this.locker.lock(hash);
    try {
      return await this._add(block, flags, id);
    } finally {
      unlock();
    }
  }
  /**
   * Add a block to the chain without a lock.
   * @private
   * @param {Block} block
   * @param {Number?} flags
   * @param {Number?} id
   * @returns {Promise<ChainEntry?>}
   */
  async _add(block, flags, id) {
    const hash = block.hash();
    if (flags == null)
      flags = common.DEFAULT_FLAGS;
    if (id == null)
      id = -1;
    // Special case for genesis block.
    if (hash.equals(this.network.genesis.hash)) {
      this.logger.debug('Saw genesis block: %x.', block.hash());
      throw new VerifyError(block, 'duplicate', 'duplicate', 0);
    }
    // Do we already have this block in the queue?
    if (this.hasPending(hash)) {
      this.logger.debug('Already have pending block: %x.', block.hash());
      throw new VerifyError(block, 'duplicate', 'duplicate', 0);
    }
    // If the block is already known to be
    // an orphan, ignore it.
    if (this.hasOrphan(hash)) {
      this.logger.debug('Already have orphan block: %x.', block.hash());
      throw new VerifyError(block, 'duplicate', 'duplicate', 0);
    }
    // Do not revalidate known invalid blocks.
    if (this.hasInvalid(block)) {
      this.logger.debug('Invalid ancestors for block: %x.', block.hash());
      throw new VerifyError(block, 'duplicate', 'duplicate', 100);
    }
    // Check the POW before doing anything.
    if (flags & common.flags.VERIFY_POW) {
      if (!block.verifyPOW())
        throw new VerifyError(block, 'invalid', 'high-hash', 50);
    }
    // Do we already have this block?
    if (await this.hasEntry(hash)) {
      this.logger.debug('Already have block: %x.', block.hash());
      throw new VerifyError(block, 'duplicate', 'duplicate', 0);
    }
    // Find the previous block entry.
    const prev = await this.getEntry(block.prevBlock);
    // If previous block wasn't ever seen,
    // add it current to orphans and return.
    if (!prev) {
      this.storeOrphan(block, flags, id);
      return null;
    }
    // Connect the block.
    const entry = await this.connect(prev, block, flags);
    // Handle any orphans.
    if (this.hasNextOrphan(hash))
      await this.handleOrphans(entry);
    return entry;
  }
  /**
   * Connect block to chain.
   * @private
   * @param {ChainEntry} prev
   * @param {Block} block
   * @param {Number} flags
   * @returns {Promise<ChainEntry?>}
   */
  async connect(prev, block, flags) {
    const start = util.bench();
    // Sanity check.
    assert(block.prevBlock.equals(prev.hash));
    // Explanation: we try to keep as much data
    // off the javascript heap as possible. Blocks
    // in the future may be 8mb or 20mb, who knows.
    // In fullnode-mode we store the blocks in
    // "compact" form (the headers plus the raw
    // Buffer object) until they're ready to be
    // fully validated here. They are deserialized,
    // validated, and connected. Hopefully the
    // deserialized blocks get cleaned up by the
    // GC quickly.
    if (block.isMemory()) {
      try {
        block = block.toBlock();
      } catch (e) {
        this.logger.error(e);
        throw new VerifyError(block,
          'malformed',
          'error parsing message',
          10,
          true);
      }
    }
    // Transactions are not allowed in any block before
    // a certain amount of chainwork has been accumulated.
    // This is a non-malleated, permanently invalid block
    // and whoever sent it should be banned.
    if (prev.height + 1 < this.network.txStart) {
      let invalid = false;
      // No transactions allowed besides coinbase
      if (block.txs.length > 1)
        invalid = true;
      if (!this.options.spv) {
        // No claims or airdrops allowed in coinbase yet.
        const cb = block.txs[0];
        if (cb.outputs.length > 1)
          invalid = true;
        // Sanity check
        if (!cb.outputs[0].covenant.isNone())
          invalid = true;
      }
      if (invalid) {
        this.setInvalid(block.hash());
        throw new VerifyError(block,
          'invalid',
          'no-tx-allowed-yet',
          100,
          false);
      }
    }
    // Create a new chain entry.
    const entry = ChainEntry.fromBlock(block, prev);
    // The block is on a alternate chain if the
    // chainwork is less than or equal to
    // our tip's. Add the block but do _not_
    // connect the inputs.
    if (entry.chainwork.lte(this.tip.chainwork)) {
      // Save block to an alternate chain.
      await this.saveAlternate(entry, block, prev, flags);
    } else {
      // Attempt to add block to the chain index.
      await this.setBestChain(entry, block, prev, flags);
    }
    // Keep track of stats.
    this.logStatus(start, block, entry);
    // Check sync state.
    this.maybeSync();
    return entry;
  }
  /**
   * Handle orphans.
   * @private
   * @param {ChainEntry} entry
   * @returns {Promise}
   */
  async handleOrphans(entry) {
    let orphan = this.resolveOrphan(entry.hash);
    while (orphan) {
      const {block, flags, id} = orphan;
      try {
        entry = await this.connect(entry, block, flags);
      } catch (err) {
        if (err.type === 'VerifyError') {
          this.logger.warning(
            'Could not resolve orphan block %x: %s.',
            block.hash(), err.message);
          this.emit('bad orphan', err, id);
          break;
        }
        throw err;
      }
      this.logger.debug(
        'Orphan block was resolved: %x (%d).',
        block.hash(), entry.height);
      this.emit('resolved', block, entry);
      orphan = this.resolveOrphan(entry.hash);
    }
  }
  /**
   * Test whether the chain has reached its slow height.
   * @private
   * @returns {Boolean}
   */
  isSlow() {
    if (this.options.spv)
      return false;
    if (this.synced)
      return true;
    if (this.height === 1 || this.height % 20 === 0)
      return true;
    if (this.height >= this.network.block.slowHeight)
      return true;
    return false;
  }
  /**
   * Calculate the time difference from
   * start time and log block.
   * @private
   * @param {Array} start
   * @param {Block} block
   * @param {ChainEntry} entry
   */
  logStatus(start, block, entry) {
    if (!this.isSlow())
      return;
    // Report memory for debugging.
    this.logger.memory();
    const elapsed = util.bench(start);
    this.logger.info(
      'Block %x (%d) added to chain (size=%d txs=%d time=%d).',
      entry.hash,
      entry.height,
      block.getSize(),
      block.txs.length,
      elapsed);
  }
  /**
   * Verify a block hash and height against the checkpoints.
   * @private
   * @param {ChainEntry} prev
   * @param {Hash} hash
   * @returns {Boolean}
   */
  verifyCheckpoint(prev, hash) {
    if (!this.options.checkpoints)
      return true;
    const height = prev.height + 1;
    const checkpoint = this.network.checkpointMap[height];
    if (!checkpoint)
      return true;
    if (hash.equals(checkpoint)) {
      this.logger.debug('Hit checkpoint block %x (%d).', hash, height);
      this.emit('checkpoint', hash, height);
      return true;
    }
    // Someone is either mining on top of
    // an old block for no reason, or the
    // consensus protocol is broken and
    // there was a 20k+ block reorg.
    this.logger.warning(
      'Checkpoint mismatch at height %d: expected=%x received=%x',
      height,
      checkpoint,
      hash
    );
    this.purgeOrphans();
    return false;
  }
  /**
   * Store an orphan.
   * @private
   * @param {Block} block
   * @param {Number?} flags
   * @param {Number?} id
   */
  storeOrphan(block, flags, id) {
    const height = block.getCoinbaseHeight();
    const orphan = this.orphanPrev.get(block.prevBlock);
    // The orphan chain forked.
    if (orphan) {
      assert(!orphan.block.hash().equals(block.hash()));
      assert(orphan.block.prevBlock.equals(block.prevBlock));
      this.logger.warning(
        'Removing forked orphan block: %x (%d).',
        orphan.block.hash(), height);
      this.removeOrphan(orphan);
    }
    this.limitOrphans();
    this.addOrphan(new Orphan(block, flags, id));
    this.logger.debug(
      'Storing orphan block: %x (%d).',
      block.hash(), height);
    this.emit('orphan', block);
  }
  /**
   * Add an orphan.
   * @private
   * @param {Orphan} orphan
   * @returns {Orphan}
   */
  addOrphan(orphan) {
    const block = orphan.block;
    const hash = block.hash();
    assert(!this.orphanMap.has(hash));
    assert(!this.orphanPrev.has(block.prevBlock));
    assert(this.orphanMap.size >= 0);
    this.orphanMap.set(hash, orphan);
    this.orphanPrev.set(block.prevBlock, orphan);
    return orphan;
  }
  /**
   * Remove an orphan.
   * @private
   * @param {Orphan} orphan
   * @returns {Orphan}
   */
  removeOrphan(orphan) {
    const block = orphan.block;
    const hash = block.hash();
    assert(this.orphanMap.has(hash));
    assert(this.orphanPrev.has(block.prevBlock));
    assert(this.orphanMap.size > 0);
    this.orphanMap.delete(hash);
    this.orphanPrev.delete(block.prevBlock);
    return orphan;
  }
  /**
   * Test whether a hash would resolve the next orphan.
   * @private
   * @param {Hash} hash - Previous block hash.
   * @returns {Boolean}
   */
  hasNextOrphan(hash) {
    return this.orphanPrev.has(hash);
  }
  /**
   * Resolve an orphan.
   * @private
   * @param {Hash} hash - Previous block hash.
   * @returns {Orphan}
   */
  resolveOrphan(hash) {
    const orphan = this.orphanPrev.get(hash);
    if (!orphan)
      return null;
    return this.removeOrphan(orphan);
  }
  /**
   * Purge any waiting orphans.
   */
  purgeOrphans() {
    const count = this.orphanMap.size;
    if (count === 0)
      return;
    this.orphanMap.clear();
    this.orphanPrev.clear();
    this.logger.debug('Purged %d orphans.', count);
  }
  /**
   * Prune orphans, only keep the orphan with the highest
   * coinbase height (likely to be the peer's tip).
   */
  limitOrphans() {
    const now = util.now();
    let oldest = null;
    for (const orphan of this.orphanMap.values()) {
      if (now < orphan.time + 60 * 60) {
        if (!oldest || orphan.time < oldest.time)
          oldest = orphan;
        continue;
      }
      this.removeOrphan(orphan);
    }
    if (this.orphanMap.size < this.options.maxOrphans)
      return;
    if (!oldest)
      return;
    this.removeOrphan(oldest);
  }
  /**
   * Test whether an invalid block hash has been seen.
   * @private
   * @param {Block} block
   * @returns {Boolean}
   */
  hasInvalid(block) {
    const hash = block.hash();
    if (this.invalid.has(hash))
      return true;
    if (this.invalid.has(block.prevBlock)) {
      this.setInvalid(hash);
      return true;
    }
    return false;
  }
  /**
   * Mark a block as invalid.
   * @private
   * @param {Hash} hash
   */
  setInvalid(hash) {
    this.invalid.set(hash, true);
  }
  /**
   * Forget an invalid block hash.
   * @private
   * @param {Hash} hash
   */
  removeInvalid(hash) {
    this.invalid.remove(hash);
  }
  /**
   * Test the chain to see if it contains
   * a block, or has recently seen a block.
   * @param {Hash} hash
   * @returns {Promise} - Returns Boolean.
   */
  async has(hash) {
    if (this.hasOrphan(hash))
      return true;
    if (this.locker.has(hash))
      return true;
    if (this.invalid.has(hash))
      return true;
    return this.hasEntry(hash);
  }
  /**
   * Find the corresponding block entry by hash or height.
   * @param {Hash|Number} hash
   * @returns {Promise<ChainEntry?>}
   */
  getEntry(hash) {
    return this.db.getEntry(hash);
  }
  /**
   * Retrieve a chain entry by height.
   * @param {Number} height
   * @returns {Promise<ChainEntry?>}
   */
  getEntryByHeight(height) {
    return this.db.getEntryByHeight(height);
  }
  /**
   * Retrieve a chain entry by hash.
   * @param {Hash} hash
   * @returns {Promise<ChainEntry?>}
   */
  getEntryByHash(hash) {
    return this.db.getEntryByHash(hash);
  }
  /**
   * Get the hash of a block by height. Note that this
   * will only return hashes in the main chain.
   * @param {Number} height
   * @returns {Promise<Hash>}
   */
  getHash(height) {
    return this.db.getHash(height);
  }
  /**
   * Get the height of a block by hash.
   * @param {Hash} hash
   * @returns {Promise<Number>}
   */
  getHeight(hash) {
    return this.db.getHeight(hash);
  }
  /**
   * Test the chain to see if it contains a block.
   * @param {Hash} hash
   * @returns {Promise<Boolean>}
   */
  hasEntry(hash) {
    return this.db.hasEntry(hash);
  }
  /**
   * Get the _next_ block hash (does not work by height).
   * @param {Hash} hash
   * @returns {Promise<Hash>}
   */
  getNextHash(hash) {
    return this.db.getNextHash(hash);
  }
  /**
   * Check whether coins are still unspent.
   * @param {TX} tx
   * @returns {Promise<Boolean>}
   */
  hasCoins(tx) {
    return this.db.hasCoins(tx);
  }
  /**
   * Get all tip hashes.
   * @returns {Promise<Hash[]>}
   */
  getTips() {
    return this.db.getTips();
  }
  /**
   * Get range of hashes.
   * @param {Number} [start=-1]
   * @param {Number} [end=-1]
   * @returns {Promise<Hash[]>}
   */
  getHashes(start = -1, end = -1) {
    return this.db.getHashes(start, end);
  }
  /**
   * Get range of entries.
   * @param {Number} [start=-1]
   * @param {Number} [end=-1]
   * @returns {Promise<ChainEntry[]>}
   */
  getEntries(start = -1, end = -1) {
    return this.db.getEntries(start, end);
  }
  /**
   * Get a coin (unspents only).
   * @param {Outpoint} prevout
   * @returns {Promise<CoinEntry?>}
   */
  readCoin(prevout) {
    return this.db.readCoin(prevout);
  }
  /**
   * Get a coin (unspents only).
   * @param {Hash} hash
   * @param {Number} index
   * @returns {Promise<Coin?>}
   */
  getCoin(hash, index) {
    return this.db.getCoin(hash, index);
  }
  /**
   * Retrieve a block from the database (not filled with coins).
   * @param {Hash} hash
   * @returns {Promise<Block?>}
   */
  getBlock(hash) {
    return this.db.getBlock(hash);
  }
  /**
   * Retrieve a block from the database (not filled with coins).
   * @param {Hash|Number} hashHeight
   * @returns {Promise<Buffer|null>}
   */
  getRawBlock(hashHeight) {
    return this.db.getRawBlock(hashHeight);
  }
  /**
   * Get a historical block coin viewpoint.
   * @param {Block} block
   * @returns {Promise<CoinView>}
   */
  getBlockView(block) {
    return this.db.getBlockView(block);
  }
  /**
   * Get a transaction with metadata.
   * @param {Hash} hash
   * @returns {Promise<TXMeta>}
   */
  getMeta(hash) {
    return this.db.getMeta(hash);
  }
  /**
   * Retrieve a transaction.
   * @param {Hash} hash
   * @returns {Promise<TX>}
   */
  getTX(hash) {
    return this.db.getTX(hash);
  }
  /**
   * @param {Hash} hash
   * @returns {Promise<Boolean>}
   */
  hasTX(hash) {
    return this.db.hasTX(hash);
  }
  /**
   * Get all coins pertinent to an address.
   * @param {Address[]} addrs
   * @returns {Promise<Coin[]>}
   */
  getCoinsByAddress(addrs) {
    return this.db.getCoinsByAddress(addrs);
  }
  /**
   * Get all transaction hashes to an address.
   * @param {Address[]} addrs
   * @returns {Promise<Hash[]>}
   */
  getHashesByAddress(addrs) {
    return this.db.getHashesByAddress(addrs);
  }
  /**
   * Get all transactions pertinent to an address.
   * @param {Address[]} addrs
   * @returns {Promise<TX[]>}
   */
  getTXByAddress(addrs) {
    return this.db.getTXByAddress(addrs);
  }
  /**
   * Get all transactions pertinent to an address.
   * @param {Address[]} addrs
   * @returns {Promise<TXMeta[]>}
   */
  getMetaByAddress(addrs) {
    return this.db.getMetaByAddress(addrs);
  }
  /**
   * Get an orphan block.
   * @param {Hash} hash
   * @returns {Block?}
   */
  getOrphan(hash) {
    return this.orphanMap.get(hash) || null;
  }
  /**
   * Test the chain to see if it contains an orphan.
   * @param {Hash} hash
   * @returns {Boolean}
   */
  hasOrphan(hash) {
    return this.orphanMap.has(hash);
  }
  /**
   * Test the chain to see if it contains a pending block in its queue.
   * @param {Hash} hash
   * @returns {Boolean} - Returns Boolean.
   */
  hasPending(hash) {
    return this.locker.pending(hash);
  }
  /**
   * Get coin viewpoint.
   * @param {TX} tx
   * @returns {Promise<CoinView>}
   */
  getCoinView(tx) {
    return this.db.getCoinView(tx);
  }
  /**
   * Get coin viewpoint (spent).
   * @param {TX} tx
   * @returns {Promise<CoinView>}
   */
  async getSpentView(tx) {
    const unlock = await this.locker.lock();
    try {
      return await this.db.getSpentView(tx);
    } finally {
      unlock();
    }
  }
  /**
   * Test the chain to see if it is synced.
   * @returns {Boolean}
   */
  isFull() {
    return this.synced;
  }
  /**
   * Potentially emit a `full` event.
   * @private
   */
  maybeSync() {
    if (this.synced)
      return;
    if (this.options.checkpoints) {
      if (this.height < this.network.lastCheckpoint)
        return;
    }
    if (!this.hasChainwork())
      return;
    if (this.tip.time < this.network.now() - this.network.block.maxTipAge)
      return;
    this.synced = true;
    this.emit('full');
  }
  /**
   * Test the chain to see if it has the
   * minimum required chainwork for the
   * network.
   * @returns {Boolean}
   */
  hasChainwork() {
    return this.tip.chainwork.gte(this.network.pow.chainwork);
  }
  /**
   * Get the fill percentage.
   * @returns {Number} percent - Ranges from 0.0 to 1.0.
   */
  getProgress() {
    const start = this.network.genesis.time;
    const current = this.tip.time - start;
    const end = this.network.now() - start - 40 * 60;
    return Math.min(1, current / end);
  }
  /**
   * Calculate chain locator (an array of hashes).
   * @param {Hash?} start - Height or hash to treat as the tip.
   * The current tip will be used if not present. Note that this can be a
   * non-existent hash, which is useful for headers-first locators.
   * @returns {Promise<Hash[]>}
   */
  async getLocator(start) {
    const unlock = await this.locker.lock();
    try {
      return await this._getLocator(start);
    } finally {
      unlock();
    }
  }
  /**
   * Calculate chain locator without a lock.
   * @private
   * @param {Hash?} start
   * @returns {Promise<Hash[]>}
   */
  async _getLocator(start) {
    if (start == null)
      start = this.tip.hash;
    assert(Buffer.isBuffer(start));
    let entry = await this.getEntry(start);
    const hashes = [];
    if (!entry) {
      entry = this.tip;
      hashes.push(start);
    }
    let main = await this.isMainChain(entry);
    let hash = entry.hash;
    let height = entry.height;
    let step = 1;
    hashes.push(hash);
    while (height > 0) {
      height -= step;
      if (height < 0)
        height = 0;
      if (hashes.length > 10)
        step *= 2;
      if (main) {
        // If we're on the main chain, we can
        // do a fast lookup of the hash.
        hash = await this.getHash(height);
        assert(hash);
      } else {
        const ancestor = await this.getAncestor(entry, height);
        assert(ancestor);
        main = await this.isMainChain(ancestor);
        hash = ancestor.hash;
      }
      hashes.push(hash);
    }
    return hashes;
  }
  /**
   * Calculate the orphan root of the hash (if it is an orphan).
   * @param {Hash} hash
   * @returns {Hash}
   */
  getOrphanRoot(hash) {
    let root = null;
    assert(hash);
    for (;;) {
      const orphan = this.orphanMap.get(hash);
      if (!orphan)
        break;
      root = hash;
      hash = orphan.block.prevBlock;
    }
    return root;
  }
  /**
   * Calculate the time difference (in seconds)
   * between two blocks by examining chainworks.
   * @param {ChainEntry} to
   * @param {ChainEntry} from
   * @returns {Number}
   */
  getProofTime(to, from) {
    const pow = this.network.pow;
    let sign, work;
    if (to.chainwork.gt(from.chainwork)) {
      work = to.chainwork.sub(from.chainwork);
      sign = 1;
    } else {
      work = from.chainwork.sub(to.chainwork);
      sign = -1;
    }
    work = work.imuln(pow.targetSpacing);
    work = work.div(this.tip.getProof());
    if (work.bitLength() > 53)
      return sign * Number.MAX_SAFE_INTEGER;
    return sign * work.toNumber();
  }
  /**
   * Calculate the next target based on the chain tip.
   * @returns {Promise} - returns Number
   * (target is in compact/mantissa form).
   */
  async getCurrentTarget() {
    return this.getTarget(this.network.now(), this.tip);
  }
  /**
   * Get median block by timestamp.
   * @param {ChainEntry} prev
   * @returns {Promise<ChainEntry>}
   */
  async getSuitableBlock(prev) {
    assert(prev);
    let z = prev;
    let y = await this.getPrevious(z);
    let x = await this.getPrevious(y);
    assert(x);
    if (x.time > z.time)
      [x, z] = [z, x];
    if (x.time > y.time)
      [x, y] = [y, x];
    if (y.time > z.time)
      [y, z] = [z, y];
    return y;
  }
  /**
   * Calculate the next target.
   * @param {Number} time - Next block timestamp.
   * @param {ChainEntry} prev - Previous entry.
   * @returns {Promise<Number>} - returns Number
   * (target is in compact/mantissa form).
   */
  async getTarget(time, prev) {
    const pow = this.network.pow;
    // Genesis
    if (!prev) {
      assert(time === this.network.genesis.time);
      return pow.bits;
    }
    // Do not retarget
    if (pow.noRetargeting)
      return pow.bits;
    // Special behavior for testnet:
    if (pow.targetReset) {
      if (time > prev.time + pow.targetSpacing * 2)
        return pow.bits;
    }
    assert(pow.blocksPerDay === 144);
    assert(pow.targetWindow === 144);
    if (prev.height < pow.blocksPerDay + 2) {
      assert(prev.bits === pow.bits);
      return pow.bits;
    }
    const last = await this.getSuitableBlock(prev);
    const height = prev.height - pow.blocksPerDay;
    assert(height >= 0);
    const ancestor = await this.getAncestor(prev, height);
    const first = await this.getSuitableBlock(ancestor);
    return this.retarget(first, last);
  }
  /**
   * Calculate the next target.
   * @param {ChainEntry} first - Suitable block from 1 day prior.
   * @param {ChainEntry} last - Last suitable block.
   * @returns {Number} target - Target in compact/mantissa form.
   */
  retarget(first, last) {
    assert(last.height > first.height);
    const pow = this.network.pow;
    const maxChainwork = ChainEntry.MAX_CHAINWORK;
    const minActual = pow.blocksPerDay / 4;
    const maxActual = pow.blocksPerDay * 4;
    assert(minActual === 36); // 72 on BCH
    assert(maxActual === 576); // 288 on BCH
    assert(minActual * pow.targetSpacing === pow.minActual);
    assert(maxActual * pow.targetSpacing === pow.maxActual);
    const work = last.chainwork.sub(first.chainwork);
    work.imuln(pow.targetSpacing);
    let actualTimespan = last.time - first.time;
    if (actualTimespan < minActual * pow.targetSpacing)
      actualTimespan = minActual * pow.targetSpacing;
    if (actualTimespan > maxActual * pow.targetSpacing)
      actualTimespan = maxActual * pow.targetSpacing;
    work.idivn(actualTimespan);
    if (work.isZero())
      return pow.bits;
    const target = maxChainwork.div(work).isubn(1);
    if (target.gt(pow.limit))
      return pow.bits;
    const cmpct = consensus.toCompact(target);
    this.logger.debug('Retargetting to: %s (0x%s).',
      consensus.fromCompact(cmpct).toString('hex', 64),
      util.hex32(cmpct));
    return cmpct;
  }
  /**
   * Find a locator. Analagous to bitcoind's `FindForkInGlobalIndex()`.
   * @param {Hash[]} locator - Hashes.
   * @returns {Promise<Hash>} (the hash of the latest known block).
   */
  async findLocator(locator) {
    for (const hash of locator) {
      if (await this.isMainHash(hash))
        return hash;
    }
    return this.network.genesis.hash;
  }
  /**
   * Check whether a versionbits deployment is active (BIP9: versionbits).
   * @example
   * await chain.isActive(tip, deployments.segwit);
   * @see https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki
   * @param {ChainEntry} prev - Previous chain entry.
   * @param {Object} deployment - Deployment.
   * @returns {Promise<Boolean>}
   */
  async isActive(prev, deployment) {
    const state = await this.getState(prev, deployment);
    return state === thresholdStates.ACTIVE;
  }
  /**
   * Get chain entry state for a deployment (BIP9: versionbits).
   * @example
   * await chain.getState(tip, deployments.segwit);
   * @see https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki
   * @param {ChainEntry} prev - Previous chain entry.
   * @param {Object} deployment - Deployment.
   * @returns {Promise<Number>}
   */
  async getState(prev, deployment) {
    const bit = deployment.bit;
    let window = this.network.minerWindow;
    let threshold = this.network.activationThreshold;
    if (deployment.threshold !== -1)
      threshold = deployment.threshold;
    if (deployment.window !== -1)
      window = deployment.window;
    if (((prev.height + 1) % window) !== 0) {
      const height = prev.height - ((prev.height + 1) % window);
      prev = await this.getAncestor(prev, height);
      if (!prev)
        return thresholdStates.DEFINED;
      assert(prev.height === height);
      assert(((prev.height + 1) % window) === 0);
    }
    let entry = prev;
    let state = thresholdStates.DEFINED;
    const compute = [];
    while (entry) {
      const cached = this.db.stateCache.get(bit, entry);
      if (cached !== -1) {
        state = cached;
        break;
      }
      const time = await this.getMedianTime(entry);
      if (time < deployment.startTime) {
        state = thresholdStates.DEFINED;
        this.db.stateCache.set(bit, entry, state);
        break;
      }
      compute.push(entry);
      const height = entry.height - window;
      entry = await this.getAncestor(entry, height);
    }
    while (compute.length) {
      const entry = compute.pop();
      switch (state) {
        case thresholdStates.DEFINED: {
          const time = await this.getMedianTime(entry);
          if (time >= deployment.timeout) {
            state = thresholdStates.FAILED;
            break;
          }
          if (time >= deployment.startTime) {
            state = thresholdStates.STARTED;
            break;
          }
          break;
        }
        case thresholdStates.STARTED: {
          const time = await this.getMedianTime(entry);
          if (time >= deployment.timeout) {
            state = thresholdStates.FAILED;
            break;
          }
          let block = entry;
          let count = 0;
          for (let i = 0; i < window; i++) {
            if (block.hasBit(bit))
              count += 1;
            if (count >= threshold) {
              state = thresholdStates.LOCKED_IN;
              break;
            }
            block = await this.getPrevious(block);
            assert(block);
          }
          break;
        }
        case thresholdStates.LOCKED_IN: {
          state = thresholdStates.ACTIVE;
          break;
        }
        case thresholdStates.FAILED:
        case thresholdStates.ACTIVE: {
          break;
        }
        default: {
          assert(false, 'Bad state.');
          break;
        }
      }
      this.db.stateCache.set(bit, entry, state);
    }
    return state;
  }
  /**
   * Get signalling statistics for BIP9/versionbits soft fork
   * @param {ChainEntry} prev - Previous chain entry.
   * @param {Object} deployment - Deployment.
   * @returns {Promise<Object>}
   */
  async getBIP9Stats(prev, deployment) {
    const state = await this.getState(prev, deployment);
    if (state !== thresholdStates.STARTED)
      throw new Error(`Deployment "${deployment.name}" not in STARTED state.`);
    const bit = deployment.bit;
    let window = this.network.minerWindow;
    let threshold = this.network.activationThreshold;
    // Deployments like `segsignal` (BIP91) have custom window & threshold
    if (deployment.window !== -1)
      window = deployment.window;
    if (deployment.threshold !== -1)
      threshold = deployment.threshold;
    let count = 0;
    let block = prev;
    while((block.height + 1) % window !== 0) {
      if (block.hasBit(bit))
        count++;
      block = await this.getPrevious(block);
      if(!block)
        break;
    }
    return {
      period: window,
      threshold: threshold,
      elapsed: (prev.height + 1) % window,
      count: count,
      possible: (window - threshold) >= ((prev.height + 1) % window) - count
    };
  }
  /**
   * Compute the version for a new block (BIP9: versionbits).
   * @see https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki
   * @param {ChainEntry} prev - Previous chain entry (usually the tip).
   * @returns {Promise<Number>}
   */
  async computeBlockVersion(prev) {
    let version = 0;
    for (const deployment of this.network.deploys) {
      const state = await this.getState(prev, deployment);
      if (state === thresholdStates.LOCKED_IN
          || state === thresholdStates.STARTED) {
        version |= 1 << deployment.bit;
      }
    }
    version >>>= 0;
    return version;
  }
  /**
   * Get the current deployment state of the chain. Called on load.
   * @private
   * @returns {Promise<DeploymentState>}
   */
  async getDeploymentState() {
    return this.readDeploymentState(this.tip);
  }
  /**
   * Get deployment state.
   * @private
   * @param {ChainEntry} tip
   * @returns {Promise<DeploymentState>}
   */
  async readDeploymentState(tip) {
    const prev = await this.getPrevious(tip);
    if (!prev) {
      assert(tip.isGenesis());
      return new DeploymentState(this.network.genesis.hash);
    }
    if (this.options.spv)
      return new DeploymentState(this.network.genesis.hash);
    return this.getDeployments(tip.time, prev);
  }
  /**
   * Get the next deployment state of the chain.
   * @returns {Promise<DeploymentState>}
   */
  async getNextState() {
    if (this.options.spv)
      return this.state;
    return this.getDeployments(this.network.now(), this.tip);
  }
  /**
   * Check transaction finality, taking into account MEDIAN_TIME_PAST
   * if it is present in the lock flags.
   * @param {ChainEntry} prev - Previous chain entry.
   * @param {TX} tx
   * @param {LockFlags} flags
   * @returns {Promise} - Returns Boolean.
   */
  async verifyFinal(prev, tx, flags) {
    const height = prev.height + 1;
    // We can skip MTP if the locktime is height.
    if (!(tx.locktime & consensus.LOCKTIME_FLAG))
      return tx.isFinal(height, -1);
    const time = await this.getMedianTime(prev);
    return tx.isFinal(height, time);
  }
  /**
   * Get the necessary minimum time and height sequence locks for a transaction.
   * @param {ChainEntry} prev
   * @param {TX} tx
   * @param {CoinView} view
   * @param {LockFlags} flags
   * @returns {Promise}
   */
  async getLocks(prev, tx, view, flags) {
    const GRANULARITY = consensus.SEQUENCE_GRANULARITY;
    const DISABLE_FLAG = consensus.SEQUENCE_DISABLE_FLAG;
    const TYPE_FLAG = consensus.SEQUENCE_TYPE_FLAG;
    const MASK = consensus.SEQUENCE_MASK;
    if (tx.isCoinbase())
      return [-1, -1];
    let minHeight = -1;
    let minTime = -1;
    for (const {prevout, sequence} of tx.inputs) {
      if (sequence & DISABLE_FLAG)
        continue;
      let height = view.getHeight(prevout);
      if (height === -1)
        height = this.height + 1;
      if (!(sequence & TYPE_FLAG)) {
        height += (sequence & MASK) - 1;
        minHeight = Math.max(minHeight, height);
        continue;
      }
      height = Math.max(height - 1, 0);
      const entry = await this.getAncestor(prev, height);
      assert(entry, 'Database is corrupt.');
      let time = await this.getMedianTime(entry);
      time += ((sequence & MASK) << GRANULARITY) - 1;
      minTime = Math.max(minTime, time);
    }
    return [minHeight, minTime];
  }
  /**
   * Verify sequence locks.
   * @param {ChainEntry} prev
   * @param {TX} tx
   * @param {CoinView} view
   * @param {LockFlags} flags
   * @returns {Promise} - Returns Boolean.
   */
  async verifyLocks(prev, tx, view, flags) {
    const [height, time] = await this.getLocks(prev, tx, view, flags);
    if (height !== -1) {
      if (height >= prev.height + 1)
        return false;
    }
    if (time !== -1) {
      const mtp = await this.getMedianTime(prev);
      if (time >= mtp)
        return false;
    }
    return true;
  }
  /**
   * Get safe tree root.
   * @returns {Promise<Hash>}
   */
  async getSafeRoot() {
    // The tree is committed on an interval.
    // Mainnet is 36 blocks, meaning at height 36,
    // the name set of the past 36 blocks are
    // inserted into the tree. The commitment for
    // that insertion actually appears in a block
    // header one block later (height 37). We
    // want the the root _before_ the current one
    // so we can calculate that with:
    //   chain_height - (chain_height % interval)
    const interval = this.network.names.treeInterval;
    let mod = this.height % interval;
    // If there's enough proof-of-work
    // on top of the most recent root,
    // it should be safe to use it.
    if (mod >= 12)
      mod = 0;
    const height = this.height - mod;
    const entry = await this.getEntryByHeight(height);
    assert(entry);
    return entry.treeRoot;
  }
}
/**
 * ChainOptions
 * @alias module:blockchain.ChainOptions
 */
class ChainOptions {
  /**
   * Create chain options.
   * @constructor
   * @param {Object} options
   */
  constructor(options) {
    this.network = Network.primary;
    this.logger = Logger.global;
    this.blocks = null;
    this.workers = null;
    this.prefix = null;
    this.location = null;
    this.treeLocation = null;
    this.memory = true;
    this.maxFiles = 64;
    this.cacheSize = 32 << 20;
    this.compression = true;
    this.spv = false;
    this.prune = false;
    this.indexTX = false;
    this.indexAddress = false;
    this.entryCache = 5000;
    this.maxOrphans = 20;
    this.checkpoints = true;
    this.chainMigrate = -1;
    this.compactTreeOnInit = false;
    this.compactTreeInitInterval = 10000;
    if (options)
      this.fromOptions(options);
  }
  /**
   * Inject properties from object.
   * @private
   * @param {Object} options
   * @returns {ChainOptions}
   */
  fromOptions(options) {
    if (!options.spv) {
      assert(typeof options.blocks === 'object', 'Chain requires BlockStore.');
      this.blocks = options.blocks;
    }
    if (options.network != null)
      this.network = Network.get(options.network);
    if (options.logger != null) {
      assert(typeof options.logger === 'object');
      this.logger = options.logger;
    }
    if (options.workers != null) {
      assert(typeof options.workers === 'object');
      this.workers = options.workers;
    }
    if (options.spv != null) {
      assert(typeof options.spv === 'boolean');
      this.spv = options.spv;
    }
    if (options.prefix != null) {
      assert(typeof options.prefix === 'string');
      this.prefix = options.prefix;
      this.location = this.spv
        ? path.join(this.prefix, 'spvchain')
        : path.join(this.prefix, 'chain');
      this.treePrefix = path.join(this.prefix, 'tree');
    }
    if (options.location != null) {
      assert(typeof options.location === 'string');
      this.location = options.location;
    }
    if (options.treePrefix != null) {
      assert(typeof options.treePrefix === 'string');
      this.treePrefix = options.treePrefix;
    }
    if (options.memory != null) {
      assert(typeof options.memory === 'boolean');
      this.memory = options.memory;
    }
    if (options.maxFiles != null) {
      assert((options.maxFiles >>> 0) === options.maxFiles);
      this.maxFiles = options.maxFiles;
    }
    if (options.cacheSize != null) {
      assert(Number.isSafeInteger(options.cacheSize));
      assert(options.cacheSize >= 0);
      this.cacheSize = options.cacheSize;
    }
    if (options.compression != null) {
      assert(typeof options.compression === 'boolean');
      this.compression = options.compression;
    }
    if (options.prune != null) {
      assert(typeof options.prune === 'boolean');
      assert(!options.prune || !options.spv, 'Can not prune in spv mode.');
      this.prune = options.prune;
    }
    if (options.indexTX != null) {
      assert(typeof options.indexTX === 'boolean');
      this.indexTX = options.indexTX;
    }
    if (options.indexAddress != null) {
      assert(typeof options.indexAddress === 'boolean');
      this.indexAddress = options.indexAddress;
    }
    if (options.entryCache != null) {
      assert((options.entryCache >>> 0) === options.entryCache);
      this.entryCache = options.entryCache;
    }
    if (options.maxOrphans != null) {
      assert((options.maxOrphans >>> 0) === options.maxOrphans);
      this.maxOrphans = options.maxOrphans;
    }
    if (options.checkpoints != null) {
      assert(typeof options.checkpoints === 'boolean');
      this.checkpoints = options.checkpoints;
    }
    if (options.chainMigrate != null) {
      assert(typeof options.chainMigrate === 'number');
      this.chainMigrate = options.chainMigrate;
    }
    if (options.compactTreeOnInit != null) {
      assert(typeof options.compactTreeOnInit === 'boolean');
      this.compactTreeOnInit = options.compactTreeOnInit;
    }
    if (options.compactTreeInitInterval != null) {
      const {keepBlocks} = this.network.block;
      assert(typeof options.compactTreeInitInterval === 'number');
      assert(options.compactTreeInitInterval >= keepBlocks,
        `compaction interval must not be smaller than ${keepBlocks}.`);
      this.compactTreeInitInterval = options.compactTreeInitInterval;
    }
    if (this.spv || this.memory)
      this.treePrefix = null;
    return this;
  }
  /**
   * Instantiate chain options from object.
   * @param {Object} options
   * @returns {ChainOptions}
   */
  static fromOptions(options) {
    return new ChainOptions().fromOptions(options);
  }
}
/**
 * Deployment State
 * @alias module:blockchain.DeploymentState
 * @property {VerifyFlags} flags
 * @property {LockFlags} lockFlags
 */
class DeploymentState {
  /**
   * Create a deployment state.
   * @param {Hash} tip
   * @constructor
   */
  constructor(tip) {
    this.tip = tip;
    this.flags = Script.flags.MANDATORY_VERIFY_FLAGS;
    this.lockFlags = common.MANDATORY_LOCKTIME_FLAGS;
    this.nameFlags = rules.MANDATORY_VERIFY_COVENANT_FLAGS;
    this.hasAirstop = false;
  }
  hasHardening() {
    return (this.nameFlags & VERIFY_COVENANTS_HARDENED) !== 0;
  }
  hasICANNLockup() {
    return (this.nameFlags & VERIFY_COVENANTS_LOCKUP) !== 0;
  }
}
/**
 * Orphan
 * @ignore
 */
class Orphan {
  /**
   * Create an orphan.
   * @constructor
   */
  constructor(block, flags, id) {
    this.block = block;
    this.flags = flags;
    this.id = id;
    this.time = util.now();
  }
}
/*
 * Helpers
 */
function cmp(a, b) {
  return a - b;
}
/*
 * Expose
 */
Chain.ChainOptions = ChainOptions;
module.exports = Chain;