diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index fa74c7c4..f895074b 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -26,24 +26,12 @@ const encoding = require('bbuf/lib/encoding'); const thresholdStates = common.thresholdStates; /** - * Represents a blockchain. + * Blockchain * @alias module:blockchain.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.maxOrphans - * @param {Boolean?} options.spv - * @property {Boolean} loaded - * @property {ChainDB} db - Note that Chain `options` will be passed - * to the instantiated ChainDB. - * @property {Lock} locker - * @property {Object} invalid + * @property {ChainDB} db * @property {ChainEntry?} tip * @property {Number} height * @property {DeploymentState} state - * @property {Object} orphan - Orphan map. * @emits Chain#open * @emits Chain#error * @emits Chain#block @@ -54,2898 +42,2904 @@ const thresholdStates = common.thresholdStates; * @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); +class Chain extends AsyncEmitter { + /** + * Create a blockchain. + * @constructor + * @param {Object} options + */ - AsyncEmitter.call(this); + constructor(options) { + super(); - this.opened = false; - this.options = new ChainOptions(options); + this.opened = false; + this.options = new ChainOptions(options); - this.network = this.options.network; - this.logger = this.options.logger.context('chain'); - this.workers = this.options.workers; + this.network = this.options.network; + this.logger = this.options.logger.context('chain'); + this.workers = this.options.workers; - this.db = new ChainDB(this.options); + this.db = new ChainDB(this.options); - this.locker = new Lock(true); - this.invalid = new LRU(100); - this.state = new DeploymentState(); + this.locker = new Lock(true); + this.invalid = new LRU(100); + this.state = new DeploymentState(); - this.tip = new ChainEntry(); - this.height = -1; - this.synced = false; + this.tip = new ChainEntry(); + this.height = -1; + this.synced = false; - this.orphanMap = new Map(); - this.orphanPrev = new Map(); -} + this.orphanMap = new Map(); + this.orphanPrev = new Map(); + } -Object.setPrototypeOf(Chain.prototype, AsyncEmitter.prototype); + /** + * Open the chain, wait for the database to load. + * @returns {Promise} + */ -/** - * Size of set to pick median time from. - * @const {Number} - * @default - */ + async open() { + assert(!this.opened, 'Chain is already open.'); + this.opened = true; -Chain.MEDIAN_TIMESPAN = 11; + this.logger.info('Chain is loading.'); -/** - * Open the chain, wait for the database to load. - * @returns {Promise} - */ + if (this.options.checkpoints) + this.logger.info('Checkpoints are enabled.'); -Chain.prototype.open = async function open() { - assert(!this.opened, 'Chain is already open.'); - this.opened = true; + if (this.options.coinCache) + this.logger.info('Coin cache is enabled.'); - this.logger.info('Chain is loading.'); + if (this.options.bip91) + this.logger.warning('BIP91 enabled. Segsignal will be enforced.'); - if (this.options.checkpoints) - this.logger.info('Checkpoints are enabled.'); + if (this.options.bip148) + this.logger.warning('BIP148 enabled. UASF will be enforced.'); - if (this.options.coinCache) - this.logger.info('Coin cache is enabled.'); + await this.db.open(); - if (this.options.bip91) - this.logger.warning('BIP91 enabled. Segsignal will be enforced.'); + const tip = await this.db.getTip(); - if (this.options.bip148) - this.logger.warning('BIP148 enabled. UASF will be enforced.'); + assert(tip); - await this.db.open(); + this.tip = tip; + this.height = tip.height; - const tip = await this.db.getTip(); + this.logger.info('Chain Height: %d', tip.height); - assert(tip); + this.logger.memory(); - this.tip = tip; - this.height = tip.height; + const state = await this.getDeploymentState(); - this.logger.info('Chain Height: %d', tip.height); + this.setDeploymentState(state); - this.logger.memory(); + this.logger.memory(); - const state = await this.getDeploymentState(); + this.emit('tip', tip); - this.setDeploymentState(state); + this.maybeSync(); + } - this.logger.memory(); + /** + * Close the chain, wait for the database to close. + * @returns {Promise} + */ - this.emit('tip', tip); + async close() { + assert(this.opened, 'Chain is not open.'); + this.opened = false; + return this.db.close(); + } - this.maybeSync(); -}; + /** + * Perform all necessary contextual verification on a block. + * @private + * @param {Block} block + * @param {ChainEntry} prev + * @param {Number} flags + * @returns {Promise} - Returns {@link ContextResult}. + */ -/** - * Close the chain, wait for the database to close. - * @returns {Promise} - */ + async verifyContext(block, prev, flags) { + // Initial non-contextual verification. + const state = await this.verify(block, prev, flags); -Chain.prototype.close = async function close() { - assert(this.opened, 'Chain is not open.'); - this.opened = false; - return this.db.close(); -}; + // Skip everything if we're in SPV mode. + if (this.options.spv) { + const view = new CoinView(); + return [view, state]; + } -/** - * Perform all necessary contextual verification on a block. - * @private - * @param {Block} block - * @param {ChainEntry} prev - * @param {Number} flags - * @returns {Promise} - Returns {@link ContextResult}. - */ + // Skip everything if we're using checkpoints. + if (this.isHistorical(prev)) { + const view = await this.updateInputs(block, prev); + return [view, state]; + } -Chain.prototype.verifyContext = async function verifyContext(block, prev, flags) { - // Initial non-contextual verification. - const state = await this.verify(block, prev, flags); + // BIP30 - Verify there are no duplicate txids. + // Note that BIP34 made it impossible to create + // duplicate txids. + if (!state.hasBIP34()) + await this.verifyDuplicates(block, prev); + + // Verify scripts, spend and add coins. + const view = await this.verifyInputs(block, prev, state); - // Skip everything if we're in SPV mode. - if (this.options.spv) { - const view = new CoinView(); return [view, state]; } - // Skip everything if we're using checkpoints. - if (this.isHistorical(prev)) { - const view = await this.updateInputs(block, prev); - return [view, state]; + /** + * Perform all necessary contextual verification + * on a block, without POW check. + * @param {Block} block + * @returns {Promise} + */ + + async verifyBlock(block) { + const unlock = await this.locker.lock(); + try { + return await this._verifyBlock(block); + } finally { + unlock(); + } } - // BIP30 - Verify there are no duplicate txids. - // Note that BIP34 made it impossible to create - // duplicate txids. - if (!state.hasBIP34()) - await this.verifyDuplicates(block, prev); + /** + * Perform all necessary contextual verification + * on a block, without POW check (no lock). + * @private + * @param {Block} block + * @returns {Promise} + */ - // Verify scripts, spend and add coins. - const view = await this.verifyInputs(block, prev, state); - - return [view, state]; -}; - -/** - * Perform all necessary contextual verification - * on a block, without POW check. - * @param {Block} block - * @returns {Promise} - */ - -Chain.prototype.verifyBlock = async function verifyBlock(block) { - const unlock = await this.locker.lock(); - try { - return await this._verifyBlock(block); - } finally { - unlock(); - } -}; - -/** - * Perform all necessary contextual verification - * on a block, without POW check (no lock). - * @private - * @param {Block} block - * @returns {Promise} - */ - -Chain.prototype._verifyBlock = async function _verifyBlock(block) { - const flags = common.flags.DEFAULT_FLAGS & ~common.flags.VERIFY_POW; - return await this.verifyContext(block, this.tip, flags); -}; - -/** - * Test whether the hash is in the main chain. - * @param {Hash} hash - * @returns {Promise} - Returns Boolean. - */ - -Chain.prototype.isMainHash = function isMainHash(hash) { - return this.db.isMainHash(hash); -}; - -/** - * Test whether the entry is in the main chain. - * @param {ChainEntry} entry - * @returns {Promise} - Returns Boolean. - */ - -Chain.prototype.isMainChain = function isMainChain(entry) { - return this.db.isMainChain(entry); -}; - -/** - * Get ancestor by `height`. - * @param {ChainEntry} entry - * @param {Number} height - * @returns {Promise} - Returns ChainEntry. - */ - -Chain.prototype.getAncestor = function getAncestor(entry, height) { - return this.db.getAncestor(entry, height); -}; - -/** - * Get previous entry. - * @param {ChainEntry} entry - * @returns {Promise} - Returns ChainEntry. - */ - -Chain.prototype.getPrevious = function getPrevious(entry) { - return this.db.getPrevious(entry); -}; - -/** - * Get previous cached entry. - * @param {ChainEntry} entry - * @returns {ChainEntry|null} - */ - -Chain.prototype.getPrevCache = function getPrevCache(entry) { - return this.db.getPrevCache(entry); -}; - -/** - * Get next entry. - * @param {ChainEntry} entry - * @returns {Promise} - Returns ChainEntry. - */ - -Chain.prototype.getNext = function getNext(entry) { - return this.db.getNext(entry); -}; - -/** - * Get next entry. - * @param {ChainEntry} entry - * @returns {Promise} - Returns ChainEntry. - */ - -Chain.prototype.getNextEntry = function getNextEntry(entry) { - return this.db.getNextEntry(entry); -}; - -/** - * Calculate median time past. - * @param {ChainEntry} prev - * @param {Number?} time - * @returns {Promise} - Returns Number. - */ - -Chain.prototype.getMedianTime = async function getMedianTime(prev, time) { - let timespan = Chain.MEDIAN_TIMESPAN; - - const median = []; - - // In case we ever want to check - // the MTP of the _current_ block - // (necessary for BIP148). - if (time != null) { - median.push(time); - timespan -= 1; + async _verifyBlock(block) { + const flags = common.flags.DEFAULT_FLAGS & ~common.flags.VERIFY_POW; + return await this.verifyContext(block, this.tip, flags); } - let entry = prev; + /** + * Test whether the hash is in the main chain. + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ - for (let i = 0; i < timespan && entry; i++) { - median.push(entry.time); - - const cache = this.getPrevCache(entry); - - if (cache) - entry = cache; - else - entry = await this.getPrevious(entry); + isMainHash(hash) { + return this.db.isMainHash(hash); } - median.sort(cmp); + /** + * Test whether the entry is in the main chain. + * @param {ChainEntry} entry + * @returns {Promise} - Returns Boolean. + */ - return median[median.length >>> 1]; -}; - -/** - * Test whether the entry is potentially - * an ancestor of a checkpoint. - * @param {ChainEntry} prev - * @returns {Boolean} - */ - -Chain.prototype.isHistorical = function isHistorical(prev) { - if (this.options.checkpoints) { - if (prev.height + 1 <= this.network.lastCheckpoint) - return true; - } - return false; -}; - -/** - * Contextual verification for a block, including - * version deployments (IsSuperMajority), versionbits, - * coinbase height, finality checks. - * @private - * @param {Block} block - * @param {ChainEntry} prev - * @param {Number} flags - * @returns {Promise} - Returns {@link DeploymentState}. - */ - -Chain.prototype.verify = async function verify(block, prev, flags) { - assert(typeof flags === 'number'); - - // Extra sanity check. - if (block.prevBlock !== prev.hash) - throw new VerifyError(block, 'invalid', 'bad-prevblk', 0); - - // Verify a checkpoint if there is one. - const hash = block.hash('hex'); - if (!this.verifyCheckpoint(prev, hash)) { - throw new VerifyError(block, - 'checkpoint', - 'checkpoint mismatch', - 100); + isMainChain(entry) { + return this.db.isMainChain(entry); } - // Skip everything when using checkpoints. - // We can do this safely because every - // block in between each checkpoint was - // validated outside in the header chain. - if (this.isHistorical(prev)) { + /** + * Get ancestor by `height`. + * @param {ChainEntry} entry + * @param {Number} height + * @returns {Promise} - Returns ChainEntry. + */ + + getAncestor(entry, height) { + return this.db.getAncestor(entry, height); + } + + /** + * Get previous entry. + * @param {ChainEntry} entry + * @returns {Promise} - Returns ChainEntry. + */ + + getPrevious(entry) { + return this.db.getPrevious(entry); + } + + /** + * Get previous cached entry. + * @param {ChainEntry} entry + * @returns {ChainEntry|null} + */ + + getPrevCache(entry) { + return this.db.getPrevCache(entry); + } + + /** + * Get next entry. + * @param {ChainEntry} entry + * @returns {Promise} - Returns ChainEntry. + */ + + getNext(entry) { + return this.db.getNext(entry); + } + + /** + * Get next entry. + * @param {ChainEntry} entry + * @returns {Promise} - Returns ChainEntry. + */ + + getNextEntry(entry) { + return this.db.getNextEntry(entry); + } + + /** + * Calculate median time past. + * @param {ChainEntry} prev + * @param {Number?} time + * @returns {Promise} - Returns Number. + */ + + async getMedianTime(prev, time) { + let timespan = consensus.MEDIAN_TIMESPAN; + + const median = []; + + // In case we ever want to check + // the MTP of the _current_ block + // (necessary for BIP148). + if (time != null) { + median.push(time); + timespan -= 1; + } + + let entry = prev; + + for (let i = 0; i < timespan && entry; i++) { + median.push(entry.time); + + const cache = this.getPrevCache(entry); + + if (cache) + entry = cache; + else + entry = await this.getPrevious(entry); + } + + median.sort(cmp); + + return median[median.length >>> 1]; + } + + /** + * Test whether the entry is potentially + * an ancestor of a checkpoint. + * @param {ChainEntry} prev + * @returns {Boolean} + */ + + isHistorical(prev) { + if (this.options.checkpoints) { + if (prev.height + 1 <= this.network.lastCheckpoint) + return true; + } + return false; + } + + /** + * Contextual verification for a block, including + * version deployments (IsSuperMajority), versionbits, + * coinbase height, finality checks. + * @private + * @param {Block} block + * @param {ChainEntry} prev + * @param {Number} flags + * @returns {Promise} - Returns {@link DeploymentState}. + */ + + async verify(block, prev, flags) { + assert(typeof flags === 'number'); + + // Extra sanity check. + if (block.prevBlock !== prev.hash) + throw new VerifyError(block, 'invalid', 'bad-prevblk', 0); + + // Verify a checkpoint if there is one. + const hash = block.hash('hex'); + if (!this.verifyCheckpoint(prev, hash)) { + throw new VerifyError(block, + 'checkpoint', + 'checkpoint mismatch', + 100); + } + + // Skip everything when using checkpoints. + // We can do this safely because every + // block in between each checkpoint was + // validated outside in the header chain. + if (this.isHistorical(prev)) { + if (this.options.spv) + return this.state; + + // Once segwit is active, we will still + // need to check for block mutability. + if (!block.hasWitness() && !block.getCommitmentHash()) + return new DeploymentState(); + + flags &= ~common.flags.VERIFY_BODY; + } + + // Non-contextual checks. + if (flags & common.flags.VERIFY_BODY) { + const [valid, reason, score] = block.checkBody(); + + if (!valid) + throw new VerifyError(block, 'invalid', reason, score, true); + } + + // Ensure the POW is what we expect. + const bits = await this.getTarget(block.time, prev); + + if (block.bits !== bits) { + throw new VerifyError(block, + 'invalid', + 'bad-diffbits', + 100); + } + + // Skip all blocks in spv mode once + // we've verified the network target. if (this.options.spv) return this.state; - // Once segwit is active, we will still - // need to check for block mutability. - if (!block.hasWitness() && !block.getCommitmentHash()) - return new DeploymentState(); + // Ensure the timestamp is correct. + const mtp = await this.getMedianTime(prev); - flags &= ~common.flags.VERIFY_BODY; - } - - // Non-contextual checks. - if (flags & common.flags.VERIFY_BODY) { - const [valid, reason, score] = block.checkBody(); - - if (!valid) - throw new VerifyError(block, 'invalid', reason, score, true); - } - - // Ensure the POW is what we expect. - const bits = await this.getTarget(block.time, prev); - - if (block.bits !== bits) { - throw new VerifyError(block, - 'invalid', - 'bad-diffbits', - 100); - } - - // Skip all blocks in spv mode once - // we've verified the network target. - if (this.options.spv) - return this.state; - - // Ensure the timestamp is correct. - const mtp = await this.getMedianTime(prev); - - if (block.time <= mtp) { - throw new VerifyError(block, - 'invalid', - 'time-too-old', - 0); - } - - // Check timestamp against adj-time+2hours. - // If this fails we may be able to accept - // the block later. - if (block.time > this.network.now() + 2 * 60 * 60) { - throw new VerifyError(block, - 'invalid', - 'time-too-new', - 0, - true); - } - - // Calculate height of current block. - const height = prev.height + 1; - - // Only allow version 2 blocks (coinbase height) - // once the majority of blocks are using it. - if (block.version < 2 && height >= this.network.block.bip34height) - 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 && height >= this.network.block.bip66height) - 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 && height >= this.network.block.bip65height) - throw new VerifyError(block, 'obsolete', 'bad-version', 0); - - // Get the new deployment state. - const state = await this.getDeployments(block.time, prev); - - // Enforce BIP91/BIP148. - if (state.hasBIP91() || state.hasBIP148()) { - const {segwit} = this.network.deployments; - if (!consensus.hasBit(block.version, segwit.bit)) - throw new VerifyError(block, 'invalid', 'bad-no-segwit', 0); - } - - // Get timestamp for tx.isFinal(). - const time = state.hasMTP() ? mtp : block.time; - - // Transactions must be finalized with - // regards to nSequence and nLockTime. - for (const tx of block.txs) { - if (!tx.isFinal(height, time)) { + if (block.time <= mtp) { throw new VerifyError(block, 'invalid', - 'bad-txns-nonfinal', - 10); + 'time-too-old', + 0); } - } - // Make sure the height contained - // in the coinbase is correct. - if (state.hasBIP34()) { - if (block.getCoinbaseHeight() !== height) { + // Check timestamp against adj-time+2hours. + // If this fails we may be able to accept + // the block later. + if (block.time > this.network.now() + 2 * 60 * 60) { throw new VerifyError(block, 'invalid', - 'bad-cb-height', - 100); - } - } - - // Check the commitment hash for segwit. - let commit = null; - if (state.hasWitness()) { - commit = block.getCommitmentHash(); - if (commit) { - // These are totally malleable. Someone - // may have even accidentally sent us - // the non-witness version of the block. - // We don't want to consider this block - // "invalid" if either of these checks - // fail. - if (!block.getWitnessNonce()) { - throw new VerifyError(block, - 'invalid', - 'bad-witness-nonce-size', - 100, - true); - } - - if (!commit.equals(block.createCommitmentHash())) { - throw new VerifyError(block, - 'invalid', - 'bad-witness-merkle-match', - 100, - true); - } - } - } - - // Blocks that do not commit to - // witness data cannot contain it. - if (!commit) { - if (block.hasWitness()) { - throw new VerifyError(block, - 'invalid', - 'unexpected-witness', - 100, + 'time-too-new', + 0, true); } - } - - // Check block weight (different from block size - // check in non-contextual verification). - if (block.getWeight() > consensus.MAX_BLOCK_WEIGHT) { - throw new VerifyError(block, - 'invalid', - 'bad-blk-weight', - 100); - } - - return state; -}; - -/** - * Check all deployments on a chain, ranging from p2sh to segwit. - * @param {Number} time - * @param {ChainEntry} prev - * @returns {Promise} - Returns {@link DeploymentState}. - */ - -Chain.prototype.getDeployments = async function getDeployments(time, prev) { - const deployments = this.network.deployments; - const height = prev.height + 1; - const state = new DeploymentState(); - - // 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 (time >= consensus.BIP16_TIME) - state.flags |= Script.flags.VERIFY_P2SH; - - // Coinbase heights are now enforced (bip34). - if (height >= this.network.block.bip34height) - state.bip34 = true; - - // Signature validation is now enforced (bip66). - if (height >= this.network.block.bip66height) - state.flags |= Script.flags.VERIFY_DERSIG; - - // CHECKLOCKTIMEVERIFY is now usable (bip65). - if (height >= this.network.block.bip65height) - state.flags |= Script.flags.VERIFY_CHECKLOCKTIMEVERIFY; - - // CHECKSEQUENCEVERIFY and median time - // past locktimes are now usable (bip9 & bip113). - if (await this.isActive(prev, deployments.csv)) { - state.flags |= Script.flags.VERIFY_CHECKSEQUENCEVERIFY; - state.lockFlags |= common.lockFlags.VERIFY_SEQUENCE; - state.lockFlags |= common.lockFlags.MEDIAN_TIME_PAST; - } - - // Check the state of the segwit deployment. - const witness = await this.getState(prev, deployments.segwit); - - // Segregrated witness (bip141) is now usable - // along with SCRIPT_VERIFY_NULLDUMMY (bip147). - if (witness === thresholdStates.ACTIVE) { - state.flags |= Script.flags.VERIFY_WITNESS; - state.flags |= Script.flags.VERIFY_NULLDUMMY; - } - - // Segsignal is now enforced (bip91). - if (this.options.bip91) { - if (witness === thresholdStates.STARTED) { - if (await this.isActive(prev, deployments.segsignal)) - state.bip91 = true; - } - } - - // UASF is now enforced (bip148) (mainnet-only). - if (this.options.bip148 && this.network === Network.main) { - if (witness !== thresholdStates.LOCKED_IN - && witness !== thresholdStates.ACTIVE) { - // The BIP148 MTP check is nonsensical in - // that it includes the _current_ entry's - // timestamp. This requires some hackery, - // since bcoin only operates on the sane - // assumption that deployment checks should - // only ever examine the values of the - // previous block (necessary for mining). - const mtp = await this.getMedianTime(prev, time); - if (mtp >= 1501545600 && mtp <= 1510704000) - state.bip148 = true; - } - } - - return state; -}; - -/** - * Set a new deployment state. - * @param {DeploymentState} state - */ - -Chain.prototype.setDeploymentState = function setDeploymentState(state) { - if (this.options.checkpoints && this.height < this.network.lastCheckpoint) { - this.state = state; - return; - } - - if (!this.state.hasP2SH() && state.hasP2SH()) - this.logger.warning('P2SH has been activated.'); - - if (!this.state.hasBIP34() && state.hasBIP34()) - this.logger.warning('BIP34 has been activated.'); - - if (!this.state.hasBIP66() && state.hasBIP66()) - this.logger.warning('BIP66 has been activated.'); - - if (!this.state.hasCLTV() && state.hasCLTV()) - this.logger.warning('BIP65 has been activated.'); - - if (!this.state.hasCSV() && state.hasCSV()) - this.logger.warning('CSV has been activated.'); - - if (!this.state.hasWitness() && state.hasWitness()) - this.logger.warning('Segwit has been activated.'); - - if (!this.state.hasBIP91() && state.hasBIP91()) - this.logger.warning('BIP91 has been activated.'); - - if (!this.state.hasBIP148() && state.hasBIP148()) - this.logger.warning('BIP148 has been activated.'); - - this.state = 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} block - * @param {ChainEntry} prev - * @returns {Promise} - */ - -Chain.prototype.verifyDuplicates = async function verifyDuplicates(block, prev) { - for (const tx of block.txs) { - if (!await this.hasCoins(tx)) - continue; + // Calculate height of current block. const height = prev.height + 1; - const hash = this.network.bip30[height]; - // Blocks 91842 and 91880 created duplicate - // txids by using the same exact output script - // and extraNonce. - if (!hash || block.hash('hex') !== hash) - throw new VerifyError(block, 'invalid', 'bad-txns-BIP30', 100); - } -}; + // Only allow version 2 blocks (coinbase height) + // once the majority of blocks are using it. + if (block.version < 2 && height >= this.network.block.bip34height) + throw new VerifyError(block, 'obsolete', 'bad-version', 0); -/** - * Spend and update inputs (checkpoints only). - * @private - * @param {Block} block - * @param {ChainEntry} prev - * @returns {Promise} - Returns {@link CoinView}. - */ + // Only allow version 3 blocks (sig validation) + // once the majority of blocks are using it. + if (block.version < 3 && height >= this.network.block.bip66height) + throw new VerifyError(block, 'obsolete', 'bad-version', 0); -Chain.prototype.updateInputs = async function updateInputs(block, prev) { - const view = new CoinView(); - const height = prev.height + 1; - const cb = block.txs[0]; + // Only allow version 4 blocks (checklocktimeverify) + // once the majority of blocks are using it. + if (block.version < 4 && height >= this.network.block.bip65height) + throw new VerifyError(block, 'obsolete', 'bad-version', 0); - view.addTX(cb, height); + // Get the new deployment state. + const state = await this.getDeployments(block.time, prev); - for (let i = 1; i < block.txs.length; i++) { - const tx = block.txs[i]; - - assert(await view.spendInputs(this.db, tx), - 'BUG: Spent inputs in historical data!'); - - view.addTX(tx, height); - } - - return view; -}; - -/** - * Check block transactions for all things pertaining - * to inputs. This function is important because it is - * what actually fills the coins into the block. This - * function will check the block reward, the sigops, - * the tx values, and execute and verify the scripts (it - * will attempt to do this on the worker pool). If - * `checkpoints` is enabled, it will skip verification - * for historical data. - * @private - * @see TX#verifyInputs - * @see TX#verify - * @param {Block} block - * @param {ChainEntry} prev - * @param {DeploymentState} state - * @returns {Promise} - Returns {@link CoinView}. - */ - -Chain.prototype.verifyInputs = async function verifyInputs(block, prev, state) { - const view = new CoinView(); - const height = prev.height + 1; - const interval = this.network.halvingInterval; - - let sigops = 0; - let reward = 0; - - // Check all transactions - for (let i = 0; i < block.txs.length; i++) { - const tx = block.txs[i]; - - // Ensure tx is not double spending an output. - if (i > 0) { - if (!await view.spendInputs(this.db, tx)) { - throw new VerifyError(block, - 'invalid', - 'bad-txns-inputs-missingorspent', - 100); - } + // Enforce BIP91/BIP148. + if (state.hasBIP91() || state.hasBIP148()) { + const {segwit} = this.network.deployments; + if (!consensus.hasBit(block.version, segwit.bit)) + throw new VerifyError(block, 'invalid', 'bad-no-segwit', 0); } - // Verify sequence locks. - if (i > 0 && tx.version >= 2) { - const valid = await this.verifyLocks(prev, tx, view, state.lockFlags); + // Get timestamp for tx.isFinal(). + const time = state.hasMTP() ? mtp : block.time; - if (!valid) { + // Transactions must be finalized with + // regards to nSequence and nLockTime. + for (const tx of block.txs) { + if (!tx.isFinal(height, time)) { throw new VerifyError(block, 'invalid', 'bad-txns-nonfinal', + 10); + } + } + + // 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); } } - // Count sigops (legacy + scripthash? + witness?) - sigops += tx.getSigopsCost(view, state.flags); + // Check the commitment hash for segwit. + let commit = null; + if (state.hasWitness()) { + commit = block.getCommitmentHash(); + if (commit) { + // These are totally malleable. Someone + // may have even accidentally sent us + // the non-witness version of the block. + // We don't want to consider this block + // "invalid" if either of these checks + // fail. + if (!block.getWitnessNonce()) { + throw new VerifyError(block, + 'invalid', + 'bad-witness-nonce-size', + 100, + true); + } - if (sigops > consensus.MAX_BLOCK_SIGOPS_COST) { - throw new VerifyError(block, - 'invalid', - 'bad-blk-sigops', - 100); - } - - // Contextual sanity checks. - if (i > 0) { - const [fee, reason, score] = tx.checkInputs(view, height); - - if (fee === -1) { - throw new VerifyError(block, - 'invalid', - reason, - score); - } - - reward += fee; - - if (reward > consensus.MAX_MONEY) { - throw new VerifyError(block, - 'invalid', - 'bad-cb-amount', - 100); + if (!commit.equals(block.createCommitmentHash())) { + throw new VerifyError(block, + 'invalid', + 'bad-witness-merkle-match', + 100, + true); + } } } - // Add new coins. - view.addTX(tx, height); - } + // Blocks that do not commit to + // witness data cannot contain it. + if (!commit) { + if (block.hasWitness()) { + throw new VerifyError(block, + 'invalid', + 'unexpected-witness', + 100, + true); + } + } - // Make sure the miner isn't trying to conjure more coins. - reward += consensus.getReward(height, interval); - - if (block.getClaimed() > reward) { - throw new VerifyError(block, - 'invalid', - 'bad-cb-amount', - 100); - } - - // Push onto verification queue. - const jobs = []; - for (let i = 1; i < block.txs.length; i++) { - const tx = block.txs[i]; - jobs.push(tx.verifyAsync(view, state.flags, this.workers)); - } - - // Verify all txs in parallel. - const results = await Promise.all(jobs); - - for (const result of results) { - if (!result) { + // Check block weight (different from block size + // check in non-contextual verification). + if (block.getWeight() > consensus.MAX_BLOCK_WEIGHT) { throw new VerifyError(block, 'invalid', - 'mandatory-script-verify-flag-failed', + 'bad-blk-weight', 100); } + + return state; } - return view; -}; + /** + * Check all deployments on a chain, ranging from p2sh to segwit. + * @param {Number} time + * @param {ChainEntry} prev + * @returns {Promise} - Returns {@link DeploymentState}. + */ -/** - * Find the block at which a fork ocurred. - * @private - * @param {ChainEntry} fork - The current chain. - * @param {ChainEntry} longer - The competing chain. - * @returns {Promise} - */ + async getDeployments(time, prev) { + const deployments = this.network.deployments; + const height = prev.height + 1; + const state = new DeploymentState(); -Chain.prototype.findFork = async function findFork(fork, longer) { - while (fork.hash !== longer.hash) { - while (longer.height > fork.height) { - longer = await this.getPrevious(longer); - if (!longer) - throw new Error('No previous entry for new tip.'); + // 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 (time >= consensus.BIP16_TIME) + state.flags |= Script.flags.VERIFY_P2SH; + + // Coinbase heights are now enforced (bip34). + if (height >= this.network.block.bip34height) + state.bip34 = true; + + // Signature validation is now enforced (bip66). + if (height >= this.network.block.bip66height) + state.flags |= Script.flags.VERIFY_DERSIG; + + // CHECKLOCKTIMEVERIFY is now usable (bip65). + if (height >= this.network.block.bip65height) + state.flags |= Script.flags.VERIFY_CHECKLOCKTIMEVERIFY; + + // CHECKSEQUENCEVERIFY and median time + // past locktimes are now usable (bip9 & bip113). + if (await this.isActive(prev, deployments.csv)) { + state.flags |= Script.flags.VERIFY_CHECKSEQUENCEVERIFY; + state.lockFlags |= common.lockFlags.VERIFY_SEQUENCE; + state.lockFlags |= common.lockFlags.MEDIAN_TIME_PAST; } - if (fork.hash === longer.hash) - return fork; + // Check the state of the segwit deployment. + const witness = await this.getState(prev, deployments.segwit); - fork = await this.getPrevious(fork); - - if (!fork) - throw new Error('No previous entry for old tip.'); - } - - return fork; -}; - -/** - * Reorganize the blockchain (connect and disconnect inputs). - * Called when a competing chain with a higher chainwork - * is received. - * @private - * @param {ChainEntry} competitor - The competing chain's tip. - * @returns {Promise} - */ - -Chain.prototype.reorganize = async function reorganize(competitor) { - const tip = this.tip; - const fork = await this.findFork(tip, competitor); - - assert(fork, 'No free space or data corruption.'); - - // Blocks to disconnect. - const disconnect = []; - let entry = tip; - while (entry.hash !== fork.hash) { - disconnect.push(entry); - entry = await this.getPrevious(entry); - assert(entry); - } - - // Blocks to connect. - const connect = []; - entry = competitor; - while (entry.hash !== fork.hash) { - connect.push(entry); - entry = await this.getPrevious(entry); - assert(entry); - } - - // Disconnect blocks/txs. - for (let i = 0; i < disconnect.length; i++) { - const entry = disconnect[i]; - await this.disconnect(entry); - } - - // Connect blocks/txs. - // We don't want to connect the new tip here. - // That will be done outside in setBestChain. - for (let i = connect.length - 1; i >= 1; i--) { - const entry = connect[i]; - await this.reconnect(entry); - } - - this.logger.warning( - 'Chain reorganization: old=%s(%d) new=%s(%d)', - tip.rhash(), - tip.height, - competitor.rhash(), - competitor.height - ); - - await this.emitAsync('reorganize', tip, competitor); -}; - -/** - * Reorganize the blockchain for SPV. This - * will reset the chain to the fork block. - * @private - * @param {ChainEntry} competitor - The competing chain's tip. - * @returns {Promise} - */ - -Chain.prototype.reorganizeSPV = async function reorganizeSPV(competitor) { - const tip = this.tip; - const fork = await this.findFork(tip, competitor); - - assert(fork, 'No free space or data corruption.'); - - // Buffer disconnected blocks. - const disconnect = []; - let entry = tip; - while (entry.hash !== fork.hash) { - disconnect.push(entry); - entry = await this.getPrevious(entry); - assert(entry); - } - - // Reset the main chain back - // to the fork block, causing - // us to redownload the blocks - // on the new main chain. - await this._reset(fork.hash, true); - - // Emit disconnection events now that - // the chain has successfully reset. - for (const entry of disconnect) { - const headers = entry.toHeaders(); - const view = new CoinView(); - await this.emitAsync('disconnect', entry, headers, view); - } - - this.logger.warning( - 'SPV reorganization: old=%s(%d) new=%s(%d)', - tip.rhash(), - tip.height, - competitor.rhash(), - competitor.height - ); - - this.logger.warning( - 'Chain replay from height %d necessary.', - fork.height); - - await this.emitAsync('reorganize', tip, competitor); -}; - -/** - * Disconnect an entry from the chain (updates the tip). - * @param {ChainEntry} entry - * @returns {Promise} - */ - -Chain.prototype.disconnect = async function disconnect(entry) { - let block = await this.getBlock(entry.hash); - - if (!block) { - if (!this.options.spv) - throw new Error('Block not found.'); - block = entry.toHeaders(); - } - - const prev = await this.getPrevious(entry); - const view = await this.db.disconnect(entry, block); - - assert(prev); - - this.tip = prev; - this.height = prev.height; - - this.emit('tip', prev); - - await this.emitAsync('disconnect', entry, block, view); -}; - -/** - * Reconnect an entry to the chain (updates the tip). - * This will do contextual-verification on the block - * (necessary because we cannot validate the inputs - * in alternate chains when they come in). - * @param {ChainEntry} entry - * @param {Number} flags - * @returns {Promise} - */ - -Chain.prototype.reconnect = async function reconnect(entry) { - const flags = common.flags.VERIFY_NONE; - - let block = await this.getBlock(entry.hash); - - if (!block) { - if (!this.options.spv) - throw new Error('Block not found.'); - block = entry.toHeaders(); - } - - const prev = await this.getPrevious(entry); - assert(prev); - - let view, state; - try { - [view, state] = await this.verifyContext(block, prev, flags); - } catch (err) { - if (err.type === 'VerifyError') { - if (!err.malleated) - this.setInvalid(entry.hash); - this.logger.warning( - 'Tried to reconnect invalid block: %s (%d).', - entry.rhash(), entry.height); + // Segregrated witness (bip141) is now usable + // along with SCRIPT_VERIFY_NULLDUMMY (bip147). + if (witness === thresholdStates.ACTIVE) { + state.flags |= Script.flags.VERIFY_WITNESS; + state.flags |= Script.flags.VERIFY_NULLDUMMY; } - throw err; + + // Segsignal is now enforced (bip91). + if (this.options.bip91) { + if (witness === thresholdStates.STARTED) { + if (await this.isActive(prev, deployments.segsignal)) + state.bip91 = true; + } + } + + // UASF is now enforced (bip148) (mainnet-only). + if (this.options.bip148 && this.network === Network.main) { + if (witness !== thresholdStates.LOCKED_IN + && witness !== thresholdStates.ACTIVE) { + // The BIP148 MTP check is nonsensical in + // that it includes the _current_ entry's + // timestamp. This requires some hackery, + // since bcoin only operates on the sane + // assumption that deployment checks should + // only ever examine the values of the + // previous block (necessary for mining). + const mtp = await this.getMedianTime(prev, time); + if (mtp >= 1501545600 && mtp <= 1510704000) + state.bip148 = true; + } + } + + return state; } - await this.db.reconnect(entry, block, view); + /** + * Set a new deployment state. + * @param {DeploymentState} state + */ - this.tip = entry; - this.height = entry.height; - this.setDeploymentState(state); - - this.emit('tip', entry); - this.emit('reconnect', entry, block); - - await this.emitAsync('connect', entry, block, view); -}; - -/** - * 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} block - * @param {ChainEntry} prev - * @param {Number} flags - * @returns {Promise} - */ - -Chain.prototype.setBestChain = async function setBestChain(entry, block, prev, flags) { - // A higher fork has arrived. - // Time to reorganize the chain. - if (entry.prevBlock !== this.tip.hash) { - this.logger.warning('WARNING: Reorganizing chain.'); - - // In spv-mode, we reset the - // chain and redownload the blocks. - if (this.options.spv) { - await this.reorganizeSPV(entry); + setDeploymentState(state) { + if (this.options.checkpoints && this.height < this.network.lastCheckpoint) { + this.state = state; return; } - await this.reorganize(entry); + if (!this.state.hasP2SH() && state.hasP2SH()) + this.logger.warning('P2SH has been activated.'); + + if (!this.state.hasBIP34() && state.hasBIP34()) + this.logger.warning('BIP34 has been activated.'); + + if (!this.state.hasBIP66() && state.hasBIP66()) + this.logger.warning('BIP66 has been activated.'); + + if (!this.state.hasCLTV() && state.hasCLTV()) + this.logger.warning('BIP65 has been activated.'); + + if (!this.state.hasCSV() && state.hasCSV()) + this.logger.warning('CSV has been activated.'); + + if (!this.state.hasWitness() && state.hasWitness()) + this.logger.warning('Segwit has been activated.'); + + if (!this.state.hasBIP91() && state.hasBIP91()) + this.logger.warning('BIP91 has been activated.'); + + if (!this.state.hasBIP148() && state.hasBIP148()) + this.logger.warning('BIP148 has been activated.'); + + this.state = state; } - // Warn of unknown versionbits. - if (entry.hasUnknown(this.network)) { - this.logger.warning( - 'Unknown version bits in block %d: %s.', - entry.height, entry.version.toString(16)); - } + /** + * 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} block + * @param {ChainEntry} prev + * @returns {Promise} + */ - // Otherwise, everything is in order. - // Do "contextual" verification on our block - // now that we're certain its previous - // block is in the chain. - let view, state; - try { - [view, state] = await this.verifyContext(block, prev, flags); - } catch (err) { - if (err.type === 'VerifyError') { - if (!err.malleated) - this.setInvalid(entry.hash); - this.logger.warning( - 'Tried to connect invalid block: %s (%d).', - entry.rhash(), entry.height); + async verifyDuplicates(block, prev) { + for (const tx of block.txs) { + if (!await this.hasCoins(tx)) + continue; + + const height = prev.height + 1; + const hash = this.network.bip30[height]; + + // Blocks 91842 and 91880 created duplicate + // txids by using the same exact output script + // and extraNonce. + if (!hash || block.hash('hex') !== hash) + throw new VerifyError(block, 'invalid', 'bad-txns-BIP30', 100); } - throw err; } - // Save block and connect inputs. - await this.db.save(entry, block, view); + /** + * Spend and update inputs (checkpoints only). + * @private + * @param {Block} block + * @param {ChainEntry} prev + * @returns {Promise} - Returns {@link CoinView}. + */ - // Expose the new state. - this.tip = entry; - this.height = entry.height; - this.setDeploymentState(state); + async updateInputs(block, prev) { + const view = new CoinView(); + const height = prev.height + 1; + const cb = block.txs[0]; - this.emit('tip', entry); - this.emit('block', block, entry); + view.addTX(cb, height); - await this.emitAsync('connect', entry, block, view); -}; + for (let i = 1; i < block.txs.length; i++) { + const tx = block.txs[i]; -/** - * Save block on an alternate chain. - * @private - * @param {ChainEntry} entry - * @param {Block} block - * @param {ChainEntry} prev - * @param {Number} flags - * @returns {Promise} - */ + assert(await view.spendInputs(this.db, tx), + 'BUG: Spent inputs in historical data!'); -Chain.prototype.saveAlternate = async function saveAlternate(entry, block, prev, flags) { - try { - // Do as much verification - // as we can before saving. - await this.verify(block, prev, flags); - } catch (err) { - if (err.type === 'VerifyError') { - if (!err.malleated) - this.setInvalid(entry.hash); - this.logger.warning( - 'Invalid block on alternate chain: %s (%d).', - entry.rhash(), entry.height); + view.addTX(tx, height); } - throw err; + + return view; } - // Warn of unknown versionbits. - if (entry.hasUnknown(this.network)) { - this.logger.warning( - 'Unknown version bits in block %d: %s.', - entry.height, entry.version.toString(16)); - } + /** + * Check block transactions for all things pertaining + * to inputs. This function is important because it is + * what actually fills the coins into the block. This + * function will check the block reward, the sigops, + * the tx values, and execute and verify the scripts (it + * will attempt to do this on the worker pool). If + * `checkpoints` is enabled, it will skip verification + * for historical data. + * @private + * @see TX#verifyInputs + * @see TX#verify + * @param {Block} block + * @param {ChainEntry} prev + * @param {DeploymentState} state + * @returns {Promise} - Returns {@link CoinView}. + */ - await this.db.save(entry, block); + async verifyInputs(block, prev, state) { + const view = new CoinView(); + const height = prev.height + 1; + const interval = this.network.halvingInterval; - this.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, - this.tip.height, - entry.height, - this.tip.rhash(), - entry.rhash(), - this.tip.chainwork.toString(), - entry.chainwork.toString(), - this.tip.chainwork.sub(entry.chainwork).toString()); + let sigops = 0; + let reward = 0; - // Emit as a "competitor" block. - this.emit('competitor', block, entry); -}; + // Check all transactions + for (let i = 0; i < block.txs.length; i++) { + const tx = block.txs[i]; -/** - * Reset the chain to the desired block. This - * is useful for replaying the blockchain download - * for SPV. - * @param {Hash|Number} block - * @returns {Promise} - */ + // Ensure tx is not double spending an output. + if (i > 0) { + if (!await view.spendInputs(this.db, tx)) { + throw new VerifyError(block, + 'invalid', + 'bad-txns-inputs-missingorspent', + 100); + } + } -Chain.prototype.reset = async function reset(block) { - const unlock = await this.locker.lock(); - try { - return await this._reset(block, false); - } finally { - unlock(); - } -}; + // Verify sequence locks. + if (i > 0 && tx.version >= 2) { + const valid = await this.verifyLocks(prev, tx, view, state.lockFlags); -/** - * Reset the chain to the desired block without a lock. - * @private - * @param {Hash|Number} block - * @returns {Promise} - */ + if (!valid) { + throw new VerifyError(block, + 'invalid', + 'bad-txns-nonfinal', + 100); + } + } -Chain.prototype._reset = async function _reset(block, silent) { - const tip = await this.db.reset(block); + // Count sigops (legacy + scripthash? + witness?) + sigops += tx.getSigopsCost(view, state.flags); - // Reset state. - this.tip = tip; - this.height = tip.height; - this.synced = false; + if (sigops > consensus.MAX_BLOCK_SIGOPS_COST) { + throw new VerifyError(block, + 'invalid', + 'bad-blk-sigops', + 100); + } - const state = await this.getDeploymentState(); + // Contextual sanity checks. + if (i > 0) { + const [fee, reason, score] = tx.checkInputs(view, height); - this.setDeploymentState(state); + if (fee === -1) { + throw new VerifyError(block, + 'invalid', + reason, + score); + } - this.emit('tip', tip); + reward += fee; - if (!silent) - await this.emitAsync('reset', tip); + if (reward > consensus.MAX_MONEY) { + throw new VerifyError(block, + 'invalid', + 'bad-cb-amount', + 100); + } + } - // Reset the orphan map completely. There may - // have been some orphans on a forked chain we - // no longer need. - this.purgeOrphans(); + // Add new coins. + view.addTX(tx, height); + } - this.maybeSync(); -}; + // Make sure the miner isn't trying to conjure more coins. + reward += consensus.getReward(height, interval); -/** - * Reset the chain to a height or hash. Useful for replaying - * the blockchain download for SPV. - * @param {Hash|Number} block - hash/height - * @returns {Promise} - */ - -Chain.prototype.replay = async function replay(block) { - const unlock = await this.locker.lock(); - try { - return await this._replay(block, true); - } finally { - unlock(); - } -}; - -/** - * Reset the chain without a lock. - * @private - * @param {Hash|Number} block - hash/height - * @param {Boolean?} silent - * @returns {Promise} - */ - -Chain.prototype._replay = async function _replay(block, silent) { - const entry = await this.getEntry(block); - - if (!entry) - throw new Error('Block not found.'); - - if (!await this.isMainChain(entry)) - throw new Error('Cannot reset on alternate chain.'); - - if (entry.isGenesis()) { - await this._reset(entry.hash, silent); - return; - } - - await this._reset(entry.prevBlock, silent); -}; - -/** - * Invalidate block. - * @param {Hash} hash - * @returns {Promise} - */ - -Chain.prototype.invalidate = async function invalidate(hash) { - const unlock = await this.locker.lock(); - try { - return await this._invalidate(hash); - } finally { - unlock(); - } -}; - -/** - * Invalidate block (no lock). - * @param {Hash} hash - * @returns {Promise} - */ - -Chain.prototype._invalidate = async function _invalidate(hash) { - await this._replay(hash, false); - this.setInvalid(hash); -}; - -/** - * Retroactively prune the database. - * @returns {Promise} - */ - -Chain.prototype.prune = async function prune() { - const unlock = await this.locker.lock(); - try { - return await this.db.prune(); - } finally { - unlock(); - } -}; - -/** - * Scan the blockchain for transactions containing specified address hashes. - * @param {Hash} start - Block hash to start at. - * @param {Bloom} filter - Bloom filter containing tx and address hashes. - * @param {Function} iter - Iterator. - * @returns {Promise} - */ - -Chain.prototype.scan = async function scan(start, filter, iter) { - const unlock = await this.locker.lock(); - try { - return await this.db.scan(start, filter, iter); - } finally { - unlock(); - } -}; - -/** - * Add a block to the chain, perform all necessary verification. - * @param {Block} block - * @param {Number?} flags - * @param {Number?} id - * @returns {Promise} - */ - -Chain.prototype.add = async function add(block, flags, id) { - const hash = block.hash('hex'); - const unlock = await this.locker.lock(hash); - try { - return await this._add(block, flags, id); - } finally { - unlock(); - } -}; - -/** - * Add a block to the chain without a lock. - * @private - * @param {Block} block - * @param {Number?} flags - * @param {Number?} id - * @returns {Promise} - */ - -Chain.prototype._add = async function _add(block, flags, id) { - const hash = block.hash('hex'); - - if (flags == null) - flags = common.flags.DEFAULT_FLAGS; - - if (id == null) - id = -1; - - // Special case for genesis block. - if (hash === this.network.genesis.hash) { - this.logger.debug('Saw genesis block: %s.', block.rhash()); - throw new VerifyError(block, 'duplicate', 'duplicate', 0); - } - - // Do we already have this block in the queue? - if (this.hasPending(hash)) { - this.logger.debug('Already have pending block: %s.', block.rhash()); - throw new VerifyError(block, 'duplicate', 'duplicate', 0); - } - - // If the block is already known to be - // an orphan, ignore it. - if (this.hasOrphan(hash)) { - this.logger.debug('Already have orphan block: %s.', block.rhash()); - throw new VerifyError(block, 'duplicate', 'duplicate', 0); - } - - // Do not revalidate known invalid blocks. - if (this.hasInvalid(block)) { - this.logger.debug('Invalid ancestors for block: %s.', block.rhash()); - throw new VerifyError(block, 'duplicate', 'duplicate', 100); - } - - // Check the POW before doing anything. - if (flags & common.flags.VERIFY_POW) { - if (!block.verifyPOW()) - throw new VerifyError(block, 'invalid', 'high-hash', 50); - } - - // Do we already have this block? - if (await this.hasEntry(hash)) { - this.logger.debug('Already have block: %s.', block.rhash()); - throw new VerifyError(block, 'duplicate', 'duplicate', 0); - } - - // Find the previous block entry. - const prev = await this.getEntry(block.prevBlock); - - // If previous block wasn't ever seen, - // add it current to orphans and return. - if (!prev) { - this.storeOrphan(block, flags, id); - return null; - } - - // Connect the block. - const entry = await this.connect(prev, block, flags); - - // Handle any orphans. - if (this.hasNextOrphan(hash)) - await this.handleOrphans(entry); - - return entry; -}; - -/** - * Connect block to chain. - * @private - * @param {ChainEntry} prev - * @param {Block} block - * @param {Number} flags - * @returns {Promise} - */ - -Chain.prototype.connect = async function connect(prev, block, flags) { - const start = util.bench(); - - // Sanity check. - assert(block.prevBlock === prev.hash); - - // Explanation: we try to keep as much data - // off the javascript heap as possible. Blocks - // in the future may be 8mb or 20mb, who knows. - // In fullnode-mode we store the blocks in - // "compact" form (the headers plus the raw - // Buffer object) until they're ready to be - // fully validated here. They are deserialized, - // validated, and connected. Hopefully the - // deserialized blocks get cleaned up by the - // GC quickly. - if (block.isMemory()) { - try { - block = block.toBlock(); - } catch (e) { - this.logger.error(e); + if (block.getClaimed() > reward) { throw new VerifyError(block, - 'malformed', - 'error parsing message', - 10, - true); + 'invalid', + 'bad-cb-amount', + 100); } + + // Push onto verification queue. + const jobs = []; + for (let i = 1; i < block.txs.length; i++) { + const tx = block.txs[i]; + jobs.push(tx.verifyAsync(view, state.flags, this.workers)); + } + + // Verify all txs in parallel. + const results = await Promise.all(jobs); + + for (const result of results) { + if (!result) { + throw new VerifyError(block, + 'invalid', + 'mandatory-script-verify-flag-failed', + 100); + } + } + + return view; } - // Create a new chain entry. - const entry = ChainEntry.fromBlock(block, prev); + /** + * Find the block at which a fork ocurred. + * @private + * @param {ChainEntry} fork - The current chain. + * @param {ChainEntry} longer - The competing chain. + * @returns {Promise} + */ - // The block is on a alternate chain if the - // chainwork is less than or equal to - // our tip's. Add the block but do _not_ - // connect the inputs. - if (entry.chainwork.lte(this.tip.chainwork)) { - // Save block to an alternate chain. - await this.saveAlternate(entry, block, prev, flags); - } else { - // Attempt to add block to the chain index. - await this.setBestChain(entry, block, prev, flags); + async findFork(fork, longer) { + while (fork.hash !== longer.hash) { + while (longer.height > fork.height) { + longer = await this.getPrevious(longer); + if (!longer) + throw new Error('No previous entry for new tip.'); + } + + if (fork.hash === longer.hash) + return fork; + + fork = await this.getPrevious(fork); + + if (!fork) + throw new Error('No previous entry for old tip.'); + } + + return fork; } - // Keep track of stats. - this.logStatus(start, block, entry); + /** + * Reorganize the blockchain (connect and disconnect inputs). + * Called when a competing chain with a higher chainwork + * is received. + * @private + * @param {ChainEntry} competitor - The competing chain's tip. + * @returns {Promise} + */ - // Check sync state. - this.maybeSync(); + async reorganize(competitor) { + const tip = this.tip; + const fork = await this.findFork(tip, competitor); - return entry; -}; + assert(fork, 'No free space or data corruption.'); -/** - * Handle orphans. - * @private - * @param {ChainEntry} entry - * @returns {Promise} - */ + // Blocks to disconnect. + const disconnect = []; + let entry = tip; + while (entry.hash !== fork.hash) { + disconnect.push(entry); + entry = await this.getPrevious(entry); + assert(entry); + } -Chain.prototype.handleOrphans = async function handleOrphans(entry) { - let orphan = this.resolveOrphan(entry.hash); + // Blocks to connect. + const connect = []; + entry = competitor; + while (entry.hash !== fork.hash) { + connect.push(entry); + entry = await this.getPrevious(entry); + assert(entry); + } - while (orphan) { - const {block, flags, id} = orphan; + // Disconnect blocks/txs. + for (let i = 0; i < disconnect.length; i++) { + const entry = disconnect[i]; + await this.disconnect(entry); + } + // Connect blocks/txs. + // We don't want to connect the new tip here. + // That will be done outside in setBestChain. + for (let i = connect.length - 1; i >= 1; i--) { + const entry = connect[i]; + await this.reconnect(entry); + } + + this.logger.warning( + 'Chain reorganization: old=%s(%d) new=%s(%d)', + tip.rhash(), + tip.height, + competitor.rhash(), + competitor.height + ); + + await this.emitAsync('reorganize', tip, competitor); + } + + /** + * Reorganize the blockchain for SPV. This + * will reset the chain to the fork block. + * @private + * @param {ChainEntry} competitor - The competing chain's tip. + * @returns {Promise} + */ + + async reorganizeSPV(competitor) { + const tip = this.tip; + const fork = await this.findFork(tip, competitor); + + assert(fork, 'No free space or data corruption.'); + + // Buffer disconnected blocks. + const disconnect = []; + let entry = tip; + while (entry.hash !== fork.hash) { + disconnect.push(entry); + entry = await this.getPrevious(entry); + assert(entry); + } + + // Reset the main chain back + // to the fork block, causing + // us to redownload the blocks + // on the new main chain. + await this._reset(fork.hash, true); + + // Emit disconnection events now that + // the chain has successfully reset. + for (const entry of disconnect) { + const headers = entry.toHeaders(); + const view = new CoinView(); + await this.emitAsync('disconnect', entry, headers, view); + } + + this.logger.warning( + 'SPV reorganization: old=%s(%d) new=%s(%d)', + tip.rhash(), + tip.height, + competitor.rhash(), + competitor.height + ); + + this.logger.warning( + 'Chain replay from height %d necessary.', + fork.height); + + await this.emitAsync('reorganize', tip, competitor); + } + + /** + * Disconnect an entry from the chain (updates the tip). + * @param {ChainEntry} entry + * @returns {Promise} + */ + + async disconnect(entry) { + let block = await this.getBlock(entry.hash); + + if (!block) { + if (!this.options.spv) + throw new Error('Block not found.'); + block = entry.toHeaders(); + } + + const prev = await this.getPrevious(entry); + const view = await this.db.disconnect(entry, block); + + assert(prev); + + this.tip = prev; + this.height = prev.height; + + this.emit('tip', prev); + + await this.emitAsync('disconnect', entry, block, view); + } + + /** + * Reconnect an entry to the chain (updates the tip). + * This will do contextual-verification on the block + * (necessary because we cannot validate the inputs + * in alternate chains when they come in). + * @param {ChainEntry} entry + * @param {Number} flags + * @returns {Promise} + */ + + async reconnect(entry) { + const flags = common.flags.VERIFY_NONE; + + let block = await this.getBlock(entry.hash); + + if (!block) { + if (!this.options.spv) + throw new Error('Block not found.'); + block = entry.toHeaders(); + } + + const prev = await this.getPrevious(entry); + assert(prev); + + let view, state; try { - entry = await this.connect(entry, block, flags); + [view, state] = await this.verifyContext(block, prev, flags); } catch (err) { if (err.type === 'VerifyError') { + if (!err.malleated) + this.setInvalid(entry.hash); this.logger.warning( - 'Could not resolve orphan block %s: %s.', - block.rhash(), err.message); - - this.emit('bad orphan', err, id); - - break; + 'Tried to reconnect invalid block: %s (%d).', + entry.rhash(), entry.height); } throw err; } - this.logger.debug( - 'Orphan block was resolved: %s (%d).', - block.rhash(), entry.height); + await this.db.reconnect(entry, block, view); - this.emit('resolved', block, entry); + this.tip = entry; + this.height = entry.height; + this.setDeploymentState(state); - orphan = this.resolveOrphan(entry.hash); - } -}; + this.emit('tip', entry); + this.emit('reconnect', entry, block); -/** - * 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.synced) - return true; - - if (this.height === 1 || this.height % 20 === 0) - return true; - - if (this.height >= this.network.block.slowHeight) - return true; - - return false; -}; - -/** - * Calculate the time difference from - * start time and log block. - * @private - * @param {Array} start - * @param {Block} block - * @param {ChainEntry} entry - */ - -Chain.prototype.logStatus = function logStatus(start, block, entry) { - if (!this.isSlow()) - return; - - // Report memory for debugging. - this.logger.memory(); - - const elapsed = util.bench(start); - - this.logger.info( - 'Block %s (%d) added to chain (size=%d txs=%d time=%d).', - entry.rhash(), - entry.height, - block.getSize(), - block.txs.length, - elapsed); - - if (this.db.coinCache.capacity > 0) { - this.logger.debug('Coin Cache: size=%dmb, items=%d.', - this.db.coinCache.size / (1 << 20), this.db.coinCache.items); - } -}; - -/** - * Verify a block hash and height against the checkpoints. - * @private - * @param {ChainEntry} prev - * @param {Hash} hash - * @returns {Boolean} - */ - -Chain.prototype.verifyCheckpoint = function verifyCheckpoint(prev, hash) { - if (!this.options.checkpoints) - return true; - - const height = prev.height + 1; - const checkpoint = this.network.checkpointMap[height]; - - if (!checkpoint) - return true; - - if (hash === checkpoint) { - this.logger.debug('Hit checkpoint block %s (%d).', - encoding.revHex(hash), height); - this.emit('checkpoint', hash, height); - return true; + await this.emitAsync('connect', entry, block, view); } - // Someone is either mining on top of - // an old block for no reason, or the - // consensus protocol is broken and - // there was a 20k+ block reorg. - this.logger.warning( - 'Checkpoint mismatch at height %d: expected=%s received=%s', - height, - encoding.revHex(checkpoint), - encoding.revHex(hash) - ); + /** + * 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} block + * @param {ChainEntry} prev + * @param {Number} flags + * @returns {Promise} + */ - this.purgeOrphans(); + async setBestChain(entry, block, prev, flags) { + // A higher fork has arrived. + // Time to reorganize the chain. + if (entry.prevBlock !== this.tip.hash) { + this.logger.warning('WARNING: Reorganizing chain.'); - return false; -}; + // In spv-mode, we reset the + // chain and redownload the blocks. + if (this.options.spv) { + await this.reorganizeSPV(entry); + return; + } -/** - * Store an orphan. - * @private - * @param {Block} block - * @param {Number?} flags - * @param {Number?} id - */ - -Chain.prototype.storeOrphan = function storeOrphan(block, flags, id) { - const height = block.getCoinbaseHeight(); - const orphan = this.orphanPrev.get(block.prevBlock); - - // The orphan chain forked. - if (orphan) { - assert(orphan.block.hash('hex') !== block.hash('hex')); - assert(orphan.block.prevBlock === block.prevBlock); - - this.logger.warning( - 'Removing forked orphan block: %s (%d).', - orphan.block.rhash(), height); - - this.removeOrphan(orphan); - } - - this.limitOrphans(); - this.addOrphan(new Orphan(block, flags, id)); - - this.logger.debug( - 'Storing orphan block: %s (%d).', - block.rhash(), height); - - this.emit('orphan', block); -}; - -/** - * Add an orphan. - * @private - * @param {Orphan} orphan - * @returns {Orphan} - */ - -Chain.prototype.addOrphan = function addOrphan(orphan) { - const block = orphan.block; - const hash = block.hash('hex'); - - assert(!this.orphanMap.has(hash)); - assert(!this.orphanPrev.has(block.prevBlock)); - assert(this.orphanMap.size >= 0); - - this.orphanMap.set(hash, orphan); - this.orphanPrev.set(block.prevBlock, orphan); - - return orphan; -}; - -/** - * Remove an orphan. - * @private - * @param {Orphan} orphan - * @returns {Orphan} - */ - -Chain.prototype.removeOrphan = function removeOrphan(orphan) { - const block = orphan.block; - const hash = block.hash('hex'); - - assert(this.orphanMap.has(hash)); - assert(this.orphanPrev.has(block.prevBlock)); - assert(this.orphanMap.size > 0); - - this.orphanMap.delete(hash); - this.orphanPrev.delete(block.prevBlock); - - return orphan; -}; - -/** - * Test whether a hash would resolve the next orphan. - * @private - * @param {Hash} hash - Previous block hash. - * @returns {Boolean} - */ - -Chain.prototype.hasNextOrphan = function hasNextOrphan(hash) { - return this.orphanPrev.has(hash); -}; - -/** - * Resolve an orphan. - * @private - * @param {Hash} hash - Previous block hash. - * @returns {Orphan} - */ - -Chain.prototype.resolveOrphan = function resolveOrphan(hash) { - const orphan = this.orphanPrev.get(hash); - - if (!orphan) - return null; - - return this.removeOrphan(orphan); -}; - -/** - * Purge any waiting orphans. - */ - -Chain.prototype.purgeOrphans = function purgeOrphans() { - const count = this.orphanMap.size; - - if (count === 0) - return; - - this.orphanMap.clear(); - this.orphanPrev.clear(); - - this.logger.debug('Purged %d orphans.', count); -}; - -/** - * Prune orphans, only keep the orphan with the highest - * coinbase height (likely to be the peer's tip). - */ - -Chain.prototype.limitOrphans = function limitOrphans() { - const now = util.now(); - - let oldest = null; - for (const orphan of this.orphanMap.values()) { - if (now < orphan.time + 60 * 60) { - if (!oldest || orphan.time < oldest.time) - oldest = orphan; - continue; + await this.reorganize(entry); } - this.removeOrphan(orphan); + // Warn of unknown versionbits. + if (entry.hasUnknown(this.network)) { + this.logger.warning( + 'Unknown version bits in block %d: %s.', + entry.height, entry.version.toString(16)); + } + + // Otherwise, everything is in order. + // Do "contextual" verification on our block + // now that we're certain its previous + // block is in the chain. + let view, state; + try { + [view, state] = await this.verifyContext(block, prev, flags); + } catch (err) { + if (err.type === 'VerifyError') { + if (!err.malleated) + this.setInvalid(entry.hash); + this.logger.warning( + 'Tried to connect invalid block: %s (%d).', + entry.rhash(), entry.height); + } + throw err; + } + + // Save block and connect inputs. + await this.db.save(entry, block, view); + + // Expose the new state. + this.tip = entry; + this.height = entry.height; + this.setDeploymentState(state); + + this.emit('tip', entry); + this.emit('block', block, entry); + + await this.emitAsync('connect', entry, block, view); } - if (this.orphanMap.size < this.options.maxOrphans) - return; + /** + * Save block on an alternate chain. + * @private + * @param {ChainEntry} entry + * @param {Block} block + * @param {ChainEntry} prev + * @param {Number} flags + * @returns {Promise} + */ - if (!oldest) - return; + async saveAlternate(entry, block, prev, flags) { + try { + // Do as much verification + // as we can before saving. + await this.verify(block, prev, flags); + } catch (err) { + if (err.type === 'VerifyError') { + if (!err.malleated) + this.setInvalid(entry.hash); + this.logger.warning( + 'Invalid block on alternate chain: %s (%d).', + entry.rhash(), entry.height); + } + throw err; + } - this.removeOrphan(oldest); -}; + // Warn of unknown versionbits. + if (entry.hasUnknown(this.network)) { + this.logger.warning( + 'Unknown version bits in block %d: %s.', + entry.height, entry.version.toString(16)); + } -/** - * Test whether an invalid block hash has been seen. - * @private - * @param {Block} block - * @returns {Boolean} - */ + await this.db.save(entry, block); -Chain.prototype.hasInvalid = function hasInvalid(block) { - const hash = block.hash('hex'); + this.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, + this.tip.height, + entry.height, + this.tip.rhash(), + entry.rhash(), + this.tip.chainwork.toString(), + entry.chainwork.toString(), + this.tip.chainwork.sub(entry.chainwork).toString()); - if (this.invalid.has(hash)) - return true; - - if (this.invalid.has(block.prevBlock)) { - this.setInvalid(hash); - return true; + // Emit as a "competitor" block. + this.emit('competitor', block, entry); } - return false; -}; + /** + * Reset the chain to the desired block. This + * is useful for replaying the blockchain download + * for SPV. + * @param {Hash|Number} block + * @returns {Promise} + */ -/** - * Mark a block as invalid. - * @private - * @param {Hash} hash - */ - -Chain.prototype.setInvalid = function setInvalid(hash) { - this.invalid.set(hash, true); -}; - -/** - * Forget an invalid block hash. - * @private - * @param {Hash} hash - */ - -Chain.prototype.removeInvalid = function removeInvalid(hash) { - this.invalid.remove(hash); -}; - -/** - * Test the chain to see if it contains - * a block, or has recently seen a block. - * @param {Hash} hash - * @returns {Promise} - Returns Boolean. - */ - -Chain.prototype.has = async function has(hash) { - if (this.hasOrphan(hash)) - return true; - - if (this.locker.has(hash)) - return true; - - if (this.invalid.has(hash)) - return true; - - return await this.hasEntry(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) { - return this.db.getEntry(hash); -}; - -/** - * Retrieve a chain entry by height. - * @param {Number} height - * @returns {Promise} - Returns {@link ChainEntry}. - */ - -Chain.prototype.getEntryByHeight = function getEntryByHeight(height) { - return this.db.getEntryByHeight(height); -}; - -/** - * Retrieve a chain entry by hash. - * @param {Hash} hash - * @returns {Promise} - Returns {@link ChainEntry}. - */ - -Chain.prototype.getEntryByHash = function getEntryByHash(hash) { - return this.db.getEntryByHash(hash); -}; - -/** - * Get the hash of a block by height. Note that this - * will only return hashes in the main chain. - * @param {Number} height - * @returns {Promise} - Returns {@link Hash}. - */ - -Chain.prototype.getHash = function getHash(height) { - return this.db.getHash(height); -}; - -/** - * Get the height of a block by hash. - * @param {Hash} hash - * @returns {Promise} - Returns Number. - */ - -Chain.prototype.getHeight = function getHeight(hash) { - return this.db.getHeight(hash); -}; - -/** - * Test the chain to see if it contains a block. - * @param {Hash} hash - * @returns {Promise} - Returns Boolean. - */ - -Chain.prototype.hasEntry = function hasEntry(hash) { - return this.db.hasEntry(hash); -}; - -/** - * Get the _next_ block hash (does not work by height). - * @param {Hash} hash - * @returns {Promise} - Returns {@link Hash}. - */ - -Chain.prototype.getNextHash = function getNextHash(hash) { - return this.db.getNextHash(hash); -}; - -/** - * Check whether coins are still unspent. Necessary for bip30. - * @see https://bitcointalk.org/index.php?topic=67738.0 - * @param {TX} tx - * @returns {Promise} - Returns Boolean. - */ - -Chain.prototype.hasCoins = function hasCoins(tx) { - return this.db.hasCoins(tx); -}; - -/** - * Get all tip hashes. - * @returns {Promise} - Returns {@link Hash}[]. - */ - -Chain.prototype.getTips = function getTips() { - return this.db.getTips(); -}; - -/** - * Get range of hashes. - * @param {Number} [start=-1] - * @param {Number} [end=-1] - * @returns {Promise} - */ - -Chain.prototype.getHashes = function getHashes(start = -1, end = -1) { - return this.db.getHashes(start, end); -}; - -/** - * Get a coin (unspents only). - * @private - * @param {Outpoint} prevout - * @returns {Promise} - Returns {@link CoinEntry}. - */ - -Chain.prototype.readCoin = function readCoin(prevout) { - return this.db.readCoin(prevout); -}; - -/** - * Get a coin (unspents only). - * @param {Hash} hash - * @param {Number} index - * @returns {Promise} - Returns {@link Coin}. - */ - -Chain.prototype.getCoin = function getCoin(hash, index) { - return this.db.getCoin(hash, index); -}; - -/** - * Retrieve a block from the database (not filled with coins). - * @param {Hash} hash - * @returns {Promise} - Returns {@link Block}. - */ - -Chain.prototype.getBlock = function getBlock(hash) { - return this.db.getBlock(hash); -}; - -/** - * Retrieve a block from the database (not filled with coins). - * @param {Hash} hash - * @returns {Promise} - Returns {@link Block}. - */ - -Chain.prototype.getRawBlock = function getRawBlock(block) { - return this.db.getRawBlock(block); -}; - -/** - * Get a historical block coin viewpoint. - * @param {Block} hash - * @returns {Promise} - Returns {@link CoinView}. - */ - -Chain.prototype.getBlockView = function getBlockView(block) { - return this.db.getBlockView(block); -}; - -/** - * Get a transaction with metadata. - * @param {Hash} hash - * @returns {Promise} - Returns {@link TXMeta}. - */ - -Chain.prototype.getMeta = function getMeta(hash) { - return this.db.getMeta(hash); -}; - -/** - * Retrieve a transaction. - * @param {Hash} hash - * @returns {Promise} - Returns {@link TX}. - */ - -Chain.prototype.getTX = function getTX(hash) { - return this.db.getTX(hash); -}; - -/** - * @param {Hash} hash - * @returns {Promise} - Returns Boolean. - */ - -Chain.prototype.hasTX = function hasTX(hash) { - return this.db.hasTX(hash); -}; - -/** - * Get all coins pertinent to an address. - * @param {Address[]} addrs - * @returns {Promise} - Returns {@link Coin}[]. - */ - -Chain.prototype.getCoinsByAddress = function getCoinsByAddress(addrs) { - return this.db.getCoinsByAddress(addrs); -}; - -/** - * Get all transaction hashes to an address. - * @param {Address[]} addrs - * @returns {Promise} - Returns {@link Hash}[]. - */ - -Chain.prototype.getHashesByAddress = function getHashesByAddress(addrs) { - return this.db.getHashesByAddress(addrs); -}; - -/** - * Get all transactions pertinent to an address. - * @param {Address[]} addrs - * @returns {Promise} - Returns {@link TX}[]. - */ - -Chain.prototype.getTXByAddress = function getTXByAddress(addrs) { - return this.db.getTXByAddress(addrs); -}; - -/** - * Get all transactions pertinent to an address. - * @param {Address[]} addrs - * @returns {Promise} - Returns {@link TXMeta}[]. - */ - -Chain.prototype.getMetaByAddress = function getMetaByAddress(addrs) { - return this.db.getMetaByAddress(addrs); -}; - -/** - * Get an orphan block. - * @param {Hash} hash - * @returns {Block} - */ - -Chain.prototype.getOrphan = function getOrphan(hash) { - return this.orphanMap.get(hash) || null; -}; - -/** - * 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.orphanMap.has(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); -}; - -/** - * Get coin viewpoint. - * @param {TX} tx - * @returns {Promise} - Returns {@link CoinView}. - */ - -Chain.prototype.getCoinView = function getCoinView(tx) { - return this.db.getCoinView(tx); -}; - -/** - * Get coin viewpoint (spent). - * @param {TX} tx - * @returns {Promise} - Returns {@link CoinView}. - */ - -Chain.prototype.getSpentView = async function getSpentView(tx) { - const unlock = await this.locker.lock(); - try { - return await this.db.getSpentView(tx); - } finally { - unlock(); + async reset(block) { + const unlock = await this.locker.lock(); + try { + return await this._reset(block, false); + } finally { + unlock(); + } } -}; -/** - * Test the chain to see if it is synced. - * @returns {Boolean} - */ + /** + * Reset the chain to the desired block without a lock. + * @private + * @param {Hash|Number} block + * @returns {Promise} + */ -Chain.prototype.isFull = function isFull() { - return this.synced; -}; + async _reset(block, silent) { + const tip = await this.db.reset(block); -/** - * Potentially emit a `full` event. - * @private - */ + // Reset state. + this.tip = tip; + this.height = tip.height; + this.synced = false; -Chain.prototype.maybeSync = function maybeSync() { - if (this.synced) - return; + const state = await this.getDeploymentState(); - if (this.options.checkpoints) { - if (this.height < this.network.lastCheckpoint) + this.setDeploymentState(state); + + this.emit('tip', tip); + + if (!silent) + await this.emitAsync('reset', tip); + + // Reset the orphan map completely. There may + // have been some orphans on a forked chain we + // no longer need. + this.purgeOrphans(); + + this.maybeSync(); + } + + /** + * Reset the chain to a height or hash. Useful for replaying + * the blockchain download for SPV. + * @param {Hash|Number} block - hash/height + * @returns {Promise} + */ + + async replay(block) { + const unlock = await this.locker.lock(); + try { + return await this._replay(block, true); + } finally { + unlock(); + } + } + + /** + * Reset the chain without a lock. + * @private + * @param {Hash|Number} block - hash/height + * @param {Boolean?} silent + * @returns {Promise} + */ + + async _replay(block, silent) { + const entry = await this.getEntry(block); + + if (!entry) + throw new Error('Block not found.'); + + if (!await this.isMainChain(entry)) + throw new Error('Cannot reset on alternate chain.'); + + if (entry.isGenesis()) { + await this._reset(entry.hash, silent); return; - } - - if (this.tip.time < util.now() - this.network.block.maxTipAge) - return; - - if (!this.hasChainwork()) - return; - - this.synced = true; - this.emit('full'); -}; - -/** - * Test the chain to see if it has the - * minimum required chainwork for the - * network. - * @returns {Boolean} - */ - -Chain.prototype.hasChainwork = function hasChainwork() { - return this.tip.chainwork.gte(this.network.pow.chainwork); -}; - -/** - * Get the fill percentage. - * @returns {Number} percent - Ranges from 0.0 to 1.0. - */ - -Chain.prototype.getProgress = function getProgress() { - const start = this.network.genesis.time; - const current = this.tip.time - start; - const end = util.now() - start - 40 * 60; - return Math.min(1, current / end); -}; - -/** - * Calculate chain locator (an array of hashes). - * @param {Hash?} start - Height or hash to treat as the tip. - * The current tip will be used if not present. Note that this can be a - * non-existent hash, which is useful for headers-first locators. - * @returns {Promise} - Returns {@link Hash}[]. - */ - -Chain.prototype.getLocator = async function getLocator(start) { - const unlock = await this.locker.lock(); - try { - return await this._getLocator(start); - } finally { - unlock(); - } -}; - -/** - * Calculate chain locator without a lock. - * @private - * @param {Hash?} start - * @returns {Promise} - */ - -Chain.prototype._getLocator = async function _getLocator(start) { - if (start == null) - start = this.tip.hash; - - assert(typeof start === 'string'); - - let entry = await this.getEntry(start); - - const hashes = []; - - if (!entry) { - entry = this.tip; - hashes.push(start); - } - - let main = await this.isMainChain(entry); - let hash = entry.hash; - let height = entry.height; - let step = 1; - - hashes.push(hash); - - while (height > 0) { - height -= step; - - if (height < 0) - height = 0; - - if (hashes.length > 10) - step *= 2; - - if (main) { - // If we're on the main chain, we can - // do a fast lookup of the hash. - hash = await this.getHash(height); - assert(hash); - } else { - const ancestor = await this.getAncestor(entry, height); - assert(ancestor); - main = await this.isMainChain(ancestor); - hash = ancestor.hash; } - hashes.push(hash); + await this._reset(entry.prevBlock, silent); } - return hashes; -}; + /** + * Invalidate block. + * @param {Hash} hash + * @returns {Promise} + */ -/** - * Calculate the orphan root of the hash (if it is an orphan). - * @param {Hash} hash - * @returns {Hash} - */ + async invalidate(hash) { + const unlock = await this.locker.lock(); + try { + return await this._invalidate(hash); + } finally { + unlock(); + } + } -Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) { - let root = null; + /** + * Invalidate block (no lock). + * @param {Hash} hash + * @returns {Promise} + */ - assert(hash); + async _invalidate(hash) { + await this._replay(hash, false); + this.setInvalid(hash); + } - for (;;) { - const orphan = this.orphanMap.get(hash); + /** + * Retroactively prune the database. + * @returns {Promise} + */ + + async prune() { + const unlock = await this.locker.lock(); + try { + return await this.db.prune(); + } finally { + unlock(); + } + } + + /** + * Scan the blockchain for transactions containing specified address hashes. + * @param {Hash} start - Block hash to start at. + * @param {Bloom} filter - Bloom filter containing tx and address hashes. + * @param {Function} iter - Iterator. + * @returns {Promise} + */ + + async scan(start, filter, iter) { + const unlock = await this.locker.lock(); + try { + return await this.db.scan(start, filter, iter); + } finally { + unlock(); + } + } + + /** + * Add a block to the chain, perform all necessary verification. + * @param {Block} block + * @param {Number?} flags + * @param {Number?} id + * @returns {Promise} + */ + + async add(block, flags, id) { + const hash = block.hash('hex'); + const unlock = await this.locker.lock(hash); + try { + return await this._add(block, flags, id); + } finally { + unlock(); + } + } + + /** + * Add a block to the chain without a lock. + * @private + * @param {Block} block + * @param {Number?} flags + * @param {Number?} id + * @returns {Promise} + */ + + async _add(block, flags, id) { + const hash = block.hash('hex'); + + if (flags == null) + flags = common.flags.DEFAULT_FLAGS; + + if (id == null) + id = -1; + + // Special case for genesis block. + if (hash === this.network.genesis.hash) { + this.logger.debug('Saw genesis block: %s.', block.rhash()); + throw new VerifyError(block, 'duplicate', 'duplicate', 0); + } + + // Do we already have this block in the queue? + if (this.hasPending(hash)) { + this.logger.debug('Already have pending block: %s.', block.rhash()); + throw new VerifyError(block, 'duplicate', 'duplicate', 0); + } + + // If the block is already known to be + // an orphan, ignore it. + if (this.hasOrphan(hash)) { + this.logger.debug('Already have orphan block: %s.', block.rhash()); + throw new VerifyError(block, 'duplicate', 'duplicate', 0); + } + + // Do not revalidate known invalid blocks. + if (this.hasInvalid(block)) { + this.logger.debug('Invalid ancestors for block: %s.', block.rhash()); + throw new VerifyError(block, 'duplicate', 'duplicate', 100); + } + + // Check the POW before doing anything. + if (flags & common.flags.VERIFY_POW) { + if (!block.verifyPOW()) + throw new VerifyError(block, 'invalid', 'high-hash', 50); + } + + // Do we already have this block? + if (await this.hasEntry(hash)) { + this.logger.debug('Already have block: %s.', block.rhash()); + throw new VerifyError(block, 'duplicate', 'duplicate', 0); + } + + // Find the previous block entry. + const prev = await this.getEntry(block.prevBlock); + + // If previous block wasn't ever seen, + // add it current to orphans and return. + if (!prev) { + this.storeOrphan(block, flags, id); + return null; + } + + // Connect the block. + const entry = await this.connect(prev, block, flags); + + // Handle any orphans. + if (this.hasNextOrphan(hash)) + await this.handleOrphans(entry); + + return entry; + } + + /** + * Connect block to chain. + * @private + * @param {ChainEntry} prev + * @param {Block} block + * @param {Number} flags + * @returns {Promise} + */ + + async connect(prev, block, flags) { + const start = util.bench(); + + // Sanity check. + assert(block.prevBlock === prev.hash); + + // Explanation: we try to keep as much data + // off the javascript heap as possible. Blocks + // in the future may be 8mb or 20mb, who knows. + // In fullnode-mode we store the blocks in + // "compact" form (the headers plus the raw + // Buffer object) until they're ready to be + // fully validated here. They are deserialized, + // validated, and connected. Hopefully the + // deserialized blocks get cleaned up by the + // GC quickly. + if (block.isMemory()) { + try { + block = block.toBlock(); + } catch (e) { + this.logger.error(e); + throw new VerifyError(block, + 'malformed', + 'error parsing message', + 10, + true); + } + } + + // Create a new chain entry. + const entry = ChainEntry.fromBlock(block, prev); + + // The block is on a alternate chain if the + // chainwork is less than or equal to + // our tip's. Add the block but do _not_ + // connect the inputs. + if (entry.chainwork.lte(this.tip.chainwork)) { + // Save block to an alternate chain. + await this.saveAlternate(entry, block, prev, flags); + } else { + // Attempt to add block to the chain index. + await this.setBestChain(entry, block, prev, flags); + } + + // Keep track of stats. + this.logStatus(start, block, entry); + + // Check sync state. + this.maybeSync(); + + return entry; + } + + /** + * Handle orphans. + * @private + * @param {ChainEntry} entry + * @returns {Promise} + */ + + async handleOrphans(entry) { + let orphan = this.resolveOrphan(entry.hash); + + while (orphan) { + const {block, flags, id} = orphan; + + try { + entry = await this.connect(entry, block, flags); + } catch (err) { + if (err.type === 'VerifyError') { + this.logger.warning( + 'Could not resolve orphan block %s: %s.', + block.rhash(), err.message); + + this.emit('bad orphan', err, id); + + break; + } + throw err; + } + + this.logger.debug( + 'Orphan block was resolved: %s (%d).', + block.rhash(), entry.height); + + this.emit('resolved', block, entry); + + orphan = this.resolveOrphan(entry.hash); + } + } + + /** + * Test whether the chain has reached its slow height. + * @private + * @returns {Boolean} + */ + + isSlow() { + if (this.options.spv) + return false; + + if (this.synced) + return true; + + if (this.height === 1 || this.height % 20 === 0) + return true; + + if (this.height >= this.network.block.slowHeight) + return true; + + return false; + } + + /** + * Calculate the time difference from + * start time and log block. + * @private + * @param {Array} start + * @param {Block} block + * @param {ChainEntry} entry + */ + + logStatus(start, block, entry) { + if (!this.isSlow()) + return; + + // Report memory for debugging. + this.logger.memory(); + + const elapsed = util.bench(start); + + this.logger.info( + 'Block %s (%d) added to chain (size=%d txs=%d time=%d).', + entry.rhash(), + entry.height, + block.getSize(), + block.txs.length, + elapsed); + + if (this.db.coinCache.capacity > 0) { + this.logger.debug('Coin Cache: size=%dmb, items=%d.', + this.db.coinCache.size / (1 << 20), this.db.coinCache.items); + } + } + + /** + * Verify a block hash and height against the checkpoints. + * @private + * @param {ChainEntry} prev + * @param {Hash} hash + * @returns {Boolean} + */ + + verifyCheckpoint(prev, hash) { + if (!this.options.checkpoints) + return true; + + const height = prev.height + 1; + const checkpoint = this.network.checkpointMap[height]; + + if (!checkpoint) + return true; + + if (hash === checkpoint) { + this.logger.debug('Hit checkpoint block %s (%d).', + encoding.revHex(hash), height); + this.emit('checkpoint', hash, height); + return true; + } + + // Someone is either mining on top of + // an old block for no reason, or the + // consensus protocol is broken and + // there was a 20k+ block reorg. + this.logger.warning( + 'Checkpoint mismatch at height %d: expected=%s received=%s', + height, + encoding.revHex(checkpoint), + encoding.revHex(hash) + ); + + this.purgeOrphans(); + + return false; + } + + /** + * Store an orphan. + * @private + * @param {Block} block + * @param {Number?} flags + * @param {Number?} id + */ + + storeOrphan(block, flags, id) { + const height = block.getCoinbaseHeight(); + const orphan = this.orphanPrev.get(block.prevBlock); + + // The orphan chain forked. + if (orphan) { + assert(orphan.block.hash('hex') !== block.hash('hex')); + assert(orphan.block.prevBlock === block.prevBlock); + + this.logger.warning( + 'Removing forked orphan block: %s (%d).', + orphan.block.rhash(), height); + + this.removeOrphan(orphan); + } + + this.limitOrphans(); + this.addOrphan(new Orphan(block, flags, id)); + + this.logger.debug( + 'Storing orphan block: %s (%d).', + block.rhash(), height); + + this.emit('orphan', block); + } + + /** + * Add an orphan. + * @private + * @param {Orphan} orphan + * @returns {Orphan} + */ + + addOrphan(orphan) { + const block = orphan.block; + const hash = block.hash('hex'); + + assert(!this.orphanMap.has(hash)); + assert(!this.orphanPrev.has(block.prevBlock)); + assert(this.orphanMap.size >= 0); + + this.orphanMap.set(hash, orphan); + this.orphanPrev.set(block.prevBlock, orphan); + + return orphan; + } + + /** + * Remove an orphan. + * @private + * @param {Orphan} orphan + * @returns {Orphan} + */ + + removeOrphan(orphan) { + const block = orphan.block; + const hash = block.hash('hex'); + + assert(this.orphanMap.has(hash)); + assert(this.orphanPrev.has(block.prevBlock)); + assert(this.orphanMap.size > 0); + + this.orphanMap.delete(hash); + this.orphanPrev.delete(block.prevBlock); + + return orphan; + } + + /** + * Test whether a hash would resolve the next orphan. + * @private + * @param {Hash} hash - Previous block hash. + * @returns {Boolean} + */ + + hasNextOrphan(hash) { + return this.orphanPrev.has(hash); + } + + /** + * Resolve an orphan. + * @private + * @param {Hash} hash - Previous block hash. + * @returns {Orphan} + */ + + resolveOrphan(hash) { + const orphan = this.orphanPrev.get(hash); if (!orphan) - break; + return null; - root = hash; - hash = orphan.block.prevBlock; + return this.removeOrphan(orphan); } - return root; -}; + /** + * Purge any waiting orphans. + */ -/** - * Calculate the time difference (in seconds) - * between two blocks by examining chainworks. - * @param {ChainEntry} to - * @param {ChainEntry} from - * @returns {Number} - */ + purgeOrphans() { + const count = this.orphanMap.size; -Chain.prototype.getProofTime = function getProofTime(to, from) { - const pow = this.network.pow; - let sign, work; + if (count === 0) + return; - if (to.chainwork.gt(from.chainwork)) { - work = to.chainwork.sub(from.chainwork); - sign = 1; - } else { - work = from.chainwork.sub(to.chainwork); - sign = -1; + this.orphanMap.clear(); + this.orphanPrev.clear(); + + this.logger.debug('Purged %d orphans.', count); } - work = work.imuln(pow.targetSpacing); - work = work.div(this.tip.getProof()); + /** + * Prune orphans, only keep the orphan with the highest + * coinbase height (likely to be the peer's tip). + */ - if (work.bitLength() > 53) - return sign * Number.MAX_SAFE_INTEGER; + limitOrphans() { + const now = util.now(); - return sign * work.toNumber(); -}; - -/** - * Calculate the next target based on the chain tip. - * @returns {Promise} - returns Number - * (target is in compact/mantissa form). - */ - -Chain.prototype.getCurrentTarget = async function getCurrentTarget() { - return await this.getTarget(this.network.now(), this.tip); -}; - -/** - * Calculate the next target. - * @param {Number} time - Next block timestamp. - * @param {ChainEntry} prev - Previous entry. - * @returns {Promise} - returns Number - * (target is in compact/mantissa form). - */ - -Chain.prototype.getTarget = async function getTarget(time, prev) { - const pow = this.network.pow; - - // Genesis - if (!prev) { - assert(time === this.network.genesis.time); - return pow.bits; - } - - // Do not retarget - if ((prev.height + 1) % pow.retargetInterval !== 0) { - if (pow.targetReset) { - // Special behavior for testnet: - if (time > prev.time + pow.targetSpacing * 2) - return pow.bits; - - while (prev.height !== 0 - && prev.height % pow.retargetInterval !== 0 - && prev.bits === pow.bits) { - const cache = this.getPrevCache(prev); - - if (cache) - prev = cache; - else - prev = await this.getPrevious(prev); - - assert(prev); + let oldest = null; + for (const orphan of this.orphanMap.values()) { + if (now < orphan.time + 60 * 60) { + if (!oldest || orphan.time < oldest.time) + oldest = orphan; + continue; } - } - return prev.bits; - } - // Back 2 weeks - const height = prev.height - (pow.retargetInterval - 1); - assert(height >= 0); - - const first = await this.getAncestor(prev, height); - 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) { - const pow = this.network.pow; - const targetTimespan = pow.targetTimespan; - - if (pow.noRetargeting) - return prev.bits; - - const target = consensus.fromCompact(prev.bits); - - let actualTimespan = prev.time - first.time; - - if (actualTimespan < targetTimespan / 4 | 0) - actualTimespan = targetTimespan / 4 | 0; - - if (actualTimespan > targetTimespan * 4) - actualTimespan = targetTimespan * 4; - - target.imuln(actualTimespan); - target.idivn(targetTimespan); - - if (target.gt(pow.limit)) - return pow.bits; - - return consensus.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 = async function findLocator(locator) { - for (const hash of locator) { - if (await this.isMainHash(hash)) - return hash; - } - - return this.network.genesis.hash; -}; - -/** - * Check whether a versionbits deployment is active (BIP9: versionbits). - * @example - * await chain.isActive(tip, deployments.segwit); - * @see https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki - * @param {ChainEntry} prev - Previous chain entry. - * @param {String} id - Deployment id. - * @returns {Promise} - Returns Number. - */ - -Chain.prototype.isActive = async function isActive(prev, deployment) { - const state = await this.getState(prev, deployment); - return state === thresholdStates.ACTIVE; -}; - -/** - * Get chain entry state for a deployment (BIP9: versionbits). - * @example - * await chain.getState(tip, deployments.segwit); - * @see https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki - * @param {ChainEntry} prev - Previous chain entry. - * @param {String} id - Deployment id. - * @returns {Promise} - Returns Number. - */ - -Chain.prototype.getState = async function getState(prev, deployment) { - const bit = deployment.bit; - - let window = this.network.minerWindow; - let threshold = this.network.activationThreshold; - - if (deployment.threshold !== -1) - threshold = deployment.threshold; - - if (deployment.window !== -1) - window = deployment.window; - - if (((prev.height + 1) % window) !== 0) { - const height = prev.height - ((prev.height + 1) % window); - - prev = await this.getAncestor(prev, height); - - if (!prev) - return thresholdStates.DEFINED; - - assert(prev.height === height); - assert(((prev.height + 1) % window) === 0); - } - - let entry = prev; - let state = thresholdStates.DEFINED; - - const compute = []; - - while (entry) { - const cached = this.db.stateCache.get(bit, entry); - - if (cached !== -1) { - state = cached; - break; + this.removeOrphan(orphan); } - const time = await this.getMedianTime(entry); + if (this.orphanMap.size < this.options.maxOrphans) + return; - if (time < deployment.startTime) { - state = thresholdStates.DEFINED; - this.db.stateCache.set(bit, entry, state); - break; - } + if (!oldest) + return; - compute.push(entry); - - const height = entry.height - window; - - entry = await this.getAncestor(entry, height); + this.removeOrphan(oldest); } - while (compute.length) { - const entry = compute.pop(); + /** + * Test whether an invalid block hash has been seen. + * @private + * @param {Block} block + * @returns {Boolean} + */ - switch (state) { - case thresholdStates.DEFINED: { - const time = await this.getMedianTime(entry); + hasInvalid(block) { + const hash = block.hash('hex'); - if (time >= deployment.timeout) { - state = thresholdStates.FAILED; - break; + if (this.invalid.has(hash)) + return true; + + if (this.invalid.has(block.prevBlock)) { + this.setInvalid(hash); + return true; + } + + return false; + } + + /** + * Mark a block as invalid. + * @private + * @param {Hash} hash + */ + + setInvalid(hash) { + this.invalid.set(hash, true); + } + + /** + * Forget an invalid block hash. + * @private + * @param {Hash} hash + */ + + removeInvalid(hash) { + this.invalid.remove(hash); + } + + /** + * Test the chain to see if it contains + * a block, or has recently seen a block. + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ + + async has(hash) { + if (this.hasOrphan(hash)) + return true; + + if (this.locker.has(hash)) + return true; + + if (this.invalid.has(hash)) + return true; + + return await this.hasEntry(hash); + } + + /** + * Find the corresponding block entry by hash or height. + * @param {Hash|Number} hash/height + * @returns {Promise} - Returns {@link ChainEntry}. + */ + + getEntry(hash) { + return this.db.getEntry(hash); + } + + /** + * Retrieve a chain entry by height. + * @param {Number} height + * @returns {Promise} - Returns {@link ChainEntry}. + */ + + getEntryByHeight(height) { + return this.db.getEntryByHeight(height); + } + + /** + * Retrieve a chain entry by hash. + * @param {Hash} hash + * @returns {Promise} - Returns {@link ChainEntry}. + */ + + getEntryByHash(hash) { + return this.db.getEntryByHash(hash); + } + + /** + * Get the hash of a block by height. Note that this + * will only return hashes in the main chain. + * @param {Number} height + * @returns {Promise} - Returns {@link Hash}. + */ + + getHash(height) { + return this.db.getHash(height); + } + + /** + * Get the height of a block by hash. + * @param {Hash} hash + * @returns {Promise} - Returns Number. + */ + + getHeight(hash) { + return this.db.getHeight(hash); + } + + /** + * Test the chain to see if it contains a block. + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ + + hasEntry(hash) { + return this.db.hasEntry(hash); + } + + /** + * Get the _next_ block hash (does not work by height). + * @param {Hash} hash + * @returns {Promise} - Returns {@link Hash}. + */ + + getNextHash(hash) { + return this.db.getNextHash(hash); + } + + /** + * Check whether coins are still unspent. Necessary for bip30. + * @see https://bitcointalk.org/index.php?topic=67738.0 + * @param {TX} tx + * @returns {Promise} - Returns Boolean. + */ + + hasCoins(tx) { + return this.db.hasCoins(tx); + } + + /** + * Get all tip hashes. + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getTips() { + return this.db.getTips(); + } + + /** + * Get range of hashes. + * @param {Number} [start=-1] + * @param {Number} [end=-1] + * @returns {Promise} + */ + + getHashes(start = -1, end = -1) { + return this.db.getHashes(start, end); + } + + /** + * Get a coin (unspents only). + * @private + * @param {Outpoint} prevout + * @returns {Promise} - Returns {@link CoinEntry}. + */ + + readCoin(prevout) { + return this.db.readCoin(prevout); + } + + /** + * Get a coin (unspents only). + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} - Returns {@link Coin}. + */ + + getCoin(hash, index) { + return this.db.getCoin(hash, index); + } + + /** + * Retrieve a block from the database (not filled with coins). + * @param {Hash} hash + * @returns {Promise} - Returns {@link Block}. + */ + + getBlock(hash) { + return this.db.getBlock(hash); + } + + /** + * Retrieve a block from the database (not filled with coins). + * @param {Hash} hash + * @returns {Promise} - Returns {@link Block}. + */ + + getRawBlock(block) { + return this.db.getRawBlock(block); + } + + /** + * Get a historical block coin viewpoint. + * @param {Block} hash + * @returns {Promise} - Returns {@link CoinView}. + */ + + getBlockView(block) { + return this.db.getBlockView(block); + } + + /** + * Get a transaction with metadata. + * @param {Hash} hash + * @returns {Promise} - Returns {@link TXMeta}. + */ + + getMeta(hash) { + return this.db.getMeta(hash); + } + + /** + * Retrieve a transaction. + * @param {Hash} hash + * @returns {Promise} - Returns {@link TX}. + */ + + getTX(hash) { + return this.db.getTX(hash); + } + + /** + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ + + hasTX(hash) { + return this.db.hasTX(hash); + } + + /** + * Get all coins pertinent to an address. + * @param {Address[]} addrs + * @returns {Promise} - Returns {@link Coin}[]. + */ + + getCoinsByAddress(addrs) { + return this.db.getCoinsByAddress(addrs); + } + + /** + * Get all transaction hashes to an address. + * @param {Address[]} addrs + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getHashesByAddress(addrs) { + return this.db.getHashesByAddress(addrs); + } + + /** + * Get all transactions pertinent to an address. + * @param {Address[]} addrs + * @returns {Promise} - Returns {@link TX}[]. + */ + + getTXByAddress(addrs) { + return this.db.getTXByAddress(addrs); + } + + /** + * Get all transactions pertinent to an address. + * @param {Address[]} addrs + * @returns {Promise} - Returns {@link TXMeta}[]. + */ + + getMetaByAddress(addrs) { + return this.db.getMetaByAddress(addrs); + } + + /** + * Get an orphan block. + * @param {Hash} hash + * @returns {Block} + */ + + getOrphan(hash) { + return this.orphanMap.get(hash) || null; + } + + /** + * Test the chain to see if it contains an orphan. + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ + + hasOrphan(hash) { + return this.orphanMap.has(hash); + } + + /** + * Test the chain to see if it contains a pending block in its queue. + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ + + hasPending(hash) { + return this.locker.pending(hash); + } + + /** + * Get coin viewpoint. + * @param {TX} tx + * @returns {Promise} - Returns {@link CoinView}. + */ + + getCoinView(tx) { + return this.db.getCoinView(tx); + } + + /** + * Get coin viewpoint (spent). + * @param {TX} tx + * @returns {Promise} - Returns {@link CoinView}. + */ + + async getSpentView(tx) { + const unlock = await this.locker.lock(); + try { + return await this.db.getSpentView(tx); + } finally { + unlock(); + } + } + + /** + * Test the chain to see if it is synced. + * @returns {Boolean} + */ + + isFull() { + return this.synced; + } + + /** + * Potentially emit a `full` event. + * @private + */ + + maybeSync() { + if (this.synced) + return; + + if (this.options.checkpoints) { + if (this.height < this.network.lastCheckpoint) + return; + } + + if (this.tip.time < util.now() - this.network.block.maxTipAge) + return; + + if (!this.hasChainwork()) + return; + + this.synced = true; + this.emit('full'); + } + + /** + * Test the chain to see if it has the + * minimum required chainwork for the + * network. + * @returns {Boolean} + */ + + hasChainwork() { + return this.tip.chainwork.gte(this.network.pow.chainwork); + } + + /** + * Get the fill percentage. + * @returns {Number} percent - Ranges from 0.0 to 1.0. + */ + + getProgress() { + const start = this.network.genesis.time; + const current = this.tip.time - start; + const end = util.now() - start - 40 * 60; + return Math.min(1, current / end); + } + + /** + * Calculate chain locator (an array of hashes). + * @param {Hash?} start - Height or hash to treat as the tip. + * The current tip will be used if not present. Note that this can be a + * non-existent hash, which is useful for headers-first locators. + * @returns {Promise} - Returns {@link Hash}[]. + */ + + async getLocator(start) { + const unlock = await this.locker.lock(); + try { + return await this._getLocator(start); + } finally { + unlock(); + } + } + + /** + * Calculate chain locator without a lock. + * @private + * @param {Hash?} start + * @returns {Promise} + */ + + async _getLocator(start) { + if (start == null) + start = this.tip.hash; + + assert(typeof start === 'string'); + + let entry = await this.getEntry(start); + + const hashes = []; + + if (!entry) { + entry = this.tip; + hashes.push(start); + } + + let main = await this.isMainChain(entry); + let hash = entry.hash; + let height = entry.height; + let step = 1; + + hashes.push(hash); + + while (height > 0) { + height -= step; + + if (height < 0) + height = 0; + + if (hashes.length > 10) + step *= 2; + + if (main) { + // If we're on the main chain, we can + // do a fast lookup of the hash. + hash = await this.getHash(height); + assert(hash); + } else { + const ancestor = await this.getAncestor(entry, height); + assert(ancestor); + main = await this.isMainChain(ancestor); + hash = ancestor.hash; + } + + hashes.push(hash); + } + + return hashes; + } + + /** + * Calculate the orphan root of the hash (if it is an orphan). + * @param {Hash} hash + * @returns {Hash} + */ + + getOrphanRoot(hash) { + let root = null; + + assert(hash); + + for (;;) { + const orphan = this.orphanMap.get(hash); + + if (!orphan) + break; + + root = hash; + hash = orphan.block.prevBlock; + } + + return root; + } + + /** + * Calculate the time difference (in seconds) + * between two blocks by examining chainworks. + * @param {ChainEntry} to + * @param {ChainEntry} from + * @returns {Number} + */ + + getProofTime(to, from) { + const pow = this.network.pow; + let sign, work; + + if (to.chainwork.gt(from.chainwork)) { + work = to.chainwork.sub(from.chainwork); + sign = 1; + } else { + work = from.chainwork.sub(to.chainwork); + sign = -1; + } + + work = work.imuln(pow.targetSpacing); + work = work.div(this.tip.getProof()); + + if (work.bitLength() > 53) + return sign * Number.MAX_SAFE_INTEGER; + + return sign * work.toNumber(); + } + + /** + * Calculate the next target based on the chain tip. + * @returns {Promise} - returns Number + * (target is in compact/mantissa form). + */ + + async getCurrentTarget() { + return await this.getTarget(this.network.now(), this.tip); + } + + /** + * Calculate the next target. + * @param {Number} time - Next block timestamp. + * @param {ChainEntry} prev - Previous entry. + * @returns {Promise} - returns Number + * (target is in compact/mantissa form). + */ + + async getTarget(time, prev) { + const pow = this.network.pow; + + // Genesis + if (!prev) { + assert(time === this.network.genesis.time); + return pow.bits; + } + + // Do not retarget + if ((prev.height + 1) % pow.retargetInterval !== 0) { + if (pow.targetReset) { + // Special behavior for testnet: + if (time > prev.time + pow.targetSpacing * 2) + return pow.bits; + + while (prev.height !== 0 + && prev.height % pow.retargetInterval !== 0 + && prev.bits === pow.bits) { + const cache = this.getPrevCache(prev); + + if (cache) + prev = cache; + else + prev = await this.getPrevious(prev); + + assert(prev); } + } + return prev.bits; + } - if (time >= deployment.startTime) { - state = thresholdStates.STARTED; - break; - } + // Back 2 weeks + const height = prev.height - (pow.retargetInterval - 1); + assert(height >= 0); + const first = await this.getAncestor(prev, height); + 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. + */ + + retarget(prev, first) { + const pow = this.network.pow; + const targetTimespan = pow.targetTimespan; + + if (pow.noRetargeting) + return prev.bits; + + const target = consensus.fromCompact(prev.bits); + + let actualTimespan = prev.time - first.time; + + if (actualTimespan < targetTimespan / 4 | 0) + actualTimespan = targetTimespan / 4 | 0; + + if (actualTimespan > targetTimespan * 4) + actualTimespan = targetTimespan * 4; + + target.imuln(actualTimespan); + target.idivn(targetTimespan); + + if (target.gt(pow.limit)) + return pow.bits; + + return consensus.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). + */ + + async findLocator(locator) { + for (const hash of locator) { + if (await this.isMainHash(hash)) + return hash; + } + + return this.network.genesis.hash; + } + + /** + * Check whether a versionbits deployment is active (BIP9: versionbits). + * @example + * await chain.isActive(tip, deployments.segwit); + * @see https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki + * @param {ChainEntry} prev - Previous chain entry. + * @param {String} id - Deployment id. + * @returns {Promise} - Returns Number. + */ + + async isActive(prev, deployment) { + const state = await this.getState(prev, deployment); + return state === thresholdStates.ACTIVE; + } + + /** + * Get chain entry state for a deployment (BIP9: versionbits). + * @example + * await chain.getState(tip, deployments.segwit); + * @see https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki + * @param {ChainEntry} prev - Previous chain entry. + * @param {String} id - Deployment id. + * @returns {Promise} - Returns Number. + */ + + async getState(prev, deployment) { + const bit = deployment.bit; + + let window = this.network.minerWindow; + let threshold = this.network.activationThreshold; + + if (deployment.threshold !== -1) + threshold = deployment.threshold; + + if (deployment.window !== -1) + window = deployment.window; + + if (((prev.height + 1) % window) !== 0) { + const height = prev.height - ((prev.height + 1) % window); + + prev = await this.getAncestor(prev, height); + + if (!prev) + return thresholdStates.DEFINED; + + assert(prev.height === height); + assert(((prev.height + 1) % window) === 0); + } + + let entry = prev; + let state = thresholdStates.DEFINED; + + const compute = []; + + while (entry) { + const cached = this.db.stateCache.get(bit, entry); + + if (cached !== -1) { + state = cached; break; } - case thresholdStates.STARTED: { - const time = await this.getMedianTime(entry); - if (time >= deployment.timeout) { - state = thresholdStates.FAILED; - break; - } + const time = await this.getMedianTime(entry); - let block = entry; - let count = 0; + if (time < deployment.startTime) { + state = thresholdStates.DEFINED; + this.db.stateCache.set(bit, entry, state); + break; + } - for (let i = 0; i < window; i++) { - if (block.hasBit(bit)) - count++; + compute.push(entry); - if (count >= threshold) { - state = thresholdStates.LOCKED_IN; + const height = entry.height - window; + + entry = await this.getAncestor(entry, height); + } + + while (compute.length) { + const entry = compute.pop(); + + switch (state) { + case thresholdStates.DEFINED: { + const time = await this.getMedianTime(entry); + + if (time >= deployment.timeout) { + state = thresholdStates.FAILED; break; } - block = await this.getPrevious(block); - assert(block); + if (time >= deployment.startTime) { + state = thresholdStates.STARTED; + break; + } + + break; } + case thresholdStates.STARTED: { + const time = await this.getMedianTime(entry); - break; + if (time >= deployment.timeout) { + state = thresholdStates.FAILED; + break; + } + + let block = entry; + let count = 0; + + for (let i = 0; i < window; i++) { + if (block.hasBit(bit)) + count++; + + if (count >= threshold) { + state = thresholdStates.LOCKED_IN; + break; + } + + block = await this.getPrevious(block); + assert(block); + } + + break; + } + case thresholdStates.LOCKED_IN: { + state = thresholdStates.ACTIVE; + break; + } + case thresholdStates.FAILED: + case thresholdStates.ACTIVE: { + break; + } + default: { + assert(false, 'Bad state.'); + break; + } } - case thresholdStates.LOCKED_IN: { - state = thresholdStates.ACTIVE; - break; - } - case thresholdStates.FAILED: - case thresholdStates.ACTIVE: { - break; - } - default: { - assert(false, 'Bad state.'); - break; + + this.db.stateCache.set(bit, entry, state); + } + + return state; + } + + /** + * 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. + */ + + async computeBlockVersion(prev) { + let version = 0; + + for (const deployment of this.network.deploys) { + const state = await this.getState(prev, deployment); + + if (state === thresholdStates.LOCKED_IN + || state === thresholdStates.STARTED) { + version |= 1 << deployment.bit; } } - this.db.stateCache.set(bit, entry, state); + version |= consensus.VERSION_TOP_BITS; + version >>>= 0; + + return version; } - return state; -}; + /** + * Get the current deployment state of the chain. Called on load. + * @private + * @returns {Promise} - Returns {@link DeploymentState}. + */ -/** - * 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. - */ + async getDeploymentState() { + const prev = await this.getPrevious(this.tip); -Chain.prototype.computeBlockVersion = async function computeBlockVersion(prev) { - let version = 0; - - for (const deployment of this.network.deploys) { - const state = await this.getState(prev, deployment); - - if (state === thresholdStates.LOCKED_IN - || state === thresholdStates.STARTED) { - version |= 1 << deployment.bit; - } - } - - version |= consensus.VERSION_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 = async function getDeploymentState() { - const prev = await this.getPrevious(this.tip); - - if (!prev) { - assert(this.tip.isGenesis()); - return this.state; - } - - if (this.options.spv) - return this.state; - - return await this.getDeployments(this.tip.time, prev); -}; - -/** - * 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.verifyFinal = async function verifyFinal(prev, tx, flags) { - const height = prev.height + 1; - - // We can skip MTP if the locktime is height. - if (tx.locktime < consensus.LOCKTIME_THRESHOLD) - return tx.isFinal(height, -1); - - if (flags & common.lockFlags.MEDIAN_TIME_PAST) { - const time = await this.getMedianTime(prev); - return tx.isFinal(height, time); - } - - return tx.isFinal(height, this.network.now()); -}; - -/** - * Get the necessary minimum time and height sequence locks for a transaction. - * @param {ChainEntry} prev - * @param {TX} tx - * @param {CoinView} view - * @param {LockFlags} flags - * @returns {Promise} - */ - -Chain.prototype.getLocks = async function getLocks(prev, tx, view, flags) { - const GRANULARITY = consensus.SEQUENCE_GRANULARITY; - const DISABLE_FLAG = consensus.SEQUENCE_DISABLE_FLAG; - const TYPE_FLAG = consensus.SEQUENCE_TYPE_FLAG; - const MASK = consensus.SEQUENCE_MASK; - - if (!(flags & common.lockFlags.VERIFY_SEQUENCE)) - return [-1, -1]; - - if (tx.isCoinbase() || tx.version < 2) - return [-1, -1]; - - let minHeight = -1; - let minTime = -1; - - for (const {prevout, sequence} of tx.inputs) { - if (sequence & DISABLE_FLAG) - continue; - - let height = view.getHeight(prevout); - - if (height === -1) - height = this.height + 1; - - if (!(sequence & TYPE_FLAG)) { - height += (sequence & MASK) - 1; - minHeight = Math.max(minHeight, height); - continue; + if (!prev) { + assert(this.tip.isGenesis()); + return this.state; } - height = Math.max(height - 1, 0); + if (this.options.spv) + return this.state; - const entry = await this.getAncestor(prev, height); - assert(entry, 'Database is corrupt.'); - - let time = await this.getMedianTime(entry); - time += ((sequence & MASK) << GRANULARITY) - 1; - minTime = Math.max(minTime, time); + return await this.getDeployments(this.tip.time, prev); } - return [minHeight, minTime]; -}; + /** + * 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. + */ -/** - * Verify sequence locks. - * @param {ChainEntry} prev - * @param {TX} tx - * @param {CoinView} view - * @param {LockFlags} flags - * @returns {Promise} - Returns Boolean. - */ + async verifyFinal(prev, tx, flags) { + const height = prev.height + 1; -Chain.prototype.verifyLocks = async function verifyLocks(prev, tx, view, flags) { - const [height, time] = await this.getLocks(prev, tx, view, flags); + // We can skip MTP if the locktime is height. + if (tx.locktime < consensus.LOCKTIME_THRESHOLD) + return tx.isFinal(height, -1); - if (height !== -1) { - if (height >= prev.height + 1) - return false; + if (flags & common.lockFlags.MEDIAN_TIME_PAST) { + const time = await this.getMedianTime(prev); + return tx.isFinal(height, time); + } + + return tx.isFinal(height, this.network.now()); } - if (time !== -1) { - const mtp = await this.getMedianTime(prev); + /** + * Get the necessary minimum time and height sequence locks for a transaction. + * @param {ChainEntry} prev + * @param {TX} tx + * @param {CoinView} view + * @param {LockFlags} flags + * @returns {Promise} + */ - if (time >= mtp) - return false; + async getLocks(prev, tx, view, flags) { + const GRANULARITY = consensus.SEQUENCE_GRANULARITY; + const DISABLE_FLAG = consensus.SEQUENCE_DISABLE_FLAG; + const TYPE_FLAG = consensus.SEQUENCE_TYPE_FLAG; + const MASK = consensus.SEQUENCE_MASK; + + if (!(flags & common.lockFlags.VERIFY_SEQUENCE)) + return [-1, -1]; + + if (tx.isCoinbase() || tx.version < 2) + return [-1, -1]; + + let minHeight = -1; + let minTime = -1; + + for (const {prevout, sequence} of tx.inputs) { + if (sequence & DISABLE_FLAG) + continue; + + let height = view.getHeight(prevout); + + if (height === -1) + height = this.height + 1; + + if (!(sequence & TYPE_FLAG)) { + height += (sequence & MASK) - 1; + minHeight = Math.max(minHeight, height); + continue; + } + + height = Math.max(height - 1, 0); + + const entry = await this.getAncestor(prev, height); + assert(entry, 'Database is corrupt.'); + + let time = await this.getMedianTime(entry); + time += ((sequence & MASK) << GRANULARITY) - 1; + minTime = Math.max(minTime, time); + } + + return [minHeight, minTime]; } - return true; -}; + /** + * Verify sequence locks. + * @param {ChainEntry} prev + * @param {TX} tx + * @param {CoinView} view + * @param {LockFlags} flags + * @returns {Promise} - Returns Boolean. + */ + + async verifyLocks(prev, tx, view, flags) { + const [height, time] = await this.getLocks(prev, tx, view, flags); + + if (height !== -1) { + if (height >= prev.height + 1) + return false; + } + + if (time !== -1) { + const mtp = await this.getMedianTime(prev); + + if (time >= mtp) + return false; + } + + return true; + } +} /** * ChainOptions * @alias module:blockchain.ChainOptions - * @constructor - * @param {Object} options */ -function ChainOptions(options) { - if (!(this instanceof ChainOptions)) - return new ChainOptions(options); +class ChainOptions { + /** + * Create chain options. + * @constructor + * @param {Object} options + */ - this.network = Network.primary; - this.logger = Logger.global; - this.workers = null; + constructor(options) { + this.network = Network.primary; + this.logger = Logger.global; + this.workers = null; - this.prefix = null; - this.location = null; - this.db = 'memory'; - this.maxFiles = 64; - this.cacheSize = 32 << 20; - this.compression = true; - this.bufferKeys = ChainDB.layout.binary; + this.prefix = null; + this.location = null; + this.db = 'memory'; + this.maxFiles = 64; + this.cacheSize = 32 << 20; + this.compression = true; + this.bufferKeys = ChainDB.layout.binary; - this.spv = false; - this.bip91 = false; - this.bip148 = false; - this.prune = false; - this.indexTX = false; - this.indexAddress = false; - this.forceFlags = false; + this.spv = false; + this.bip91 = false; + this.bip148 = false; + this.prune = false; + this.indexTX = false; + this.indexAddress = false; + this.forceFlags = false; - this.coinCache = 0; - this.entryCache = 5000; - this.maxOrphans = 20; - this.checkpoints = true; + this.coinCache = 0; + this.entryCache = 5000; + this.maxOrphans = 20; + this.checkpoints = true; - if (options) - this.fromOptions(options); + if (options) + this.fromOptions(options); + } + + /** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {ChainOptions} + */ + + fromOptions(options) { + if (options.network != null) + this.network = Network.get(options.network); + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.workers != null) { + assert(typeof options.workers === 'object'); + this.workers = options.workers; + } + + if (options.spv != null) { + assert(typeof options.spv === 'boolean'); + this.spv = options.spv; + } + + if (options.prefix != null) { + assert(typeof options.prefix === 'string'); + this.prefix = options.prefix; + this.location = this.spv + ? path.join(this.prefix, 'spvchain') + : path.join(this.prefix, 'chain'); + } + + if (options.location != null) { + assert(typeof options.location === 'string'); + this.location = options.location; + } + + if (options.db != null) { + assert(typeof options.db === 'string'); + this.db = options.db; + } + + if (options.maxFiles != null) { + assert((options.maxFiles >>> 0) === options.maxFiles); + this.maxFiles = options.maxFiles; + } + + if (options.cacheSize != null) { + assert(Number.isSafeInteger(options.cacheSize)); + assert(options.cacheSize >= 0); + this.cacheSize = options.cacheSize; + } + + if (options.compression != null) { + assert(typeof options.compression === 'boolean'); + this.compression = options.compression; + } + + if (options.prune != null) { + assert(typeof options.prune === 'boolean'); + this.prune = options.prune; + } + + if (options.indexTX != null) { + assert(typeof options.indexTX === 'boolean'); + this.indexTX = options.indexTX; + } + + if (options.indexAddress != null) { + assert(typeof options.indexAddress === 'boolean'); + this.indexAddress = options.indexAddress; + } + + if (options.forceFlags != null) { + assert(typeof options.forceFlags === 'boolean'); + this.forceFlags = options.forceFlags; + } + + if (options.bip91 != null) { + assert(typeof options.bip91 === 'boolean'); + this.bip91 = options.bip91; + } + + if (options.bip148 != null) { + assert(typeof options.bip148 === 'boolean'); + this.bip148 = options.bip148; + } + + if (options.coinCache != null) { + assert((options.coinCache >>> 0) === options.coinCache); + this.coinCache = options.coinCache; + } + + if (options.entryCache != null) { + assert((options.entryCache >>> 0) === options.entryCache); + this.entryCache = options.entryCache; + } + + if (options.maxOrphans != null) { + assert((options.maxOrphans >>> 0) === options.maxOrphans); + this.maxOrphans = options.maxOrphans; + } + + if (options.checkpoints != null) { + assert(typeof options.checkpoints === 'boolean'); + this.checkpoints = options.checkpoints; + } + + return this; + } + + /** + * Instantiate chain options from object. + * @param {Object} options + * @returns {ChainOptions} + */ + + static fromOptions(options) { + return new ChainOptions().fromOptions(options); + } } /** - * Inject properties from object. - * @private - * @param {Object} options - * @returns {ChainOptions} - */ - -ChainOptions.prototype.fromOptions = function fromOptions(options) { - if (options.network != null) - this.network = Network.get(options.network); - - if (options.logger != null) { - assert(typeof options.logger === 'object'); - this.logger = options.logger; - } - - if (options.workers != null) { - assert(typeof options.workers === 'object'); - this.workers = options.workers; - } - - if (options.spv != null) { - assert(typeof options.spv === 'boolean'); - this.spv = options.spv; - } - - if (options.prefix != null) { - assert(typeof options.prefix === 'string'); - this.prefix = options.prefix; - this.location = this.spv - ? path.join(this.prefix, 'spvchain') - : path.join(this.prefix, 'chain'); - } - - if (options.location != null) { - assert(typeof options.location === 'string'); - this.location = options.location; - } - - if (options.db != null) { - assert(typeof options.db === 'string'); - this.db = options.db; - } - - if (options.maxFiles != null) { - assert((options.maxFiles >>> 0) === options.maxFiles); - this.maxFiles = options.maxFiles; - } - - if (options.cacheSize != null) { - assert(Number.isSafeInteger(options.cacheSize)); - assert(options.cacheSize >= 0); - this.cacheSize = options.cacheSize; - } - - if (options.compression != null) { - assert(typeof options.compression === 'boolean'); - this.compression = options.compression; - } - - if (options.prune != null) { - assert(typeof options.prune === 'boolean'); - this.prune = options.prune; - } - - if (options.indexTX != null) { - assert(typeof options.indexTX === 'boolean'); - this.indexTX = options.indexTX; - } - - if (options.indexAddress != null) { - assert(typeof options.indexAddress === 'boolean'); - this.indexAddress = options.indexAddress; - } - - if (options.forceFlags != null) { - assert(typeof options.forceFlags === 'boolean'); - this.forceFlags = options.forceFlags; - } - - if (options.bip91 != null) { - assert(typeof options.bip91 === 'boolean'); - this.bip91 = options.bip91; - } - - if (options.bip148 != null) { - assert(typeof options.bip148 === 'boolean'); - this.bip148 = options.bip148; - } - - if (options.coinCache != null) { - assert((options.coinCache >>> 0) === options.coinCache); - this.coinCache = options.coinCache; - } - - if (options.entryCache != null) { - assert((options.entryCache >>> 0) === options.entryCache); - this.entryCache = options.entryCache; - } - - if (options.maxOrphans != null) { - assert((options.maxOrphans >>> 0) === options.maxOrphans); - this.maxOrphans = options.maxOrphans; - } - - if (options.checkpoints != null) { - assert(typeof options.checkpoints === 'boolean'); - this.checkpoints = options.checkpoints; - } - - return this; -}; - -/** - * Instantiate chain options from object. - * @param {Object} options - * @returns {ChainOptions} - */ - -ChainOptions.fromOptions = function fromOptions(options) { - return new ChainOptions().fromOptions(options); -}; - -/** - * Represents the deployment state of the chain. + * Deployment State * @alias module:blockchain.DeploymentState - * @constructor * @property {VerifyFlags} flags * @property {LockFlags} lockFlags * @property {Boolean} bip34 */ -function DeploymentState() { - if (!(this instanceof DeploymentState)) - return new DeploymentState(); +class DeploymentState { + /** + * Create a deployment state. + * @constructor + */ - this.flags = Script.flags.MANDATORY_VERIFY_FLAGS; - this.flags &= ~Script.flags.VERIFY_P2SH; - this.lockFlags = common.lockFlags.MANDATORY_LOCKTIME_FLAGS; - this.bip34 = false; - this.bip91 = false; - this.bip148 = false; + constructor() { + this.flags = Script.flags.MANDATORY_VERIFY_FLAGS; + this.flags &= ~Script.flags.VERIFY_P2SH; + this.lockFlags = common.lockFlags.MANDATORY_LOCKTIME_FLAGS; + this.bip34 = false; + this.bip91 = false; + this.bip148 = false; + } + + /** + * Test whether p2sh is active. + * @returns {Boolean} + */ + + hasP2SH() { + return (this.flags & Script.flags.VERIFY_P2SH) !== 0; + } + + /** + * Test whether bip34 (coinbase height) is active. + * @returns {Boolean} + */ + + hasBIP34() { + return this.bip34; + } + + /** + * Test whether bip66 (VERIFY_DERSIG) is active. + * @returns {Boolean} + */ + + hasBIP66() { + return (this.flags & Script.flags.VERIFY_DERSIG) !== 0; + } + + /** + * Test whether cltv is active. + * @returns {Boolean} + */ + + hasCLTV() { + return (this.flags & Script.flags.VERIFY_CHECKLOCKTIMEVERIFY) !== 0; + } + + /** + * Test whether median time past locktime is active. + * @returns {Boolean} + */ + + hasMTP() { + return (this.lockFlags & common.lockFlags.MEDIAN_TIME_PAST) !== 0; + } + + /** + * Test whether csv is active. + * @returns {Boolean} + */ + + hasCSV() { + return (this.flags & Script.flags.VERIFY_CHECKSEQUENCEVERIFY) !== 0; + } + + /** + * Test whether segwit is active. + * @returns {Boolean} + */ + + hasWitness() { + return (this.flags & Script.flags.VERIFY_WITNESS) !== 0; + } + + /** + * Test whether bip91 is active. + * @returns {Boolean} + */ + + hasBIP91() { + return this.bip91; + } + + /** + * Test whether bip148 is active. + * @returns {Boolean} + */ + + hasBIP148() { + return this.bip148; + } } -/** - * Test whether p2sh is active. - * @returns {Boolean} - */ - -DeploymentState.prototype.hasP2SH = function hasP2SH() { - return (this.flags & Script.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 & Script.flags.VERIFY_DERSIG) !== 0; -}; - -/** - * Test whether cltv is active. - * @returns {Boolean} - */ - -DeploymentState.prototype.hasCLTV = function hasCLTV() { - return (this.flags & Script.flags.VERIFY_CHECKLOCKTIMEVERIFY) !== 0; -}; - -/** - * Test whether median time past locktime is active. - * @returns {Boolean} - */ - -DeploymentState.prototype.hasMTP = function hasMTP() { - return (this.lockFlags & common.lockFlags.MEDIAN_TIME_PAST) !== 0; -}; - -/** - * Test whether csv is active. - * @returns {Boolean} - */ - -DeploymentState.prototype.hasCSV = function hasCSV() { - return (this.flags & Script.flags.VERIFY_CHECKSEQUENCEVERIFY) !== 0; -}; - -/** - * Test whether segwit is active. - * @returns {Boolean} - */ - -DeploymentState.prototype.hasWitness = function hasWitness() { - return (this.flags & Script.flags.VERIFY_WITNESS) !== 0; -}; - -/** - * Test whether bip91 is active. - * @returns {Boolean} - */ - -DeploymentState.prototype.hasBIP91 = function hasBIP91() { - return this.bip91; -}; - -/** - * Test whether bip148 is active. - * @returns {Boolean} - */ - -DeploymentState.prototype.hasBIP148 = function hasBIP148() { - return this.bip148; -}; - /** * Orphan - * @constructor * @ignore */ -function Orphan(block, flags, id) { - this.block = block; - this.flags = flags; - this.id = id; - this.time = util.now(); +class Orphan { + /** + * Create an orphan. + * @constructor + */ + + constructor(block, flags, id) { + this.block = block; + this.flags = flags; + this.id = id; + this.time = util.now(); + } } /* diff --git a/lib/mempool/mempool.js b/lib/mempool/mempool.js index ed80e895..ecd6e091 100644 --- a/lib/mempool/mempool.js +++ b/lib/mempool/mempool.js @@ -690,7 +690,7 @@ Mempool.prototype.has = function has(hash) { */ Mempool.prototype.exists = function exists(hash) { - if (this.locker.hasPending(hash)) + if (this.locker.pending(hash)) return true; if (this.hasOrphan(hash)) diff --git a/lib/protocol/consensus.js b/lib/protocol/consensus.js index 8a3d8608..91d10b98 100644 --- a/lib/protocol/consensus.js +++ b/lib/protocol/consensus.js @@ -93,6 +93,14 @@ exports.MAX_BLOCK_SIGOPS = 1000000 / 50; exports.MAX_BLOCK_SIGOPS_COST = 80000; +/** + * Size of set to pick median time from. + * @const {Number} + * @default + */ + +exports.MEDIAN_TIMESPAN = 11; + /** * What bits to set in version * for versionbits blocks. diff --git a/lib/utils/asyncemitter.js b/lib/utils/asyncemitter.js index 422587d2..b10d7082 100644 --- a/lib/utils/asyncemitter.js +++ b/lib/utils/asyncemitter.js @@ -9,388 +9,428 @@ const assert = require('assert'); /** - * Represents a promise-resolving event emitter. + * Async Emitter * @alias module:utils.AsyncEmitter * @see EventEmitter - * @constructor */ -function AsyncEmitter() { - if (!(this instanceof AsyncEmitter)) - return new AsyncEmitter(); +class AsyncEmitter { + /** + * Create an async emitter. + * @constructor + */ - this._events = Object.create(null); + constructor() { + this._events = Object.create(null); + } + + /** + * Add a listener. + * @param {String} type + * @param {Function} handler + */ + + addListener(type, handler) { + return this._push(type, handler, false); + } + + /** + * Add a listener. + * @param {String} type + * @param {Function} handler + */ + + on(type, handler) { + return this.addListener(type, handler); + } + + /** + * Add a listener to execute once. + * @param {String} type + * @param {Function} handler + */ + + once(type, handler) { + return this._push(type, handler, true); + } + + /** + * Prepend a listener. + * @param {String} type + * @param {Function} handler + */ + + prependListener(type, handler) { + return this._unshift(type, handler, false); + } + + /** + * Prepend a listener to execute once. + * @param {String} type + * @param {Function} handler + */ + + prependOnceListener(type, handler) { + return this._unshift(type, handler, true); + } + + /** + * Push a listener. + * @private + * @param {String} type + * @param {Function} handler + * @param {Boolean} once + */ + + _push(type, handler, once) { + assert(typeof type === 'string', '`type` must be a string.'); + + if (!this._events[type]) + this._events[type] = []; + + this.emit('newListener', type, handler); + + this._events[type].push(new Listener(handler, once)); + } + + /** + * Unshift a listener. + * @param {String} type + * @param {Function} handler + * @param {Boolean} once + */ + + _unshift(type, handler, once) { + assert(typeof type === 'string', '`type` must be a string.'); + + if (!this._events[type]) + this._events[type] = []; + + this.emit('newListener', type, handler); + + this._events[type].unshift(new Listener(handler, once)); + } + + /** + * Remove a listener. + * @param {String} type + * @param {Function} handler + */ + + removeListener(type, handler) { + assert(typeof type === 'string', '`type` must be a string.'); + + const listeners = this._events[type]; + + if (!listeners) + return; + + let index = -1; + + for (let i = 0; i < listeners.length; i++) { + const listener = listeners[i]; + if (listener.handler === handler) { + index = i; + break; + } + } + + if (index === -1) + return; + + splice(listeners, index); + + if (listeners.length === 0) + delete this._events[type]; + + this.emit('removeListener', type, handler); + } + + /** + * Set max listeners. + * @param {Number} max + */ + + setMaxListeners(max) { + assert(typeof max === 'number', '`max` must be a number.'); + assert(max >= 0, '`max` must be non-negative.'); + assert(Number.isSafeInteger(max), '`max` must be an integer.'); + } + + /** + * Remove all listeners. + * @param {String?} type + */ + + removeAllListeners(type) { + if (arguments.length === 0) { + this._events = Object.create(null); + return; + } + + assert(typeof type === 'string', '`type` must be a string.'); + + delete this._events[type]; + } + + /** + * Get listeners array. + * @param {String} type + * @returns {Function[]} + */ + + listeners(type) { + assert(typeof type === 'string', '`type` must be a string.'); + + const listeners = this._events[type]; + + if (!listeners) + return []; + + const result = []; + + for (const {handler} of listeners) + result.push(handler); + + return result; + } + + /** + * Get listener count for an event. + * @param {String} type + */ + + listenerCount(type) { + assert(typeof type === 'string', '`type` must be a string.'); + + const listeners = this._events[type]; + + if (!listeners) + return 0; + + return listeners.length; + } + + /** + * Get event names. + * @returns {String[]} + */ + + eventNames() { + return Object.keys(this._events); + } + + /** + * Emit an event synchronously. + * @param {String} type + * @param {...Object} args + * @returns {Promise} + */ + + emit(type) { + try { + this._emit.apply(this, arguments); + } catch (e) { + if (type === 'error') + throw e; + + this._emit('error', e); + } + } + + /** + * Emit an event synchronously. + * @private + * @param {String} type + * @param {...Object} args + * @returns {Promise} + */ + + _emit(type) { + assert(typeof type === 'string', '`type` must be a string.'); + + const listeners = this._events[type]; + + if (!listeners) { + if (type === 'error') { + const msg = arguments[1]; + + if (msg instanceof Error) + throw msg; + + const err = new Error(`Uncaught, unspecified "error" event. (${msg})`); + err.context = msg; + throw err; + } + return; + } + + assert(listeners.length > 0); + + let args = null; + + for (let i = 0; i < listeners.length; i++) { + const listener = listeners[i]; + const handler = listener.handler; + + if (listener.once) { + splice(listeners, i); + if (listeners.length === 0) + delete this._events[type]; + i -= 1; + } + + switch (arguments.length) { + case 1: + handler(); + break; + case 2: + handler(arguments[1]); + break; + case 3: + handler(arguments[1], arguments[2]); + break; + case 4: + handler(arguments[1], arguments[2], arguments[3]); + break; + default: + if (!args) { + args = new Array(arguments.length - 1); + for (let j = 1; j < arguments.length; j++) + args[j - 1] = arguments[j]; + } + handler.apply(null, args); + break; + } + } + } + + /** + * Emit an event. Wait for promises to resolve. + * @method + * @param {String} type + * @param {...Object} args + * @returns {Promise} + */ + + async emitAsync(type) { + try { + await this._emitAsync.apply(this, arguments); + } catch (e) { + if (type === 'error') + throw e; + + await this._emitAsync('error', e); + } + } + + /** + * Emit an event. Wait for promises to resolve. + * @private + * @param {String} type + * @param {...Object} args + * @returns {Promise} + */ + + async _emitAsync(type) { + assert(typeof type === 'string', '`type` must be a string.'); + + const listeners = this._events[type]; + + if (!listeners) { + if (type === 'error') { + const msg = arguments[1]; + + if (msg instanceof Error) + throw msg; + + const err = new Error(`Uncaught, unspecified "error" event. (${msg})`); + err.context = msg; + throw err; + } + return; + } + + assert(listeners.length > 0); + + let args = null; + + for (let i = 0; i < listeners.length; i++) { + const listener = listeners[i]; + const handler = listener.handler; + + if (listener.once) { + splice(listeners, i); + if (listeners.length === 0) + delete this._events[type]; + i -= 1; + } + + switch (arguments.length) { + case 1: + await handler(); + break; + case 2: + await handler(arguments[1]); + break; + case 3: + await handler(arguments[1], arguments[2]); + break; + case 4: + await handler(arguments[1], arguments[2], arguments[3]); + break; + default: + if (!args) { + args = new Array(arguments.length - 1); + for (let j = 1; j < arguments.length; j++) + args[j - 1] = arguments[j]; + } + await handler.apply(null, args); + break; + } + } + } } -/** - * Add a listener. - * @param {String} type - * @param {Function} handler - */ - -AsyncEmitter.prototype.addListener = function addListener(type, handler) { - return this._push(type, handler, false); -}; - -/** - * Add a listener. - * @param {String} type - * @param {Function} handler - */ - -AsyncEmitter.prototype.on = function on(type, handler) { - return this.addListener(type, handler); -}; - -/** - * Add a listener to execute once. - * @param {String} type - * @param {Function} handler - */ - -AsyncEmitter.prototype.once = function once(type, handler) { - return this._push(type, handler, true); -}; - -/** - * Prepend a listener. - * @param {String} type - * @param {Function} handler - */ - -AsyncEmitter.prototype.prependListener = function prependListener(type, handler) { - return this._unshift(type, handler, false); -}; - -/** - * Prepend a listener to execute once. - * @param {String} type - * @param {Function} handler - */ - -AsyncEmitter.prototype.prependOnceListener = function prependOnceListener(type, handler) { - return this._unshift(type, handler, true); -}; - -/** - * Push a listener. - * @private - * @param {String} type - * @param {Function} handler - * @param {Boolean} once - */ - -AsyncEmitter.prototype._push = function _push(type, handler, once) { - assert(typeof type === 'string', '`type` must be a string.'); - - if (!this._events[type]) - this._events[type] = []; - - this._events[type].push(new Listener(handler, once)); - - this.emit('newListener', type, handler); -}; - -/** - * Unshift a listener. - * @param {String} type - * @param {Function} handler - * @param {Boolean} once - */ - -AsyncEmitter.prototype._unshift = function _unshift(type, handler, once) { - assert(typeof type === 'string', '`type` must be a string.'); - - if (!this._events[type]) - this._events[type] = []; - - this._events[type].unshift(new Listener(handler, once)); - - this.emit('newListener', type, handler); -}; - -/** - * Remove a listener. - * @param {String} type - * @param {Function} handler - */ - -AsyncEmitter.prototype.removeListener = function removeListener(type, handler) { - assert(typeof type === 'string', '`type` must be a string.'); - - const listeners = this._events[type]; - - if (!listeners) - return; - - let index = -1; - - for (let i = 0; i < listeners.length; i++) { - const listener = listeners[i]; - if (listener.handler === handler) { - index = i; - break; - } - } - - if (index === -1) - return; - - listeners.splice(index, 1); - - if (listeners.length === 0) - delete this._events[type]; - - this.emit('removeListener', type, handler); -}; - -/** - * Set max listeners. - * @param {Number} max - */ - -AsyncEmitter.prototype.setMaxListeners = function setMaxListeners(max) { - assert(typeof max === 'number', '`max` must be a number.'); - assert(max >= 0, '`max` must be non-negative.'); - assert(Number.isSafeInteger(max), '`max` must be an integer.'); -}; - -/** - * Remove all listeners. - * @param {String?} type - */ - -AsyncEmitter.prototype.removeAllListeners = function removeAllListeners(type) { - if (arguments.length === 0) { - this._events = Object.create(null); - return; - } - - assert(typeof type === 'string', '`type` must be a string.'); - - delete this._events[type]; -}; - -/** - * Get listeners array. - * @param {String} type - * @returns {Function[]} - */ - -AsyncEmitter.prototype.listeners = function listeners(type) { - assert(typeof type === 'string', '`type` must be a string.'); - - const listeners = this._events[type]; - - if (!listeners) - return []; - - const result = []; - - for (const {handler} of listeners) - result.push(handler); - - return result; -}; - -/** - * Get listener count for an event. - * @param {String} type - */ - -AsyncEmitter.prototype.listenerCount = function listenerCount(type) { - assert(typeof type === 'string', '`type` must be a string.'); - - const listeners = this._events[type]; - - if (!listeners) - return 0; - - return listeners.length; -}; - -/** - * Emit an event synchronously. - * @param {String} type - * @param {...Object} args - * @returns {Promise} - */ - -AsyncEmitter.prototype.emit = function emit(type) { - try { - this._emit.apply(this, arguments); - } catch (e) { - if (type === 'error') - throw e; - - this._emit('error', e); - } -}; - -/** - * Emit an event synchronously. - * @private - * @param {String} type - * @param {...Object} args - * @returns {Promise} - */ - -AsyncEmitter.prototype._emit = function _emit(type) { - assert(typeof type === 'string', '`type` must be a string.'); - - const listeners = this._events[type]; - - if (!listeners) { - if (type === 'error') { - const error = arguments[1]; - - if (error instanceof Error) - throw error; - - const err = new Error(`Uncaught, unspecified "error" event. (${error})`); - err.context = error; - throw err; - } - return; - } - - assert(listeners.length > 0); - - let args = null; - - for (let i = 0; i < listeners.length; i++) { - const listener = listeners[i]; - const handler = listener.handler; - - if (listener.once) { - listeners.splice(i, 1); - i -= 1; - } - - switch (arguments.length) { - case 1: - handler(); - break; - case 2: - handler(arguments[1]); - break; - case 3: - handler(arguments[1], arguments[2]); - break; - case 4: - handler(arguments[1], arguments[2], arguments[3]); - break; - default: - if (!args) { - args = new Array(arguments.length - 1); - for (let j = 1; j < arguments.length; j++) - args[j - 1] = arguments[j]; - } - handler.apply(null, args); - break; - } - } -}; - -/** - * Emit an event. Wait for promises to resolve. - * @method - * @param {String} type - * @param {...Object} args - * @returns {Promise} - */ - -AsyncEmitter.prototype.emitAsync = async function emitAsync(type) { - try { - await this._emitAsync.apply(this, arguments); - } catch (e) { - if (type === 'error') - throw e; - - await this._emitAsync('error', e); - } -}; - -/** - * Emit an event. Wait for promises to resolve. - * @private - * @param {String} type - * @param {...Object} args - * @returns {Promise} - */ - -AsyncEmitter.prototype._emitAsync = async function _emitAsync(type) { - assert(typeof type === 'string', '`type` must be a string.'); - - const listeners = this._events[type]; - - if (!listeners) { - if (type === 'error') { - const error = arguments[1]; - - if (error instanceof Error) - throw error; - - const err = new Error(`Uncaught, unspecified "error" event. (${error})`); - err.context = error; - throw err; - } - return; - } - - assert(listeners.length > 0); - - let args = null; - - for (let i = 0; i < listeners.length; i++) { - const listener = listeners[i]; - const handler = listener.handler; - - if (listener.once) { - listeners.splice(i, 1); - i -= 1; - } - - switch (arguments.length) { - case 1: - await handler(); - break; - case 2: - await handler(arguments[1]); - break; - case 3: - await handler(arguments[1], arguments[2]); - break; - case 4: - await handler(arguments[1], arguments[2], arguments[3]); - break; - default: - if (!args) { - args = new Array(arguments.length - 1); - for (let j = 1; j < arguments.length; j++) - args[j - 1] = arguments[j]; - } - await handler.apply(null, args); - break; - } - } -}; - /** * Event Listener - * @constructor * @ignore - * @param {Function} handler - * @param {Boolean} once * @property {Function} handler * @property {Boolean} once */ -function Listener(handler, once) { - assert(typeof handler === 'function', '`handler` must be a function.'); - assert(typeof once === 'boolean', '`once` must be a function.'); - this.handler = handler; - this.once = once; +class Listener { + /** + * Create an event listener. + * @constructor + * @param {Function} handler + * @param {Boolean} once + */ + + constructor(handler, once) { + assert(typeof handler === 'function', '`handler` must be a function.'); + assert(typeof once === 'boolean', '`once` must be a function.'); + this.handler = handler; + this.once = once; + } +} + +/* + * Helpers + */ + +function splice(list, i) { + if (i === 0) { + list.shift(); + return; + } + + let k = i + 1; + + while (k < list.length) + list[i++] = list[k++]; + + list.pop(); } /* diff --git a/lib/utils/binary.js b/lib/utils/binary.js index 04a8d9d6..46b9a984 100644 --- a/lib/utils/binary.js +++ b/lib/utils/binary.js @@ -79,7 +79,25 @@ exports.remove = function remove(items, item, compare) { if (i === -1) return false; - items.splice(i, 1); + splice(items, i); return true; }; + +/* + * Helpers + */ + +function splice(list, i) { + if (i === 0) { + list.shift(); + return; + } + + let k = i + 1; + + while (k < list.length) + list[i++] = list[k++]; + + list.pop(); +} diff --git a/lib/utils/heap.js b/lib/utils/heap.js index 17eef4cd..6aa2f91c 100644 --- a/lib/utils/heap.js +++ b/lib/utils/heap.js @@ -11,221 +11,224 @@ const assert = require('assert'); /** * Binary Heap * @alias module:utils.Heap - * @constructor - * @param {Function?} compare */ -function Heap(compare) { - if (!(this instanceof Heap)) - return new Heap(compare); +class Heap { + /** + * Create a binary heap. + * @constructor + * @param {Function?} compare + */ - this.compare = comparator; - this.items = []; + constructor(compare) { + this.compare = comparator; + this.items = []; - if (compare) - this.set(compare); + if (compare) + this.set(compare); + } + + /** + * Initialize and sort heap. + */ + + init() { + const n = this.items.length; + + if (n <= 1) + return; + + for (let i = (n / 2 | 0) - 1; i >= 0; i--) + this.down(i, n); + } + + /** + * Get heap size. + * @returns {Number} + */ + + size() { + return this.items.length; + } + + /** + * Set comparator. + * @param {Function} compare + */ + + set(compare) { + assert(typeof compare === 'function', + 'Comparator must be a function.'); + this.compare = compare; + } + + /** + * Push item onto heap. + * @param {Object} item + * @returns {Number} + */ + + insert(item) { + this.items.push(item); + this.up(this.items.length - 1); + return this.items.length; + } + + /** + * Pop next item off of heap. + * @param {Object} item + * @returns {Object} + */ + + shift() { + if (this.items.length === 0) + return null; + + const n = this.items.length - 1; + + this.swap(0, n); + this.down(0, n); + + return this.items.pop(); + } + + /** + * Remove item from heap. + * @param {Number} i + * @returns {Object} + */ + + remove(i) { + if (this.items.length === 0) + return null; + + const n = this.items.length - 1; + + if (i < 0 || i > n) + return null; + + if (n !== i) { + this.swap(i, n); + this.down(i, n); + this.up(i); + } + + return this.items.pop(); + } + + /** + * Swap indicies. + * @private + * @param {Number} a + * @param {Number} b + */ + + swap(a, b) { + const x = this.items[a]; + const y = this.items[b]; + this.items[a] = y; + this.items[b] = x; + } + + /** + * Compare indicies. + * @private + * @param {Number} i + * @param {Number} j + * @returns {Boolean} + */ + + less(i, j) { + return this.compare(this.items[i], this.items[j]) < 0; + } + + /** + * Bubble item down. + * @private + * @param {Number} i + * @param {Number} n + */ + + down(i, n) { + for (;;) { + const l = 2 * i + 1; + + assert(l >= 0); + + if (l < 0 || l >= n) + break; + + let j = l; + const r = l + 1; + + if (r < n && !this.less(l, r)) + j = r; + + if (!this.less(j, i)) + break; + + this.swap(i, j); + i = j; + } + } + + /** + * Bubble item up. + * @private + * @param {Number} i + */ + + up(i) { + for (;;) { + const j = (i - 1) / 2 | 0; + + assert(j >= 0); + + if (j < 0 || j === i) + break; + + if (!this.less(i, j)) + break; + + this.swap(j, i); + i = j; + } + } + + /** + * Convert heap to sorted array. + * @returns {Object[]} + */ + + toArray() { + const heap = new Heap(); + const result = []; + + heap.compare = this.compare; + heap.items = this.items.slice(); + + while (heap.size() > 0) + result.push(heap.shift()); + + return result; + } + + /** + * Instantiate heap from array and comparator. + * @param {Function} compare + * @param {Object[]} items + * @returns {Heap} + */ + + static fromArray(compare, items) { + const heap = new Heap(); + heap.set(compare); + heap.items = items; + heap.init(); + return heap; + } } -/** - * Initialize and sort heap. - */ - -Heap.prototype.init = function init() { - const n = this.items.length; - - if (n <= 1) - return; - - for (let i = (n / 2 | 0) - 1; i >= 0; i--) - this.down(i, n); -}; - -/** - * Get heap size. - * @returns {Number} - */ - -Heap.prototype.size = function size() { - return this.items.length; -}; - -/** - * Set comparator. - * @param {Function} compare - */ - -Heap.prototype.set = function set(compare) { - assert(typeof compare === 'function', - 'Comparator must be a function.'); - this.compare = compare; -}; - -/** - * Push item onto heap. - * @param {Object} item - * @returns {Number} - */ - -Heap.prototype.insert = function insert(item) { - this.items.push(item); - this.up(this.items.length - 1); - return this.items.length; -}; - -/** - * Pop next item off of heap. - * @param {Object} item - * @returns {Object} - */ - -Heap.prototype.shift = function shift() { - if (this.items.length === 0) - return null; - - const n = this.items.length - 1; - - this.swap(0, n); - this.down(0, n); - - return this.items.pop(); -}; - -/** - * Remove item from heap. - * @param {Number} i - * @returns {Object} - */ - -Heap.prototype.remove = function remove(i) { - if (this.items.length === 0) - return null; - - const n = this.items.length - 1; - - if (i < 0 || i > n) - return null; - - if (n !== i) { - this.swap(i, n); - this.down(i, n); - this.up(i); - } - - return this.items.pop(); -}; - -/** - * Swap indicies. - * @private - * @param {Number} a - * @param {Number} b - */ - -Heap.prototype.swap = function swap(a, b) { - const x = this.items[a]; - const y = this.items[b]; - this.items[a] = y; - this.items[b] = x; -}; - -/** - * Compare indicies. - * @private - * @param {Number} i - * @param {Number} j - * @returns {Boolean} - */ - -Heap.prototype.less = function less(i, j) { - return this.compare(this.items[i], this.items[j]) < 0; -}; - -/** - * Bubble item down. - * @private - * @param {Number} i - * @param {Number} n - */ - -Heap.prototype.down = function down(i, n) { - for (;;) { - const l = 2 * i + 1; - - assert(l >= 0); - - if (l < 0 || l >= n) - break; - - let j = l; - const r = l + 1; - - if (r < n && !this.less(l, r)) - j = r; - - if (!this.less(j, i)) - break; - - this.swap(i, j); - i = j; - } -}; - -/** - * Bubble item up. - * @private - * @param {Number} i - */ - -Heap.prototype.up = function up(i) { - for (;;) { - const j = (i - 1) / 2 | 0; - - assert(j >= 0); - - if (j < 0 || j === i) - break; - - if (!this.less(i, j)) - break; - - this.swap(j, i); - i = j; - } -}; - -/** - * Convert heap to sorted array. - * @returns {Object[]} - */ - -Heap.prototype.toArray = function toArray() { - const heap = new Heap(); - const result = []; - - heap.compare = this.compare; - heap.items = this.items.slice(); - - while (heap.size() > 0) - result.push(heap.shift()); - - return result; -}; - -/** - * Instantiate heap from array and comparator. - * @param {Function} compare - * @param {Object[]} items - * @returns {Heap} - */ - -Heap.fromArray = function fromArray(compare, items) { - const heap = new Heap(); - heap.set(compare); - heap.items = items; - heap.init(); - return heap; -}; - /* * Helpers */ diff --git a/lib/utils/list.js b/lib/utils/list.js index 75844e17..97009805 100644 --- a/lib/utils/list.js +++ b/lib/utils/list.js @@ -9,263 +9,273 @@ const assert = require('assert'); /** - * A double linked list. + * Double Linked List * @alias module:utils.List - * @constructor - * @property {ListItem|null} head - * @property {ListItem|null} tail - * @property {Number} size */ -function List() { - if (!(this instanceof List)) - return new List(); +class List { + /** + * Create a list. + * @constructor + * @property {ListItem|null} head + * @property {ListItem|null} tail + * @property {Number} size + */ - this.head = null; - this.tail = null; - this.size = 0; -} - -/** - * Reset the cache. Clear all items. - */ - -List.prototype.reset = function reset() { - let item, next; - - for (item = this.head; item; item = next) { - next = item.next; - item.prev = null; - item.next = null; + constructor() { + this.head = null; + this.tail = null; + this.size = 0; } - assert(!item); + /** + * Reset the cache. Clear all items. + */ - this.head = null; - this.tail = null; - this.size = 0; -}; + reset() { + let item, next; -/** - * Remove the first item in the list. - * @returns {ListItem} - */ - -List.prototype.shift = function shift() { - const item = this.head; - - if (!item) - return null; - - this.remove(item); - - return item; -}; - -/** - * Prepend an item to the linked list (sets new head). - * @param {ListItem} - * @returns {Boolean} - */ - -List.prototype.unshift = function unshift(item) { - return this.insert(null, item); -}; - -/** - * Append an item to the linked list (sets new tail). - * @param {ListItem} - * @returns {Boolean} - */ - -List.prototype.push = function push(item) { - return this.insert(this.tail, item); -}; - -/** - * Remove the last item in the list. - * @returns {ListItem} - */ - -List.prototype.pop = function pop() { - const item = this.tail; - - if (!item) - return null; - - this.remove(item); - - return item; -}; - -/** - * Insert item into the linked list. - * @private - * @param {ListItem|null} ref - * @param {ListItem} item - * @returns {Boolean} - */ - -List.prototype.insert = function insert(ref, item) { - if (item.prev || item.next || item === this.head) - return false; - - assert(!item.prev); - assert(!item.next); - - if (ref == null) { - if (!this.head) { - this.head = item; - this.tail = item; - } else { - this.head.prev = item; - item.next = this.head; - this.head = item; + for (item = this.head; item; item = next) { + next = item.next; + item.prev = null; + item.next = null; } - this.size++; + + assert(!item); + + this.head = null; + this.tail = null; + this.size = 0; + } + + /** + * Remove the first item in the list. + * @returns {ListItem} + */ + + shift() { + const item = this.head; + + if (!item) + return null; + + this.remove(item); + + return item; + } + + /** + * Prepend an item to the linked list (sets new head). + * @param {ListItem} + * @returns {Boolean} + */ + + unshift(item) { + return this.insert(null, item); + } + + /** + * Append an item to the linked list (sets new tail). + * @param {ListItem} + * @returns {Boolean} + */ + + push(item) { + return this.insert(this.tail, item); + } + + /** + * Remove the last item in the list. + * @returns {ListItem} + */ + + pop() { + const item = this.tail; + + if (!item) + return null; + + this.remove(item); + + return item; + } + + /** + * Insert item into the linked list. + * @private + * @param {ListItem|null} ref + * @param {ListItem} item + * @returns {Boolean} + */ + + insert(ref, item) { + if (item.prev || item.next || item === this.head) + return false; + + assert(!item.prev); + assert(!item.next); + + if (ref == null) { + if (!this.head) { + this.head = item; + this.tail = item; + } else { + this.head.prev = item; + item.next = this.head; + this.head = item; + } + this.size += 1; + return true; + } + + item.next = ref.next; + item.prev = ref; + ref.next = item; + + if (ref === this.tail) + this.tail = item; + + this.size += 1; + return true; } - item.next = ref.next; - item.prev = ref; - ref.next = item; + /** + * Remove item from the linked list. + * @private + * @param {ListItem} + * @returns {Boolean} + */ - if (ref === this.tail) - this.tail = item; + remove(item) { + if (!item.prev && !item.next && item !== this.head) + return false; - this.size++; + if (item.prev) + item.prev.next = item.next; - return true; -}; + if (item.next) + item.next.prev = item.prev; -/** - * Remove item from the linked list. - * @private - * @param {ListItem} - * @returns {Boolean} - */ + if (item === this.head) + this.head = item.next; -List.prototype.remove = function remove(item) { - if (!item.prev && !item.next && item !== this.head) - return false; + if (item === this.tail) + this.tail = item.prev || this.head; - if (item.prev) - item.prev.next = item.next; + if (!this.head) + assert(!this.tail); - if (item.next) - item.next.prev = item.prev; + if (!this.tail) + assert(!this.head); - if (item === this.head) - this.head = item.next; - - if (item === this.tail) - this.tail = item.prev || this.head; - - if (!this.head) - assert(!this.tail); - - if (!this.tail) - assert(!this.head); - - item.prev = null; - item.next = null; - - this.size--; - - return true; -}; - -/** - * Replace an item in-place. - * @param {ListItem} ref - * @param {ListItem} item - */ - -List.prototype.replace = function replace(ref, item) { - if (ref.prev) - ref.prev.next = item; - - if (ref.next) - ref.next.prev = item; - - item.prev = ref.prev; - item.next = ref.next; - - ref.next = null; - ref.prev = null; - - if (this.head === ref) - this.head = item; - - if (this.tail === ref) - this.tail = item; -}; - -/** - * Slice the list to an array of items. - * Will remove the items sliced. - * @param {Number?} total - * @returns {ListItem[]} - */ - -List.prototype.slice = function slice(total) { - const items = []; - let item, next; - - if (total == null) - total = -1; - - for (item = this.head; item; item = next) { - next = item.next; item.prev = null; item.next = null; - this.size--; + this.size -= 1; - items.push(item); - - if (items.length === total) - break; + return true; } - if (next) { - this.head = next; - next.prev = null; - } else { - this.head = null; - this.tail = null; + /** + * Replace an item in-place. + * @param {ListItem} ref + * @param {ListItem} item + */ + + replace(ref, item) { + if (ref.prev) + ref.prev.next = item; + + if (ref.next) + ref.next.prev = item; + + item.prev = ref.prev; + item.next = ref.next; + + ref.next = null; + ref.prev = null; + + if (this.head === ref) + this.head = item; + + if (this.tail === ref) + this.tail = item; } - return items; -}; + /** + * Slice the list to an array of items. + * Will remove the items sliced. + * @param {Number?} total + * @returns {ListItem[]} + */ + + slice(total) { + if (total == null) + total = -1; + + const items = []; + + let next = null; + + for (let item = this.head; item; item = next) { + next = item.next; + item.prev = null; + item.next = null; + + this.size -= 1; + + items.push(item); + + if (items.length === total) + break; + } + + if (next) { + this.head = next; + next.prev = null; + } else { + this.head = null; + this.tail = null; + } + + return items; + } + + /** + * Convert the list to an array of items. + * @returns {ListItem[]} + */ + + toArray() { + const items = []; + + for (let item = this.head; item; item = item.next) + items.push(item); + + return items; + } +} /** - * Convert the list to an array of items. - * @returns {ListItem[]} - */ - -List.prototype.toArray = function toArray() { - const items = []; - - for (let item = this.head; item; item = item.next) - items.push(item); - - return items; -}; - -/** - * Represents an linked list item. + * List Item * @alias module:utils.ListItem - * @constructor - * @private - * @param {String} key - * @param {Object} value */ -function ListItem(value) { - this.next = null; - this.prev = null; - this.value = value; +class ListItem { + /** + * Create a list item. + * @constructor + * @private + * @param {String} key + * @param {Object} value + */ + + constructor(value) { + this.next = null; + this.prev = null; + this.value = value; + } } /* diff --git a/lib/utils/lock.js b/lib/utils/lock.js index 8b0e7fa7..688604aa 100644 --- a/lib/utils/lock.js +++ b/lib/utils/lock.js @@ -10,194 +10,196 @@ const assert = require('assert'); /** - * Represents a mutex lock for locking asynchronous object methods. + * Mutex Lock * @alias module:utils.Lock - * @constructor - * @param {Boolean?} named - Whether to - * maintain a map of queued jobs by job name. */ -function Lock(named) { - if (!(this instanceof Lock)) - return Lock.create(named); +class Lock { + /** + * Create a lock. + * @constructor + * @param {Boolean?} named - Whether to + * maintain a map of queued jobs by job name. + */ - this.named = named === true; + constructor(named = false) { + this.named = named === true; - this.jobs = []; - this.busy = false; - this.destroyed = false; + this.jobs = []; + this.busy = false; + this.destroyed = false; - this.map = new Map(); - this.current = null; + this.map = new Map(); + this.current = null; - this.unlocker = this.unlock.bind(this); + this.unlocker = this.unlock.bind(this); + } + + /** + * Create a closure scoped lock. + * @param {Boolean?} named + * @returns {Function} Lock method. + */ + + static create(named) { + const lock = new Lock(named); + return function _lock(arg1, arg2) { + return lock.lock(arg1, arg2); + }; + } + + /** + * Test whether the lock has a pending + * job or a job in progress (by name). + * @param {String} name + * @returns {Boolean} + */ + + has(name) { + assert(this.named, 'Must use named jobs.'); + + if (this.current === name) + return true; + + return this.pending(name); + } + + /** + * Test whether the lock has + * a pending job by name. + * @param {String} name + * @returns {Boolean} + */ + + pending(name) { + assert(this.named, 'Must use named jobs.'); + + const count = this.map.get(name); + + if (count == null) + return false; + + return count > 0; + } + + /** + * Lock the parent object and all its methods + * which use the lock. Begin to queue calls. + * @param {String?} name - Job name. + * @param {Boolean?} force - Bypass the lock. + * @returns {Promise} - Returns {Function}, must be + * called once the method finishes executing in order + * to resolve the queue. + */ + + lock(arg1, arg2) { + let name, force; + + if (this.named) { + name = arg1 || null; + force = arg2 || false; + } else { + name = null; + force = arg1 || false; + } + + if (this.destroyed) + return Promise.reject(new Error('Lock is destroyed.')); + + if (force) { + assert(this.busy); + return Promise.resolve(nop); + } + + if (this.busy) { + if (name) { + const count = this.map.get(name) || 0; + this.map.set(name, count + 1); + } + return new Promise((resolve, reject) => { + this.jobs.push(new Job(resolve, reject, name)); + }); + } + + this.busy = true; + this.current = name; + + return Promise.resolve(this.unlocker); + } + + /** + * The actual unlock callback. + * @private + */ + + unlock() { + assert(this.destroyed || this.busy); + + this.busy = false; + this.current = null; + + if (this.jobs.length === 0) + return; + + assert(!this.destroyed); + + const job = this.jobs.shift(); + + if (job.name) { + let count = this.map.get(job.name); + assert(count > 0); + if (--count === 0) + this.map.delete(job.name); + else + this.map.set(job.name, count); + } + + this.busy = true; + this.current = job.name; + + job.resolve(this.unlocker); + } + + /** + * Destroy the lock. Purge all pending calls. + */ + + destroy() { + assert(!this.destroyed, 'Lock is already destroyed.'); + + this.destroyed = true; + + const jobs = this.jobs; + + this.busy = false; + this.jobs = []; + this.map.clear(); + this.current = null; + + for (const job of jobs) + job.reject(new Error('Lock was destroyed.')); + } } -/** - * Create a closure scoped lock. - * @param {Boolean?} named - * @returns {Function} Lock method. - */ - -Lock.create = function create(named) { - const lock = new Lock(named); - return function _lock(arg1, arg2) { - return lock.lock(arg1, arg2); - }; -}; - -/** - * Test whether the lock has a pending - * job or a job in progress (by name). - * @param {String} name - * @returns {Boolean} - */ - -Lock.prototype.has = function has(name) { - assert(this.named, 'Must use named jobs.'); - - if (this.current === name) - return true; - - const count = this.map.get(name); - - if (count == null) - return false; - - return count > 0; -}; - -/** - * Test whether the lock has - * a pending job by name. - * @param {String} name - * @returns {Boolean} - */ - -Lock.prototype.hasPending = function hasPending(name) { - assert(this.named, 'Must use named jobs.'); - - const count = this.map.get(name); - - if (count == null) - return false; - - return count > 0; -}; - -/** - * Lock the parent object and all its methods - * which use the lock. Begin to queue calls. - * @param {String?} name - Job name. - * @param {Boolean?} force - Bypass the lock. - * @returns {Promise} - Returns {Function}, must be - * called once the method finishes executing in order - * to resolve the queue. - */ - -Lock.prototype.lock = function lock(arg1, arg2) { - let name, force; - - if (this.named) { - name = arg1 || null; - force = arg2; - } else { - name = null; - force = arg1; - } - - if (this.destroyed) - return Promise.reject(new Error('Lock is destroyed.')); - - if (force) { - assert(this.busy); - return Promise.resolve(nop); - } - - if (this.busy) { - if (name) { - let count = this.map.get(name); - if (!count) - count = 0; - this.map.set(name, count + 1); - } - return new Promise((resolve, reject) => { - this.jobs.push(new Job(resolve, reject, name)); - }); - } - - this.busy = true; - this.current = name; - - return Promise.resolve(this.unlocker); -}; - -/** - * The actual unlock callback. - * @private - */ - -Lock.prototype.unlock = function unlock() { - assert(this.destroyed || this.busy); - - this.busy = false; - this.current = null; - - if (this.jobs.length === 0) - return; - - assert(!this.destroyed); - - const job = this.jobs.shift(); - - if (job.name) { - let count = this.map.get(job.name); - assert(count > 0); - if (--count === 0) - this.map.delete(job.name); - else - this.map.set(job.name, count); - } - - this.busy = true; - this.current = job.name; - - job.resolve(this.unlocker); -}; - -/** - * Destroy the lock. Purge all pending calls. - */ - -Lock.prototype.destroy = function destroy() { - assert(!this.destroyed, 'Lock is already destroyed.'); - - this.destroyed = true; - - const jobs = this.jobs; - - this.busy = false; - this.jobs = []; - this.map.clear(); - this.current = null; - - for (const job of jobs) - job.reject(new Error('Lock was destroyed.')); -}; - /** * Lock Job - * @constructor * @ignore - * @param {Function} resolve - * @param {Function} reject - * @param {String?} name */ -function Job(resolve, reject, name) { - this.resolve = resolve; - this.reject = reject; - this.name = name || null; +class Job { + /** + * Create a lock job. + * @constructor + * @param {Function} resolve + * @param {Function} reject + * @param {String?} name + */ + + constructor(resolve, reject, name) { + this.resolve = resolve; + this.reject = reject; + this.name = name || null; + } } /* diff --git a/lib/utils/lru.js b/lib/utils/lru.js index 3f4ec104..58f56e06 100644 --- a/lib/utils/lru.js +++ b/lib/utils/lru.js @@ -10,489 +10,515 @@ const assert = require('assert'); /** - * An LRU cache, used for caching {@link ChainEntry}s. + * LRU Cache * @alias module:utils.LRU - * @constructor - * @param {Number} capacity - * @param {Function?} getSize */ -function LRU(capacity, getSize) { - if (!(this instanceof LRU)) - return new LRU(capacity, getSize); +class LRU { + /** + * Create an LRU cache. + * @constructor + * @param {Number} capacity + * @param {Function?} getSize + */ - this.map = new Map(); - this.size = 0; - this.items = 0; - this.head = null; - this.tail = null; - this.pending = null; + constructor(capacity, getSize) { + this.map = new Map(); + this.size = 0; + this.items = 0; + this.head = null; + this.tail = null; + this.pending = null; - assert(typeof capacity === 'number', 'Capacity must be a number.'); - assert(capacity >= 0, 'Capacity cannot be negative.'); - assert(!getSize || typeof getSize === 'function', 'Bad size callback.'); + assert(typeof capacity === 'number', 'Capacity must be a number.'); + assert(capacity >= 0, 'Capacity cannot be negative.'); + assert(!getSize || typeof getSize === 'function', 'Bad size callback.'); - this.capacity = capacity; - this.getSize = getSize; + this.capacity = capacity; + this.getSize = getSize; + } + + /** + * Calculate size of an item. + * @private + * @param {LRUItem} item + * @returns {Number} Size. + */ + + _getSize(item) { + if (this.getSize) { + const keySize = Math.floor(item.key.length * 1.375); + return 120 + keySize + this.getSize(item.value); + } + + return 1; + } + + /** + * Compact the LRU linked list. + * @private + */ + + _compact() { + if (this.size <= this.capacity) + return; + + let item = null; + let next = null; + + for (item = this.head; item; item = next) { + if (this.size <= this.capacity) + break; + + this.size -= this._getSize(item); + this.items -= 1; + this.map.delete(item.key); + + next = item.next; + + item.prev = null; + item.next = null; + } + + if (!item) { + this.head = null; + this.tail = null; + return; + } + + this.head = item; + item.prev = null; + } + + /** + * Reset the cache. Clear all items. + */ + + reset() { + let item, next; + + for (item = this.head; item; item = next) { + this.map.delete(item.key); + this.items -= 1; + next = item.next; + item.prev = null; + item.next = null; + } + + assert(!item); + + this.size = 0; + this.head = null; + this.tail = null; + } + + /** + * Add an item to the cache. + * @param {String|Number} key + * @param {Object} value + */ + + set(key, value) { + if (this.capacity === 0) + return; + + key = String(key); + + let item = this.map.get(key); + + if (item) { + this.size -= this._getSize(item); + item.value = value; + this.size += this._getSize(item); + this._removeList(item); + this._appendList(item); + this._compact(); + return; + } + + item = new LRUItem(key, value); + + this.map.set(key, item); + + this._appendList(item); + + this.size += this._getSize(item); + this.items += 1; + + this._compact(); + } + + /** + * Retrieve an item from the cache. + * @param {String|Number} key + * @returns {Object} Item. + */ + + get(key) { + if (this.capacity === 0) + return null; + + key = String(key); + + const item = this.map.get(key); + + if (!item) + return null; + + this._removeList(item); + this._appendList(item); + + return item.value; + } + + /** + * Test whether the cache contains a key. + * @param {String|Number} key + * @returns {Boolean} + */ + + has(key) { + if (this.capacity === 0) + return false; + return this.map.has(String(key)); + } + + /** + * Remove an item from the cache. + * @param {String|Number} key + * @returns {Boolean} Whether an item was removed. + */ + + remove(key) { + if (this.capacity === 0) + return false; + + key = String(key); + + const item = this.map.get(key); + + if (!item) + return false; + + this.size -= this._getSize(item); + this.items -= 1; + + this.map.delete(key); + + this._removeList(item); + + return true; + } + + /** + * Prepend an item to the linked list (sets new head). + * @private + * @param {LRUItem} + */ + + _prependList(item) { + this._insertList(null, item); + } + + /** + * Append an item to the linked list (sets new tail). + * @private + * @param {LRUItem} + */ + + _appendList(item) { + this._insertList(this.tail, item); + } + + /** + * Insert item into the linked list. + * @private + * @param {LRUItem|null} ref + * @param {LRUItem} item + */ + + _insertList(ref, item) { + assert(!item.next); + assert(!item.prev); + + if (ref == null) { + if (!this.head) { + this.head = item; + this.tail = item; + } else { + this.head.prev = item; + item.next = this.head; + this.head = item; + } + return; + } + + item.next = ref.next; + item.prev = ref; + ref.next = item; + + if (ref === this.tail) + this.tail = item; + } + + /** + * Remove item from the linked list. + * @private + * @param {LRUItem} + */ + + _removeList(item) { + if (item.prev) + item.prev.next = item.next; + + if (item.next) + item.next.prev = item.prev; + + if (item === this.head) + this.head = item.next; + + if (item === this.tail) + this.tail = item.prev || this.head; + + if (!this.head) + assert(!this.tail); + + if (!this.tail) + assert(!this.head); + + item.prev = null; + item.next = null; + } + + /** + * Collect all keys in the cache, sorted by LRU. + * @returns {String[]} + */ + + keys() { + const items = []; + + for (let item = this.head; item; item = item.next) { + if (item === this.head) + assert(!item.prev); + if (!item.prev) + assert(item === this.head); + if (!item.next) + assert(item === this.tail); + items.push(item.key); + } + + return items; + } + + /** + * Collect all values in the cache, sorted by LRU. + * @returns {String[]} + */ + + values() { + const items = []; + + for (let item = this.head; item; item = item.next) + items.push(item.value); + + return items; + } + + /** + * Convert the LRU cache to an array of items. + * @returns {Object[]} + */ + + toArray() { + const items = []; + + for (let item = this.head; item; item = item.next) + items.push(item); + + return items; + } + + /** + * Create an atomic batch for the lru + * (used for caching database writes). + * @returns {LRUBatch} + */ + + batch() { + return new LRUBatch(this); + } + + /** + * Start the pending batch. + */ + + start() { + assert(!this.pending); + this.pending = this.batch(); + } + + /** + * Clear the pending batch. + */ + + clear() { + assert(this.pending); + this.pending.clear(); + } + + /** + * Drop the pending batch. + */ + + drop() { + assert(this.pending); + this.pending = null; + } + + /** + * Commit the pending batch. + */ + + commit() { + assert(this.pending); + this.pending.commit(); + this.pending = null; + } + + /** + * Push an item onto the pending batch. + * @param {String} key + * @param {Object} value + */ + + push(key, value) { + assert(this.pending); + + if (this.capacity === 0) + return; + + this.pending.set(key, value); + } + + /** + * Push a removal onto the pending batch. + * @param {String} key + */ + + unpush(key) { + assert(this.pending); + + if (this.capacity === 0) + return; + + this.pending.remove(key); + } } /** - * Calculate size of an item. - * @private - * @param {LRUItem} item - * @returns {Number} Size. - */ - -LRU.prototype._getSize = function _getSize(item) { - if (this.getSize) { - const keySize = Math.floor(item.key.length * 1.375); - return 120 + keySize + this.getSize(item.value); - } - - return 1; -}; - -/** - * Compact the LRU linked list. - * @private - */ - -LRU.prototype._compact = function _compact() { - if (this.size <= this.capacity) - return; - - let item, next; - for (item = this.head; item; item = next) { - if (this.size <= this.capacity) - break; - this.size -= this._getSize(item); - this.items--; - this.map.delete(item.key); - next = item.next; - item.prev = null; - item.next = null; - } - - if (!item) { - this.head = null; - this.tail = null; - return; - } - - this.head = item; - item.prev = null; -}; - -/** - * Reset the cache. Clear all items. - */ - -LRU.prototype.reset = function reset() { - let item, next; - - for (item = this.head; item; item = next) { - this.map.delete(item.key); - this.items--; - next = item.next; - item.prev = null; - item.next = null; - } - - assert(!item); - - this.size = 0; - this.head = null; - this.tail = null; -}; - -/** - * Add an item to the cache. - * @param {String|Number} key - * @param {Object} value - */ - -LRU.prototype.set = function set(key, value) { - if (this.capacity === 0) - return; - - key = String(key); - - let item = this.map.get(key); - - if (item) { - this.size -= this._getSize(item); - item.value = value; - this.size += this._getSize(item); - this._removeList(item); - this._appendList(item); - this._compact(); - return; - } - - item = new LRUItem(key, value); - - this.map.set(key, item); - - this._appendList(item); - - this.size += this._getSize(item); - this.items++; - - this._compact(); -}; - -/** - * Retrieve an item from the cache. - * @param {String|Number} key - * @returns {Object} Item. - */ - -LRU.prototype.get = function get(key) { - if (this.capacity === 0) - return null; - - key = String(key); - - const item = this.map.get(key); - - if (!item) - return null; - - this._removeList(item); - this._appendList(item); - - return item.value; -}; - -/** - * Test whether the cache contains a key. - * @param {String|Number} key - * @returns {Boolean} - */ - -LRU.prototype.has = function has(key) { - if (this.capacity === 0) - return false; - return this.map.has(String(key)); -}; - -/** - * Remove an item from the cache. - * @param {String|Number} key - * @returns {Boolean} Whether an item was removed. - */ - -LRU.prototype.remove = function remove(key) { - if (this.capacity === 0) - return false; - - key = String(key); - - const item = this.map.get(key); - - if (!item) - return false; - - this.size -= this._getSize(item); - this.items--; - - this.map.delete(key); - - this._removeList(item); - - return true; -}; - -/** - * Prepend an item to the linked list (sets new head). - * @private - * @param {LRUItem} - */ - -LRU.prototype._prependList = function _prependList(item) { - this._insertList(null, item); -}; - -/** - * Append an item to the linked list (sets new tail). - * @private - * @param {LRUItem} - */ - -LRU.prototype._appendList = function _appendList(item) { - this._insertList(this.tail, item); -}; - -/** - * Insert item into the linked list. - * @private - * @param {LRUItem|null} ref - * @param {LRUItem} item - */ - -LRU.prototype._insertList = function _insertList(ref, item) { - assert(!item.next); - assert(!item.prev); - - if (ref == null) { - if (!this.head) { - this.head = item; - this.tail = item; - } else { - this.head.prev = item; - item.next = this.head; - this.head = item; - } - return; - } - - item.next = ref.next; - item.prev = ref; - ref.next = item; - - if (ref === this.tail) - this.tail = item; -}; - -/** - * Remove item from the linked list. - * @private - * @param {LRUItem} - */ - -LRU.prototype._removeList = function _removeList(item) { - if (item.prev) - item.prev.next = item.next; - - if (item.next) - item.next.prev = item.prev; - - if (item === this.head) - this.head = item.next; - - if (item === this.tail) - this.tail = item.prev || this.head; - - if (!this.head) - assert(!this.tail); - - if (!this.tail) - assert(!this.head); - - item.prev = null; - item.next = null; -}; - -/** - * Collect all keys in the cache, sorted by LRU. - * @returns {String[]} - */ - -LRU.prototype.keys = function keys() { - const items = []; - - for (let item = this.head; item; item = item.next) { - if (item === this.head) - assert(!item.prev); - if (!item.prev) - assert(item === this.head); - if (!item.next) - assert(item === this.tail); - items.push(item.key); - } - - return items; -}; - -/** - * Collect all values in the cache, sorted by LRU. - * @returns {String[]} - */ - -LRU.prototype.values = function values() { - const items = []; - - for (let item = this.head; item; item = item.next) - items.push(item.value); - - return items; -}; - -/** - * Convert the LRU cache to an array of items. - * @returns {Object[]} - */ - -LRU.prototype.toArray = function toArray() { - const items = []; - - for (let item = this.head; item; item = item.next) - items.push(item); - - return items; -}; - -/** - * Create an atomic batch for the lru - * (used for caching database writes). - * @returns {LRUBatch} - */ - -LRU.prototype.batch = function batch() { - return new LRUBatch(this); -}; - -/** - * Start the pending batch. - */ - -LRU.prototype.start = function start() { - assert(!this.pending); - this.pending = this.batch(); -}; - -/** - * Clear the pending batch. - */ - -LRU.prototype.clear = function clear() { - assert(this.pending); - this.pending.clear(); -}; - -/** - * Drop the pending batch. - */ - -LRU.prototype.drop = function drop() { - assert(this.pending); - this.pending = null; -}; - -/** - * Commit the pending batch. - */ - -LRU.prototype.commit = function commit() { - assert(this.pending); - this.pending.commit(); - this.pending = null; -}; - -/** - * Push an item onto the pending batch. - * @param {String} key - * @param {Object} value - */ - -LRU.prototype.push = function push(key, value) { - assert(this.pending); - - if (this.capacity === 0) - return; - - this.pending.set(key, value); -}; - -/** - * Push a removal onto the pending batch. - * @param {String} key - */ - -LRU.prototype.unpush = function unpush(key) { - assert(this.pending); - - if (this.capacity === 0) - return; - - this.pending.remove(key); -}; - -/** - * Represents an LRU item. + * LRU Item * @alias module:utils.LRUItem - * @constructor - * @private - * @param {String} key - * @param {Object} value */ -function LRUItem(key, value) { - this.key = key; - this.value = value; - this.next = null; - this.prev = null; +class LRUItem { + /** + * Create an LRU item. + * @constructor + * @private + * @param {String} key + * @param {Object} value + */ + + constructor(key, value) { + this.key = key; + this.value = value; + this.next = null; + this.prev = null; + } } /** * LRU Batch * @alias module:utils.LRUBatch - * @constructor - * @param {LRU} lru */ -function LRUBatch(lru) { - this.lru = lru; - this.ops = []; -} +class LRUBatch { + /** + * Create an LRU batch. + * @constructor + * @param {LRU} lru + */ -/** - * Push an item onto the batch. - * @param {String} key - * @param {Object} value - */ - -LRUBatch.prototype.set = function set(key, value) { - this.ops.push(new LRUOp(false, key, value)); -}; - -/** - * Push a removal onto the batch. - * @param {String} key - */ - -LRUBatch.prototype.remove = function remove(key) { - this.ops.push(new LRUOp(true, key, null)); -}; - -/** - * Clear the batch. - */ - -LRUBatch.prototype.clear = function clear() { - this.ops.length = 0; -}; - -/** - * Commit the batch. - */ - -LRUBatch.prototype.commit = function commit() { - for (const op of this.ops) { - if (op.remove) { - this.lru.remove(op.key); - continue; - } - this.lru.set(op.key, op.value); + constructor(lru) { + this.lru = lru; + this.ops = []; } - this.ops.length = 0; -}; + /** + * Push an item onto the batch. + * @param {String} key + * @param {Object} value + */ + + set(key, value) { + this.ops.push(new LRUOp(false, key, value)); + } + + /** + * Push a removal onto the batch. + * @param {String} key + */ + + remove(key) { + this.ops.push(new LRUOp(true, key, null)); + } + + /** + * Clear the batch. + */ + + clear() { + this.ops.length = 0; + } + + /** + * Commit the batch. + */ + + commit() { + for (const op of this.ops) { + if (op.remove) { + this.lru.remove(op.key); + continue; + } + this.lru.set(op.key, op.value); + } + + this.ops.length = 0; + } +} /** * LRU Op * @alias module:utils.LRUOp - * @constructor * @private - * @param {Boolean} remove - * @param {String} key - * @param {Object} value */ -function LRUOp(remove, key, value) { - this.remove = remove; - this.key = key; - this.value = value; +class LRUOp { + /** + * Create an LRU op. + * @constructor + * @param {Boolean} remove + * @param {String} key + * @param {Object} value + */ + + constructor(remove, key, value) { + this.remove = remove; + this.key = key; + this.value = value; + } } /* diff --git a/lib/utils/mappedlock.js b/lib/utils/mappedlock.js index a9d7fe97..d3673af3 100644 --- a/lib/utils/mappedlock.js +++ b/lib/utils/mappedlock.js @@ -10,154 +10,162 @@ const assert = require('assert'); /** - * Represents a mutex lock for locking asynchronous object methods. - * Locks methods according to passed-in key. + * Mapped Lock * @alias module:utils.MappedLock - * @constructor */ -function MappedLock() { - if (!(this instanceof MappedLock)) - return MappedLock.create(); +class MappedLock { + /** + * Create a mapped lock. + * @constructor + */ - this.jobs = new Map(); - this.busy = new Set(); - this.destroyed = false; + constructor() { + this.jobs = new Map(); + this.busy = new Set(); + this.destroyed = false; + } + + /** + * Create a closure scoped lock. + * @returns {Function} Lock method. + */ + + static create() { + const lock = new MappedLock(); + return function _lock(key, force) { + return lock.lock(key, force); + }; + } + + /** + * Test whether the lock has a pending + * job or a job in progress (by name). + * @param {String} name + * @returns {Boolean} + */ + + has(name) { + return this.busy.has(name); + } + + /** + * Test whether the lock has + * a pending job by name. + * @param {String} name + * @returns {Boolean} + */ + + pending(name) { + return this.jobs.has(name); + } + + /** + * Lock the parent object and all its methods + * which use the lock with a specified key. + * Begin to queue calls. + * @param {String|Number} key + * @param {Boolean} [force=false] - Force a call. + * @returns {Promise} - Returns {Function}, must be + * called once the method finishes executing in order + * to resolve the queue. + */ + + lock(key, force = false) { + if (this.destroyed) + return Promise.reject(new Error('Lock is destroyed.')); + + if (key == null) + return Promise.resolve(nop); + + if (force) { + assert(this.busy.has(key)); + return Promise.resolve(nop); + } + + if (this.busy.has(key)) { + return new Promise((resolve, reject) => { + if (!this.jobs.has(key)) + this.jobs.set(key, []); + this.jobs.get(key).push(new Job(resolve, reject)); + }); + } + + this.busy.add(key); + + return Promise.resolve(this.unlock(key)); + } + + /** + * Create an unlock callback. + * @private + * @param {String} key + * @returns {Function} Unlocker. + */ + + unlock(key) { + const self = this; + return function unlocker() { + const jobs = self.jobs.get(key); + + assert(self.destroyed || self.busy.has(key)); + self.busy.delete(key); + + if (!jobs) + return; + + assert(!self.destroyed); + + const job = jobs.shift(); + assert(job); + + if (jobs.length === 0) + self.jobs.delete(key); + + self.busy.add(key); + + job.resolve(unlocker); + }; + } + + /** + * Destroy the lock. Purge all pending calls. + */ + + destroy() { + assert(!this.destroyed, 'Lock is already destroyed.'); + + const map = this.jobs; + + this.destroyed = true; + + this.jobs = new Map(); + this.busy = new Map(); + + for (const jobs of map.values()) { + for (const job of jobs) + job.reject(new Error('Lock was destroyed.')); + } + } } -/** - * Create a closure scoped lock. - * @returns {Function} Lock method. - */ - -MappedLock.create = function create() { - const lock = new MappedLock(); - return function _lock(key, force) { - return lock.lock(key, force); - }; -}; - -/** - * Test whether the lock has a pending - * job or a job in progress (by name). - * @param {String} name - * @returns {Boolean} - */ - -MappedLock.prototype.has = function has(name) { - return this.busy.has(name); -}; - -/** - * Test whether the lock has - * a pending job by name. - * @param {String} name - * @returns {Boolean} - */ - -MappedLock.prototype.hasPending = function hasPending(name) { - return this.jobs.has(name); -}; - -/** - * Lock the parent object and all its methods - * which use the lock with a specified key. - * Begin to queue calls. - * @param {String|Number} key - * @param {Boolean?} force - Force a call. - * @returns {Promise} - Returns {Function}, must be - * called once the method finishes executing in order - * to resolve the queue. - */ - -MappedLock.prototype.lock = function lock(key, force) { - if (this.destroyed) - return Promise.reject(new Error('Lock is destroyed.')); - - if (key == null) - return Promise.resolve(nop); - - if (force) { - assert(this.busy.has(key)); - return Promise.resolve(nop); - } - - if (this.busy.has(key)) { - return new Promise((resolve, reject) => { - if (!this.jobs.has(key)) - this.jobs.set(key, []); - this.jobs.get(key).push(new Job(resolve, reject)); - }); - } - - this.busy.add(key); - - return Promise.resolve(this.unlock(key)); -}; - -/** - * Create an unlock callback. - * @private - * @param {String} key - * @returns {Function} Unlocker. - */ - -MappedLock.prototype.unlock = function unlock(key) { - const self = this; - return function unlocker() { - const jobs = self.jobs.get(key); - - assert(self.destroyed || self.busy.has(key)); - self.busy.delete(key); - - if (!jobs) - return; - - assert(!self.destroyed); - - const job = jobs.shift(); - assert(job); - - if (jobs.length === 0) - self.jobs.delete(key); - - self.busy.add(key); - - job.resolve(unlocker); - }; -}; - -/** - * Destroy the lock. Purge all pending calls. - */ - -MappedLock.prototype.destroy = function destroy() { - assert(!this.destroyed, 'Lock is already destroyed.'); - - const map = this.jobs; - - this.destroyed = true; - - this.jobs = new Map(); - this.busy = new Map(); - - for (const jobs of map.values()) { - for (const job of jobs) - job.reject(new Error('Lock was destroyed.')); - } -}; - /** * Lock Job - * @constructor * @ignore - * @param {Function} resolve - * @param {Function} reject */ -function Job(resolve, reject) { - this.resolve = resolve; - this.reject = reject; +class Job { + /** + * Create a lock job. + * @constructor + * @param {Function} resolve + * @param {Function} reject + */ + + constructor(resolve, reject) { + this.resolve = resolve; + this.reject = reject; + } } /* diff --git a/lib/wallet/nodeclient.js b/lib/wallet/nodeclient.js index 2381a947..14da4a4b 100644 --- a/lib/wallet/nodeclient.js +++ b/lib/wallet/nodeclient.js @@ -10,210 +10,209 @@ const assert = require('assert'); const AsyncEmitter = require('../utils/asyncemitter'); /** - * NodeClient - * Sort of a fake local client for separation of concerns. + * Node Client * @alias module:node.NodeClient - * @constructor */ -function NodeClient(node) { - if (!(this instanceof NodeClient)) - return new NodeClient(node); +class NodeClient extends AsyncEmitter { + /** + * Create a node client. + * @constructor + */ - AsyncEmitter.call(this); + constructor(node) { + super(); - this.node = node; - this.network = node.network; - this.filter = null; - this.opened = false; + this.node = node; + this.network = node.network; + this.filter = null; + this.opened = false; - this.init(); + this.init(); + } + + /** + * Initialize the client. + */ + + init() { + this.node.on('connect', (entry, block) => { + if (!this.opened) + return; + + this.emit('block connect', entry, block.txs); + }); + + this.node.on('disconnect', (entry, block) => { + if (!this.opened) + return; + + this.emit('block disconnect', entry); + }); + + this.node.on('tx', (tx) => { + if (!this.opened) + return; + + this.emit('tx', tx); + }); + + this.node.on('reset', (tip) => { + if (!this.opened) + return; + + this.emit('chain reset', tip); + }); + } + + /** + * Open the client. + * @returns {Promise} + */ + + async open(options) { + assert(!this.opened, 'NodeClient is already open.'); + this.opened = true; + setImmediate(() => this.emit('connect')); + } + + /** + * Close the client. + * @returns {Promise} + */ + + async close() { + assert(this.opened, 'NodeClient is not open.'); + this.opened = false; + setImmediate(() => this.emit('disconnect')); + } + + /** + * Add a listener. + * @param {String} type + * @param {Function} handler + */ + + bind(type, handler) { + return this.on(type, handler); + } + + /** + * Add a listener. + * @param {String} type + * @param {Function} handler + */ + + hook(type, handler) { + return this.on(type, handler); + } + + /** + * Get chain tip. + * @returns {Promise} + */ + + async getTip() { + return this.node.chain.tip; + } + + /** + * Get chain entry. + * @param {Hash} hash + * @returns {Promise} + */ + + async getEntry(hash) { + const entry = await this.node.chain.getEntry(hash); + + if (!entry) + return null; + + if (!await this.node.chain.isMainChain(entry)) + return null; + + return entry; + } + + /** + * Send a transaction. Do not wait for promise. + * @param {TX} tx + * @returns {Promise} + */ + + async send(tx) { + this.node.relay(tx); + } + + /** + * Set bloom filter. + * @param {Bloom} filter + * @returns {Promise} + */ + + async setFilter(filter) { + this.filter = filter; + this.node.pool.setFilter(filter); + } + + /** + * Add data to filter. + * @param {Buffer} data + * @returns {Promise} + */ + + async addFilter(data) { + this.node.pool.queueFilterLoad(); + } + + /** + * Reset filter. + * @returns {Promise} + */ + + async resetFilter() { + this.node.pool.queueFilterLoad(); + } + + /** + * Esimate smart fee. + * @param {Number?} blocks + * @returns {Promise} + */ + + async estimateFee(blocks) { + if (!this.node.fees) + return this.network.feeRate; + + return this.node.fees.estimateFee(blocks); + } + + /** + * Get hash range. + * @param {Number} start + * @param {Number} end + * @returns {Promise} + */ + + async getHashes(start = -1, end = -1) { + return this.node.chain.getHashes(start, end); + } + + /** + * Rescan for any missed transactions. + * @param {Number|Hash} start - Start block. + * @param {Bloom} filter + * @param {Function} iter - Iterator. + * @returns {Promise} + */ + + async rescan(start) { + return this.node.chain.scan(start, this.filter, (entry, txs) => { + return this.emitAsync('block rescan', entry, txs); + }); + } } -Object.setPrototypeOf(NodeClient.prototype, AsyncEmitter.prototype); - -/** - * Initialize the client. - * @returns {Promise} - */ - -NodeClient.prototype.init = function init() { - this.node.on('connect', (entry, block) => { - if (!this.opened) - return; - - this.emit('block connect', entry, block.txs); - }); - - this.node.on('disconnect', (entry, block) => { - if (!this.opened) - return; - - this.emit('block disconnect', entry); - }); - - this.node.on('tx', (tx) => { - if (!this.opened) - return; - - this.emit('tx', tx); - }); - - this.node.on('reset', (tip) => { - if (!this.opened) - return; - - this.emit('chain reset', tip); - }); -}; - -/** - * Open the client. - * @returns {Promise} - */ - -NodeClient.prototype.open = async function open(options) { - assert(!this.opened, 'NodeClient is already open.'); - this.opened = true; - setImmediate(() => this.emit('connect')); -}; - -/** - * Close the client. - * @returns {Promise} - */ - -NodeClient.prototype.close = async function close() { - assert(this.opened, 'NodeClient is not open.'); - this.opened = false; - setImmediate(() => this.emit('disconnect')); -}; - -/** - * Add a listener. - * @param {String} type - * @param {Function} handler - */ - -NodeClient.prototype.bind = function bind(type, handler) { - return this.on(type, handler); -}; - -/** - * Add a listener. - * @param {String} type - * @param {Function} handler - */ - -NodeClient.prototype.hook = function hook(type, handler) { - return this.on(type, handler); -}; - -/** - * Get chain tip. - * @returns {Promise} - */ - -NodeClient.prototype.getTip = async function getTip() { - return this.node.chain.tip; -}; - -/** - * Get chain entry. - * @param {Hash} hash - * @returns {Promise} - */ - -NodeClient.prototype.getEntry = async function getEntry(hash) { - const entry = await this.node.chain.getEntry(hash); - - if (!entry) - return null; - - if (!await this.node.chain.isMainChain(entry)) - return null; - - return entry; -}; - -/** - * Send a transaction. Do not wait for promise. - * @param {TX} tx - * @returns {Promise} - */ - -NodeClient.prototype.send = async function send(tx) { - this.node.relay(tx); -}; - -/** - * Set bloom filter. - * @param {Bloom} filter - * @returns {Promise} - */ - -NodeClient.prototype.setFilter = async function setFilter(filter) { - this.filter = filter; - this.node.pool.setFilter(filter); -}; - -/** - * Add data to filter. - * @param {Buffer} data - * @returns {Promise} - */ - -NodeClient.prototype.addFilter = async function addFilter(data) { - this.node.pool.queueFilterLoad(); -}; - -/** - * Reset filter. - * @returns {Promise} - */ - -NodeClient.prototype.resetFilter = async function resetFilter() { - this.node.pool.queueFilterLoad(); -}; - -/** - * Esimate smart fee. - * @param {Number?} blocks - * @returns {Promise} - */ - -NodeClient.prototype.estimateFee = async function estimateFee(blocks) { - if (!this.node.fees) - return this.network.feeRate; - - return this.node.fees.estimateFee(blocks); -}; - -/** - * Get hash range. - * @param {Number} start - * @param {Number} end - * @returns {Promise} - */ - -NodeClient.prototype.getHashes = async function getHashes(start = -1, end = -1) { - return this.node.chain.getHashes(start, end); -}; - -/** - * Rescan for any missed transactions. - * @param {Number|Hash} start - Start block. - * @param {Bloom} filter - * @param {Function} iter - Iterator. - * @returns {Promise} - */ - -NodeClient.prototype.rescan = async function rescan(start) { - return this.node.chain.scan(start, this.filter, (entry, txs) => { - return this.emitAsync('block rescan', entry, txs); - }); -}; - /* * Expose */