/*!
 * chain.js - blockchain management for bcoin
 * Copyright (c) 2014-2015, Fedor Indutny (MIT License)
 * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

var AsyncObject = require('../utils/async');
var Network = require('../protocol/network');
var Logger = require('../node/logger');
var ChainDB = require('./chaindb');
var constants = require('../protocol/constants');
var utils = require('../utils/utils');
var Locker = require('../utils/locker');
var ChainEntry = require('./chainentry');
var assert = require('assert');
var VerifyError = require('../utils/errors').VerifyError;
var VerifyResult = utils.VerifyResult;
var time = require('../net/timedata');
var co = require('../utils/co');

/**
 * Represents a blockchain.
 * @exports Chain
 * @constructor
 * @param {Object} options
 * @param {String?} options.name - Database name.
 * @param {String?} options.location - Database file location.
 * @param {String?} options.db - Database backend (`"leveldb"` by default).
 * @param {Number?} options.orphanLimit
 * @param {Number?} options.pendingLimit
 * @param {Boolean?} options.spv
 * @property {Boolean} loaded
 * @property {ChainDB} db - Note that Chain `options` will be passed
 * to the instantiated ChainDB.
 * @property {Number} total
 * @property {Number} orphanLimit
 * @property {Locker} locker
 * @property {Object} invalid
 * @property {Number} bestHeight
 * @property {ChainEntry?} tip
 * @property {Number} height
 * @property {DeploymentState} state
 * @property {Object} orphan - Orphan map.
 * @emits Chain#open
 * @emits Chain#error
 * @emits Chain#block
 * @emits Chain#competitor
 * @emits Chain#resolved
 * @emits Chain#checkpoint
 * @emits Chain#fork
 * @emits Chain#reorganize
 * @emits Chain#invalid
 * @emits Chain#exists
 * @emits Chain#purge
 * @emits Chain#connect
 * @emits Chain#reconnect
 * @emits Chain#disconnect
 */

function Chain(options) {
  if (!(this instanceof Chain))
    return new Chain(options);

  AsyncObject.call(this);

  if (!options)
    options = {};

  this.options = options;

  this.network = Network.get(options.network);
  this.logger = options.logger || Logger.global;
  this.db = new ChainDB(this);
  this.total = 0;
  this.currentBlock = null;
  this.orphanLimit = options.orphanLimit || (20 << 20);
  this.locker = new Locker(true);
  this.invalid = {};
  this.bestHeight = -1;
  this.tip = null;
  this.height = -1;
  this.synced = false;
  this.state = new DeploymentState();
  this.stateCache = {};
  this._time = utils.hrtime();

  this.orphan = {
    map: {},
    bmap: {},
    count: 0,
    size: 0
  };

  this._init();
}

utils.inherits(Chain, AsyncObject);

/**
 * Initialize the chain.
 * @private
 */

Chain.prototype._init = function _init() {
  var self = this;
  var keys = Object.keys(this.network.deployments);
  var i, id;

  // Setup state caches.
  for (i = 0; i < keys.length; i++) {
    id = keys[i];
    this.stateCache[id] = {};
  }

  this.locker.on('purge', function(total, size) {
    self.logger.warning('Warning: %dmb of pending objects. Purging.', utils.mb(size));
  });

  this.on('competitor', function(block, entry) {
    self.logger.warning('Heads up: Competing chain at height %d:'
      + ' tip-height=%d competitor-height=%d'
      + ' tip-hash=%s competitor-hash=%s'
      + ' tip-chainwork=%s competitor-chainwork=%s'
      + ' chainwork-diff=%s',
      entry.height,
      self.tip.height,
      entry.height,
      self.tip.rhash,
      entry.rhash,
      self.tip.chainwork.toString(),
      entry.chainwork.toString(),
      self.tip.chainwork.sub(entry.chainwork).toString());
  });

  this.on('resolved', function(block, entry) {
    self.logger.debug('Orphan %s (%d) was resolved.',
      block.rhash, entry.height);
  });

  this.on('checkpoint', function(block, height) {
    self.logger.debug('Hit checkpoint block %s (%d).',
      block.rhash, height);
  });

  this.on('fork', function(block, height, expected) {
    self.logger.warning(
      'Fork at height %d: expected=%s received=%s',
      height,
      utils.revHex(expected),
      block.rhash
    );
  });

  this.on('reorganize', function(block, height, expected) {
    self.logger.warning(
      'Reorg at height %d: old=%s new=%s',
      height,
      utils.revHex(expected),
      block.rhash
    );
  });

  this.on('invalid', function(block, height) {
    self.logger.warning('Invalid block at height %d: hash=%s',
      height, block.rhash);
  });

  this.on('exists', function(block, height) {
    self.logger.debug('Already have block %s (%d).', block.rhash, height);
  });

  this.on('orphan', function(block, height) {
    self.logger.debug('Handled orphan %s (%d).', block.rhash, height);
  });

  this.on('purge', function(count, size) {
    self.logger.debug('Warning: %d (%dmb) orphans cleared!',
      count, utils.mb(size));
  });
};

/**
 * Open the chain, wait for the database to load.
 * @alias Chain#open
 * @returns {Promise}
 */

Chain.prototype._open = co(function* open() {
  var tip;

  this.logger.info('Chain is loading.');

  if (this.options.useCheckpoints)
    this.logger.info('Checkpoints are enabled.');

  if (this.options.coinCache)
    this.logger.info('Coin cache is enabled.');

  yield this.db.open();

  tip = yield this.db.getTip();

  assert(tip);

  this.tip = tip;
  this.height = tip.height;

  this.logger.info('Chain Height: %d', tip.height);

  if (tip.height > this.bestHeight) {
    this.bestHeight = tip.height;
    this.network.updateHeight(tip.height);
  }

  this.logger.memory();

  this.state = yield this.getDeploymentState();

  this.logger.memory();

  this.emit('tip', tip);

  if (!this.synced && this.isFull()) {
    this.synced = true;
    this.emit('full');
  }
});

/**
 * Close the chain, wait for the database to close.
 * @alias Chain#close
 * @returns {Promise}
 */

Chain.prototype._close = function close() {
  return this.db.close();
};

/**
 * Perform all necessary contextual verification on a block.
 * @private
 * @param {Block|MerkleBlock} block
 * @param {ChainEntry} entry
 * @returns {Promise}
 */

Chain.prototype.verifyContext = co(function* verifyContext(block, prev) {
  var state, view;

  state = yield this.verify(block, prev);

  yield this.checkDuplicates(block, prev);

  view = yield this.checkInputs(block, prev, state);

  // Expose the state globally.
  this.state = state;

  return view;
});

/**
 * Test whether a block is the genesis block.
 * @param {Block} block
 * @returns {Boolean}
 */

Chain.prototype.isGenesis = function isGenesis(block) {
  return block.hash('hex') === this.network.genesis.hash;
};

/**
 * Contextual verification for a block, including
 * version deployments (IsSuperMajority), versionbits,
 * coinbase height, finality checks.
 * @private
 * @param {Block|MerkleBlock} block
 * @param {ChainEntry} entry
 * @returns {Promise}
 * [{@link VerifyError}, {@link VerifyFlags}].
 */

Chain.prototype.verify = co(function* verify(block, prev) {
  var ret = new VerifyResult();
  var i, height, ts, tx, medianTime, commitmentHash, ancestors, state;

  if (!block.verify(ret)) {
    throw new VerifyError(block,
      'invalid',
      ret.reason,
      ret.score);
  }

  // Skip the genesis block. Skip all blocks in spv mode.
  if (this.options.spv || this.isGenesis(block))
    return this.state;

  // Ensure it's not an orphan
  if (!prev) {
    throw new VerifyError(block,
      'invalid',
      'bad-prevblk',
      0);
  }

  if (prev.isHistorical())
    return this.state;

  ancestors = yield prev.getRetargetAncestors();

  height = prev.height + 1;
  medianTime = prev.getMedianTime(ancestors);

  // Ensure the timestamp is correct
  if (block.ts <= medianTime) {
    throw new VerifyError(block,
      'invalid',
      'time-too-old',
      0);
  }

  if (block.bits !== this.getTarget(block, prev, ancestors)) {
    throw new VerifyError(block,
      'invalid',
      'bad-diffbits',
      100);
  }

  state = yield this.getDeployments(block, prev, ancestors);

  // Can't verify any further when merkleblock or headers.
  if (this.options.spv)
    return state;

  // Make sure the height contained in the coinbase is correct.
  if (state.hasBIP34()) {
    if (block.getCoinbaseHeight() !== height) {
      throw new VerifyError(block,
        'invalid',
        'bad-cb-height',
        100);
    }
  }

  // Check the commitment hash for segwit.
  if (state.hasWitness()) {
    commitmentHash = block.commitmentHash;
    if (commitmentHash) {
      if (!block.witnessNonce) {
        throw new VerifyError(block,
          'invalid',
          'bad-witness-merkle-size',
          100);
      }
      if (commitmentHash !== block.getCommitmentHash('hex')) {
        throw new VerifyError(block,
          'invalid',
          'bad-witness-merkle-match',
          100);
      }
    }
  }

  // Blocks that do not commit to
  // witness data cannot contain it.
  if (!commitmentHash) {
    if (block.hasWitness()) {
      throw new VerifyError(block,
        'invalid',
        'unexpected-witness',
        100);
    }
  }

  // Check block weight (different from block size
  // check in non-contextual verification).
  if (block.getWeight() > constants.block.MAX_WEIGHT) {
    throw new VerifyError(block,
      'invalid',
      'bad-blk-weight',
      100);
  }

  // Get timestamp for tx.isFinal().
  ts = state.hasMTP() ? medianTime : block.ts;

  // Check all transactions
  for (i = 0; i < block.txs.length; i++) {
    tx = block.txs[i];

    // Transactions must be finalized with
    // regards to nSequence and nLockTime.
    if (!tx.isFinal(height, ts)) {
      throw new VerifyError(block,
        'invalid',
        'bad-txns-nonfinal',
        10);
    }
  }

  return state;
});

/**
 * Check all deployments on a chain, ranging from p2sh to segwit.
 * @param {Block} block
 * @param {ChainEntry} prev
 * @param {ChainEntry[]} ancestors
 * @returns {Promise}
 * [{@link VerifyError}, {@link DeploymentState}].
 */

Chain.prototype.getDeployments = co(function* getDeployments(block, prev, ancestors) {
  var state = new DeploymentState();
  var active;

  // For some reason bitcoind has p2sh in the
  // mandatory flags by default, when in reality
  // it wasn't activated until march 30th 2012.
  // The first p2sh output and redeem script
  // appeared on march 7th 2012, only it did
  // not have a signature. See:
  // 6a26d2ecb67f27d1fa5524763b49029d7106e91e3cc05743073461a719776192
  // 9c08a4d78931342b37fd5f72900fb9983087e6f46c4a097d8a1f52c74e28eaf6
  if (block.ts >= constants.block.BIP16_TIME) {
    state.flags |= constants.flags.VERIFY_P2SH;
    if (!this.state.hasP2SH())
      this.logger.warning('P2SH has been activated.');
  }

  // Only allow version 2 blocks (coinbase height)
  // once the majority of blocks are using it.
  if (block.version < 2 && prev.isOutdated(2, ancestors))
    throw new VerifyError(block, 'obsolete', 'bad-version', 0);

  // Only allow version 3 blocks (sig validation)
  // once the majority of blocks are using it.
  if (block.version < 3 && prev.isOutdated(3, ancestors))
    throw new VerifyError(block, 'obsolete', 'bad-version', 0);

  // Only allow version 4 blocks (checklocktimeverify)
  // once the majority of blocks are using it.
  if (block.version < 4 && prev.isOutdated(4, ancestors))
    throw new VerifyError(block, 'obsolete', 'bad-version', 0);

  // Only allow version 5 blocks (bip141 - segnet3)
  // once the majority of blocks are using it.
  if (this.options.witness && this.network.oldWitness) {
    if (block.version < 5 && prev.isOutdated(5, ancestors))
      throw new VerifyError(block, 'obsolete', 'bad-version', 0);
  }

  // Make sure the height contained in the coinbase is correct.
  if (block.version >= 2 && prev.isUpgraded(2, ancestors)) {
    state.bip34 = true;
    if (!this.state.hasBIP34())
      this.logger.warning('BIP34 has been activated.');
  }

  // Signature validation is now enforced (bip66)
  if (block.version >= 3 && prev.isUpgraded(3, ancestors)) {
    state.flags |= constants.flags.VERIFY_DERSIG;
    if (!this.state.hasBIP66())
      this.logger.warning('BIP66 has been activated.');
  }

  // CHECKLOCKTIMEVERIFY is now usable (bip65)
  if (block.version >= 4 && prev.isUpgraded(4, ancestors)) {
    state.flags |= constants.flags.VERIFY_CHECKLOCKTIMEVERIFY;
    if (!this.state.hasCLTV())
      this.logger.warning('BIP65 has been activated.');
  }

  // Segregrated witness is now usable (bip141 - segnet3)
  if (this.options.witness && this.network.oldWitness) {
    if (block.version >= 5 && prev.isUpgraded(5, ancestors)) {
      state.flags |= constants.flags.VERIFY_WITNESS;
      if (!this.state.hasWitness())
        this.logger.warning('Segwit has been activated.');
    }
  }

  // CHECKSEQUENCEVERIFY and median time
  // past locktimes are now usable (bip9 & bip113).
  active = yield this.isActive(prev, 'csv');
  if (active) {
    state.flags |= constants.flags.VERIFY_CHECKSEQUENCEVERIFY;
    state.lockFlags |= constants.flags.VERIFY_SEQUENCE;
    state.lockFlags |= constants.flags.MEDIAN_TIME_PAST;
    if (!this.state.hasCSV())
      this.logger.warning('CSV has been activated.');
  }

  // Segregrated witness is now usable (bip141 - segnet4)
  if (!this.network.oldWitness) {
    active = yield this.isActive(prev, 'witness');
    if (active) {
      // BIP147
      // state.flags |= constants.flags.VERIFY_NULLDUMMY;
      if (this.options.witness) {
        state.flags |= constants.flags.VERIFY_WITNESS;
        if (!this.state.hasWitness())
          this.logger.warning('Segwit has been activated.');
      }
    }
  }

  return state;
});

/**
 * Determine whether to check block for duplicate txids in blockchain
 * history (BIP30). If we're on a chain that has bip34 activated, we
 * can skip this.
 * @private
 * @see https://github.com/bitcoin/bips/blob/master/bip-0030.mediawiki
 * @param {Block|MerkleBlock} block
 * @param {ChainEntry} prev
 * @returns {Promise}
 */

Chain.prototype.checkDuplicates = co(function* checkDuplicates(block, prev) {
  var height = prev.height + 1;
  var entry;

  if (this.options.spv)
    return;

  if (this.isGenesis(block))
    return;

  if (prev.isHistorical())
    return;

  if (this.network.block.bip34height === -1
      || height <= this.network.block.bip34height) {
    yield this.findDuplicates(block, prev);
    return;
  }

  // It was no longer possible to create duplicate
  // TXs once bip34 went into effect. We can check
  // for this to avoid a DB lookup.
  entry = yield this.db.get(this.network.block.bip34height);

  if (entry && entry.hash === this.network.block.bip34hash)
    return;

  yield this.findDuplicates(block, prev);
});

/**
 * Check block for duplicate txids in blockchain
 * history (BIP30). Note that txids are only considered
 * duplicate if they are not yet completely spent.
 * @private
 * @see https://github.com/bitcoin/bips/blob/master/bip-0030.mediawiki
 * @param {Block|MerkleBlock} block
 * @param {ChainEntry} prev
 * @returns {Promise}
 */

Chain.prototype.findDuplicates = co(function* findDuplicates(block, prev) {
  var height = prev.height + 1;
  var i, tx, result;

  // Check all transactions
  for (i = 0; i < block.txs.length; i++) {
    tx = block.txs[i];
    result = yield this.db.hasCoins(tx.hash());

    if (result) {
      // Blocks 91842 and 91880 created duplicate
      // txids by using the same exact output script
      // and extraNonce.
      if (constants.bip30[height]) {
        if (block.hash('hex') === constants.bip30[height])
          continue;
      }
      throw new VerifyError(block, 'invalid', 'bad-txns-BIP30', 100);
    }
  }
});

/**
 * 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
 * useCheckpoints is enabled, it will skip verification
 * for historical data.
 * @private
 * @see TX#checkInputs
 * @param {Block} block
 * @param {ChainEntry} prev
 * @param {DeploymentState} state
 * @returns {Promise}
 */

Chain.prototype.checkInputs = co(function* checkInputs(block, prev, state) {
  var height = prev.height + 1;
  var historical = prev.isHistorical();
  var sigops = 0;
  var jobs = [];
  var ret = new VerifyResult();
  var i, view, tx, valid;

  if (this.options.spv)
    return;

  if (this.isGenesis(block))
    return;

  view = yield this.db.getCoinView(block);

  // Check all transactions
  for (i = 0; i < block.txs.length; i++) {
    tx = block.txs[i];

    // Ensure tx is not double spending an output.
    if (!tx.isCoinbase()) {
      if (!view.fillCoins(tx)) {
        assert(!historical, 'BUG: Spent inputs in historical data!');
        throw new VerifyError(block,
          'invalid',
          'bad-txns-inputs-missingorspent',
          100);
      }
    }

    // Skip everything if we're
    // using checkpoints.
    if (historical) {
      view.addTX(tx);
      continue;
    }

    // Verify sequence locks.
    valid = yield this.checkLocks(prev, tx, state.lockFlags);

    if (!valid) {
      throw new VerifyError(block,
        'invalid',
        'bad-txns-nonfinal',
        100);
    }

    // Count sigops (legacy + scripthash? + witness?)
    sigops += tx.getSigopsWeight(state.flags);

    if (sigops > constants.block.MAX_SIGOPS_WEIGHT) {
      throw new VerifyError(block,
        'invalid',
        'bad-blk-sigops',
        100);
    }

    // Contextual sanity checks.
    if (!tx.isCoinbase()) {
      if (!tx.checkInputs(height, ret)) {
        throw new VerifyError(block,
          'invalid',
          ret.reason,
          ret.score);
      }

      // Push onto verification queue.
      jobs.push(tx.verifyAsync(state.flags));
    }

    // Add new coins.
    view.addTX(tx);
  }

  if (historical)
    return view;

  // Verify all txs in parallel.
  valid = yield co.every(jobs);

  if (!valid) {
    throw new VerifyError(block,
      'invalid',
      'mandatory-script-verify-flag-failed',
      100);
  }

  // Make sure the miner isn't trying to conjure more coins.
  if (block.getClaimed() > block.getReward(this.network)) {
    throw new VerifyError(block,
      'invalid',
      'bad-cb-amount',
      100);
  }

  return view;
});

/**
 * Get the cached height for a hash if present.
 * @private
 * @param {Hash} hash
 * @returns {Number}
 */

Chain.prototype._getCachedHeight = function _getCachedHeight(hash) {
  if (this.db.hasCache(hash))
    return this.db.getCache(hash).height;

  return -1;
};

/**
 * Find the block at which a fork ocurred.
 * @private
 * @param {ChainEntry} fork - The current chain.
 * @param {ChainEntry} longer - The competing chain.
 * @returns {Promise}
 */

Chain.prototype.findFork = co(function* findFork(fork, longer) {
  while (fork.hash !== longer.hash) {
    while (longer.height > fork.height) {
      longer = yield longer.getPrevious();
      if (!longer)
        throw new Error('No previous entry for new tip.');
    }

    if (fork.hash === longer.hash)
      return fork;

    fork = yield fork.getPrevious();

    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} entry - The competing chain's tip.
 * @param {Block|MerkleBlock} block - The being being added.
 * @returns {Promise}
 */

Chain.prototype.reorganize = co(function* reorganize(entry, block) {
  var tip = this.tip;
  var fork = yield this.findFork(tip, entry);
  var disconnect = [];
  var connect = [];
  var i, e;

  assert(fork);

  // Disconnect blocks/txs.
  e = tip;
  while (e.hash !== fork.hash) {
    disconnect.push(e);
    e = yield e.getPrevious();
    assert(e);
  }

  for (i = 0; i < disconnect.length; i++) {
    e = disconnect[i];
    yield this.disconnect(e);
  }

  // Disconnect blocks/txs.
  e = entry;
  while (e.hash !== fork.hash) {
    connect.push(e);
    e = yield e.getPrevious();
    assert(e);
  }

  // We don't want to connect the new tip here.
  // That will be done outside in setBestChain.
  for (i = connect.length - 1; i >= 1; i--) {
    e = connect[i];
    yield this.reconnect(e);
  }

  this.emit('reorganize', block, tip.height, tip.hash);
});

/**
 * Disconnect an entry from the chain (updates the tip).
 * @param {ChainEntry} entry
 * @returns {Promise}
 */

Chain.prototype.disconnect = co(function* disconnect(entry) {
  var block = yield this.db.disconnect(entry);
  var prev = yield entry.getPrevious();

  assert(prev);

  this.tip = prev;
  this.height = prev.height;

  this.bestHeight = prev.height;
  this.network.updateHeight(prev.height);

  this.emit('tip', prev);
  this.emit('disconnect', entry, block);
});

/**
 * 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}
 */

Chain.prototype.reconnect = co(function* reconnect(entry) {
  var block = yield this.db.getBlock(entry.hash);
  var prev, view;

  if (!block) {
    assert(this.options.spv);
    block = entry.toHeaders();
  }

  prev = yield entry.getPrevious();
  assert(prev);

  try {
    view = yield this.verifyContext(block, prev);
  } catch (e) {
    if (e.type === 'VerifyError') {
      this.invalid[entry.hash] = true;
      this.emit('invalid', block, entry.height);
    }
    throw e;
  }

  yield this.db.reconnect(entry, block, view);

  this.tip = entry;
  this.height = entry.height;

  this.bestHeight = entry.height;
  this.network.updateHeight(entry.height);

  this.emit('tip', entry);
  this.emit('reconnect', entry, block);
  this.emit('connect', entry, block);
});

/**
 * Set the best chain. This is called on every valid block
 * that comes in. It may add and connect the block (main chain),
 * save the block without connection (alternate chain), or
 * reorganize the chain (a higher fork).
 * @private
 * @param {ChainEntry} entry
 * @param {Block|MerkleBlock} block
 * @param {ChainEntry} prev
 * @returns {Promise}
 */

Chain.prototype.setBestChain = co(function* setBestChain(entry, block, prev) {
  var view;

  assert(this.tip);

  // A higher fork has arrived.
  // Time to reorganize the chain.
  if (entry.prevBlock !== this.tip.hash) {
    this.logger.warning('WARNING: Reorganizing chain.');
    yield this.reorganize(entry, block);
  }

  // Otherwise, everything is in order.
  // Do "contextual" verification on our block
  // now that we're certain its previous
  // block is in the chain.
  try {
    view = yield this.verifyContext(block, prev);
  } catch (e) {
    // Couldn't verify block.
    // Revert the height.
    block.setHeight(-1);

    if (e.type === 'VerifyError') {
      this.invalid[entry.hash] = true;
      this.emit('invalid', block, entry.height);
    }

    throw e;
  }

  // Save block and connect inputs.
  yield this.db.save(entry, block, view);

  this.tip = entry;
  this.height = entry.height;

  this.emit('tip', entry);
});

/**
 * Reset the chain to the desired height. This
 * is useful for replaying the blockchain download
 * for SPV.
 * @param {Number} height
 * @returns {Promise}
 */

Chain.prototype.reset = co(function* reset(height) {
  var unlock = yield this.locker.lock();
  try {
    return yield this._reset(height);
  } finally {
    unlock();
  }
});

/**
 * Reset the chain to the desired height without a lock.
 * @private
 * @param {Number} height
 * @returns {Promise}
 */

Chain.prototype._reset = co(function* reset(height) {
  var result = yield this.db.reset(height);

  // Reset the orphan map completely. There may
  // have been some orphans on a forked chain we
  // no longer need.
  this.purgeOrphans();

  return result;
});

/**
 * Reset the chain to the desired timestamp (within 2
 * hours). This is useful for replaying the blockchain
 * download for SPV.
 * @param {Number} ts - Timestamp.
 * @returns {Promise}
 */

Chain.prototype.resetTime = co(function* resetTime(ts) {
  var unlock = yield this.locker.lock();
  try {
    return yield this._resetTime(ts);
  } finally {
    unlock();
  }
});

/**
 * Reset the chain to the desired timestamp without a lock.
 * @private
 * @param {Number} ts - Timestamp.
 * @returns {Promise}
 */

Chain.prototype._resetTime = co(function* resetTime(ts) {
  var entry = yield this.byTime(ts);

  if (!entry)
    return;

  yield this._reset(entry.height);
});

/**
 * Wait for the chain to drain (finish processing
 * all of the blocks in its queue).
 * @returns {Promise}
 */

Chain.prototype.onDrain = function onDrain() {
  return this.locker.onDrain();
};

/**
 * Test whether the chain is in the process of adding blocks.
 * @returns {Boolean}
 */

Chain.prototype.isBusy = function isBusy() {
  if (this.currentBlock)
    return true;
  return this.locker.pending.length > 0;
};

/**
 * Add a block to the chain, perform all necessary verification.
 * @param {Block|MerkleBlock|MemBlock} block
 * @returns {Promise}
 */

Chain.prototype.add = co(function* add(block) {
  var unlock = yield this.locker.lock(block);

  assert(!this.currentBlock);

  this.currentBlock = block.hash('hex');

  try {
    return yield this._add(block);
  } finally {
    this.currentBlock = null;
    unlock();
  }
});

/**
 * Add a block to the chain without a lock.
 * @private
 * @param {Block|MerkleBlock|MemBlock} block
 * @returns {Promise}
 */

Chain.prototype._add = co(function* add(block) {
  var ret = new VerifyResult();
  var initial = true;
  var hash, prevBlock, height, checkpoint;
  var orphan, entry, existing, prev;

  while (block) {
    hash = block.hash('hex');
    prevBlock = block.prevBlock;
    height = -1;

    // Mark the start time.
    this.mark();

    // Do not revalidate known invalid blocks.
    if (this.invalid[hash] || this.invalid[prevBlock]) {
      this.emit('invalid', block, block.getCoinbaseHeight());
      this.invalid[hash] = true;
      throw new VerifyError(block, 'duplicate', 'duplicate', 100);
    }

    // Do we already have this block?
    if (this.hasPending(hash)) {
      this.emit('exists', block, block.getCoinbaseHeight());
      throw new VerifyError(block, 'duplicate', 'duplicate', 0);
    }

    // If the block is already known to be
    // an orphan, ignore it.
    orphan = this.orphan.map[prevBlock];
    if (orphan) {
      // The orphan chain forked.
      if (orphan.hash('hex') !== hash) {
        this.emit('fork', block,
          block.getCoinbaseHeight(),
          orphan.hash('hex'));
      }

      this.emit('orphan', block, block.getCoinbaseHeight());

      throw new VerifyError(block, 'invalid', 'bad-prevblk', 0);
    }

    // Special case for genesis block.
    if (hash === this.network.genesis.hash)
      break;

    // Validate the block we want to add.
    // This is only necessary for new
    // blocks coming in, not the resolving
    // orphans.
    if (initial && !block.verify(ret)) {
      this.invalid[hash] = true;
      this.emit('invalid', block, block.getCoinbaseHeight());
      throw new VerifyError(block, 'invalid', ret.reason, ret.score);
    }

    existing = yield this.db.has(hash);

    // Do we already have this block?
    if (existing) {
      this.emit('exists', block, block.getCoinbaseHeight());
      throw new VerifyError(block, 'duplicate', 'duplicate', 0);
    }

    // Find the previous block height/index.
    prev = yield this.db.get(prevBlock);

    if (prev)
      height = prev.height + 1;

    if (height > this.bestHeight) {
      this.bestHeight = height;
      this.network.updateHeight(height);
    }

    // If previous block wasn't ever seen,
    // add it current to orphans and break.
    if (!prev) {
      this.orphan.count++;
      this.orphan.size += block.getSize();
      this.orphan.map[prevBlock] = block;
      this.orphan.bmap[hash] = block;

      // Update the best height based on the coinbase.
      // We do this even for orphans (peers will send
      // us their highest block during the initial
      // getblocks sync, making it an orphan).
      if (block.getCoinbaseHeight() > this.bestHeight) {
        this.bestHeight = block.getCoinbaseHeight();
        this.network.updateHeight(this.bestHeight);
      }

      this.emit('orphan', block, block.getCoinbaseHeight());

      throw new VerifyError(block, 'invalid', 'bad-prevblk', 0);
    }

    // Verify the checkpoint.
    if (this.options.useCheckpoints) {
      checkpoint = this.network.checkpoints[height];
      if (checkpoint) {
        // Someone is either trying to fool us, or
        // the consensus protocol is broken and
        // there was a 20k+ block reorg.
        if (hash !== checkpoint) {
          this.logger.warning('Checkpoint mismatch!');

          this.purgeOrphans();

          this.emit('fork', block, height, checkpoint);

          throw new VerifyError(block,
            'checkpoint',
            'checkpoint mismatch',
            100);
        }

        this.emit('checkpoint', block, height);
      }
    }

    // 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 emitted. Hopefully the deserialized
    // blocks get cleaned up by the GC quickly.
    if (block.memory) {
      try {
        block = block.toBlock();
      } catch (e) {
        this.logger.error(e);
        throw new VerifyError(block,
          'malformed',
          'error parsing message',
          100);
      }
    }

    // Update the block height early
    // Some things in verifyContext may
    // need access to height on txs.
    block.setHeight(height);

    // Create a new chain entry.
    entry = ChainEntry.fromBlock(this, 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.cmp(this.tip.chainwork) <= 0) {
      yield this.db.save(entry, block);

      this.emit('competitor', block, entry);

      if (!initial)
        this.emit('competitor resolved', block, entry);
    } else {
      // Attempt to add block to the chain index.
      yield this.setBestChain(entry, block, prev);

      // Emit our block (and potentially resolved
      // orphan) only if it is on the main chain.
      this.emit('block', block, entry);
      this.emit('connect', entry, block);

      if (!initial)
        this.emit('resolved', block, entry);
    }

    // Keep track of stats.
    this.finish(block, entry);

    // No orphan chain.
    if (!this.orphan.map[hash])
      break;

    // An orphan chain was found, start resolving.
    initial = false;
    block = this.orphan.map[hash];
    delete this.orphan.bmap[block.hash('hex')];
    delete this.orphan.map[hash];
    this.orphan.count--;
    this.orphan.size -= block.getSize();
  }

  // Failsafe for large orphan chains. Do not
  // allow more than 20mb stored in memory.
  if (this.orphan.size > this.orphanLimit)
    this.pruneOrphans();

  yield co.wait();

  if (!this.synced && this.isFull()) {
    this.synced = true;
    this.emit('full');
  }
});

/**
 * Test whether the chain has reached its slow height.
 * @private
 * @returns {Boolean}
 */

Chain.prototype.isSlow = function isSlow() {
  if (this.options.spv)
    return false;

  if (this.total === 1 || this.total % 20 === 0)
    return true;

  return this.synced || this.height >= this.network.block.slowHeight;
};

/**
 * Mark the start time for block processing.
 * @private
 */

Chain.prototype.mark = function mark() {
  this._time = utils.hrtime();
};

/**
 * Calculate the time difference from
 * start time and log block.
 * @private
 * @param {Block} block
 * @param {ChainEntry} entry
 */

Chain.prototype.finish = function finish(block, entry) {
  var elapsed, time;

  // Keep track of total blocks handled.
  this.total += 1;

  if (!this.isSlow())
    return;

  // Report memory for debugging.
  utils.gc();
  this.logger.memory();

  elapsed = utils.hrtime(this._time);
  time = elapsed[0] * 1000 + elapsed[1] / 1e6;

  this.logger.info(
    'Block %s (%d) added to chain (size=%d, txs=%d time=%d).',
    entry.rhash,
    entry.height,
    block.getSize(),
    block.txs.length,
    time);
};

/**
 * Purge any waiting orphans.
 */

Chain.prototype.purgeOrphans = function purgeOrphans() {
  this.emit('purge', this.orphan.count, this.orphan.size);
  this.orphan.map = {};
  this.orphan.bmap = {};
  this.orphan.count = 0;
  this.orphan.size = 0;
};

/**
 * Prune orphans, only keep the orphan with the highest
 * coinbase height (likely to be the peer's tip).
 */

Chain.prototype.pruneOrphans = function pruneOrphans() {
  var i, hashes, hash, orphan, height, best, last;

  hashes = Object.keys(this.orphan.map);

  if (hashes.length === 0)
    return;

  for (i = 0; i < hashes.length; i++) {
    hash = hashes[i];
    orphan = this.orphan.map[hash];
    height = orphan.getCoinbaseHeight();

    delete this.orphan.map[hash];

    if (!best || height > best.getCoinbaseHeight())
      best = orphan;

    last = orphan;
  }

  // Save the best for last... or the
  // last for best in this case.
  if (best.getCoinbaseHeight() <= 0)
    best = last;

  hashes = Object.keys(this.orphan.bmap);

  for (i = 0; i < hashes.length; i++) {
    hash = hashes[i];
    orphan = this.orphan.bmap[hash];

    delete this.orphan.bmap[hash];

    if (orphan !== best)
      this.emit('unresolved', orphan);
  }

  this.emit('purge',
    this.orphan.count - 1,
    this.orphan.size - best.getSize());

  this.orphan.map[best.prevBlock] = best;
  this.orphan.bmap[best.hash('hex')] = best;
  this.orphan.count = 1;
  this.orphan.size = best.getSize();
};

/**
 * Test the chain to see if it has a block, orphan, or pending block.
 * @param {Hash} hash
 * @returns {Promise} - Returns Boolean.
 */

Chain.prototype.has = co(function* has(hash) {
  if (this.hasOrphan(hash))
    return true;

  if (this.hasPending(hash))
    return true;

  if (hash === this.currentBlock)
    return true;

  return yield this.hasBlock(hash);
});

/**
 * Find a block entry by timestamp.
 * @param {Number} ts - Timestamp.
 * @returns {Promise} - Returns {@link ChainEntry}.
 */

Chain.prototype.byTime = co(function* byTime(ts) {
  var start = 0;
  var end = this.height;
  var pos, delta, entry;

  if (ts >= this.tip.ts)
    return this.tip;

  // Do a binary search for a block
  // mined within an hour of the
  // timestamp.
  while (start < end) {
    pos = (start + end) >>> 1;
    entry = yield this.db.get(pos);

    if (!entry)
      return;

    delta = Math.abs(ts - entry.ts);

    if (delta <= 60 * 60)
      break;

    if (ts < entry.ts)
      end = pos - 1;
    else
      start = pos + 1;
  }

  return entry;
});

/**
 * Test the chain to see if it contains a block.
 * @param {Hash} hash
 * @returns {Promise} - Returns Boolean.
 */

Chain.prototype.hasBlock = function hasBlock(hash) {
  return this.db.has(hash);
};

/**
 * Test the chain to see if it contains an orphan.
 * @param {Hash} hash
 * @returns {Promise} - Returns Boolean.
 */

Chain.prototype.hasOrphan = function hasOrphan(hash) {
  return !!this.getOrphan(hash);
};

/**
 * Test the chain to see if it contains a pending block in its queue.
 * @param {Hash} hash
 * @returns {Promise} - Returns Boolean.
 */

Chain.prototype.hasPending = function hasPending(hash) {
  return this.locker.hasPending(hash);
};

/**
 * Find the corresponding block entry by hash or height.
 * @param {Hash|Number} hash/height
 * @returns {Promise} - Returns {@link ChainEntry}.
 */

Chain.prototype.getEntry = function getEntry(hash, callback) {
  return this.db.get(hash);
};

/**
 * Get an orphan block.
 * @param {Hash} hash
 * @returns {Block|MerkleBlock|MemBlock}
 */

Chain.prototype.getOrphan = function getOrphan(hash) {
  return this.orphan.bmap[hash] || null;
};

/**
 * Test the chain to see if it is synced.
 * @returns {Boolean}
 */

Chain.prototype.isFull = function isFull() {
  return !this.isInitial();
};

/**
 * Test the chain to see if it is still in the initial
 * syncing phase. Mimic's bitcoind's `IsInitialBlockDownload()`
 * function.
 * @see IsInitalBlockDownload()
 * @returns {Boolean}
 */

Chain.prototype.isInitial = function isInitial() {
  if (this.synced)
    return false;

  if (this.height < this.network.checkpoints.lastHeight)
    return true;

  if (this.height < this.bestHeight - 24 * 6)
    return true;

  if (this.tip.ts < utils.now() - this.network.block.maxTipAge)
    return true;

  return false;
};

/**
 * Get the fill percentage.
 * @returns {Number} percent - Ranges from 0.0 to 1.0.
 */

Chain.prototype.getProgress = function getProgress() {
  var start = this.network.genesis.ts;
  var current = this.tip.ts - start;
  var end = utils.now() - start - 40 * 60;
  return Math.min(1, current / end);
};

/**
 * Calculate chain locator (an array of hashes).
 * @param {(Number|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} - Returns {@link Hash}[].
 */

Chain.prototype.getLocator = co(function* getLocator(start) {
  var unlock = yield this.locker.lock();
  try {
    return yield this._getLocator(start);
  } finally {
    unlock();
  }
});

/**
 * Calculate chain locator without a lock.
 * @private
 * @param {(Number|Hash)?} start
 * @returns {Promise}
 */

Chain.prototype._getLocator = co(function* getLocator(start) {
  var hashes = [];
  var step = 1;
  var height, entry, main, hash;

  if (start == null)
    start = this.tip.hash;

  entry = yield this.db.get(start);

  if (!entry) {
    // We could simply return `start` here,
    // but there is no required "spacing"
    // for locator hashes. Pretend this hash
    // is our tip. This is useful for
    // getheaders.
    if (typeof start === 'string')
      hashes.push(start);
    entry = this.tip;
  }

  height = entry.height;
  main = yield entry.isMainChain();
  hash = entry.hash;

  while (hash) {
    hashes.push(hash);

    if (height === 0)
      break;

    height = Math.max(height - step, 0);

    if (hashes.length > 10)
      step *= 2;

    if (height === 0) {
      hash = this.network.genesis.hash;
      continue;
    }

    // If we're on the main chain, we can
    // do a fast lookup of the hash.
    if (main) {
      hash = yield this.db.getHash(height);
      continue;
    }

    entry = yield entry.getAncestorByHeight(height);

    if (!entry)
      break;

    hash = entry.hash;
  }

  return hashes;
});

/**
 * Calculate the orphan root of the hash (if it is an orphan).
 * Will also calculate "orphan soil" -- the block needed
 * in * order to resolve the orphan root.
 * @param {Hash} hash
 * @returns {Object?} root - { root: {@link Hash}, soil: {@link Hash} }.
 */

Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) {
  var root;

  assert(hash);

  while (this.orphan.bmap[hash]) {
    root = hash;
    hash = this.orphan.bmap[hash].prevBlock;
  }

  return root;
};

/**
 * Calculate the next target based on the chain tip.
 * @returns {Promise} - returns Number
 * (target is in compact/mantissa form).
 */

Chain.prototype.getCurrentTarget = co(function* getCurrentTarget() {
  return yield this.getTargetAsync(null, this.tip);
});

/**
 * Calculate the target based on the passed-in chain entry.
 * @param {ChainEntry} prev - Previous entry.
 * @param {Block|MerkleBlock|null} - Current block.
 * @returns {Promise} - returns Number
 * (target is in compact/mantissa form).
 */

Chain.prototype.getTargetAsync = co(function* getTargetAsync(block, prev) {
  var ancestors;

  if ((prev.height + 1) % this.network.pow.retargetInterval !== 0) {
    if (!this.network.pow.difficultyReset)
      return this.getTarget(block, prev);
  }

  ancestors = yield prev.getAncestors(this.network.pow.retargetInterval);

  return this.getTarget(block, prev, ancestors);
});

/**
 * Calculate the target synchronously. _Must_
 * have ancestors pre-allocated.
 * @param {Block|MerkleBlock|null} - Current block.
 * @param {ChainEntry} prev - Previous entry.
 * @returns {Promise} - returns Number
 * (target is in compact/mantissa form).
 */

Chain.prototype.getTarget = function getTarget(block, prev, ancestors) {
  var ts, first, i;

  // Genesis
  if (!prev)
    return this.network.pow.bits;

  // Do not retarget
  if ((prev.height + 1) % this.network.pow.retargetInterval !== 0) {
    if (this.network.pow.difficultyReset) {
      // Special behavior for testnet:
      ts = block ? (block.ts || block) : time.now();
      if (ts > prev.ts + this.network.pow.targetSpacing * 2)
        return this.network.pow.bits;

      i = 1;
      while (ancestors[i]
        && prev.height % this.network.pow.retargetInterval !== 0
        && prev.bits === this.network.pow.bits) {
        prev = ancestors[i++];
      }
    }
    return prev.bits;
  }

  // Back 2 weeks
  first = ancestors[this.network.pow.retargetInterval - 1];

  assert(first);

  return this.retarget(prev, first);
};

/**
 * Retarget. This is called when the chain height
 * hits a retarget diff interval.
 * @param {ChainEntry} prev - Previous entry.
 * @param {ChainEntry} first - Chain entry from 2 weeks prior.
 * @returns {Number} target - Target in compact/mantissa form.
 */

Chain.prototype.retarget = function retarget(prev, first) {
  var targetTimespan = this.network.pow.targetTimespan;
  var actualTimespan, target;

  if (this.network.pow.noRetargeting)
    return prev.bits;

  actualTimespan = prev.ts - first.ts;
  target = utils.fromCompact(prev.bits);

  if (actualTimespan < targetTimespan / 4)
    actualTimespan = targetTimespan / 4;

  if (actualTimespan > targetTimespan * 4)
    actualTimespan = targetTimespan * 4;

  target.imuln(actualTimespan);
  target.idivn(targetTimespan);

  if (target.cmp(this.network.pow.limit) > 0)
    return this.network.pow.bits;

  return utils.toCompact(target);
};

/**
 * Find a locator. Analagous to bitcoind's `FindForkInGlobalIndex()`.
 * @param {Hash[]} locator - Hashes.
 * @returns {Promise} - Returns {@link Hash} (the
 * hash of the latest known block).
 */

Chain.prototype.findLocator = co(function* findLocator(locator) {
  var i, hash, main;

  for (i = 0; i < locator.length; i++) {
    hash = locator[i];
    main = yield this.db.isMainChain(hash);

    if (main)
      return hash;
  }

  return this.network.genesis.hash;
});

/**
 * Check whether a versionbits deployment is active (BIP9: versionbits).
 * @example
 * chain.isActive(entry, 'witness', callback);
 * @see https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki
 * @param {ChainEntry} prev - Previous chain entry.
 * @param {String} id - Deployment id.
 * @returns {Promise} - Returns Number.
 */

Chain.prototype.isActive = co(function* isActive(prev, id) {
  var state;

  if (prev.isHistorical())
    return false;

  state = yield this.getState(prev, id);

  return state === constants.thresholdStates.ACTIVE;
});

/**
 * Get chain entry state for a deployment (BIP9: versionbits).
 * @example
 * chain.getState(entry, 'witness', callback);
 * @see https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki
 * @param {ChainEntry} prev - Previous chain entry.
 * @param {String} id - Deployment id.
 * @returns {Promise} - Returns Number.
 */

Chain.prototype.getState = co(function* getState(prev, id) {
  var period = this.network.minerWindow;
  var threshold = this.network.activationThreshold;
  var deployment = this.network.deployments[id];
  var stateCache = this.stateCache[id];
  var timeStart, timeTimeout, compute, height;
  var i, entry, count, state, block, medianTime;

  assert(deployment);

  timeStart = deployment.startTime;
  timeTimeout = deployment.timeout;
  compute = [];

  if (!prev)
    return constants.thresholdStates.DEFINED;

  if (((prev.height + 1) % period) !== 0) {
    height = prev.height - ((prev.height + 1) % period);
    prev = yield prev.getAncestorByHeight(height);

    if (prev) {
      assert(prev.height === height);
      assert(((prev.height + 1) % period) === 0);
    }
  }

  entry = prev;
  state = constants.thresholdStates.DEFINED;

  while (entry) {
    if (stateCache[entry.hash] != null) {
      state = stateCache[entry.hash];
      break;
    }

    medianTime = yield entry.getMedianTimeAsync();

    if (medianTime < timeStart) {
      state = constants.thresholdStates.DEFINED;
      stateCache[entry.hash] = state;
      break;
    }

    compute.push(entry);

    height = entry.height - period;

    entry = yield entry.getAncestorByHeight(height);
  }

  while (compute.length) {
    entry = compute.pop();

    switch (state) {
      case constants.thresholdStates.DEFINED:
        medianTime = yield entry.getMedianTimeAsync();

        if (medianTime >= timeTimeout) {
          state = constants.thresholdStates.FAILED;
          break;
        }

        if (medianTime >= timeStart) {
          state = constants.thresholdStates.STARTED;
          break;
        }

        break;
      case constants.thresholdStates.STARTED:
        medianTime = yield entry.getMedianTimeAsync();

        if (medianTime >= timeTimeout) {
          state = constants.thresholdStates.FAILED;
          break;
        }

        block = entry;
        count = 0;

        for (i = 0; i < period; i++) {
          if (block.hasBit(deployment))
            count++;

          if (count >= threshold) {
            state = constants.thresholdStates.LOCKED_IN;
            break;
          }

          block = yield block.getPrevious();
          assert(block);
        }

        break;
      case constants.thresholdStates.LOCKED_IN:
        state = constants.thresholdStates.ACTIVE;
        break;
      case constants.thresholdStates.FAILED:
      case constants.thresholdStates.ACTIVE:
        break;
      default:
        assert(false, 'Bad state.');
        break;
    }

    stateCache[entry.hash] = state;
  }

  return state;
});

/**
 * 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} - Returns Number.
 */

Chain.prototype.computeBlockVersion = co(function* computeBlockVersion(prev) {
  var keys = Object.keys(this.network.deployments);
  var version = 0;
  var i, id, deployment, state;

  for (i = 0; i < keys.length; i++) {
    id = keys[i];
    deployment = this.network.deployments[id];
    state = yield this.getState(prev, id);

    if (state === constants.thresholdStates.LOCKED_IN
        || state === constants.thresholdStates.STARTED) {
      version |= (1 << deployment.bit);
    }
  }

  version |= constants.versionbits.TOP_BITS;
  version >>>= 0;

  return version;
});

/**
 * Get the current deployment state of the chain. Called on load.
 * @private
 * @returns {Promise} - Returns {@link DeploymentState}.
 */

Chain.prototype.getDeploymentState = co(function* getDeploymentState() {
  var prev = yield this.tip.getPrevious();
  var ancestors;

  if (!prev)
    return this.state;

  ancestors = yield prev.getRetargetAncestors();

  return yield this.getDeployments(this.tip, prev, ancestors);
});

/**
 * 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.
 */

Chain.prototype.checkFinal = co(function* checkFinal(prev, tx, flags) {
  var height = prev.height + 1;
  var ts;

  // We can skip MTP if the locktime is height.
  if (tx.locktime < constants.LOCKTIME_THRESHOLD)
    return tx.isFinal(height, -1);

  if (flags & constants.flags.MEDIAN_TIME_PAST) {
    ts = yield prev.getMedianTimeAsync();
    return tx.isFinal(height, ts);
  }

  return tx.isFinal(height, time.now());
});

/**
 * Get the necessary minimum time and height sequence locks for a transaction.
 * @param {TX} tx
 * @param {LockFlags} flags
 * @param {ChainEntry} prev
 * @returns {Promise}
 * [Error, Number(minTime), Number(minHeight)].
 */

Chain.prototype.getLocks = co(function* getLocks(prev, tx, flags) {
  var mask = constants.sequence.MASK;
  var granularity = constants.sequence.GRANULARITY;
  var disableFlag = constants.sequence.DISABLE_FLAG;
  var typeFlag = constants.sequence.TYPE_FLAG;
  var hasFlag = flags & constants.flags.VERIFY_SEQUENCE;
  var minHeight = -1;
  var minTime = -1;
  var coinHeight, coinTime;
  var i, input, entry;

  if (tx.isCoinbase() || tx.version < 2 || !hasFlag)
    return new LockTimes(minHeight, minTime);

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];

    if (input.sequence & disableFlag)
      continue;

    coinHeight = input.coin.height !== -1
      ? input.coin.height
      : this.height + 1;

    if ((input.sequence & typeFlag) === 0) {
      coinHeight += (input.sequence & mask) - 1;
      minHeight = Math.max(minHeight, coinHeight);
      continue;
    }

    entry = yield prev.getAncestorByHeight(Math.max(coinHeight - 1, 0));
    assert(entry, 'Database is corrupt.');

    coinTime = yield entry.getMedianTimeAsync();
    coinTime += ((input.sequence & mask) << granularity) - 1;
    minTime = Math.max(minTime, coinTime);
  }

  return new LockTimes(minHeight, minTime);
});

/**
 * Evaluate sequence locks.
 * @param {ChainEntry} prev
 * @param {Number} minHeight
 * @param {Number} minTime
 * @returns {Promise} - Returns Boolean.
 */

Chain.prototype.evalLocks = co(function* evalLocks(prev, minHeight, minTime) {
  var medianTime;

  if (minHeight >= prev.height + 1)
    return false;

  if (minTime === -1)
    return true;

  medianTime = yield prev.getMedianTimeAsync();

  if (minTime >= medianTime)
    return false;

  return true;
});

/**
 * Verify sequence locks.
 * @param {TX} tx
 * @param {LockFlags} flags
 * @param {ChainEntry} prev
 * @returns {Promise} - Returns Boolean.
 */

Chain.prototype.checkLocks = co(function* checkLocks(prev, tx, flags) {
  var times = yield this.getLocks(prev, tx, flags);
  return yield this.evalLocks(prev, times.height, times.time);
});

/**
 * Represents the deployment state of the chain.
 * @constructor
 * @property {VerifyFlags} flags
 * @property {LockFlags} lockFlags
 * @property {Boolean} bip34
 */

function DeploymentState() {
  if (!(this instanceof DeploymentState))
    return new DeploymentState();

  this.flags = constants.flags.MANDATORY_VERIFY_FLAGS;
  this.flags &= ~constants.flags.VERIFY_P2SH;
  this.lockFlags = constants.flags.MANDATORY_LOCKTIME_FLAGS;
  this.bip34 = false;
}

/**
 * Test whether p2sh is active.
 * @returns {Boolean}
 */

DeploymentState.prototype.hasP2SH = function hasP2SH() {
  return (this.flags & constants.flags.VERIFY_P2SH) !== 0;
};

/**
 * Test whether bip34 (coinbase height) is active.
 * @returns {Boolean}
 */

DeploymentState.prototype.hasBIP34 = function hasBIP34() {
  return this.bip34;
};

/**
 * Test whether bip66 (VERIFY_DERSIG) is active.
 * @returns {Boolean}
 */

DeploymentState.prototype.hasBIP66 = function hasBIP66() {
  return (this.flags & constants.flags.VERIFY_DERSIG) !== 0;
};

/**
 * Test whether cltv is active.
 * @returns {Boolean}
 */

DeploymentState.prototype.hasCLTV = function hasCLTV() {
  return (this.flags & constants.flags.VERIFY_CHECKLOCKTIMEVERIFY) !== 0;
};

/**
 * Test whether median time past locktime is active.
 * @returns {Boolean}
 */

DeploymentState.prototype.hasMTP = function hasMTP() {
  return (this.lockFlags & constants.flags.MEDIAN_TIME_PAST) !== 0;
};

/**
 * Test whether csv is active.
 * @returns {Boolean}
 */

DeploymentState.prototype.hasCSV = function hasCSV() {
  return (this.flags & constants.flags.VERIFY_CHECKSEQUENCEVERIFY) !== 0;
};

/**
 * Test whether segwit is active.
 * @returns {Boolean}
 */

DeploymentState.prototype.hasWitness = function hasWitness() {
  return (this.flags & constants.flags.VERIFY_WITNESS) !== 0;
};

/*
 * LockTimes
 */

function LockTimes(height, time) {
  this.height = height;
  this.time = time;
}

/*
 * Expose
 */

module.exports = Chain;
