<commit_before>/**
 * pool.js - peer management for bcoin
 * Copyright (c) 2014-2015, <NAME> (MIT License)
 * https://github.com/indutny/bcoin
 */

var async = require('async');
var inherits = require('inherits');
var EventEmitter = require('events').EventEmitter;

var bcoin = require('../bcoin');
var utils = bcoin.utils;
var assert = utils.assert;
var network = bcoin.protocol.network;

/**
 * Pool
 */

function Pool(options) {
  var self = this;
  var Chain;

  if (!(this instanceof Pool))
    return new Pool(options);

  EventEmitter.call(this);

  this.options = options || {};

  if (this.options.network)
    network.set(this.options.network);

  this.options.fullNode = !!this.options.fullNode;
  this.options.headers = this.options.headers;
  this.options.multiplePeers = this.options.multiplePeers;
  this.options.relay = this.options.relay == null
    ? (this.options.fullNode ? true : false)
    : this.options.relay;

  this.storage = this.options.storage;
  this.destroyed = false;
  this.size = options.size || 32;
  this.parallel = options.parallel || 2000;
  this.redundancy = options.redundancy || 1;
  this.seeds = options.seeds
    ? options.seeds.slice()
    : network.seeds.slice();

  this._createConnection = options.createConnection;
  this._createSocket = options.createSocket;

  if (!this.options.fullNode) {
    if (this.options.headers == null)
      this.options.headers = true;
    if (this.options.multiplePeers == null)
      this.options.multiplePeers = true;
  } else {
    if (this.options.headers == null)
      this.options.headers = false;
    if (this.options.multiplePeers == null)
      this.options.multiplePeers = false;
  }

  if (!this.options.headers)
    this.options.multiplePeers = false;

  this.backoff = {
    delta: options.backoffDelta || 500,
    max: options.backoffMax || 5000
  };

  this.load = {
    timeout: options.loadTimeout || 30000,
    interval: options.loadInterval || 5000,
    window: options.loadWindow || 250,
    lastRange: null,
    rangeWindow: options.rangeWindow || 1000,
    timer: null,
    lwm: options.lwm || this.parallel * 2,
    hwm: options.hwm || this.parallel * 8,
    hiReached: false
  };

  this.maxRetries = options.maxRetries || 42;
  this.requestTimeout = options.requestTimeout || 10000;

  this.chain = new bcoin.chain({
    storage: this.storage,
    fullNode: this.options.fullNode
  });

  this.watchMap = {};

  this.bloom = new bcoin.bloom(
    8 * 1024,
    10,
    (Math.random() * 0xffffffff) | 0
  );

  this.peers = {
    // Peers that are loading blocks themselves
    block: [],
    // Peers that are still connecting
    pending: [],
    // Peers that are loading block ids
    load: null,
    // All peers
    all: []
  };

  this.block = {
    bestHeight: 0,
    bestHash: null,
    type: this.options.fullNode ? 'block' : 'filtered'
  };

  this.request = {
    map: {},
    active: 0,
    queue: []
  };

  this.validate = {
    // 5 days scan delta for obtaining TXs
    delta: 5 * 24 * 3600,

    // Minimum verification depth
    minDepth: options.minValidateDepth || 0,

    // getTX map
    map: {}
  };

  // Currently broadcasted TXs
  this.inv = {
    list: [],
    timeout: options.txTimeout || 60000
  };

  // Added and watched wallets
  this.options.wallets = this.options.wallets || [];
  this.wallets = [];

  this.chain.on('debug', function() {
    var args = Array.prototype.slice.call(arguments);
    self.emit.apply(self, ['debug'].concat(args));
  });

  Pool.global = this;

  this.loading = true;

  this.chain.once('load', function() {
    self.loading = false;
    self.emit('load');
    self._init();
  });
}

inherits(Pool, EventEmitter);

Pool.prototype._init = function _init() {
  var self = this;
  var i;

  this._addLoader();

  for (i = 0; i < this.size; i++)
    this._addPeer(0);

  this.chain.on('block', function(block, peer) {
    self.emit('block', block, peer);
  });

  this.chain.on('fork', function(data) {
    var peer = self.peers.load;

    this.emit('debug',
      'Fork at height %d: expected=%s received=%s checkpoint=%s',
      data.height,
      utils.revHex(data.expected),
      utils.revHex(data.received),
      data.checkpoint
    );

    if (!peer)
      return;

    peer.destroy();
  });

  this.options.wallets.forEach(function(w) {
    self.addWallet(w);
  });
};

Pool.prototype._startTimer = function _startTimer() {
  var self = this;

  this._stopTimer();

  function destroy() {
    // Chain is full and up-to-date
    if (self.chain.isFull()) {
      self._stopTimer();
      self.emit('full');
      return;
    }

    if (self.peers.load)
      self.peers.load.destroy();
  }

  this._timer = setTimeout(destroy, this.load.timeout);
};

Pool.prototype._stopTimer = function _stopTimer() {
  if (!this._timer)
    return;

  clearTimeout(this._timer);
  delete this._timer;
};

Pool.prototype._startInterval = function _startInterval() {
  var self = this;

  this._stopInterval();

  function load() {
    // self._load();
  }

  this._interval = setInterval(load, this.load.interval);
};

Pool.prototype._stopInterval = function _stopInterval() {
  if (!this._interval)
    return;

  clearInterval(this._interval);
  delete this._interval;
};

Pool.prototype.createConnection = function createConnection(peer, pool) {
  var addr, parts, host, net, socket, port;

  if (pool._createConnection)
    return pool._createConnection(peer, pool);

  addr = pool.usableSeed(true);

  parts = addr.split(':');
  host = parts[0];
  port = +parts[1] || network.port;

  peer.host = host;
  peer.port = port;

  if (this._createSocket) {
    socket = this._createSocket(port, host);
  } else {
    net = require('net');
    socket = net.connect(port, host);
  }

  socket.on('connect', function() {
    pool.emit('debug', 'Connected to %s:%d', host, port);
  });

  return socket;
};

Pool.prototype._addLoader = function _addLoader() {
  var self = this;
  var peer;

  if (this.destroyed)
    return;

  if (this.peers.load != null)
    return;

  peer = this._createPeer(750 * Math.random());

  peer.once('socket', function() {
    self.emit('debug', 'Added loader peer: %s', peer.host);
  });

  this.peers.load = peer;
  this.peers.all.push(peer);

  peer.once('close', function() {
    self._stopInterval();
    self._stopTimer();
    self._removePeer(peer);
    if (self.destroyed)
      return;
    self._addLoader();
  });

  peer.once('ack', function() {
    peer.updateWatch();
    if (!self._load())
      self._stopTimer();
  });

  peer.on('merkleblock', function(block) {
    // If the peer sent us a block that was added
    // to the chain (not orphans), reset the timeout.
    if (self._handleBlock(block, peer)) {
      self._startInterval();
      self._startTimer();
    }
  });

  peer.on('block', function(block) {
    // If the peer sent us a block that was added
    // to the chain (not orphans), reset the timeout.
    if (self._handleBlock(block, peer)) {
      self._startInterval();
      self._startTimer();
    }
  });

  if (self.options.headers) {
    peer.on('blocks', function(hashes) {
      self._handleInv(hashes, peer);
    });

    peer.on('headers', function(headers) {
      self._handleHeaders(headers, peer);
    });
  } else {
    peer.on('blocks', function(hashes) {
      self._handleBlocks(hashes, peer);
    });
  }

  this._startInterval();
  this._startTimer();
};

Pool.prototype._handleHeaders = function _handleHeaders(headers, peer) {
  var i, header, last, block;

  assert(this.options.headers);

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

  this.emit('debug',
    'Recieved %s headers from %s',
    headers.length,
    peer.host);

  if (headers.length > 2000) {
    peer.destroy();
    return;
  }

  this.emit('headers', headers);

  for (i = 0; i < headers.length; i++) {
    block = bcoin.block(headers[i], 'header');

    if (last && block.prevBlock !== last.hash('hex'))
      break;

    if (!block.verify())
      break;

    if (!this.chain.has(block))
      this._request(this.block.type, block.hash('hex'));

    last = block;
  }

  // Restart the getheaders process
  // Technically `last` is not indexed yet so
  // the locator hashes will not be entirely
  // accurate. However, it shouldn't matter
  // that much since FindForkInGlobalIndex
  // simply tries to find the latest block in
  // the peer's chain.
  if (last && headers.length === 2000)
    peer.loadHeaders(this.chain.locatorHashes(last), null);

  // Push our getdata packet
  this._scheduleRequests();

  // Reset interval to avoid calling getheaders unnecessarily
  this._startInterval();

  // Reset timeout to avoid killing the loader
  this._startTimer();
};

Pool.prototype._handleBlocks = function _handleBlocks(hashes, peer) {
  var i, hash;

  assert(!this.options.headers);

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

  this.emit('debug',
    'Recieved %s block hashes from %s',
    hashes.length,
    peer.host);

  if (hashes.length > 500) {
    peer.destroy();
    return;
  }

  this.emit('blocks', hashes);

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

    // Resolve orphan chain
    if (this.chain.hasOrphan(hash)) {
      peer.loadBlocks(
        this.chain.locatorHashes(),
        this.chain.getOrphanRoot(hash)
      );
      continue;
    }

    // Request block if we don't have it or if
    // this is the last hash: this is done as
    // a failsafe because we _need_ to request
    // the hashContinue no matter what.
    if (!this.chain.has(hash) || i === hashes.length - 1) {
      this._request(this.block.type, hash);
      continue;
    }
  }

  // Push our getdata packet
  this._scheduleRequests();

  // Reset interval to avoid calling getblocks unnecessarily
  this._startInterval();

  // Reset timeout to avoid killing the loader
  this._startTimer();
};

Pool.prototype._handleInv = function _handleInv(hashes, peer) {
  var i, hash;

  // Ignore for now if we're still syncing
  if (!this.chain.isFull())
    return;

  for (i = 0; i < hashes.length; i++) {
    hash = utils.toHex(hashes[i]);
    if (!this.chain.has(hash)) {
      if (this.options.headers)
        this.peers.load.loadHeaders(this.chain.locatorHashes(), hash);
      else
        this._request(this.block.type, hash);
    }
  }

  if (!this.options.headers)
    this._scheduleRequests();
};

Pool.prototype._handleBlock = function _handleBlock(block, peer) {
  var self = this;

  var requested = this._response(block);

  // Emulate bip37 - emit all the "watched" txs
  if (this.options.fullNode && this.listeners('watched').length > 0) {
    block.txs.forEach(function(tx) {
      if (self.isWatched(tx))
        self.emit('watched', tx, peer);
    });
  }

  // Ignore if we already have
  if (this.chain.has(block)) {
    this.emit('debug', 'Already have block %s (%s)', block.height, peer.host);
    return false;
  }

  // Make sure the block is valid
  if (!block.verify()) {
    this.emit('debug',
      'Block verification failed for %s (%s)',
      block.rhash, peer.host);
    return false;
  }

  // Someone is sending us blocks without us requesting them.
  if (!requested) {
    this.emit('debug',
      'Recieved unrequested block: %s (%s)',
      block.rhash, peer.host);
  }

  // Resolve orphan chain
  if (!this.options.headers) {
    if (!this.chain.hasBlock(block.prevBlock)) {
      // NOTE: If we were to emit new orphans here, we
      // would not need to store full blocks as orphans.
      // However, the listener would not be able to see
      // the height until later.
      if (this._addIndex(block, peer))
        this.emit('pool block', block, peer);
      this.peers.load.loadBlocks(
        this.chain.locatorHashes(),
        this.chain.getOrphanRoot(block)
      );
      this.emit('debug', 'Handled orphan %s (%s)', block.rhash, peer.host);
      return false;
    }
  }

  // Add to index and emit/save
  if (this._addIndex(block, peer)) {
    this.emit('pool block', block, peer);
    return true;
  }
};

Pool.prototype._addIndex = function _addIndex(block, peer) {
  var added = this.chain.add(block, peer);

  this.emit('chain-progress', this.chain.fillPercent(), peer);

  return added > 0;
};

Pool.prototype.isFull = function isFull() {
  return this.chain.isFull();
};

Pool.prototype._loadRange = function _loadRange(hashes, force) {
  var now = +new Date();
  var last;

  if (this.options.fullNode)
    return;

  if (!hashes)
    return;

  if (hashes.length <= 1)
    return;

  // Limit number of requests
  if (!force && now - this.load.lastRange < this.load.rangeWindow)
    return;

  this.load.lastRange = now;

  if (!this.peers.load)
    this._addLoader();

  last = hashes[hashes.length - 1];

  hashes.slice(0, -1).forEach(function(hash) {
    this.peers.load.loadItems([hash], last);
  }, this);
};

Pool.prototype._load = function _load() {
  var self = this;
  var next;

  if (this.request.queue.length >= this.load.hwm) {
    this.load.hiReached = true;
    return false;
  }

  this.load.hiReached = false;

  if (!this.peers.load) {
    this._addLoader();
    return true;
  }

  if (this.options.headers)
    this.peers.load.loadHeaders(this.chain.locatorHashes(), null);
  else
    this.peers.load.loadBlocks(this.chain.locatorHashes(), null);

  return true;
};

Pool.prototype.loadMempool = function loadMempool() {
  this.pool.peers.block.forEach(function(peer) {
    peer.loadMempool();
  });
};

Pool.prototype._createPeer = function _createPeer(backoff) {
  var self = this;

  var peer = new bcoin.peer(this, this.createConnection, {
    backoff: backoff,
    startHeight: this.options.startHeight,
    relay: this.options.relay
  });

  peer._retry = 0;

  peer.on('error', function(err) {
    self.emit('error', err, peer);
  });

  peer.on('debug', function() {
    var args = Array.prototype.slice.call(arguments);
    self.emit.apply(self, ['debug'].concat(args));
  });

  peer.on('reject', function(payload) {
    self.emit('debug',
      'Reject: msg=%s ccode=%s reason=%s data=%s',
      payload.message, payload.ccode, payload.reason,
      utils.toHex(payload.data));
    self.emit('reject', payload, peer);
  });

  peer.on('notfound', function(items) {
    items.forEach(function(item) {
      var req = self.request.map[utils.toHex(item.hash)];
      if (req && req.peer === peer)
        req.finish(null);
    });
  });

  peer.on('tx', function(tx) {
    self._response(tx);
    self.emit('tx', tx, peer);

    if (!self.options.fullNode && tx.block)
      self.emit('watched', tx, peer);
  });

  peer.on('addr', function(addr) {
    var host = addr.ipv4 + ':' + addr.port;

    if (self.seeds.length > self.size * 2)
      self.seeds = network.seeds.slice();

    if (self.seeds.indexOf(host) !== -1) {
      self.emit('debug', 'Found new peer: %s', host);
      self.seeds.push(host);
    }

    self.emit('addr', addr, peer);
  });

  peer.on('txs', function(txs) {
    self.emit('txs', txs, peer);
  });

  peer.on('version', function(version) {
    if (version.height > self.block.bestHeight)
      self.block.bestHeight = version.height;
    self.emit('version', version, peer);
  });

  return peer;
};

Pool.prototype._addPeer = function _addPeer(backoff) {
  var self = this;
  var peer;

  if (this.destroyed)
    return;

  if (this.peers.block.length + this.peers.pending.length >= this.size)
    return;

  if (!this._createConnection && !this.usableSeed()) {
    setTimeout(this._addPeer.bind(this, 0), this.backoff.max);
    return;
  }

  peer = this._createPeer(backoff);

  this.peers.pending.push(peer);
  this.peers.all.push(peer);

  peer.once('close', function() {
    self._removePeer(peer);
    if (self.destroyed)
      return;
    self._addPeer(Math.min(backoff + self.backoff.delta, self.backoff.max));
  });

  peer.once('ack', function() {
    var i;

    if (self.destroyed)
      return;

    i = self.peers.pending.indexOf(peer);
    if (i !== -1) {
      self.peers.pending.splice(i, 1);
      self.peers.block.push(peer);
    }

    peer.updateWatch();

    self.inv.list.forEach(function(entry) {
      var result = peer.broadcast(entry.msg);
      if (!result)
        return;

      result[0].once('request', function() {
        entry.e.emit('ack', peer);
      });
    });

    self._scheduleRequests();
  });

  peer.on('merkleblock', function(block) {
    backoff = 0;
    self._handleBlock(block, peer);
  });

  peer.on('block', function(block) {
    backoff = 0;
    self._handleBlock(block, peer);
  });

  peer.on('blocks', function(hashes) {
    self._handleInv(hashes, peer);
  });

  utils.nextTick(function() {
    self.emit('peer', peer);
  });
};

Pool.prototype.bestPeer = function bestPeer() {
  return this.peers.block.reduce(function(best, peer) {
    if (!peer.version || !peer.socket)
      return;

    if (!best || peer.version.height > best.version.height)
      return peer;

    return best;
  }, null);
};

Pool.prototype._removePeer = function _removePeer(peer) {
  var i = this.peers.pending.indexOf(peer);
  if (i !== -1)
    this.peers.pending.splice(i, 1);

  i = this.peers.block.indexOf(peer);
  if (i !== -1)
    this.peers.block.splice(i, 1);

  i = this.peers.all.indexOf(peer);
  if (i !== -1)
    this.peers.all.splice(i, 1);

  if (this.peers.load === peer) {
    this.emit('debug', 'Removed loader peer (%s).', peer.host);
    this.peers.load = null;
  }
};

Pool.prototype.watch = function watch(id) {
  var hid, i;

  if (id instanceof bcoin.wallet) {
    this.watchWallet(id);
    return;
  }

  if (id) {
    hid = utils.toHex(id);
    if (this.watchMap[hid])
      this.watchMap[hid]++;
    else
      this.watchMap[hid] = 1;

    if (this.bloom.test(id, 'hex'))
      return;

    this.bloom.add(id, 'hex');
  }

  if (this.peers.load)
    this.peers.load.updateWatch();

  for (i = 0; i < this.peers.block.length; i++)
    this.peers.block[i].updateWatch();
};

Pool.prototype.unwatch = function unwatch(id) {
  var i;

  id = utils.toHex(id);

  if (!this.bloom.test(id, 'hex'))
    return;

  if (!this.watchMap[id] || --this.watchMap[id] !== 0)
    return;

  delete this.watchMap[id];

  // Reset bloom filter
  this.bloom.reset();
  Object.keys(this.watchMap).forEach(function(id) {
    this.bloom.add(id, 'hex');
  }, this);

  // Resend it to peers
  if (this.peers.load)
    this.peers.load.updateWatch();

  for (i = 0; i < this.peers.block.length; i++)
    this.peers.block[i].updateWatch();
};

// See "Filter matching algorithm":
// https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
Pool.prototype.isWatched = function(tx, bloom) {
  var i, input, output, prev;

  if (!bloom)
    bloom = this.bloom;

  function testScript(script) {
    return script.some(function(chunk) {
      if (!Array.isArray(chunk) || chunk.length === 0)
        return false;
      return bloom.test(chunk);
    });
  }

  // 1. Test the tx hash
  if (bloom.test(tx.hash()))
    return true;

  // 2. Test data elements in output scripts
  //    (may need to update filter on match)
  for (i = 0; i < tx.outputs.length; i++) {
    output = tx.outputs[i];
    // Test the output script
    if (testScript(output.script))
      return true;
  }

  // 3. Test prev_out structure
  // 4. Test data elements in input scripts
  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];
    prev = input.out.hash;

    if (typeof prev === 'string')
      prev = utils.toArray(prev, 'hex');

    // Test the prev_out tx hash
    if (bloom.test(prev))
      return true;

    // Test the prev_out script
    if (input.out.tx) {
      prev = input.out.tx.outputs[input.out.index];
      if (testScript(prev.script))
        return true;
    }

    // Test the input script
    if (testScript(input.script))
      return true;
  }

  // 5. No match
  return false;
};

Pool.prototype.addWallet = function addWallet(w, defaultTs) {
  var self = this;
  var e;

  if (this.wallets.indexOf(w) !== -1)
    return false;

  this.watchWallet(w);
  this.wallets.push(w);

  e = new EventEmitter();

  function search(ts) {
    // Relay pending TXs
    // NOTE: It is important to do it after search, because search could
    // add TS to pending TXs, thus making them confirmed
    w.pending().forEach(function(tx) {
      self.sendTX(tx);
    });

    if (self.options.fullNode)
      return;

    // Search for last week by default
    if (!ts)
      ts = defaultTs || (utils.now() - 7 * 24 * 3600);

    // self.search(false, ts, e);
    self.searchWallet(ts);
  }

  if (w.loaded)
    search(w.lastTs);
  else
    w.once('load', function() { search(w.lastTs) });

  return e;
};

Pool.prototype.removeWallet = function removeWallet(w) {
  var i = this.wallets.indexOf(w);
  if (i == -1)
    return;
  this.wallets.splice(i, 1);
  this.unwatchWallet(w);
};

Pool.prototype.watchWallet = function watchWallet(w) {
  if (w.type === 'scripthash') {
    // For the redeem script hash in outputs:
    this.watch(w.getScriptHash());
    // For the redeem script in inputs:
    this.watch(w.getScript());
  }

  // For the pubkey hash in outputs:
  this.watch(w.getKeyHash());
  // For the pubkey in inputs:
  this.watch(w.getPublicKey());
};

Pool.prototype.unwatchWallet = function unwatchWallet(w) {
  if (w.type === 'scripthash') {
    // For the redeem script hash in p2sh outputs:
    this.unwatch(w.getScriptHash());
    // For the redeem script in p2sh inputs:
    this.unwatch(w.getScript());
  }

  // For the pubkey hash in p2pk/multisig outputs:
  this.unwatch(w.getKeyHash());
  // For the pubkey in p2pkh inputs:
  this.unwatch(w.getPublicKey());
};

Pool.prototype.searchWallet = function(w) {
  var self = this;
  var ts;

  if (this.options.fullNode)
    return;

  if (!w) {
    ts = this.wallets.reduce(function(ts, w) {
      if (w.lastTs < ts)
        return w.lastTs;
      return ts;
    }, Infinity);
    assert(ts !== Infinity);
  } else if (typeof w === 'number') {
    ts = w;
  } else {
    if (!w.loaded) {
      w.once('load', function() {
        self.searchWallet(w);
      });
      return;
    }
    ts = w.lastTs;
    if (!ts)
      ts = utils.now() - 7 * 24 * 3600;
  }

  utils.nextTick(function() {
    self.emit('debug', 'Wallet time: %s', new Date(ts * 1000));
    self.emit('debug',
      'Reverted chain to height=%d (%s)',
      self.chain.height(),
      new Date(self.chain.getTip().ts * 1000)
    );
  });

  // this.search(ts);
  this.chain.resetTime(ts);
};

Pool.prototype.search = function search(id, range, e) {
  var self = this;
  var hashes, pending, listener, timeout, done, total, cb;

  if (this.options.fullNode)
    return;

  if (typeof e === 'function') {
    cb = e;
    e = null;
  }

  e = e || new EventEmitter();

  // Optional id argument
  if ((id !== null
      && typeof id === 'object'
      && !utils.isBuffer(id))
      || typeof id === 'number') {
    range = id;
    id = null;
  }

  if (typeof id === 'string')
    id = utils.toArray(id, 'hex');

  if (typeof range === 'number')
    range = { start: range, end: null };
  else if (range)
    range = { start: range.start, end: range.end };
  else
    range = { start: 0, end: 0 };

  // Last 5 days by default, this covers 1000 blocks that we have in the
  // chain by default
  if (!range.end)
    range.end = utils.now();
  if (!range.start)
    range.start = utils.now() - 432000;

  if (cb) {
    e.once('end', function(empty) {
      if (empty)
        return cb(new Error('Not found.'), false);
      return cb(null, true);
    });
  }

  if (range.start < this.chain.index.lastTs) {
    if (id)
      this.watch(id);

    done = function(empty) {
      e.emit('end', empty);
      clearInterval(timeout);
      self.removeListener('block', listener);
      if (id)
        self.unwatch(id);
    };

    this.on('block', listener = function(block) {
      if (block.ts >= range.end)
        done();
    });

    // Estimated number of blocks in time range
    total = (range.end - range.start) / network.powTargetSpacing | 0;

    if (total === 0)
      total = 1;

    // 500 blocks every 3 seconds
    total = (total / 500 | 0) * 3;

    // Add half the total time and convert to ms
    total = (total + Math.ceil(total / 2)) * 1000;

    timeout = setTimeout(done.bind(null, true), total);

    this.chain.resetTime(range.start);

    if (this.peers.load)
      this.peers.load.destroy();

    this._load();

    return e;
  }

  hashes = this.chain.hashRange(range.start, range.end);
  pending = hashes.length;

  if (hashes.length === 0) {
    bcoin.utils.nextTick(function() {
      e.emit('end', true);
    });
    return e;
  }

  if (id)
    this.watch(id);
<commit_msg>pool: add startSync and stopSync.<commit_after>/**
 * pool.js - peer management for bcoin
 * Copyright (c) 2014-2015, <NAME> (MIT License)
 * https://github.com/indutny/bcoin
 */

var async = require('async');
var inherits = require('inherits');
var EventEmitter = require('events').EventEmitter;

var bcoin = require('../bcoin');
var utils = bcoin.utils;
var assert = utils.assert;
var network = bcoin.protocol.network;

/**
 * Pool
 */

function Pool(options) {
  var self = this;
  var Chain;

  if (!(this instanceof Pool))
    return new Pool(options);

  EventEmitter.call(this);

  this.options = options || {};

  if (this.options.network)
    network.set(this.options.network);

  this.options.fullNode = !!this.options.fullNode;
  this.options.headers = this.options.headers;
  this.options.multiplePeers = this.options.multiplePeers;
  this.options.relay = this.options.relay == null
    ? (this.options.fullNode ? true : false)
    : this.options.relay;

  this.storage = this.options.storage;
  this.destroyed = false;
  this.size = options.size || 32;
  this.parallel = options.parallel || 2000;
  this.redundancy = options.redundancy || 1;
  this.seeds = options.seeds
    ? options.seeds.slice()
    : network.seeds.slice();

  this._createConnection = options.createConnection;
  this._createSocket = options.createSocket;

  if (!this.options.fullNode) {
    if (this.options.headers == null)
      this.options.headers = true;
    if (this.options.multiplePeers == null)
      this.options.multiplePeers = true;
  } else {
    if (this.options.headers == null)
      this.options.headers = false;
    if (this.options.multiplePeers == null)
      this.options.multiplePeers = false;
  }

  if (!this.options.headers)
    this.options.multiplePeers = false;

  this.syncing = false;

  this.backoff = {
    delta: options.backoffDelta || 500,
    max: options.backoffMax || 5000
  };

  this.load = {
    timeout: options.loadTimeout || 30000,
    interval: options.loadInterval || 5000,
    window: options.loadWindow || 250,
    lastRange: null,
    rangeWindow: options.rangeWindow || 1000,
    timer: null,
    lwm: options.lwm || this.parallel * 2,
    hwm: options.hwm || this.parallel * 8,
    hiReached: false
  };

  this.maxRetries = options.maxRetries || 42;
  this.requestTimeout = options.requestTimeout || 10000;

  this.chain = new bcoin.chain({
    storage: this.storage,
    fullNode: this.options.fullNode
  });

  this.watchMap = {};

  this.bloom = new bcoin.bloom(
    8 * 1024,
    10,
    (Math.random() * 0xffffffff) | 0
  );

  this.peers = {
    // Peers that are loading blocks themselves
    block: [],
    // Peers that are still connecting
    pending: [],
    // Peers that are loading block ids
    load: null,
    // All peers
    all: []
  };

  this.block = {
    bestHeight: 0,
    bestHash: null,
    type: this.options.fullNode ? 'block' : 'filtered'
  };

  this.request = {
    map: {},
    active: 0,
    queue: []
  };

  this.validate = {
    // 5 days scan delta for obtaining TXs
    delta: 5 * 24 * 3600,

    // Minimum verification depth
    minDepth: options.minValidateDepth || 0,

    // getTX map
    map: {}
  };

  // Currently broadcasted TXs
  this.inv = {
    list: [],
    timeout: options.txTimeout || 60000
  };

  // Added and watched wallets
  this.options.wallets = this.options.wallets || [];
  this.wallets = [];

  this.chain.on('debug', function() {
    var args = Array.prototype.slice.call(arguments);
    self.emit.apply(self, ['debug'].concat(args));
  });

  Pool.global = this;

  this.loading = true;

  this.chain.once('load', function() {
    self.loading = false;
    self.emit('load');
    self._init();
  });
}

inherits(Pool, EventEmitter);

Pool.prototype._init = function _init() {
  var self = this;
  var i;

  this._addLoader();

  for (i = 0; i < this.size; i++)
    this._addPeer(0);

  this.chain.on('block', function(block, peer) {
    self.emit('block', block, peer);
  });

  this.chain.on('fork', function(data) {
    var peer = self.peers.load;

    this.emit('debug',
      'Fork at height %d: expected=%s received=%s checkpoint=%s',
      data.height,
      utils.revHex(data.expected),
      utils.revHex(data.received),
      data.checkpoint
    );

    if (!peer)
      return;

    peer.destroy();
  });

  this.options.wallets.forEach(function(w) {
    self.addWallet(w);
  });
};

Pool.prototype._startTimer = function _startTimer() {
  var self = this;

  this._stopTimer();

  function destroy() {
    if (!self.syncing)
      return;

    // Chain is full and up-to-date
    if (self.chain.isFull()) {
      self._stopTimer();
      self.emit('full');
      return;
    }

    if (self.peers.load)
      self.peers.load.destroy();
  }

  this._timer = setTimeout(destroy, this.load.timeout);
};

Pool.prototype._stopTimer = function _stopTimer() {
  if (!this._timer)
    return;

  clearTimeout(this._timer);
  delete this._timer;
};

Pool.prototype._startInterval = function _startInterval() {
  var self = this;

  this._stopInterval();

  function load() {
    if (!self.syncing)
      return;
    // self._load();
  }

  this._interval = setInterval(load, this.load.interval);
};

Pool.prototype._stopInterval = function _stopInterval() {
  if (!this._interval)
    return;

  clearInterval(this._interval);
  delete this._interval;
};

Pool.prototype.createConnection = function createConnection(peer, pool) {
  var addr, parts, host, net, socket, port;

  if (pool._createConnection)
    return pool._createConnection(peer, pool);

  addr = pool.usableSeed(true);

  parts = addr.split(':');
  host = parts[0];
  port = +parts[1] || network.port;

  peer.host = host;
  peer.port = port;

  if (this._createSocket) {
    socket = this._createSocket(port, host);
  } else {
    net = require('net');
    socket = net.connect(port, host);
  }

  socket.on('connect', function() {
    pool.emit('debug', 'Connected to %s:%d', host, port);
  });

  return socket;
};

Pool.prototype._addLoader = function _addLoader() {
  var self = this;
  var peer;

  if (this.destroyed)
    return;

  if (this.peers.load != null)
    return;

  peer = this._createPeer(750 * Math.random());

  peer.once('socket', function() {
    self.emit('debug', 'Added loader peer: %s', peer.host);
  });

  this.peers.load = peer;
  this.peers.all.push(peer);

  peer.once('close', function() {
    self._stopInterval();
    self._stopTimer();
    self._removePeer(peer);
    if (self.destroyed)
      return;
    self._addLoader();
  });

  peer.once('ack', function() {
    if (!self.syncing)
      return;
    peer.updateWatch();
    if (!self._load())
      self._stopTimer();
  });

  peer.on('merkleblock', function(block) {
    if (!self.syncing)
      return;
    // If the peer sent us a block that was added
    // to the chain (not orphans), reset the timeout.
    if (self._handleBlock(block, peer)) {
      self._startInterval();
      self._startTimer();
    }
  });

  peer.on('block', function(block) {
    if (!self.syncing)
      return;
    // If the peer sent us a block that was added
    // to the chain (not orphans), reset the timeout.
    if (self._handleBlock(block, peer)) {
      self._startInterval();
      self._startTimer();
    }
  });

  if (self.options.headers) {
    peer.on('blocks', function(hashes) {
      if (!self.syncing)
        return;
      self._handleInv(hashes, peer);
    });

    peer.on('headers', function(headers) {
      if (!self.syncing)
        return;
      self._handleHeaders(headers, peer);
    });
  } else {
    peer.on('blocks', function(hashes) {
      if (!self.syncing)
        return;
      self._handleBlocks(hashes, peer);
    });
  }
};

Pool.prototype.startSync = function startSync() {
  this.syncing = true;

  this._startInterval();
  this._startTimer();

  if (!this.peers.load) {
    this._addLoader();
    return;
  }

  if (this.peers.load.ack) {
    if (!this._load())
      this._stopTimer();
  }
};

Pool.prototype.stopSync = function stopSync() {
  if (!this.syncing)
    return;

  this.syncing = false;

  this._stopInterval();
  this._stopTimer();
};

Pool.prototype._handleHeaders = function _handleHeaders(headers, peer) {
  var i, header, last, block;

  assert(this.options.headers);

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

  this.emit('debug',
    'Recieved %s headers from %s',
    headers.length,
    peer.host);

  if (headers.length > 2000) {
    peer.destroy();
    return;
  }

  this.emit('headers', headers);

  for (i = 0; i < headers.length; i++) {
    block = bcoin.block(headers[i], 'header');

    if (last && block.prevBlock !== last.hash('hex'))
      break;

    if (!block.verify())
      break;

    if (!this.chain.has(block))
      this._request(this.block.type, block.hash('hex'));

    last = block;
  }

  // Restart the getheaders process
  // Technically `last` is not indexed yet so
  // the locator hashes will not be entirely
  // accurate. However, it shouldn't matter
  // that much since FindForkInGlobalIndex
  // simply tries to find the latest block in
  // the peer's chain.
  if (last && headers.length === 2000)
    peer.loadHeaders(this.chain.locatorHashes(last), null);

  // Push our getdata packet
  this._scheduleRequests();

  // Reset interval to avoid calling getheaders unnecessarily
  this._startInterval();

  // Reset timeout to avoid killing the loader
  this._startTimer();
};

Pool.prototype._handleBlocks = function _handleBlocks(hashes, peer) {
  var i, hash;

  assert(!this.options.headers);

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

  this.emit('debug',
    'Recieved %s block hashes from %s',
    hashes.length,
    peer.host);

  if (hashes.length > 500) {
    peer.destroy();
    return;
  }

  this.emit('blocks', hashes);

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

    // Resolve orphan chain
    if (this.chain.hasOrphan(hash)) {
      peer.loadBlocks(
        this.chain.locatorHashes(),
        this.chain.getOrphanRoot(hash)
      );
      continue;
    }

    // Request block if we don't have it or if
    // this is the last hash: this is done as
    // a failsafe because we _need_ to request
    // the hashContinue no matter what.
    if (!this.chain.has(hash) || i === hashes.length - 1) {
      this._request(this.block.type, hash);
      continue;
    }
  }

  // Push our getdata packet
  this._scheduleRequests();

  // Reset interval to avoid calling getblocks unnecessarily
  this._startInterval();

  // Reset timeout to avoid killing the loader
  this._startTimer();
};

Pool.prototype._handleInv = function _handleInv(hashes, peer) {
  var i, hash;

  // Ignore for now if we're still syncing
  if (!this.chain.isFull())
    return;

  for (i = 0; i < hashes.length; i++) {
    hash = utils.toHex(hashes[i]);
    if (!this.chain.has(hash)) {
      if (this.options.headers)
        this.peers.load.loadHeaders(this.chain.locatorHashes(), hash);
      else
        this._request(this.block.type, hash);
    }
  }

  if (!this.options.headers)
    this._scheduleRequests();
};

Pool.prototype._handleBlock = function _handleBlock(block, peer) {
  var self = this;

  var requested = this._response(block);

  // Emulate bip37 - emit all the "watched" txs
  if (this.options.fullNode && this.listeners('watched').length > 0) {
    block.txs.forEach(function(tx) {
      if (self.isWatched(tx))
        self.emit('watched', tx, peer);
    });
  }

  // Ignore if we already have
  if (this.chain.has(block)) {
    this.emit('debug', 'Already have block %s (%s)', block.height, peer.host);
    return false;
  }

  // Make sure the block is valid
  if (!block.verify()) {
    this.emit('debug',
      'Block verification failed for %s (%s)',
      block.rhash, peer.host);
    return false;
  }

  // Someone is sending us blocks without us requesting them.
  if (!requested) {
    this.emit('debug',
      'Recieved unrequested block: %s (%s)',
      block.rhash, peer.host);
  }

  // Resolve orphan chain
  if (!this.options.headers) {
    if (!this.chain.hasBlock(block.prevBlock)) {
      // NOTE: If we were to emit new orphans here, we
      // would not need to store full blocks as orphans.
      // However, the listener would not be able to see
      // the height until later.
      if (this._addIndex(block, peer))
        this.emit('pool block', block, peer);
      this.peers.load.loadBlocks(
        this.chain.locatorHashes(),
        this.chain.getOrphanRoot(block)
      );
      this.emit('debug', 'Handled orphan %s (%s)', block.rhash, peer.host);
      return false;
    }
  }

  // Add to index and emit/save
  if (this._addIndex(block, peer)) {
    this.emit('pool block', block, peer);
    return true;
  }
};

Pool.prototype._addIndex = function _addIndex(block, peer) {
  var added = this.chain.add(block, peer);

  this.emit('chain-progress', this.chain.fillPercent(), peer);

  return added > 0;
};

Pool.prototype.isFull = function isFull() {
  return this.chain.isFull();
};

Pool.prototype._loadRange = function _loadRange(hashes, force) {
  var now = +new Date();
  var last;

  if (this.options.fullNode)
    return;

  if (!hashes)
    return;

  if (hashes.length <= 1)
    return;

  // Limit number of requests
  if (!force && now - this.load.lastRange < this.load.rangeWindow)
    return;

  this.load.lastRange = now;

  if (!this.peers.load)
    this._addLoader();

  last = hashes[hashes.length - 1];

  hashes.slice(0, -1).forEach(function(hash) {
    this.peers.load.loadItems([hash], last);
  }, this);
};

Pool.prototype._load = function _load() {
  var self = this;
  var next;

  if (!this.syncing)
    return true;

  if (this.request.queue.length >= this.load.hwm) {
    this.load.hiReached = true;
    return false;
  }

  this.load.hiReached = false;

  if (!this.peers.load) {
    this._addLoader();
    return true;
  }

  if (this.options.headers)
    this.peers.load.loadHeaders(this.chain.locatorHashes(), null);
  else
    this.peers.load.loadBlocks(this.chain.locatorHashes(), null);

  return true;
};

Pool.prototype.loadMempool = function loadMempool() {
  this.pool.peers.block.forEach(function(peer) {
    peer.loadMempool();
  });
};

Pool.prototype._createPeer = function _createPeer(backoff) {
  var self = this;

  var peer = new bcoin.peer(this, this.createConnection, {
    backoff: backoff,
    startHeight: this.options.startHeight,
    relay: this.options.relay
  });

  peer._retry = 0;

  peer.on('error', function(err) {
    self.emit('error', err, peer);
  });

  peer.on('debug', function() {
    var args = Array.prototype.slice.call(arguments);
    self.emit.apply(self, ['debug'].concat(args));
  });

  peer.on('reject', function(payload) {
    self.emit('debug',
      'Reject: msg=%s ccode=%s reason=%s data=%s',
      payload.message, payload.ccode, payload.reason,
      utils.toHex(payload.data));
    self.emit('reject', payload, peer);
  });

  peer.on('notfound', function(items) {
    items.forEach(function(item) {
      var req = self.request.map[utils.toHex(item.hash)];
      if (req && req.peer === peer)
        req.finish(null);
    });
  });

  peer.on('tx', function(tx) {
    self._response(tx);
    self.emit('tx', tx, peer);

    if (!self.options.fullNode && tx.block)
      self.emit('watched', tx, peer);
  });

  peer.on('addr', function(addr) {
    var host = addr.ipv4 + ':' + addr.port;

    if (self.seeds.length > self.size * 2)
      self.seeds = network.seeds.slice();

    if (self.seeds.indexOf(host) !== -1) {
      self.emit('debug', 'Found new peer: %s', host);
      self.seeds.push(host);
    }

    self.emit('addr', addr, peer);
  });

  peer.on('txs', function(txs) {
    self.emit('txs', txs, peer);
  });

  peer.on('version', function(version) {
    if (version.height > self.block.bestHeight)
      self.block.bestHeight = version.height;
    self.emit('version', version, peer);
  });

  return peer;
};

Pool.prototype._addPeer = function _addPeer(backoff) {
  var self = this;
  var peer;

  if (this.destroyed)
    return;

  if (this.peers.block.length + this.peers.pending.length >= this.size)
    return;

  if (!this._createConnection && !this.usableSeed()) {
    setTimeout(this._addPeer.bind(this, 0), this.backoff.max);
    return;
  }

  peer = this._createPeer(backoff);

  this.peers.pending.push(peer);
  this.peers.all.push(peer);

  peer.once('close', function() {
    self._removePeer(peer);
    if (self.destroyed)
      return;
    self._addPeer(Math.min(backoff + self.backoff.delta, self.backoff.max));
  });

  peer.once('ack', function() {
    var i;

    if (self.destroyed)
      return;

    i = self.peers.pending.indexOf(peer);
    if (i !== -1) {
      self.peers.pending.splice(i, 1);
      self.peers.block.push(peer);
    }

    peer.updateWatch();

    self.inv.list.forEach(function(entry) {
      var result = peer.broadcast(entry.msg);
      if (!result)
        return;

      result[0].once('request', function() {
        entry.e.emit('ack', peer);
      });
    });

    self._scheduleRequests();
  });

  peer.on('merkleblock', function(block) {
    backoff = 0;
    self._handleBlock(block, peer);
  });

  peer.on('block', function(block) {
    backoff = 0;
    self._handleBlock(block, peer);
  });

  peer.on('blocks', function(hashes) {
    self._handleInv(hashes, peer);
  });

  utils.nextTick(function() {
    self.emit('peer', peer);
  });
};

Pool.prototype.bestPeer = function bestPeer() {
  return this.peers.block.reduce(function(best, peer) {
    if (!peer.version || !peer.socket)
      return;

    if (!best || peer.version.height > best.version.height)
      return peer;

    return best;
  }, null);
};

Pool.prototype._removePeer = function _removePeer(peer) {
  var i = this.peers.pending.indexOf(peer);
  if (i !== -1)
    this.peers.pending.splice(i, 1);

  i = this.peers.block.indexOf(peer);
  if (i !== -1)
    this.peers.block.splice(i, 1);

  i = this.peers.all.indexOf(peer);
  if (i !== -1)
    this.peers.all.splice(i, 1);

  if (this.peers.load === peer) {
    this.emit('debug', 'Removed loader peer (%s).', peer.host);
    this.peers.load = null;
  }
};

Pool.prototype.watch = function watch(id) {
  var hid, i;

  if (id instanceof bcoin.wallet) {
    this.watchWallet(id);
    return;
  }

  if (id) {
    hid = utils.toHex(id);
    if (this.watchMap[hid])
      this.watchMap[hid]++;
    else
      this.watchMap[hid] = 1;

    if (this.bloom.test(id, 'hex'))
      return;

    this.bloom.add(id, 'hex');
  }

  if (this.peers.load)
    this.peers.load.updateWatch();

  for (i = 0; i < this.peers.block.length; i++)
    this.peers.block[i].updateWatch();
};

Pool.prototype.unwatch = function unwatch(id) {
  var i;

  id = utils.toHex(id);

  if (!this.bloom.test(id, 'hex'))
    return;

  if (!this.watchMap[id] || --this.watchMap[id] !== 0)
    return;

  delete this.watchMap[id];

  // Reset bloom filter
  this.bloom.reset();
  Object.keys(this.watchMap).forEach(function(id) {
    this.bloom.add(id, 'hex');
  }, this);

  // Resend it to peers
  if (this.peers.load)
    this.peers.load.updateWatch();

  for (i = 0; i < this.peers.block.length; i++)
    this.peers.block[i].updateWatch();
};

// See "Filter matching algorithm":
// https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
Pool.prototype.isWatched = function(tx, bloom) {
  var i, input, output, prev;

  if (!bloom)
    bloom = this.bloom;

  function testScript(script) {
    return script.some(function(chunk) {
      if (!Array.isArray(chunk) || chunk.length === 0)
        return false;
      return bloom.test(chunk);
    });
  }

  // 1. Test the tx hash
  if (bloom.test(tx.hash()))
    return true;

  // 2. Test data elements in output scripts
  //    (may need to update filter on match)
  for (i = 0; i < tx.outputs.length; i++) {
    output = tx.outputs[i];
    // Test the output script
    if (testScript(output.script))
      return true;
  }

  // 3. Test prev_out structure
  // 4. Test data elements in input scripts
  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];
    prev = input.out.hash;

    if (typeof prev === 'string')
      prev = utils.toArray(prev, 'hex');

    // Test the prev_out tx hash
    if (bloom.test(prev))
      return true;

    // Test the prev_out script
    if (input.out.tx) {
      prev = input.out.tx.outputs[input.out.index];
      if (testScript(prev.script))
        return true;
    }

    // Test the input script
    if (testScript(input.script))
      return true;
  }

  // 5. No match
  return false;
};

Pool.prototype.addWallet = function addWallet(w, defaultTs) {
  var self = this;
  var e;

  if (this.wallets.indexOf(w) !== -1)
    return false;

  this.watchWallet(w);
  this.wallets.push(w);

  e = new EventEmitter();

  function search(ts) {
    // Relay pending TXs
    // NOTE: It is important to do it after search, because search could
    // add TS to pending TXs, thus making them confirmed
    w.pending().forEach(function(tx) {
      self.sendTX(tx);
    });

    if (self.options.fullNode)
      return;

    // Search for last week by default
    if (!ts)
      ts = defaultTs || (utils.now() - 7 * 24 * 3600);

    // self.search(false, ts, e);
    self.searchWallet(ts);
  }

  if (w.loaded)
    search(w.lastTs);
  else
    w.once('load', function() { search(w.lastTs) });

  return e;
};

Pool.prototype.removeWallet = function removeWallet(w) {
  var i = this.wallets.indexOf(w);
  if (i == -1)
    return;
  this.wallets.splice(i, 1);
  this.unwatchWallet(w);
};

Pool.prototype.watchWallet = function watchWallet(w) {
  if (w.type === 'scripthash') {
    // For the redeem script hash in outputs:
    this.watch(w.getScriptHash());
    // For the redeem script in inputs:
    this.watch(w.getScript());
  }

  // For the pubkey hash in outputs:
  this.watch(w.getKeyHash());
  // For the pubkey in inputs:
  this.watch(w.getPublicKey());
};

Pool.prototype.unwatchWallet = function unwatchWallet(w) {
  if (w.type === 'scripthash') {
    // For the redeem script hash in p2sh outputs:
    this.unwatch(w.getScriptHash());
    // For the redeem script in p2sh inputs:
    this.unwatch(w.getScript());
  }

  // For the pubkey hash in p2pk/multisig outputs:
  this.unwatch(w.getKeyHash());
  // For the pubkey in p2pkh inputs:
  this.unwatch(w.getPublicKey());
};

Pool.prototype.searchWallet = function(w) {
  var self = this;
  var ts;

  if (this.options.fullNode)
    return;

  if (!w) {
    ts = this.wallets.reduce(function(ts, w) {
      if (w.lastTs < ts)
        return w.lastTs;
      return ts;
    }, Infinity);
    assert(ts !== Infinity);
  } else if (typeof w === 'number') {
    ts = w;
  } else {
    if (!w.loaded) {
      w.once('load', function() {
        self.searchWallet(w);
      });
      return;
    }
    ts = w.lastTs;
    if (!ts)
      ts = utils.now() - 7 * 24 * 3600;
  }

  utils.nextTick(function() {
    self.emit('debug', 'Wallet time: %s', new Date(ts * 1000));
    self.emit('debug',
      'Reverted chain to height=%d (%s)',
      self.chain.height(),
      new Date(self.chain.getTip().ts * 1000)
    );
  });

  this.chain.resetTime(ts);
};

Pool.prototype.search = function search(id, range, e) {
  var self = this;
  var hashes, pending, listener, timeout, done, total, cb;

  if (this.options.fullNode)
    return;

  if (typeof e === 'function') {
    cb = e;
    e = null;
  }

  e = e || new EventEmitter();

  // Optional id argument
  if ((id !== null
      && typeof id === 'object'
      && !utils.isBuffer(id))
      || typeof id === 'number') {
    range = id;
    id = null;
  }

  if (typeof id === 'string')
    id = utils.toArray(id, 'hex');

  if (typeof range === 'number')
    range = { start: range, end: null };
  else if (range)
    range = { start: range.start, end: range.end };
  else
    range = { start: 0, end: 0 };

  // Last 5 days by default, this covers 1000 blocks that we have in the
  // chain by default
  if (!range.end)
    range.end = utils.now();
  if (!range.start)
    range.start = utils.now() - 432000;

  if (cb) {
    e.once('end', function(empty) {
      if (empty)
        return cb(new Error('Not found.'), false);
      return cb(null, true);
    });
  }

  if (range.start < this.chain.index.lastTs) {
    if (id)
      this.watch(id);

    done = function(empty) {
      e.emit('end', empty);
      clearInterval(timeout);
      self.removeListener('block', listener);
      if (id)
        self.unwatch(id);
    };

    this.on('block', listener = function(block) {
      if (block.ts >= range.end)
        done();
    });

    // Estimated number of blocks in time range
    total = (range.end - range.start) / network.powTargetSpacing | 0;

    if (total === 0)
      total = 1;

    // 500 blocks every 3 seconds
    total = (total / 500 | 0) * 3;

    // Add half the total time and convert to ms
    total = (total + Math.ceil(total / 2)) * 1000;

    timeout = setTimeout(done.bind(null, true), total);

    this.chain.resetTime(range.start);

    this.stopSync();

    if (this.peers.load)
      this.peers.load.destroy();

    this.startSync();

    return e;
  }

  hashes = this.chain.hashRange(range.start, range.end);
  pending = hashes.length;

  if (hashes.length === 0) {
    bcoin.utils.nextTick(function() {
      e.emit('end', true);
    });
    return e;
  }

  if (id)
    this.watch(id);

{'fraction_non_alphanumeric': 0.15872190674238182, 'fraction_numerical': 0.007838118851475645, 'mean_word_length': 2.72536312849162, 'pattern_counts': {'":': 2, '<': 29, '<?xml version=': 0, '>': 25, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6371163', 'n_tokens_mistral': 17633, 'n_tokens_neox': 16876, 'n_words': 3928}