UNPKG

mxbit

Version:
2,115 lines (1,688 loc) 73.8 kB
/*! * chain.js - blockchain management for bcoin * Copyright (c) 2014-2015, Fedor Indutny (MIT License) * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; const assert = require('bsert'); const path = require('path'); const AsyncEmitter = require('bevent'); const Logger = require('blgr'); const {Lock} = require('bmutex'); const BN = require('bcrypto/lib/bn.js'); const LRU = require('blru'); const {BufferMap} = require('buffer-map'); const Network = require('../protocol/network'); const ChainDB = require('./chaindb'); const common = require('./common'); const consensus = require('../protocol/consensus'); const util = require('../utils/util'); const ChainEntry = require('./chainentry'); const CoinView = require('../coins/coinview'); const Script = require('../script/script'); const {VerifyError} = require('../protocol/errors'); const thresholdStates = common.thresholdStates; /** * Blockchain * @alias module:blockchain.Chain * @property {ChainDB} db * @property {ChainEntry?} tip * @property {Number} height * @property {DeploymentState} state */ class Chain extends AsyncEmitter { /** * Create a blockchain. * @constructor * @param {Object} options */ constructor(options) { super(); this.opened = false; this.options = new ChainOptions(options); this.network = this.options.network; this.logger = this.options.logger.context('chain'); this.workers = this.options.workers; this.db = new ChainDB(this.options); this.locker = new Lock(true, BufferMap); this.invalid = new LRU(100, null, BufferMap); this.state = new DeploymentState(); this.tip = new ChainEntry(); this.height = -1; this.synced = false; this.orphanMap = new BufferMap(); this.orphanPrev = new BufferMap(); } /** * Open the chain, wait for the database to load. * @returns {Promise} */ async open() { assert(!this.opened, 'Chain is already open.'); this.opened = true; this.logger.info('Chain is loading.'); if (this.options.checkpoints) this.logger.info('Checkpoints are enabled.'); if (this.options.coinCache) this.logger.info('Coin cache is enabled.'); await this.db.open(); const tip = await this.db.getTip(); assert(tip); this.tip = tip; this.height = tip.height; this.logger.info('Chain Height: %d', tip.height); this.logger.memory(); const state = await this.getDeploymentState(); this.setDeploymentState(state); this.logger.memory(); this.emit('tip', tip); this.maybeSync(); } /** * Close the chain, wait for the database to close. * @returns {Promise} */ async close() { assert(this.opened, 'Chain is not open.'); this.opened = false; return this.db.close(); } /** * Perform all necessary contextual verification on a block. * @private * @param {Block} block * @param {ChainEntry} prev * @param {Number} flags * @returns {Promise} - Returns {@link ContextResult}. */ async verifyContext(block, prev, flags) { // Initial non-contextual verification. const state = await this.verify(block, prev, flags); // Skip everything if we're in SPV mode. if (this.options.spv) { const view = new CoinView(); return [view, state]; } // Skip everything if we're using checkpoints. if (this.isHistorical(prev)) { const view = await this.updateInputs(block, prev); return [view, state]; } // 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); 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(); } } /** * Perform all necessary contextual verification * on a block, without POW check (no lock). * @private * @param {Block} block * @returns {Promise} */ async _verifyBlock(block) { const flags = common.flags.DEFAULT_FLAGS & ~common.flags.VERIFY_POW; return this.verifyContext(block, this.tip, flags); } /** * Test whether the hash is in the main chain. * @param {Hash} hash * @returns {Promise} - Returns Boolean. */ isMainHash(hash) { return this.db.isMainHash(hash); } /** * Test whether the entry is in the main chain. * @param {ChainEntry} entry * @returns {Promise} - Returns Boolean. */ isMainChain(entry) { return this.db.isMainChain(entry); } /** * 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.equals(prev.hash)) throw new VerifyError(block, 'invalid', 'bad-prevblk', 0); // Verify a checkpoint if there is one. const hash = block.hash(); if (!this.verifyCheckpoint(prev, hash)) { throw new VerifyError(block, 'checkpoint', 'checkpoint mismatch', 100); } // Skip everything when using checkpoints. // We can do this safely because every // block in between each checkpoint was // validated outside in the header chain. if (this.isHistorical(prev)) { // Check merkle root. if (flags & common.flags.VERIFY_BODY) { assert(typeof block.createMerkleRoot === 'function'); const root = block.createMerkleRoot(); if (!root || !block.merkleRoot.equals(root)) { throw new VerifyError(block, 'invalid', 'bad-txnmrklroot', 100, true); } flags &= ~common.flags.VERIFY_BODY; } } // Non-contextual checks. if (flags & common.flags.VERIFY_BODY) { const [valid, reason, score] = block.checkBody(); if (!valid){ throw new VerifyError(block, 'invalid', reason, score, true); } } /* TODO VERIFY VERSION BIT // Ensure the POW is what we expect. const bits = await this.getTarget(block.time, prev); if (block.bits !== bits) { console.log(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); // 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)) { 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); } } // Check block size (different from block size // check in non-contextual verification). if (block.getSize() > state.maxBlockSize()) { throw new VerifyError(block, 'invalid', 'bad-blk-length', 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}. */ async 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; } // UAHF is now enabled. if (height > this.network.block.uahfHeight) { state.flags |= Script.flags.VERIFY_STRICTENC; state.flags |= Script.flags.VERIFY_SIGHASH_FORKID; } // DAA is now enabled. if (height > this.network.block.daaHeight) { state.daa = true; state.flags |= Script.flags.VERIFY_LOW_S; state.flags |= Script.flags.VERIFY_NULLFAIL; } const mtp = await this.getMedianTime(prev); // Magnetic anomaly is enabled if (mtp >= this.network.block.magneticAnomalyActivationTime) { state.magneticAnomaly = true; state.flags |= Script.flags.VERIFY_CHECKDATASIG; state.flags |= Script.flags.VERIFY_SIGPUSHONLY; state.flags |= Script.flags.VERIFY_CLEANSTACK; } return state; } /** * Set a new deployment state. * @param {DeploymentState} state */ setDeploymentState(state) { if (this.options.checkpoints && this.height < this.network.lastCheckpoint) { this.state = state; return; } if (!this.state.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.hasUAHF() && state.hasUAHF()) this.logger.warning('UAHF has been activated.'); if (!this.state.hasDAA() && state.hasDAA()) this.logger.warning('DAA has been activated.'); if (!this.state.hasMagneticAnomaly() && state.hasMagneticAnomaly()) this.logger.warning('Magnetic Anomaly 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} */ 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().equals(hash)) throw new VerifyError(block, 'invalid', 'bad-txns-BIP30', 100); } } /** * Spend and update inputs (checkpoints only). * @private * @param {Block} block * @param {ChainEntry} prev * @returns {Promise} - Returns {@link CoinView}. */ async updateInputs(block, prev) { const view = new CoinView(); const height = prev.height + 1; const cb = block.txs[0]; view.addTX(cb, height); 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}. */ async verifyInputs(block, prev, state) { const view = new CoinView(); const height = prev.height + 1; const interval = this.network.halvingInterval; const magneticAnomaly = state.hasMagneticAnomaly(); let sigops = 0; let reward = 0; if (magneticAnomaly) { for (const tx of block.txs) view.addTX(tx, height); } // 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); } } // Verify sequence locks. if (i > 0 && tx.version >= 2) { const valid = await this.verifyLocks(prev, tx, view, state.lockFlags); if (!valid) { throw new VerifyError(block, 'invalid', 'bad-txns-nonfinal', 100); } } // Count sigops (legacy + scripthash? + witness?) const txSigops = tx.getSigopsCount(view, state.flags); if (txSigops > consensus.MAX_TX_SIGOPS) { throw new VerifyError(block, 'invalid', 'bad-txn-sigops', 100 ); } sigops += txSigops; if (sigops > consensus.maxBlockSigops(block.getSize())) { 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 (magneticAnomaly) continue; // Add new coins. view.addTX(tx, height); } // Make sure the miner isn't trying to conjure more coins. reward += consensus.getReward(height, interval); if (block.getClaimed() > reward) { throw new VerifyError(block, 'invalid', 'bad-cb-amount', 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; } /** * Find the block at which a fork ocurred. * @private * @param {ChainEntry} fork - The current chain. * @param {ChainEntry} longer - The competing chain. * @returns {Promise} */ async findFork(fork, longer) { while (!fork.hash.equals(longer.hash)) { while (longer.height > fork.height) { longer = await this.getPrevious(longer); if (!longer) throw new Error('No previous entry for new tip.'); } if (fork.hash.equals(longer.hash)) return fork; fork = await this.getPrevious(fork); if (!fork) throw new Error('No previous entry for old tip.'); } return fork; } /** * Reorganize the blockchain (connect and disconnect inputs). * Called when a competing chain with a higher chainwork * is received. * @private * @param {ChainEntry} competitor - The competing chain's tip. * @returns {Promise} */ async reorganize(competitor) { const tip = this.tip; const fork = await this.findFork(tip, competitor); assert(fork, 'No free space or data corruption.'); // Blocks to disconnect. const disconnect = []; let entry = tip; while (!entry.hash.equals(fork.hash)) { disconnect.push(entry); entry = await this.getPrevious(entry); assert(entry); } // Blocks to connect. const connect = []; entry = competitor; while (!entry.hash.equals(fork.hash)) { connect.push(entry); entry = await this.getPrevious(entry); assert(entry); } // Disconnect blocks/txs. for (let i = 0; i < disconnect.length; i++) { const entry = disconnect[i]; await this.disconnect(entry); } // Connect blocks/txs. // We don't want to connect the new tip here. // That will be done outside in setBestChain. for (let i = connect.length - 1; i >= 1; i--) { const entry = connect[i]; await this.reconnect(entry); } this.logger.warning( 'Chain reorganization: old=%h(%d) new=%h(%d)', tip.hash, tip.height, competitor.hash, 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.equals(fork.hash)) { disconnect.push(entry); entry = await this.getPrevious(entry); assert(entry); } // Reset the main chain back // to the fork block, causing // us to redownload the blocks // on the new main chain. await this._reset(fork.hash, 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=%h(%d) new=%h(%d)', tip.hash, tip.height, competitor.hash, competitor.height ); this.logger.warning( 'Chain replay from height %d necessary.', fork.height); return this.emitAsync('reorganize', tip, competitor); } /** * Disconnect an entry from the chain (updates the tip). * @param {ChainEntry} entry * @returns {Promise} */ async disconnect(entry) { let block = await this.getBlock(entry.hash); if (!block) { if (!this.options.spv) throw new Error('Block not found.'); block = entry.toHeaders(); } const prev = await this.getPrevious(entry); const view = await this.db.disconnect(entry, block); assert(prev); this.tip = prev; this.height = prev.height; this.emit('tip', prev); return this.emitAsync('disconnect', entry, block, view); } /** * Reconnect an entry to the chain (updates the tip). * This will do contextual-verification on the block * (necessary because we cannot validate the inputs * in alternate chains when they come in). * @param {ChainEntry} entry * @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 { [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: %h (%d).', entry.hash, entry.height); } throw err; } await this.db.reconnect(entry, block, view); this.tip = entry; this.height = entry.height; this.setDeploymentState(state); this.emit('tip', entry); this.emit('reconnect', entry, block); return 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} */ async setBestChain(entry, block, prev, flags) { // A higher fork has arrived. // Time to reorganize the chain. if (!entry.prevBlock.equals(this.tip.hash)) { this.logger.warning('WARNING: Reorganizing chain.'); // In spv-mode, we reset the // chain and redownload the blocks. if (this.options.spv) return this.reorganizeSPV(entry); await this.reorganize(entry); } // Warn of unknown versionbits. if (entry.hasUnknown(this.network)) { this.logger.warning( 'Unknown version bits in block %d: %s.', entry.height, 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: %h (%d).', entry.hash, 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); return this.emitAsync('connect', entry, block, view); } /** * Save block on an alternate chain. * @private * @param {ChainEntry} entry * @param {Block} block * @param {ChainEntry} prev * @param {Number} flags * @returns {Promise} */ async saveAlternate(entry, block, prev, flags) { 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: %h (%d).', entry.hash, entry.height); } throw err; } // Warn of unknown versionbits. if (entry.hasUnknown(this.network)) { this.logger.warning( 'Unknown version bits in block %d: %s.', entry.height, entry.version.toString(16)); } await this.db.save(entry, block); this.logger.warning('Heads up: Competing chain at height %d:' + ' tip-height=%d competitor-height=%d' + ' tip-hash=%h competitor-hash=%h' + ' tip-chainwork=%s competitor-chainwork=%s' + ' chainwork-diff=%s', entry.height, this.tip.height, entry.height, this.tip.hash, entry.hash, this.tip.chainwork.toString(), entry.chainwork.toString(), this.tip.chainwork.sub(entry.chainwork).toString()); // Emit as a "competitor" block. this.emit('competitor', block, entry); } /** * Reset the chain to the desired block. This * is useful for replaying the blockchain download * for SPV. * @param {Hash|Number} block * @returns {Promise} */ async reset(block) { const unlock = await this.locker.lock(); try { return await this._reset(block, false); } finally { unlock(); } } /** * Reset the chain to the desired block without a lock. * @private * @param {Hash|Number} block * @returns {Promise} */ async _reset(block, silent) { const tip = await this.db.reset(block); // Reset state. this.tip = tip; this.height = tip.height; this.synced = false; const state = await this.getDeploymentState(); this.setDeploymentState(state); this.emit('tip', tip); if (!silent) await this.emitAsync('reset', tip); // Reset the orphan map completely. There may // have been some orphans on a forked chain we // no longer need. this.purgeOrphans(); this.maybeSync(); } /** * Reset the chain to a height or hash. Useful for replaying * the blockchain download for SPV. * @param {Hash|Number} block - hash/height * @returns {Promise} */ async replay(block) { const unlock = await this.locker.lock(); try { return await this._replay(block, true); } finally { unlock(); } } /** * Reset the chain without a lock. * @private * @param {Hash|Number} block - hash/height * @param {Boolean?} silent * @returns {Promise} */ async _replay(block, silent) { const entry = await this.getEntry(block); if (!entry) throw new Error('Block not found.'); if (!await this.isMainChain(entry)) throw new Error('Cannot reset on alternate chain.'); if (entry.isGenesis()) { await this._reset(entry.hash, silent); return; } await this._reset(entry.prevBlock, silent); } /** * Invalidate block. * @param {Hash} hash * @returns {Promise} */ async invalidate(hash) { const unlock = await this.locker.lock(); try { return await this._invalidate(hash); } finally { unlock(); } } /** * Invalidate block (no lock). * @param {Hash} hash * @returns {Promise} */ async _invalidate(hash) { await this._replay(hash, false); this.setInvalid(hash); } /** * Retroactively prune the database. * @returns {Promise} */ 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(); 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(); if (flags == null) flags = common.flags.DEFAULT_FLAGS; if (id == null) id = -1; // Special case for genesis block. if (hash.equals(this.network.genesis.hash)) { this.logger.debug('Saw genesis block: %h.', block.hash()); throw new VerifyError(block, 'duplicate', 'duplicate', 0); } // Do we already have this block in the queue? if (this.hasPending(hash)) { this.logger.debug('Already have pending block: %h.', block.hash()); throw new VerifyError(block, 'duplicate', 'duplicate', 0); } // If the block is already known to be // an orphan, ignore it. if (this.hasOrphan(hash)) { this.logger.debug('Already have orphan block: %h.', block.hash()); throw new VerifyError(block, 'duplicate', 'duplicate', 0); } // Do not revalidate known invalid blocks. if (this.hasInvalid(block)) { this.logger.debug('Invalid ancestors for block: %h.', block.hash()); throw new VerifyError(block, 'duplicate', 'duplicate', 100); } // Do we already have this block? if (await this.hasEntry(hash)) { this.logger.debug('Already have block: %h.', block.hash()); throw new VerifyError(block, 'duplicate', 'duplicate', 0); } // Find the previous block entry. const prev = await this.getEntry(block.prevBlock); // Check the POW before doing anything. this.logger.info('Verifying Block at Height:' + prev.height); if (flags & common.flags.VERIFY_POW) { if (!block.verifyPOW(prev.height)) throw new VerifyError(block, 'invalid', 'high-hash', 50); } // 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} TODO Database */ async connect(prev, block, flags) { const start = util.bench(); // Sanity check. assert(block.prevBlock.equals(prev.hash)); // Explanation: we try to keep as much data // off the javascript heap as possible. Blocks // in the future may be 8mb or 20mb, who knows. // In fullnode-mode we store the blocks in // "compact" form (the headers plus the raw // Buffer object) until they're ready to be // fully validated here. They are deserialized, // validated, and connected. Hopefully the // deserialized blocks get cleaned up by the // GC quickly. if (block.isMemory()) { try { block = block.toBlock(); } catch (e) { this.logger.error(e); throw new VerifyError(block, 'malformed', 'error parsing message', 10, true); } } // 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 %h: %s.', block.hash(), err.message); this.emit('bad orphan', err, id); break; } throw err; } this.logger.debug( 'Orphan block was resolved: %h (%d).', block.hash(), entry.height); this.emit('resolved', block, entry); orphan = this.resolveOrphan(entry.hash); } } /** * Test whether the chain has reached its slow height. * @private * @returns {Boolean} */ isSlow() { if (this.options.spv) return false; if (this.synced) return true; if (this.height === 1 || this.height % 20 === 0) return true; if (this.height >= this.network.block.slowHeight) return true; return false; } /** * Calculate the time difference from * start time and log block. * @private * @param {Array} start * @param {Block} block * @param {ChainEntry} entry */ logStatus(start, block, entry) { if (!this.isSlow()) return; // Report memory for debugging. this.logger.memory(); const elapsed = util.bench(start); this.logger.info( 'Block %h (%d) added to chain (size=%d txs=%d time=%d).', entry.hash, 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.equals(checkpoint)) { this.logger.debug('Hit checkpoint block %h (%d).', hash, height); this.emit('checkpoint', hash, height); return true; } // Someone is either mining on top of // an old block for no reason, or the // consensus protocol is broken and // there was a 20k+ block reorg. this.logger.warning( 'Checkpoint mismatch at height %d: expected=%h received=%h', height, checkpoint, hash ); this.purgeOrphans(); return false; } /** * Store an orphan. * @private * @param {Block} block * @param {Number?} flags * @param {Number?} id */ storeOrphan(block, flags, id) { const height = block.getCoinbaseHeight(); const orphan = this.orphanPrev.get(block.prevBlock); // The orphan chain forked. if (orphan) { assert(!orphan.block.hash().equals(block.hash())); assert(orphan.block.prevBlock.equals(block.prevBlock)); this.logger.warning( 'Removing forked orphan block: %h (%d).', orphan.block.hash(), height); this.removeOrphan(orphan); } this.limitOrphans(); this.addOrphan(new Orphan(block, flags, id)); this.logger.debug( 'Storing orphan block: %h (%d).', block.hash(), height); this.emit('orphan', block); } /** * Add an orphan. * @private * @param {Orphan} orphan * @returns {Orphan} */ addOrphan(orphan) { const block = orphan.block; const hash = block.hash(); assert(!this.orphanMap.has(hash)); assert(!this.orphanPrev.has(block.prevBlock)); assert(this.orphanMap.size >= 0); this.orphanMap.set(hash, orphan); this.orphanPrev.set(block.prevBlock, orphan); return orphan; } /** * Remove an orphan. * @private * @param {Orphan} orphan * @returns {Orphan} */ removeOrphan(orphan) { const block = orphan.block; const hash = block.hash(); assert(this.orphanMap.has(hash)); assert(this.orphanPrev.has(block.prevBlock)); assert(this.orphanMap.size > 0); this.orphanMap.delete(hash); this.orphanPrev.delete(block.prevBlock); return orphan; } /** * Test whether a hash would resolve the next orphan. * @private * @param {Hash} hash - Previous block hash. * @returns {Boolean} */ hasNextOrphan(hash) { return this.orphanPrev.has(hash); } /** * Resolve an orphan. * @private * @param {Hash} hash - Previous block hash. * @returns {Orphan} */ resolveOrphan(hash) { const orphan = this.orphanPrev.get(hash); if (!orphan) return null; return this.removeOrphan(orphan); } /** * Purge any waiting orphans. */ purgeOrphans() { const count = this.orphanMap.size; if (count === 0) return; this.orphanMap.clear(); this.orphanPrev.clear(); this.logger.debug('Purged %d orphans.', count); } /** * Prune orphans, only keep the orphan with the highest * coinbase height (likely to be the peer's tip). */ limitOrphans() { const now = util.now(); let oldest = null; for (const orphan of this.orphanMap.values()) { if (now < orphan.time + 60 * 60) { if (!oldest || orphan.time < oldest.time) oldest = orphan; continue; } this.removeOrphan(orphan); } if (this.orphanMap.size < this.options.maxOrphans) return; if (!oldest) return; this.removeOrphan(oldest); } /** * Test whether an invalid block hash has been seen. * @private * @param {Block} block * @returns {Boolean} */ hasInvalid(block) { const hash = block.hash(); if (this.invalid.has(hash)) return true; if (this.invalid.has(block.prevBlock)) { this.setInvalid(hash); return true; } return false; } /** * Mark a block as invalid. * @private * @param {Hash} hash */ setInvalid(hash) { this.invalid.set(hash, true); } /** * Forget an invalid block hash. * @private * @param {Hash} hash */ removeInvalid(hash) { this.invalid.remove(hash); } /** * Test the chain to see if it contains * a block, or has recently seen a block. * @param {Hash} hash * @returns {Promise} - Returns Boolean. */ async has(hash) { if (this.hasOrphan(hash)) return true; if (this.locker.has(hash)) return true; if (this.invalid.has(hash)) return true; return this.hasEntry(hash); } /** * Find the corresponding block entry by hash or height. * @param {Hash|Number} hash/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 {TXMeta} meta * @returns {Promise} - Returns {@link CoinView}. */ async getSpentView(meta) { const unlock = await this.locker.lock(); try { return await this.db.getSpentView(meta); } 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