From daa55a05bc66842e93c074205d970cfd2a68952a Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 15 Nov 2017 18:54:47 -0800 Subject: [PATCH] primitives: classify primitives. --- lib/blockchain/chaindb.js | 4337 ++++++++++++++--------------- lib/blockchain/chainentry.js | 644 ++--- lib/coins/coinentry.js | 386 +-- lib/coins/coins.js | 390 +-- lib/coins/coinview.js | 962 +++---- lib/coins/undocoins.js | 223 +- lib/mining/miner.js | 2 +- lib/mining/template.js | 2 +- lib/net/bip152.js | 1664 +++++------ lib/primitives/abstractblock.js | 419 +-- lib/primitives/address.js | 1654 +++++------ lib/primitives/block.js | 1442 +++++----- lib/primitives/coin.js | 790 +++--- lib/primitives/headers.js | 498 ++-- lib/primitives/input.js | 897 +++--- lib/primitives/invitem.js | 268 +- lib/primitives/keyring.js | 1702 ++++++------ lib/primitives/memblock.js | 356 +-- lib/primitives/merkleblock.js | 1187 ++++---- lib/primitives/mtx.js | 3294 +++++++++++----------- lib/primitives/netaddress.js | 874 +++--- lib/primitives/outpoint.js | 628 ++--- lib/primitives/output.js | 651 ++--- lib/primitives/tx.js | 4564 ++++++++++++++++--------------- lib/primitives/txmeta.js | 562 ++-- lib/wallet/walletkey.js | 511 ++-- test/util/memwallet.js | 4 +- 27 files changed, 14507 insertions(+), 14404 deletions(-) diff --git a/lib/blockchain/chaindb.js b/lib/blockchain/chaindb.js index b1ca9fbf..8d443cf6 100644 --- a/lib/blockchain/chaindb.js +++ b/lib/blockchain/chaindb.js @@ -28,37 +28,1965 @@ const u8 = encoding.u8; const u32 = encoding.u32; /** - * The database backend for the {@link Chain} object. + * ChainDB * @alias module:blockchain.ChainDB - * @constructor - * @param {Boolean?} options.prune - Whether to prune the chain. - * @param {Boolean?} options.spv - SPV-mode, will not save block - * data, only entries. - * @param {String?} options.name - Database name - * @param {String?} options.location - Database location - * @param {String?} options.db - Database backend name - * @property {Boolean} prune - * @emits ChainDB#open - * @emits ChainDB#error */ -function ChainDB(options) { - if (!(this instanceof ChainDB)) - return new ChainDB(options); +class ChainDB { + /** + * Create a chaindb. + * @constructor + */ - this.options = options; - this.network = this.options.network; - this.logger = this.options.logger.context('chaindb'); + constructor(options) { + this.options = options; + this.network = this.options.network; + this.logger = this.options.logger.context('chaindb'); - this.db = new BDB(this.options); - this.stateCache = new StateCache(this.network); - this.state = new ChainState(); - this.pending = null; - this.current = null; + this.db = new BDB(this.options); + this.stateCache = new StateCache(this.network); + this.state = new ChainState(); + this.pending = null; + this.current = null; - this.coinCache = new LRU(this.options.coinCache, getSize); - this.cacheHash = new LRU(this.options.entryCache); - this.cacheHeight = new LRU(this.options.entryCache); + this.coinCache = new LRU(this.options.coinCache, getSize); + this.cacheHash = new LRU(this.options.entryCache); + this.cacheHeight = new LRU(this.options.entryCache); + } + + /** + * Open and wait for the database to load. + * @returns {Promise} + */ + + async open() { + this.logger.info('Opening ChainDB...'); + + await this.db.open(); + await this.db.checkVersion('V', 3); + + const state = await this.getState(); + + if (state) { + // Verify options have not changed. + await this.verifyFlags(state); + + // Verify deployment params have not changed. + await this.verifyDeployments(); + + // Load state caches. + this.stateCache = await this.getStateCache(); + + // Grab the chainstate if we have one. + this.state = state; + + this.logger.info('ChainDB successfully loaded.'); + } else { + // Database is fresh. + // Write initial state. + await this.saveFlags(); + await this.saveDeployments(); + await this.saveGenesis(); + + this.logger.info('ChainDB successfully initialized.'); + } + + this.logger.info( + 'Chain State: hash=%s tx=%d coin=%d value=%s.', + this.state.rhash(), + this.state.tx, + this.state.coin, + Amount.btc(this.state.value)); + } + + /** + * Close and wait for the database to close. + * @returns {Promise} + */ + + close() { + return this.db.close(); + } + + /** + * Start a batch. + * @returns {Batch} + */ + + start() { + assert(!this.current); + assert(!this.pending); + + this.current = this.db.batch(); + this.pending = this.state.clone(); + + this.coinCache.start(); + this.cacheHash.start(); + this.cacheHeight.start(); + + return this.current; + } + + /** + * Put key and value to current batch. + * @param {String} key + * @param {Buffer} value + */ + + put(key, value) { + assert(this.current); + this.current.put(key, value); + } + + /** + * Delete key from current batch. + * @param {String} key + */ + + del(key) { + assert(this.current); + this.current.del(key); + } + + /** + * Get current batch. + * @returns {Batch} + */ + + batch() { + assert(this.current); + return this.current; + } + + /** + * Drop current batch. + * @returns {Batch} + */ + + drop() { + const batch = this.current; + + assert(this.current); + assert(this.pending); + + this.current = null; + this.pending = null; + + this.coinCache.drop(); + this.cacheHash.drop(); + this.cacheHeight.drop(); + this.stateCache.drop(); + + batch.clear(); + } + + /** + * Commit current batch. + * @returns {Promise} + */ + + async commit() { + assert(this.current); + assert(this.pending); + + try { + await this.current.write(); + } catch (e) { + this.current = null; + this.pending = null; + this.coinCache.drop(); + this.cacheHash.drop(); + this.cacheHeight.drop(); + throw e; + } + + // Overwrite the entire state + // with our new best state + // only if it is committed. + // Note that alternate chain + // tips do not commit anything. + if (this.pending.committed) + this.state = this.pending; + + this.current = null; + this.pending = null; + + this.coinCache.commit(); + this.cacheHash.commit(); + this.cacheHeight.commit(); + this.stateCache.commit(); + } + + /** + * Test the cache for a present entry hash or height. + * @param {Hash|Number} block - Hash or height. + */ + + hasCache(block) { + if (typeof block === 'number') + return this.cacheHeight.has(block); + + assert(typeof block === 'string'); + + return this.cacheHash.has(block); + } + + /** + * Get an entry directly from the LRU cache. + * @param {Hash|Number} block - Hash or height. + */ + + getCache(block) { + if (typeof block === 'number') + return this.cacheHeight.get(block); + + assert(typeof block === 'string'); + + return this.cacheHash.get(block); + } + + /** + * Get the height of a block by hash. + * @param {Hash} hash + * @returns {Promise} - Returns Number. + */ + + async getHeight(hash) { + if (typeof hash === 'number') + return hash; + + assert(typeof hash === 'string'); + + if (hash === encoding.NULL_HASH) + return -1; + + const entry = this.cacheHash.get(hash); + + if (entry) + return entry.height; + + const height = await this.db.get(layout.h(hash)); + + if (!height) + return -1; + + return height.readUInt32LE(0, true); + } + + /** + * 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}. + */ + + async getHash(height) { + if (typeof height === 'string') + return height; + + assert(typeof height === 'number'); + + if (height < 0) + return null; + + const entry = this.cacheHeight.get(height); + + if (entry) + return entry.hash; + + const hash = await this.db.get(layout.H(height)); + + if (!hash) + return null; + + return hash.toString('hex'); + } + + /** + * Retrieve a chain entry by height. + * @param {Number} height + * @returns {Promise} - Returns {@link ChainEntry}. + */ + + async getEntryByHeight(height) { + assert(typeof height === 'number'); + + if (height < 0) + return null; + + const cache = this.cacheHeight.get(height); + + if (cache) + return cache; + + const data = await this.db.get(layout.H(height)); + + if (!data) + return null; + + const hash = data.toString('hex'); + + const state = this.state; + const entry = await this.getEntryByHash(hash); + + if (!entry) + return null; + + // By the time getEntry has completed, + // a reorg may have occurred. This entry + // may not be on the main chain anymore. + if (this.state === state) + this.cacheHeight.set(entry.height, entry); + + return entry; + } + + /** + * Retrieve a chain entry by hash. + * @param {Hash} hash + * @returns {Promise} - Returns {@link ChainEntry}. + */ + + async getEntryByHash(hash) { + assert(typeof hash === 'string'); + + if (hash === encoding.NULL_HASH) + return null; + + const cache = this.cacheHash.get(hash); + + if (cache) + return cache; + + const raw = await this.db.get(layout.e(hash)); + + if (!raw) + return null; + + const entry = ChainEntry.fromRaw(raw); + + // There's no efficient way to check whether + // this is in the main chain or not, so + // don't add it to the height cache. + this.cacheHash.set(entry.hash, entry); + + return entry; + } + + /** + * Retrieve a chain entry. + * @param {Number|Hash} block - Height or hash. + * @returns {Promise} - Returns {@link ChainEntry}. + */ + + getEntry(block) { + if (typeof block === 'number') + return this.getEntryByHeight(block); + return this.getEntryByHash(block); + } + + /** + * Test whether the chain contains a block. + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ + + async hasEntry(hash) { + const height = await this.getHeight(hash); + return height !== -1; + } + + /** + * Get ancestor by `height`. + * @param {ChainEntry} entry + * @param {Number} height + * @returns {Promise} - Returns ChainEntry. + */ + + async getAncestor(entry, height) { + if (height < 0) + return null; + + assert(height >= 0); + assert(height <= entry.height); + + if (await this.isMainChain(entry)) + return await this.getEntryByHeight(height); + + while (entry.height !== height) { + const cache = this.getPrevCache(entry); + + if (cache) + entry = cache; + else + entry = await this.getPrevious(entry); + + assert(entry); + } + + return entry; + } + + /** + * Get previous entry. + * @param {ChainEntry} entry + * @returns {Promise} - Returns ChainEntry. + */ + + getPrevious(entry) { + return this.getEntryByHash(entry.prevBlock); + } + + /** + * Get previous cached entry. + * @param {ChainEntry} entry + * @returns {ChainEntry|null} + */ + + getPrevCache(entry) { + return this.cacheHash.get(entry.prevBlock) || null; + } + + /** + * Get next entry. + * @param {ChainEntry} entry + * @returns {Promise} - Returns ChainEntry. + */ + + async getNext(entry) { + const hash = await this.getNextHash(entry.hash); + + if (!hash) + return null; + + return await this.getEntryByHash(hash); + } + + /** + * Get next entry. + * @param {ChainEntry} entry + * @returns {Promise} - Returns ChainEntry. + */ + + async getNextEntry(entry) { + const next = await this.getEntryByHeight(entry.height + 1); + + if (!next) + return null; + + // Not on main chain. + if (next.prevBlock !== entry.hash) + return null; + + return next; + } + + /** + * Retrieve the tip entry from the tip record. + * @returns {Promise} - Returns {@link ChainEntry}. + */ + + getTip() { + return this.getEntryByHash(this.state.tip); + } + + /** + * Retrieve the tip entry from the tip record. + * @returns {Promise} - Returns {@link ChainState}. + */ + + async getState() { + const data = await this.db.get(layout.R); + + if (!data) + return null; + + return ChainState.fromRaw(data); + } + + /** + * Write genesis block to database. + * @returns {Promise} + */ + + async saveGenesis() { + const genesis = this.network.genesisBlock; + const block = Block.fromRaw(genesis, 'hex'); + const entry = ChainEntry.fromBlock(block); + + this.logger.info('Writing genesis block to ChainDB.'); + + await this.save(entry, block, new CoinView()); + } + + /** + * Retrieve the database flags. + * @returns {Promise} - Returns {@link ChainFlags}. + */ + + async getFlags() { + const data = await this.db.get(layout.O); + + if (!data) + return null; + + return ChainFlags.fromRaw(data); + } + + /** + * Verify current options against db options. + * @param {ChainState} state + * @returns {Promise} + */ + + async verifyFlags(state) { + const options = this.options; + const flags = await this.getFlags(); + + let needsSave = false; + let needsPrune = false; + + if (!flags) + throw new Error('No flags found.'); + + if (options.network !== flags.network) + throw new Error('Network mismatch for chain.'); + + if (options.spv && !flags.spv) + throw new Error('Cannot retroactively enable SPV.'); + + if (!options.spv && flags.spv) + throw new Error('Cannot retroactively disable SPV.'); + + if (!flags.witness) { + if (!options.forceFlags) + throw new Error('Cannot retroactively enable witness.'); + needsSave = true; + } + + if (options.bip91 !== flags.bip91) { + if (!options.forceFlags) + throw new Error('Cannot retroactively alter BIP91 flag.'); + needsSave = true; + } + + if (options.bip148 !== flags.bip148) { + if (!options.forceFlags) + throw new Error('Cannot retroactively alter BIP148 flag.'); + needsSave = true; + } + + if (options.prune && !flags.prune) { + if (!options.forceFlags) + throw new Error('Cannot retroactively prune.'); + needsPrune = true; + } + + if (!options.prune && flags.prune) + throw new Error('Cannot retroactively unprune.'); + + if (options.indexTX && !flags.indexTX) + throw new Error('Cannot retroactively enable TX indexing.'); + + if (!options.indexTX && flags.indexTX) + throw new Error('Cannot retroactively disable TX indexing.'); + + if (options.indexAddress && !flags.indexAddress) + throw new Error('Cannot retroactively enable address indexing.'); + + if (!options.indexAddress && flags.indexAddress) + throw new Error('Cannot retroactively disable address indexing.'); + + if (needsSave) { + await this.logger.info('Rewriting chain flags.'); + await this.saveFlags(); + } + + if (needsPrune) { + await this.logger.info('Retroactively pruning chain.'); + await this.prune(state.tip); + } + } + + /** + * Get state caches. + * @returns {Promise} - Returns {@link StateCache}. + */ + + async getStateCache() { + const stateCache = new StateCache(this.network); + + const items = await this.db.range({ + gte: layout.v(0, encoding.ZERO_HASH), + lte: layout.v(255, encoding.MAX_HASH), + values: true + }); + + for (const item of items) { + const [bit, hash] = layout.vv(item.key); + const state = item.value[0]; + stateCache.insert(bit, hash, state); + } + + return stateCache; + } + + /** + * Save deployment table. + * @returns {Promise} + */ + + saveDeployments() { + const batch = this.db.batch(); + this.writeDeployments(batch); + return batch.write(); + } + + /** + * Save deployment table. + * @returns {Promise} + */ + + writeDeployments(batch) { + const bw = new StaticWriter(1 + 17 * this.network.deploys.length); + + bw.writeU8(this.network.deploys.length); + + for (const deployment of this.network.deploys) { + bw.writeU8(deployment.bit); + bw.writeU32(deployment.startTime); + bw.writeU32(deployment.timeout); + bw.writeI32(deployment.threshold); + bw.writeI32(deployment.window); + } + + batch.put(layout.V, bw.render()); + } + + /** + * Check for outdated deployments. + * @private + * @returns {Promise} + */ + + async checkDeployments() { + const raw = await this.db.get(layout.V); + + assert(raw, 'No deployment table found.'); + + const br = new BufferReader(raw); + const count = br.readU8(); + const invalid = []; + + for (let i = 0; i < count; i++) { + const bit = br.readU8(); + const start = br.readU32(); + const timeout = br.readU32(); + const threshold = br.readI32(); + const window = br.readI32(); + const deployment = this.network.byBit(bit); + + if (deployment + && start === deployment.startTime + && timeout === deployment.timeout + && threshold === deployment.threshold + && window === deployment.window) { + continue; + } + + invalid.push(bit); + } + + return invalid; + } + + /** + * Potentially invalidate state cache. + * @returns {Promise} + */ + + async verifyDeployments() { + let invalid; + + try { + invalid = await this.checkDeployments(); + } catch (e) { + if (e.type !== 'EncodingError') + throw e; + invalid = []; + for (let i = 0; i < 32; i++) + invalid.push(i); + } + + if (invalid.length === 0) + return true; + + const batch = this.db.batch(); + + for (const bit of invalid) { + this.logger.warning('Versionbit deployment params modified.'); + this.logger.warning('Invalidating cache for bit %d.', bit); + await this.invalidateCache(bit, batch); + } + + this.writeDeployments(batch); + + await batch.write(); + + return false; + } + + /** + * Invalidate state cache. + * @private + * @returns {Promise} + */ + + async invalidateCache(bit, batch) { + const keys = await this.db.keys({ + gte: layout.v(bit, encoding.ZERO_HASH), + lte: layout.v(bit, encoding.MAX_HASH) + }); + + for (const key of keys) + batch.del(key); + } + + /** + * Retroactively prune the database. + * @returns {Promise} + */ + + async prune() { + const options = this.options; + const keepBlocks = this.network.block.keepBlocks; + const pruneAfter = this.network.block.pruneAfterHeight; + + const flags = await this.getFlags(); + + if (flags.prune) + throw new Error('Chain is already pruned.'); + + const height = await this.getHeight(this.state.tip); + + if (height <= pruneAfter + keepBlocks) + return false; + + const start = pruneAfter + 1; + const end = height - keepBlocks; + const batch = this.db.batch(); + + for (let i = start; i <= end; i++) { + const hash = await this.getHash(i); + + if (!hash) + throw new Error(`Cannot find hash for ${i}.`); + + batch.del(layout.b(hash)); + batch.del(layout.u(hash)); + } + + try { + options.prune = true; + + const flags = ChainFlags.fromOptions(options); + assert(flags.prune); + + batch.put(layout.O, flags.toRaw()); + + await batch.write(); + } catch (e) { + options.prune = false; + throw e; + } + + await this.db.compactRange(); + + return true; + } + + /** + * Get the _next_ block hash (does not work by height). + * @param {Hash} hash + * @returns {Promise} - Returns {@link Hash}. + */ + + async getNextHash(hash) { + const data = await this.db.get(layout.n(hash)); + + if (!data) + return null; + + return data.toString('hex'); + } + + /** + * Check to see if a block is on the main chain. + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ + + async isMainHash(hash) { + assert(typeof hash === 'string'); + + if (hash === encoding.NULL_HASH) + return false; + + if (hash === this.network.genesis.hash) + return true; + + if (hash === this.state.tip) + return true; + + const cacheHash = this.cacheHash.get(hash); + + if (cacheHash) { + const cacheHeight = this.cacheHeight.get(cacheHash.height); + if (cacheHeight) + return cacheHeight.hash === hash; + } + + if (await this.getNextHash(hash)) + return true; + + return false; + } + + /** + * Test whether the entry is in the main chain. + * @param {ChainEntry} entry + * @returns {Promise} - Returns Boolean. + */ + + async isMainChain(entry) { + if (entry.isGenesis()) + return true; + + if (entry.hash === this.state.tip) + return true; + + const cache = this.getCache(entry.height); + + if (cache) + return entry.hash === cache.hash; + + if (await this.getNextHash(entry.hash)) + return true; + + return false; + } + + /** + * Get hash range. + * @param {Number} [start=-1] + * @param {Number} [end=-1] + * @returns {Promise} + */ + + getHashes(start = -1, end = -1) { + if (start === -1) + start = 0; + + if (end === -1) + end >>>= 0; + + assert((start >>> 0) === start); + assert((end >>> 0) === end); + + return this.db.values({ + gte: layout.H(start), + lte: layout.H(end), + parse: data => data.toString('hex') + }); + } + + /** + * Get all entries. + * @returns {Promise} - Returns {@link ChainEntry}[]. + */ + + getEntries() { + return this.db.values({ + gte: layout.e(encoding.ZERO_HASH), + lte: layout.e(encoding.MAX_HASH), + parse: value => ChainEntry.fromRaw(value) + }); + } + + /** + * Get all tip hashes. + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getTips() { + return this.db.keys({ + gte: layout.p(encoding.ZERO_HASH), + lte: layout.p(encoding.MAX_HASH), + parse: layout.pp + }); + } + + /** + * Get a coin (unspents only). + * @private + * @param {Outpoint} prevout + * @returns {Promise} - Returns {@link CoinEntry}. + */ + + async readCoin(prevout) { + if (this.options.spv) + return null; + + const {hash, index} = prevout; + const key = prevout.toKey(); + const state = this.state; + + const cache = this.coinCache.get(key); + + if (cache) + return CoinEntry.fromRaw(cache); + + const raw = await this.db.get(layout.c(hash, index)); + + if (!raw) + return null; + + if (state === this.state) + this.coinCache.set(key, raw); + + return CoinEntry.fromRaw(raw); + } + + /** + * Get a coin (unspents only). + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} - Returns {@link Coin}. + */ + + async getCoin(hash, index) { + const prevout = new Outpoint(hash, index); + const coin = await this.readCoin(prevout); + + if (!coin) + return null; + + return coin.toCoin(prevout); + } + + /** + * 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. + */ + + async hasCoins(tx) { + for (let i = 0; i < tx.outputs.length; i++) { + const key = layout.c(tx.hash(), i); + if (await this.db.has(key)) + return true; + } + return false; + } + + /** + * Get coin viewpoint. + * @param {TX} tx + * @returns {Promise} - Returns {@link CoinView}. + */ + + async getCoinView(tx) { + const view = new CoinView(); + + for (const {prevout} of tx.inputs) { + const coin = await this.readCoin(prevout); + + if (coin) + view.addEntry(prevout, coin); + } + + return view; + } + + /** + * Get coin viewpoint (historical). + * @param {TX} tx + * @returns {Promise} - Returns {@link CoinView}. + */ + + async getSpentView(tx) { + const view = await this.getCoinView(tx); + + for (const {prevout} of tx.inputs) { + if (view.hasEntry(prevout)) + continue; + + const {hash, index} = prevout; + const meta = await this.getMeta(hash); + + if (!meta) + continue; + + const {tx, height} = meta; + + if (index < tx.outputs.length) + view.addIndex(tx, index, height); + } + + return view; + } + + /** + * Get coins necessary to be resurrected during a reorg. + * @param {Hash} hash + * @returns {Promise} - Returns {@link Coin}[]. + */ + + async getUndoCoins(hash) { + const data = await this.db.get(layout.u(hash)); + + if (!data) + return new UndoCoins(); + + return UndoCoins.fromRaw(data); + } + + /** + * Retrieve a block from the database (not filled with coins). + * @param {Hash} hash + * @returns {Promise} - Returns {@link Block}. + */ + + async getBlock(hash) { + const data = await this.getRawBlock(hash); + + if (!data) + return null; + + return Block.fromRaw(data); + } + + /** + * Retrieve a block from the database (not filled with coins). + * @param {Hash} hash + * @returns {Promise} - Returns {@link Block}. + */ + + async getRawBlock(block) { + if (this.options.spv) + return null; + + const hash = await this.getHash(block); + + if (!hash) + return null; + + return await this.db.get(layout.b(hash)); + } + + /** + * Get a historical block coin viewpoint. + * @param {Block} hash + * @returns {Promise} - Returns {@link CoinView}. + */ + + async getBlockView(block) { + const view = new CoinView(); + const undo = await this.getUndoCoins(block.hash()); + + if (undo.isEmpty()) + return view; + + for (let i = block.txs.length - 1; i > 0; i--) { + const tx = block.txs[i]; + + for (let j = tx.inputs.length - 1; j >= 0; j--) { + const input = tx.inputs[j]; + undo.apply(view, input.prevout); + } + } + + // Undo coins should be empty. + assert(undo.isEmpty(), 'Undo coins data inconsistency.'); + + return view; + } + + /** + * Get a transaction with metadata. + * @param {Hash} hash + * @returns {Promise} - Returns {@link TXMeta}. + */ + + async getMeta(hash) { + if (!this.options.indexTX) + return null; + + const data = await this.db.get(layout.t(hash)); + + if (!data) + return null; + + return TXMeta.fromRaw(data); + } + + /** + * Retrieve a transaction. + * @param {Hash} hash + * @returns {Promise} - Returns {@link TX}. + */ + + async getTX(hash) { + const meta = await this.getMeta(hash); + + if (!meta) + return null; + + return meta.tx; + } + + /** + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ + + async hasTX(hash) { + if (!this.options.indexTX) + return false; + + return await this.db.has(layout.t(hash)); + } + + /** + * Get all coins pertinent to an address. + * @param {Address[]} addrs + * @returns {Promise} - Returns {@link Coin}[]. + */ + + async getCoinsByAddress(addrs) { + if (!this.options.indexAddress) + return []; + + if (!Array.isArray(addrs)) + addrs = [addrs]; + + const coins = []; + + for (const addr of addrs) { + const hash = Address.getHash(addr); + + const keys = await this.db.keys({ + gte: layout.C(hash, encoding.ZERO_HASH, 0), + lte: layout.C(hash, encoding.MAX_HASH, 0xffffffff), + parse: layout.Cc + }); + + for (const [hash, index] of keys) { + const coin = await this.getCoin(hash, index); + assert(coin); + coins.push(coin); + } + } + + return coins; + } + + /** + * Get all transaction hashes to an address. + * @param {Address[]} addrs + * @returns {Promise} - Returns {@link Hash}[]. + */ + + async getHashesByAddress(addrs) { + if (!this.options.indexTX || !this.options.indexAddress) + return []; + + const hashes = Object.create(null); + + for (const addr of addrs) { + const hash = Address.getHash(addr); + + await this.db.keys({ + gte: layout.T(hash, encoding.ZERO_HASH), + lte: layout.T(hash, encoding.MAX_HASH), + parse: (key) => { + const hash = layout.Tt(key); + hashes[hash] = true; + } + }); + } + + return Object.keys(hashes); + } + + /** + * Get all transactions pertinent to an address. + * @param {Address[]} addrs + * @returns {Promise} - Returns {@link TX}[]. + */ + + async getTXByAddress(addrs) { + const mtxs = await this.getMetaByAddress(addrs); + const out = []; + + for (const mtx of mtxs) + out.push(mtx.tx); + + return out; + } + + /** + * Get all transactions pertinent to an address. + * @param {Address[]} addrs + * @returns {Promise} - Returns {@link TXMeta}[]. + */ + + async getMetaByAddress(addrs) { + if (!this.options.indexTX || !this.options.indexAddress) + return []; + + if (!Array.isArray(addrs)) + addrs = [addrs]; + + const hashes = await this.getHashesByAddress(addrs); + const txs = []; + + for (const hash of hashes) { + const tx = await this.getMeta(hash); + assert(tx); + txs.push(tx); + } + + return txs; + } + + /** + * 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) { + if (start == null) + start = this.network.genesis.hash; + + if (typeof start === 'number') + this.logger.info('Scanning from height %d.', start); + else + this.logger.info('Scanning from block %s.', encoding.revHex(start)); + + let entry = await this.getEntry(start); + + if (!entry) + return; + + if (!await this.isMainChain(entry)) + throw new Error('Cannot rescan an alternate chain.'); + + let total = 0; + + while (entry) { + const block = await this.getBlock(entry.hash); + const txs = []; + + total += 1; + + if (!block) { + if (!this.options.spv && !this.options.prune) + throw new Error('Block not found.'); + await iter(entry, txs); + entry = await this.getNext(entry); + continue; + } + + this.logger.info( + 'Scanning block %s (%d).', + entry.rhash(), entry.height); + + for (let i = 0; i < block.txs.length; i++) { + const tx = block.txs[i]; + let found = false; + + for (let j = 0; j < tx.outputs.length; j++) { + const output = tx.outputs[j]; + const hash = output.getHash(); + + if (!hash) + continue; + + if (filter.test(hash)) { + const prevout = Outpoint.fromTX(tx, j); + filter.add(prevout.toRaw()); + found = true; + } + } + + if (found) { + txs.push(tx); + continue; + } + + if (i === 0) + continue; + + for (const {prevout} of tx.inputs) { + if (filter.test(prevout.toRaw())) { + txs.push(tx); + break; + } + } + } + + await iter(entry, txs); + + entry = await this.getNext(entry); + } + + this.logger.info('Finished scanning %d blocks.', total); + } + + /** + * Save an entry to the database and optionally + * connect it as the tip. Note that this method + * does _not_ perform any verification which is + * instead performed in {@link Chain#add}. + * @param {ChainEntry} entry + * @param {Block} block + * @param {CoinView?} view - Will not connect if null. + * @returns {Promise} + */ + + async save(entry, block, view) { + this.start(); + try { + await this._save(entry, block, view); + } catch (e) { + this.drop(); + throw e; + } + await this.commit(); + } + + /** + * Save an entry without a batch. + * @private + * @param {ChainEntry} entry + * @param {Block} block + * @param {CoinView?} view + * @returns {Promise} + */ + + async _save(entry, block, view) { + const hash = block.hash(); + + // Hash->height index. + this.put(layout.h(hash), u32(entry.height)); + + // Entry data. + this.put(layout.e(hash), entry.toRaw()); + this.cacheHash.push(entry.hash, entry); + + // Tip index. + this.del(layout.p(entry.prevBlock)); + this.put(layout.p(hash), null); + + // Update state caches. + this.saveUpdates(); + + if (!view) { + // Save block data. + await this.saveBlock(entry, block); + return; + } + + // Hash->next-block index. + if (!entry.isGenesis()) + this.put(layout.n(entry.prevBlock), hash); + + // Height->hash index. + this.put(layout.H(entry.height), hash); + this.cacheHeight.push(entry.height, entry); + + // Connect block and save data. + await this.saveBlock(entry, block, view); + + // Commit new chain state. + this.put(layout.R, this.pending.commit(hash)); + } + + /** + * Reconnect the block to the chain. + * @param {ChainEntry} entry + * @param {Block} block + * @param {CoinView} view + * @returns {Promise} + */ + + async reconnect(entry, block, view) { + this.start(); + try { + await this._reconnect(entry, block, view); + } catch (e) { + this.drop(); + throw e; + } + await this.commit(); + } + + /** + * Reconnect block without a batch. + * @private + * @param {ChainEntry} entry + * @param {Block} block + * @param {CoinView} view + * @returns {Promise} + */ + + async _reconnect(entry, block, view) { + const hash = block.hash(); + + assert(!entry.isGenesis()); + + // We can now add a hash->next-block index. + this.put(layout.n(entry.prevBlock), hash); + + // We can now add a height->hash index. + this.put(layout.H(entry.height), hash); + this.cacheHeight.push(entry.height, entry); + + // Re-insert into cache. + this.cacheHash.push(entry.hash, entry); + + // Update state caches. + this.saveUpdates(); + + // Connect inputs. + await this.connectBlock(entry, block, view); + + // Update chain state. + this.put(layout.R, this.pending.commit(hash)); + } + + /** + * Disconnect block from the chain. + * @param {ChainEntry} entry + * @param {Block} block + * @returns {Promise} + */ + + async disconnect(entry, block) { + this.start(); + + let view; + try { + view = await this._disconnect(entry, block); + } catch (e) { + this.drop(); + throw e; + } + + await this.commit(); + + return view; + } + + /** + * Disconnect block without a batch. + * @private + * @param {ChainEntry} entry + * @param {Block} block + * @returns {Promise} - Returns {@link CoinView}. + */ + + async _disconnect(entry, block) { + // Remove hash->next-block index. + this.del(layout.n(entry.prevBlock)); + + // Remove height->hash index. + this.del(layout.H(entry.height)); + this.cacheHeight.unpush(entry.height); + + // Update state caches. + this.saveUpdates(); + + // Disconnect inputs. + const view = await this.disconnectBlock(entry, block); + + // Revert chain state to previous tip. + this.put(layout.R, this.pending.commit(entry.prevBlock)); + + return view; + } + + /** + * Save state cache updates. + * @private + */ + + saveUpdates() { + const updates = this.stateCache.updates; + + if (updates.length === 0) + return; + + this.logger.info('Saving %d state cache updates.', updates.length); + + for (const update of updates) { + const {bit, hash} = update; + this.put(layout.v(bit, hash), update.toRaw()); + } + } + + /** + * 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 reset(block) { + 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 (this.options.prune) + throw new Error('Cannot reset when pruned.'); + + // We need to remove all alternate + // chains first. This is ugly, but + // it's the only safe way to reset + // the chain. + await this.removeChains(); + + let tip = await this.getTip(); + assert(tip); + + this.logger.debug('Resetting main chain to: %s', entry.rhash()); + + for (;;) { + this.start(); + + // Stop once we hit our target tip. + if (tip.hash === entry.hash) { + this.put(layout.R, this.pending.commit(tip.hash)); + await this.commit(); + break; + } + + assert(!tip.isGenesis()); + + // Revert the tip index. + this.del(layout.p(tip.hash)); + this.put(layout.p(tip.prevBlock), null); + + // Remove all records (including + // main-chain-only records). + this.del(layout.H(tip.height)); + this.del(layout.h(tip.hash)); + this.del(layout.e(tip.hash)); + this.del(layout.n(tip.prevBlock)); + + // Disconnect and remove block data. + try { + await this.removeBlock(tip); + } catch (e) { + this.drop(); + throw e; + } + + // Revert chain state to previous tip. + this.put(layout.R, this.pending.commit(tip.prevBlock)); + + await this.commit(); + + // Update caches _after_ successful commit. + this.cacheHeight.remove(tip.height); + this.cacheHash.remove(tip.hash); + + tip = await this.getPrevious(tip); + assert(tip); + } + + return tip; + } + + /** + * Remove all alternate chains. + * @returns {Promise} + */ + + async removeChains() { + const tips = await this.getTips(); + + // Note that this has to be + // one giant atomic write! + this.start(); + + try { + for (const tip of tips) + await this._removeChain(tip); + } catch (e) { + this.drop(); + throw e; + } + + await this.commit(); + } + + /** + * Remove an alternate chain. + * @private + * @param {Hash} hash - Alternate chain tip. + * @returns {Promise} + */ + + async _removeChain(hash) { + let tip = await this.getEntryByHash(hash); + + if (!tip) + throw new Error('Alternate chain tip not found.'); + + this.logger.debug('Removing alternate chain: %s.', tip.rhash()); + + for (;;) { + if (await this.isMainChain(tip)) + break; + + assert(!tip.isGenesis()); + + // Remove all non-main-chain records. + this.del(layout.p(tip.hash)); + this.del(layout.h(tip.hash)); + this.del(layout.e(tip.hash)); + this.del(layout.b(tip.hash)); + + // Queue up hash to be removed + // on successful write. + this.cacheHash.unpush(tip.hash); + + tip = await this.getPrevious(tip); + assert(tip); + } + } + + /** + * Save a block (not an entry) to the + * database and potentially connect the inputs. + * @param {ChainEntry} entry + * @param {Block} block + * @param {CoinView?} view + * @returns {Promise} - Returns {@link Block}. + */ + + async saveBlock(entry, block, view) { + const hash = block.hash(); + + if (this.options.spv) + return; + + // Write actual block data (this may be + // better suited to flat files in the future). + this.put(layout.b(hash), block.toRaw()); + + if (!view) + return; + + await this.connectBlock(entry, block, view); + } + + /** + * Remove a block (not an entry) to the database. + * Disconnect inputs. + * @param {ChainEntry} entry + * @returns {Promise} - Returns {@link Block}. + */ + + async removeBlock(entry) { + if (this.options.spv) + return new CoinView(); + + const block = await this.getBlock(entry.hash); + + if (!block) + throw new Error('Block not found.'); + + this.del(layout.b(block.hash())); + + return await this.disconnectBlock(entry, block); + } + + /** + * Commit coin view to database. + * @private + * @param {CoinView} view + */ + + saveView(view) { + for (const [hash, coins] of view.map) { + for (const [index, coin] of coins.outputs) { + if (coin.spent) { + this.del(layout.c(hash, index)); + this.coinCache.unpush(hash + index); + continue; + } + + const raw = coin.toRaw(); + + this.put(layout.c(hash, index), raw); + this.coinCache.push(hash + index, raw); + } + } + } + + /** + * Connect block inputs. + * @param {ChainEntry} entry + * @param {Block} block + * @param {CoinView} view + * @returns {Promise} - Returns {@link Block}. + */ + + async connectBlock(entry, block, view) { + if (this.options.spv) + return; + + const hash = block.hash(); + + this.pending.connect(block); + + // Genesis block's coinbase is unspendable. + if (entry.isGenesis()) + return; + + // Update chain state value. + for (let i = 0; i < block.txs.length; i++) { + const tx = block.txs[i]; + + if (i > 0) { + for (const {prevout} of tx.inputs) + this.pending.spend(view.getOutput(prevout)); + } + + for (const output of tx.outputs) { + if (output.script.isUnspendable()) + continue; + + this.pending.add(output); + } + + // Index the transaction if enabled. + this.indexTX(tx, view, entry, i); + } + + // Commit new coin state. + this.saveView(view); + + // Write undo coins (if there are any). + if (!view.undo.isEmpty()) + this.put(layout.u(hash), view.undo.commit()); + + // Prune height-288 if pruning is enabled. + await this.pruneBlock(entry); + } + + /** + * Disconnect block inputs. + * @param {ChainEntry} entry + * @param {Block} block + * @returns {Promise} - Returns {@link CoinView}. + */ + + async disconnectBlock(entry, block) { + const view = new CoinView(); + + if (this.options.spv) + return view; + + const hash = block.hash(); + const undo = await this.getUndoCoins(hash); + + this.pending.disconnect(block); + + // Disconnect all transactions. + for (let i = block.txs.length - 1; i >= 0; i--) { + const tx = block.txs[i]; + + if (i > 0) { + for (let j = tx.inputs.length - 1; j >= 0; j--) { + const {prevout} = tx.inputs[j]; + undo.apply(view, prevout); + this.pending.add(view.getOutput(prevout)); + } + } + + // Remove any created coins. + view.removeTX(tx, entry.height); + + for (let j = tx.outputs.length - 1; j >= 0; j--) { + const output = tx.outputs[j]; + + if (output.script.isUnspendable()) + continue; + + this.pending.spend(output); + } + + // Remove from transaction index. + this.unindexTX(tx, view); + } + + // Undo coins should be empty. + assert(undo.isEmpty(), 'Undo coins data inconsistency.'); + + // Commit new coin state. + this.saveView(view); + + // Remove undo coins. + this.del(layout.u(hash)); + + return view; + } + + /** + * Prune a block from the chain and + * add current block to the prune queue. + * @private + * @param {ChainEntry} entry + * @returns {Promise} + */ + + async pruneBlock(entry) { + if (this.options.spv) + return; + + if (!this.options.prune) + return; + + const height = entry.height - this.network.block.keepBlocks; + + if (height <= this.network.block.pruneAfterHeight) + return; + + const hash = await this.getHash(height); + + if (!hash) + return; + + this.del(layout.b(hash)); + this.del(layout.u(hash)); + } + + /** + * Save database options. + * @returns {Promise} + */ + + saveFlags() { + const flags = ChainFlags.fromOptions(this.options); + const batch = this.db.batch(); + batch.put(layout.O, flags.toRaw()); + return batch.write(); + } + + /** + * Index a transaction by txid and address. + * @private + * @param {TX} tx + * @param {CoinView} view + * @param {ChainEntry} entry + * @param {Number} index + */ + + indexTX(tx, view, entry, index) { + const hash = tx.hash(); + + if (this.options.indexTX) { + const meta = TXMeta.fromTX(tx, entry, index); + + this.put(layout.t(hash), meta.toRaw()); + + if (this.options.indexAddress) { + const hashes = tx.getHashes(view); + for (const addr of hashes) + this.put(layout.T(addr, hash), null); + } + } + + if (!this.options.indexAddress) + return; + + if (!tx.isCoinbase()) { + for (const {prevout} of tx.inputs) { + const addr = view.getOutput(prevout).getHash(); + + if (!addr) + continue; + + this.del(layout.C(addr, prevout.hash, prevout.index)); + } + } + + for (let i = 0; i < tx.outputs.length; i++) { + const output = tx.outputs[i]; + const addr = output.getHash(); + + if (!addr) + continue; + + this.put(layout.C(addr, hash, i), null); + } + } + + /** + * Remove transaction from index. + * @private + * @param {TX} tx + * @param {CoinView} view + */ + + unindexTX(tx, view) { + const hash = tx.hash(); + + if (this.options.indexTX) { + this.del(layout.t(hash)); + if (this.options.indexAddress) { + const hashes = tx.getHashes(view); + for (const addr of hashes) + this.del(layout.T(addr, hash)); + } + } + + if (!this.options.indexAddress) + return; + + if (!tx.isCoinbase()) { + for (const {prevout} of tx.inputs) { + const addr = view.getOutput(prevout).getHash(); + + if (!addr) + continue; + + this.put(layout.C(addr, prevout.hash, prevout.index), null); + } + } + + for (let i = 0; i < tx.outputs.length; i++) { + const output = tx.outputs[i]; + const addr = output.getHash(); + + if (!addr) + continue; + + this.del(layout.C(addr, hash, i)); + } + } } /** @@ -69,2210 +1997,297 @@ function ChainDB(options) { ChainDB.layout = layout; /** - * Open the chain db, wait for the database to load. - * @returns {Promise} + * ChainFlags */ -ChainDB.prototype.open = async function open() { - this.logger.info('Opening ChainDB...'); +class ChainFlags { + /** + * Create chain flags. + * @alias module:blockchain.ChainFlags + * @constructor + */ - await this.db.open(); - await this.db.checkVersion('V', 3); + constructor(options) { + this.network = Network.primary; + this.spv = false; + this.witness = true; + this.bip91 = false; + this.bip148 = false; + this.prune = false; + this.indexTX = false; + this.indexAddress = false; - const state = await this.getState(); - - if (state) { - // Verify options have not changed. - await this.verifyFlags(state); - - // Verify deployment params have not changed. - await this.verifyDeployments(); - - // Load state caches. - this.stateCache = await this.getStateCache(); - - // Grab the chainstate if we have one. - this.state = state; - - this.logger.info('ChainDB successfully loaded.'); - } else { - // Database is fresh. - // Write initial state. - await this.saveFlags(); - await this.saveDeployments(); - await this.saveGenesis(); - - this.logger.info('ChainDB successfully initialized.'); + if (options) + this.fromOptions(options); } - this.logger.info( - 'Chain State: hash=%s tx=%d coin=%d value=%s.', - this.state.rhash(), - this.state.tx, - this.state.coin, - Amount.btc(this.state.value)); -}; + fromOptions(options) { + this.network = Network.get(options.network); -/** - * Close the chain db, wait for the database to close. - * @returns {Promise} - */ - -ChainDB.prototype.close = function close() { - return this.db.close(); -}; - -/** - * Start a batch. - * @returns {Batch} - */ - -ChainDB.prototype.start = function start() { - assert(!this.current); - assert(!this.pending); - - this.current = this.db.batch(); - this.pending = this.state.clone(); - - this.coinCache.start(); - this.cacheHash.start(); - this.cacheHeight.start(); - - return this.current; -}; - -/** - * Put key and value to current batch. - * @param {String} key - * @param {Buffer} value - */ - -ChainDB.prototype.put = function put(key, value) { - assert(this.current); - this.current.put(key, value); -}; - -/** - * Delete key from current batch. - * @param {String} key - */ - -ChainDB.prototype.del = function del(key) { - assert(this.current); - this.current.del(key); -}; - -/** - * Get current batch. - * @returns {Batch} - */ - -ChainDB.prototype.batch = function batch() { - assert(this.current); - return this.current; -}; - -/** - * Drop current batch. - * @returns {Batch} - */ - -ChainDB.prototype.drop = function drop() { - const batch = this.current; - - assert(this.current); - assert(this.pending); - - this.current = null; - this.pending = null; - - this.coinCache.drop(); - this.cacheHash.drop(); - this.cacheHeight.drop(); - this.stateCache.drop(); - - batch.clear(); -}; - -/** - * Commit current batch. - * @returns {Promise} - */ - -ChainDB.prototype.commit = async function commit() { - assert(this.current); - assert(this.pending); - - try { - await this.current.write(); - } catch (e) { - this.current = null; - this.pending = null; - this.coinCache.drop(); - this.cacheHash.drop(); - this.cacheHeight.drop(); - throw e; - } - - // Overwrite the entire state - // with our new best state - // only if it is committed. - // Note that alternate chain - // tips do not commit anything. - if (this.pending.committed) - this.state = this.pending; - - this.current = null; - this.pending = null; - - this.coinCache.commit(); - this.cacheHash.commit(); - this.cacheHeight.commit(); - this.stateCache.commit(); -}; - -/** - * Test the cache for a present entry hash or height. - * @param {Hash|Number} block - Hash or height. - */ - -ChainDB.prototype.hasCache = function hasCache(block) { - if (typeof block === 'number') - return this.cacheHeight.has(block); - - assert(typeof block === 'string'); - - return this.cacheHash.has(block); -}; - -/** - * Get an entry directly from the LRU cache. - * @param {Hash|Number} block - Hash or height. - */ - -ChainDB.prototype.getCache = function getCache(block) { - if (typeof block === 'number') - return this.cacheHeight.get(block); - - assert(typeof block === 'string'); - - return this.cacheHash.get(block); -}; - -/** - * Get the height of a block by hash. - * @param {Hash} hash - * @returns {Promise} - Returns Number. - */ - -ChainDB.prototype.getHeight = async function getHeight(hash) { - if (typeof hash === 'number') - return hash; - - assert(typeof hash === 'string'); - - if (hash === encoding.NULL_HASH) - return -1; - - const entry = this.cacheHash.get(hash); - - if (entry) - return entry.height; - - const height = await this.db.get(layout.h(hash)); - - if (!height) - return -1; - - return height.readUInt32LE(0, true); -}; - -/** - * 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}. - */ - -ChainDB.prototype.getHash = async function getHash(height) { - if (typeof height === 'string') - return height; - - assert(typeof height === 'number'); - - if (height < 0) - return null; - - const entry = this.cacheHeight.get(height); - - if (entry) - return entry.hash; - - const hash = await this.db.get(layout.H(height)); - - if (!hash) - return null; - - return hash.toString('hex'); -}; - -/** - * Retrieve a chain entry by height. - * @param {Number} height - * @returns {Promise} - Returns {@link ChainEntry}. - */ - -ChainDB.prototype.getEntryByHeight = async function getEntryByHeight(height) { - assert(typeof height === 'number'); - - if (height < 0) - return null; - - const cache = this.cacheHeight.get(height); - - if (cache) - return cache; - - const data = await this.db.get(layout.H(height)); - - if (!data) - return null; - - const hash = data.toString('hex'); - - const state = this.state; - const entry = await this.getEntryByHash(hash); - - if (!entry) - return null; - - // By the time getEntry has completed, - // a reorg may have occurred. This entry - // may not be on the main chain anymore. - if (this.state === state) - this.cacheHeight.set(entry.height, entry); - - return entry; -}; - -/** - * Retrieve a chain entry by hash. - * @param {Hash} hash - * @returns {Promise} - Returns {@link ChainEntry}. - */ - -ChainDB.prototype.getEntryByHash = async function getEntryByHash(hash) { - assert(typeof hash === 'string'); - - if (hash === encoding.NULL_HASH) - return null; - - const cache = this.cacheHash.get(hash); - - if (cache) - return cache; - - const raw = await this.db.get(layout.e(hash)); - - if (!raw) - return null; - - const entry = ChainEntry.fromRaw(raw); - - // There's no efficient way to check whether - // this is in the main chain or not, so - // don't add it to the height cache. - this.cacheHash.set(entry.hash, entry); - - return entry; -}; - -/** - * Retrieve a chain entry. - * @param {Number|Hash} block - Height or hash. - * @returns {Promise} - Returns {@link ChainEntry}. - */ - -ChainDB.prototype.getEntry = function getEntry(block) { - if (typeof block === 'number') - return this.getEntryByHeight(block); - return this.getEntryByHash(block); -}; - -/** - * Test whether the chain contains a block. - * @param {Hash} hash - * @returns {Promise} - Returns Boolean. - */ - -ChainDB.prototype.hasEntry = async function hasEntry(hash) { - const height = await this.getHeight(hash); - return height !== -1; -}; - -/** - * Get ancestor by `height`. - * @param {ChainEntry} entry - * @param {Number} height - * @returns {Promise} - Returns ChainEntry. - */ - -ChainDB.prototype.getAncestor = async function getAncestor(entry, height) { - if (height < 0) - return null; - - assert(height >= 0); - assert(height <= entry.height); - - if (await this.isMainChain(entry)) - return await this.getEntryByHeight(height); - - while (entry.height !== height) { - const cache = this.getPrevCache(entry); - - if (cache) - entry = cache; - else - entry = await this.getPrevious(entry); - - assert(entry); - } - - return entry; -}; - -/** - * Get previous entry. - * @param {ChainEntry} entry - * @returns {Promise} - Returns ChainEntry. - */ - -ChainDB.prototype.getPrevious = function getPrevious(entry) { - return this.getEntryByHash(entry.prevBlock); -}; - -/** - * Get previous cached entry. - * @param {ChainEntry} entry - * @returns {ChainEntry|null} - */ - -ChainDB.prototype.getPrevCache = function getPrevCache(entry) { - return this.cacheHash.get(entry.prevBlock) || null; -}; - -/** - * Get next entry. - * @param {ChainEntry} entry - * @returns {Promise} - Returns ChainEntry. - */ - -ChainDB.prototype.getNext = async function getNext(entry) { - const hash = await this.getNextHash(entry.hash); - - if (!hash) - return null; - - return await this.getEntryByHash(hash); -}; - -/** - * Get next entry. - * @param {ChainEntry} entry - * @returns {Promise} - Returns ChainEntry. - */ - -ChainDB.prototype.getNextEntry = async function getNextEntry(entry) { - const next = await this.getEntryByHeight(entry.height + 1); - - if (!next) - return null; - - // Not on main chain. - if (next.prevBlock !== entry.hash) - return null; - - return next; -}; - -/** - * Retrieve the tip entry from the tip record. - * @returns {Promise} - Returns {@link ChainEntry}. - */ - -ChainDB.prototype.getTip = function getTip() { - return this.getEntryByHash(this.state.tip); -}; - -/** - * Retrieve the tip entry from the tip record. - * @returns {Promise} - Returns {@link ChainState}. - */ - -ChainDB.prototype.getState = async function getState() { - const data = await this.db.get(layout.R); - - if (!data) - return null; - - return ChainState.fromRaw(data); -}; - -/** - * Write genesis block to database. - * @returns {Promise} - */ - -ChainDB.prototype.saveGenesis = async function saveGenesis() { - const genesis = this.network.genesisBlock; - const block = Block.fromRaw(genesis, 'hex'); - const entry = ChainEntry.fromBlock(block); - - this.logger.info('Writing genesis block to ChainDB.'); - - await this.save(entry, block, new CoinView()); -}; - -/** - * Retrieve the database flags. - * @returns {Promise} - Returns {@link ChainFlags}. - */ - -ChainDB.prototype.getFlags = async function getFlags() { - const data = await this.db.get(layout.O); - - if (!data) - return null; - - return ChainFlags.fromRaw(data); -}; - -/** - * Verify current options against db options. - * @param {ChainState} state - * @returns {Promise} - */ - -ChainDB.prototype.verifyFlags = async function verifyFlags(state) { - const options = this.options; - const flags = await this.getFlags(); - - let needsSave = false; - let needsPrune = false; - - if (!flags) - throw new Error('No flags found.'); - - if (options.network !== flags.network) - throw new Error('Network mismatch for chain.'); - - if (options.spv && !flags.spv) - throw new Error('Cannot retroactively enable SPV.'); - - if (!options.spv && flags.spv) - throw new Error('Cannot retroactively disable SPV.'); - - if (!flags.witness) { - if (!options.forceFlags) - throw new Error('Cannot retroactively enable witness.'); - needsSave = true; - } - - if (options.bip91 !== flags.bip91) { - if (!options.forceFlags) - throw new Error('Cannot retroactively alter BIP91 flag.'); - needsSave = true; - } - - if (options.bip148 !== flags.bip148) { - if (!options.forceFlags) - throw new Error('Cannot retroactively alter BIP148 flag.'); - needsSave = true; - } - - if (options.prune && !flags.prune) { - if (!options.forceFlags) - throw new Error('Cannot retroactively prune.'); - needsPrune = true; - } - - if (!options.prune && flags.prune) - throw new Error('Cannot retroactively unprune.'); - - if (options.indexTX && !flags.indexTX) - throw new Error('Cannot retroactively enable TX indexing.'); - - if (!options.indexTX && flags.indexTX) - throw new Error('Cannot retroactively disable TX indexing.'); - - if (options.indexAddress && !flags.indexAddress) - throw new Error('Cannot retroactively enable address indexing.'); - - if (!options.indexAddress && flags.indexAddress) - throw new Error('Cannot retroactively disable address indexing.'); - - if (needsSave) { - await this.logger.info('Rewriting chain flags.'); - await this.saveFlags(); - } - - if (needsPrune) { - await this.logger.info('Retroactively pruning chain.'); - await this.prune(state.tip); - } -}; - -/** - * Get state caches. - * @returns {Promise} - Returns {@link StateCache}. - */ - -ChainDB.prototype.getStateCache = async function getStateCache() { - const stateCache = new StateCache(this.network); - - const items = await this.db.range({ - gte: layout.v(0, encoding.ZERO_HASH), - lte: layout.v(255, encoding.MAX_HASH), - values: true - }); - - for (const item of items) { - const [bit, hash] = layout.vv(item.key); - const state = item.value[0]; - stateCache.insert(bit, hash, state); - } - - return stateCache; -}; - -/** - * Save deployment table. - * @returns {Promise} - */ - -ChainDB.prototype.saveDeployments = function saveDeployments() { - const batch = this.db.batch(); - this.writeDeployments(batch); - return batch.write(); -}; - -/** - * Save deployment table. - * @returns {Promise} - */ - -ChainDB.prototype.writeDeployments = function writeDeployments(batch) { - const bw = new StaticWriter(1 + 17 * this.network.deploys.length); - - bw.writeU8(this.network.deploys.length); - - for (const deployment of this.network.deploys) { - bw.writeU8(deployment.bit); - bw.writeU32(deployment.startTime); - bw.writeU32(deployment.timeout); - bw.writeI32(deployment.threshold); - bw.writeI32(deployment.window); - } - - batch.put(layout.V, bw.render()); -}; - -/** - * Check for outdated deployments. - * @private - * @returns {Promise} - */ - -ChainDB.prototype.checkDeployments = async function checkDeployments() { - const raw = await this.db.get(layout.V); - - assert(raw, 'No deployment table found.'); - - const br = new BufferReader(raw); - const count = br.readU8(); - const invalid = []; - - for (let i = 0; i < count; i++) { - const bit = br.readU8(); - const start = br.readU32(); - const timeout = br.readU32(); - const threshold = br.readI32(); - const window = br.readI32(); - const deployment = this.network.byBit(bit); - - if (deployment - && start === deployment.startTime - && timeout === deployment.timeout - && threshold === deployment.threshold - && window === deployment.window) { - continue; + if (options.spv != null) { + assert(typeof options.spv === 'boolean'); + this.spv = options.spv; } - invalid.push(bit); - } - - return invalid; -}; - -/** - * Potentially invalidate state cache. - * @returns {Promise} - */ - -ChainDB.prototype.verifyDeployments = async function verifyDeployments() { - let invalid; - - try { - invalid = await this.checkDeployments(); - } catch (e) { - if (e.type !== 'EncodingError') - throw e; - invalid = []; - for (let i = 0; i < 32; i++) - invalid.push(i); - } - - if (invalid.length === 0) - return true; - - const batch = this.db.batch(); - - for (const bit of invalid) { - this.logger.warning('Versionbit deployment params modified.'); - this.logger.warning('Invalidating cache for bit %d.', bit); - await this.invalidateCache(bit, batch); - } - - this.writeDeployments(batch); - - await batch.write(); - - return false; -}; - -/** - * Invalidate state cache. - * @private - * @returns {Promise} - */ - -ChainDB.prototype.invalidateCache = async function invalidateCache(bit, batch) { - const keys = await this.db.keys({ - gte: layout.v(bit, encoding.ZERO_HASH), - lte: layout.v(bit, encoding.MAX_HASH) - }); - - for (const key of keys) - batch.del(key); -}; - -/** - * Retroactively prune the database. - * @returns {Promise} - */ - -ChainDB.prototype.prune = async function prune() { - const options = this.options; - const keepBlocks = this.network.block.keepBlocks; - const pruneAfter = this.network.block.pruneAfterHeight; - - const flags = await this.getFlags(); - - if (flags.prune) - throw new Error('Chain is already pruned.'); - - const height = await this.getHeight(this.state.tip); - - if (height <= pruneAfter + keepBlocks) - return false; - - const start = pruneAfter + 1; - const end = height - keepBlocks; - const batch = this.db.batch(); - - for (let i = start; i <= end; i++) { - const hash = await this.getHash(i); - - if (!hash) - throw new Error(`Cannot find hash for ${i}.`); - - batch.del(layout.b(hash)); - batch.del(layout.u(hash)); - } - - try { - options.prune = true; - - const flags = ChainFlags.fromOptions(options); - assert(flags.prune); - - batch.put(layout.O, flags.toRaw()); - - await batch.write(); - } catch (e) { - options.prune = false; - throw e; - } - - await this.db.compactRange(); - - return true; -}; - -/** - * Get the _next_ block hash (does not work by height). - * @param {Hash} hash - * @returns {Promise} - Returns {@link Hash}. - */ - -ChainDB.prototype.getNextHash = async function getNextHash(hash) { - const data = await this.db.get(layout.n(hash)); - - if (!data) - return null; - - return data.toString('hex'); -}; - -/** - * Check to see if a block is on the main chain. - * @param {Hash} hash - * @returns {Promise} - Returns Boolean. - */ - -ChainDB.prototype.isMainHash = async function isMainHash(hash) { - assert(typeof hash === 'string'); - - if (hash === encoding.NULL_HASH) - return false; - - if (hash === this.network.genesis.hash) - return true; - - if (hash === this.state.tip) - return true; - - const cacheHash = this.cacheHash.get(hash); - - if (cacheHash) { - const cacheHeight = this.cacheHeight.get(cacheHash.height); - if (cacheHeight) - return cacheHeight.hash === hash; - } - - if (await this.getNextHash(hash)) - return true; - - return false; -}; - -/** - * Test whether the entry is in the main chain. - * @param {ChainEntry} entry - * @returns {Promise} - Returns Boolean. - */ - -ChainDB.prototype.isMainChain = async function isMainChain(entry) { - if (entry.isGenesis()) - return true; - - if (entry.hash === this.state.tip) - return true; - - const cache = this.getCache(entry.height); - - if (cache) - return entry.hash === cache.hash; - - if (await this.getNextHash(entry.hash)) - return true; - - return false; -}; - -/** - * Get hash range. - * @param {Number} [start=-1] - * @param {Number} [end=-1] - * @returns {Promise} - */ - -ChainDB.prototype.getHashes = function getHashes(start = -1, end = -1) { - if (start === -1) - start = 0; - - if (end === -1) - end >>>= 0; - - assert((start >>> 0) === start); - assert((end >>> 0) === end); - - return this.db.values({ - gte: layout.H(start), - lte: layout.H(end), - parse: data => data.toString('hex') - }); -}; - -/** - * Get all entries. - * @returns {Promise} - Returns {@link ChainEntry}[]. - */ - -ChainDB.prototype.getEntries = function getEntries() { - return this.db.values({ - gte: layout.e(encoding.ZERO_HASH), - lte: layout.e(encoding.MAX_HASH), - parse: value => ChainEntry.fromRaw(value) - }); -}; - -/** - * Get all tip hashes. - * @returns {Promise} - Returns {@link Hash}[]. - */ - -ChainDB.prototype.getTips = function getTips() { - return this.db.keys({ - gte: layout.p(encoding.ZERO_HASH), - lte: layout.p(encoding.MAX_HASH), - parse: layout.pp - }); -}; - -/** - * Get a coin (unspents only). - * @private - * @param {Outpoint} prevout - * @returns {Promise} - Returns {@link CoinEntry}. - */ - -ChainDB.prototype.readCoin = async function readCoin(prevout) { - if (this.options.spv) - return null; - - const {hash, index} = prevout; - const key = prevout.toKey(); - const state = this.state; - - const cache = this.coinCache.get(key); - - if (cache) - return CoinEntry.fromRaw(cache); - - const raw = await this.db.get(layout.c(hash, index)); - - if (!raw) - return null; - - if (state === this.state) - this.coinCache.set(key, raw); - - return CoinEntry.fromRaw(raw); -}; - -/** - * Get a coin (unspents only). - * @param {Hash} hash - * @param {Number} index - * @returns {Promise} - Returns {@link Coin}. - */ - -ChainDB.prototype.getCoin = async function getCoin(hash, index) { - const prevout = new Outpoint(hash, index); - const coin = await this.readCoin(prevout); - - if (!coin) - return null; - - return coin.toCoin(prevout); -}; - -/** - * 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. - */ - -ChainDB.prototype.hasCoins = async function hasCoins(tx) { - for (let i = 0; i < tx.outputs.length; i++) { - const key = layout.c(tx.hash(), i); - if (await this.db.has(key)) - return true; - } - return false; -}; - -/** - * Get coin viewpoint. - * @param {TX} tx - * @returns {Promise} - Returns {@link CoinView}. - */ - -ChainDB.prototype.getCoinView = async function getCoinView(tx) { - const view = new CoinView(); - - for (const {prevout} of tx.inputs) { - const coin = await this.readCoin(prevout); - - if (coin) - view.addEntry(prevout, coin); - } - - return view; -}; - -/** - * Get coin viewpoint (historical). - * @param {TX} tx - * @returns {Promise} - Returns {@link CoinView}. - */ - -ChainDB.prototype.getSpentView = async function getSpentView(tx) { - const view = await this.getCoinView(tx); - - for (const {prevout} of tx.inputs) { - if (view.hasEntry(prevout)) - continue; - - const {hash, index} = prevout; - const meta = await this.getMeta(hash); - - if (!meta) - continue; - - const {tx, height} = meta; - - if (index < tx.outputs.length) - view.addIndex(tx, index, height); - } - - return view; -}; - -/** - * Get coins necessary to be resurrected during a reorg. - * @param {Hash} hash - * @returns {Promise} - Returns {@link Coin}[]. - */ - -ChainDB.prototype.getUndoCoins = async function getUndoCoins(hash) { - const data = await this.db.get(layout.u(hash)); - - if (!data) - return new UndoCoins(); - - return UndoCoins.fromRaw(data); -}; - -/** - * Retrieve a block from the database (not filled with coins). - * @param {Hash} hash - * @returns {Promise} - Returns {@link Block}. - */ - -ChainDB.prototype.getBlock = async function getBlock(hash) { - const data = await this.getRawBlock(hash); - - if (!data) - return null; - - return Block.fromRaw(data); -}; - -/** - * Retrieve a block from the database (not filled with coins). - * @param {Hash} hash - * @returns {Promise} - Returns {@link Block}. - */ - -ChainDB.prototype.getRawBlock = async function getRawBlock(block) { - if (this.options.spv) - return null; - - const hash = await this.getHash(block); - - if (!hash) - return null; - - return await this.db.get(layout.b(hash)); -}; - -/** - * Get a historical block coin viewpoint. - * @param {Block} hash - * @returns {Promise} - Returns {@link CoinView}. - */ - -ChainDB.prototype.getBlockView = async function getBlockView(block) { - const view = new CoinView(); - const undo = await this.getUndoCoins(block.hash()); - - if (undo.isEmpty()) - return view; - - for (let i = block.txs.length - 1; i > 0; i--) { - const tx = block.txs[i]; - - for (let j = tx.inputs.length - 1; j >= 0; j--) { - const input = tx.inputs[j]; - undo.apply(view, input.prevout); - } - } - - // Undo coins should be empty. - assert(undo.isEmpty(), 'Undo coins data inconsistency.'); - - return view; -}; - -/** - * Get a transaction with metadata. - * @param {Hash} hash - * @returns {Promise} - Returns {@link TXMeta}. - */ - -ChainDB.prototype.getMeta = async function getMeta(hash) { - if (!this.options.indexTX) - return null; - - const data = await this.db.get(layout.t(hash)); - - if (!data) - return null; - - return TXMeta.fromRaw(data); -}; - -/** - * Retrieve a transaction. - * @param {Hash} hash - * @returns {Promise} - Returns {@link TX}. - */ - -ChainDB.prototype.getTX = async function getTX(hash) { - const meta = await this.getMeta(hash); - - if (!meta) - return null; - - return meta.tx; -}; - -/** - * @param {Hash} hash - * @returns {Promise} - Returns Boolean. - */ - -ChainDB.prototype.hasTX = async function hasTX(hash) { - if (!this.options.indexTX) - return false; - - return await this.db.has(layout.t(hash)); -}; - -/** - * Get all coins pertinent to an address. - * @param {Address[]} addrs - * @returns {Promise} - Returns {@link Coin}[]. - */ - -ChainDB.prototype.getCoinsByAddress = async function getCoinsByAddress(addrs) { - if (!this.options.indexAddress) - return []; - - if (!Array.isArray(addrs)) - addrs = [addrs]; - - const coins = []; - - for (const addr of addrs) { - const hash = Address.getHash(addr); - - const keys = await this.db.keys({ - gte: layout.C(hash, encoding.ZERO_HASH, 0), - lte: layout.C(hash, encoding.MAX_HASH, 0xffffffff), - parse: layout.Cc - }); - - for (const [hash, index] of keys) { - const coin = await this.getCoin(hash, index); - assert(coin); - coins.push(coin); - } - } - - return coins; -}; - -/** - * Get all transaction hashes to an address. - * @param {Address[]} addrs - * @returns {Promise} - Returns {@link Hash}[]. - */ - -ChainDB.prototype.getHashesByAddress = async function getHashesByAddress(addrs) { - if (!this.options.indexTX || !this.options.indexAddress) - return []; - - const hashes = Object.create(null); - - for (const addr of addrs) { - const hash = Address.getHash(addr); - - await this.db.keys({ - gte: layout.T(hash, encoding.ZERO_HASH), - lte: layout.T(hash, encoding.MAX_HASH), - parse: (key) => { - const hash = layout.Tt(key); - hashes[hash] = true; - } - }); - } - - return Object.keys(hashes); -}; - -/** - * Get all transactions pertinent to an address. - * @param {Address[]} addrs - * @returns {Promise} - Returns {@link TX}[]. - */ - -ChainDB.prototype.getTXByAddress = async function getTXByAddress(addrs) { - const mtxs = await this.getMetaByAddress(addrs); - const out = []; - - for (const mtx of mtxs) - out.push(mtx.tx); - - return out; -}; - -/** - * Get all transactions pertinent to an address. - * @param {Address[]} addrs - * @returns {Promise} - Returns {@link TXMeta}[]. - */ - -ChainDB.prototype.getMetaByAddress = async function getMetaByAddress(addrs) { - if (!this.options.indexTX || !this.options.indexAddress) - return []; - - if (!Array.isArray(addrs)) - addrs = [addrs]; - - const hashes = await this.getHashesByAddress(addrs); - const txs = []; - - for (const hash of hashes) { - const tx = await this.getMeta(hash); - assert(tx); - txs.push(tx); - } - - return txs; -}; - -/** - * 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} - */ - -ChainDB.prototype.scan = async function scan(start, filter, iter) { - if (start == null) - start = this.network.genesis.hash; - - if (typeof start === 'number') - this.logger.info('Scanning from height %d.', start); - else - this.logger.info('Scanning from block %s.', encoding.revHex(start)); - - let entry = await this.getEntry(start); - - if (!entry) - return; - - if (!await this.isMainChain(entry)) - throw new Error('Cannot rescan an alternate chain.'); - - let total = 0; - - while (entry) { - const block = await this.getBlock(entry.hash); - const txs = []; - - total += 1; - - if (!block) { - if (!this.options.spv && !this.options.prune) - throw new Error('Block not found.'); - await iter(entry, txs); - entry = await this.getNext(entry); - continue; + if (options.bip91 != null) { + assert(typeof options.bip91 === 'boolean'); + this.bip91 = options.bip91; } - this.logger.info( - 'Scanning block %s (%d).', - entry.rhash(), entry.height); - - for (let i = 0; i < block.txs.length; i++) { - const tx = block.txs[i]; - let found = false; - - for (let j = 0; j < tx.outputs.length; j++) { - const output = tx.outputs[j]; - const hash = output.getHash(); - - if (!hash) - continue; - - if (filter.test(hash)) { - const prevout = Outpoint.fromTX(tx, j); - filter.add(prevout.toRaw()); - found = true; - } - } - - if (found) { - txs.push(tx); - continue; - } - - if (i === 0) - continue; - - for (const {prevout} of tx.inputs) { - if (filter.test(prevout.toRaw())) { - txs.push(tx); - break; - } - } + if (options.bip148 != null) { + assert(typeof options.bip148 === 'boolean'); + this.bip148 = options.bip148; } - await iter(entry, txs); - - entry = await this.getNext(entry); - } - - this.logger.info('Finished scanning %d blocks.', total); -}; - -/** - * Save an entry to the database and optionally - * connect it as the tip. Note that this method - * does _not_ perform any verification which is - * instead performed in {@link Chain#add}. - * @param {ChainEntry} entry - * @param {Block} block - * @param {CoinView?} view - Will not connect if null. - * @returns {Promise} - */ - -ChainDB.prototype.save = async function save(entry, block, view) { - this.start(); - try { - await this._save(entry, block, view); - } catch (e) { - this.drop(); - throw e; - } - await this.commit(); -}; - -/** - * Save an entry without a batch. - * @private - * @param {ChainEntry} entry - * @param {Block} block - * @param {CoinView?} view - * @returns {Promise} - */ - -ChainDB.prototype._save = async function _save(entry, block, view) { - const hash = block.hash(); - - // Hash->height index. - this.put(layout.h(hash), u32(entry.height)); - - // Entry data. - this.put(layout.e(hash), entry.toRaw()); - this.cacheHash.push(entry.hash, entry); - - // Tip index. - this.del(layout.p(entry.prevBlock)); - this.put(layout.p(hash), null); - - // Update state caches. - this.saveUpdates(); - - if (!view) { - // Save block data. - await this.saveBlock(entry, block); - return; - } - - // Hash->next-block index. - if (!entry.isGenesis()) - this.put(layout.n(entry.prevBlock), hash); - - // Height->hash index. - this.put(layout.H(entry.height), hash); - this.cacheHeight.push(entry.height, entry); - - // Connect block and save data. - await this.saveBlock(entry, block, view); - - // Commit new chain state. - this.put(layout.R, this.pending.commit(hash)); -}; - -/** - * Reconnect the block to the chain. - * @param {ChainEntry} entry - * @param {Block} block - * @param {CoinView} view - * @returns {Promise} - */ - -ChainDB.prototype.reconnect = async function reconnect(entry, block, view) { - this.start(); - try { - await this._reconnect(entry, block, view); - } catch (e) { - this.drop(); - throw e; - } - await this.commit(); -}; - -/** - * Reconnect block without a batch. - * @private - * @param {ChainEntry} entry - * @param {Block} block - * @param {CoinView} view - * @returns {Promise} - */ - -ChainDB.prototype._reconnect = async function _reconnect(entry, block, view) { - const hash = block.hash(); - - assert(!entry.isGenesis()); - - // We can now add a hash->next-block index. - this.put(layout.n(entry.prevBlock), hash); - - // We can now add a height->hash index. - this.put(layout.H(entry.height), hash); - this.cacheHeight.push(entry.height, entry); - - // Re-insert into cache. - this.cacheHash.push(entry.hash, entry); - - // Update state caches. - this.saveUpdates(); - - // Connect inputs. - await this.connectBlock(entry, block, view); - - // Update chain state. - this.put(layout.R, this.pending.commit(hash)); -}; - -/** - * Disconnect block from the chain. - * @param {ChainEntry} entry - * @param {Block} block - * @returns {Promise} - */ - -ChainDB.prototype.disconnect = async function disconnect(entry, block) { - this.start(); - - let view; - try { - view = await this._disconnect(entry, block); - } catch (e) { - this.drop(); - throw e; - } - - await this.commit(); - - return view; -}; - -/** - * Disconnect block without a batch. - * @private - * @param {ChainEntry} entry - * @param {Block} block - * @returns {Promise} - Returns {@link CoinView}. - */ - -ChainDB.prototype._disconnect = async function _disconnect(entry, block) { - // Remove hash->next-block index. - this.del(layout.n(entry.prevBlock)); - - // Remove height->hash index. - this.del(layout.H(entry.height)); - this.cacheHeight.unpush(entry.height); - - // Update state caches. - this.saveUpdates(); - - // Disconnect inputs. - const view = await this.disconnectBlock(entry, block); - - // Revert chain state to previous tip. - this.put(layout.R, this.pending.commit(entry.prevBlock)); - - return view; -}; - -/** - * Save state cache updates. - * @private - */ - -ChainDB.prototype.saveUpdates = function saveUpdates() { - const updates = this.stateCache.updates; - - if (updates.length === 0) - return; - - this.logger.info('Saving %d state cache updates.', updates.length); - - for (const update of updates) { - const {bit, hash} = update; - this.put(layout.v(bit, hash), update.toRaw()); - } -}; - -/** - * Reset the chain to a height or hash. Useful for replaying - * the blockchain download for SPV. - * @param {Hash|Number} block - hash/height - * @returns {Promise} - */ - -ChainDB.prototype.reset = async function reset(block) { - 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 (this.options.prune) - throw new Error('Cannot reset when pruned.'); - - // We need to remove all alternate - // chains first. This is ugly, but - // it's the only safe way to reset - // the chain. - await this.removeChains(); - - let tip = await this.getTip(); - assert(tip); - - this.logger.debug('Resetting main chain to: %s', entry.rhash()); - - for (;;) { - this.start(); - - // Stop once we hit our target tip. - if (tip.hash === entry.hash) { - this.put(layout.R, this.pending.commit(tip.hash)); - await this.commit(); - break; + if (options.prune != null) { + assert(typeof options.prune === 'boolean'); + this.prune = options.prune; } - assert(!tip.isGenesis()); - - // Revert the tip index. - this.del(layout.p(tip.hash)); - this.put(layout.p(tip.prevBlock), null); - - // Remove all records (including - // main-chain-only records). - this.del(layout.H(tip.height)); - this.del(layout.h(tip.hash)); - this.del(layout.e(tip.hash)); - this.del(layout.n(tip.prevBlock)); - - // Disconnect and remove block data. - try { - await this.removeBlock(tip); - } catch (e) { - this.drop(); - throw e; + if (options.indexTX != null) { + assert(typeof options.indexTX === 'boolean'); + this.indexTX = options.indexTX; } - // Revert chain state to previous tip. - this.put(layout.R, this.pending.commit(tip.prevBlock)); - - await this.commit(); - - // Update caches _after_ successful commit. - this.cacheHeight.remove(tip.height); - this.cacheHash.remove(tip.hash); - - tip = await this.getPrevious(tip); - assert(tip); - } - - return tip; -}; - -/** - * Remove all alternate chains. - * @returns {Promise} - */ - -ChainDB.prototype.removeChains = async function removeChains() { - const tips = await this.getTips(); - - // Note that this has to be - // one giant atomic write! - this.start(); - - try { - for (const tip of tips) - await this._removeChain(tip); - } catch (e) { - this.drop(); - throw e; - } - - await this.commit(); -}; - -/** - * Remove an alternate chain. - * @private - * @param {Hash} hash - Alternate chain tip. - * @returns {Promise} - */ - -ChainDB.prototype._removeChain = async function _removeChain(hash) { - let tip = await this.getEntryByHash(hash); - - if (!tip) - throw new Error('Alternate chain tip not found.'); - - this.logger.debug('Removing alternate chain: %s.', tip.rhash()); - - for (;;) { - if (await this.isMainChain(tip)) - break; - - assert(!tip.isGenesis()); - - // Remove all non-main-chain records. - this.del(layout.p(tip.hash)); - this.del(layout.h(tip.hash)); - this.del(layout.e(tip.hash)); - this.del(layout.b(tip.hash)); - - // Queue up hash to be removed - // on successful write. - this.cacheHash.unpush(tip.hash); - - tip = await this.getPrevious(tip); - assert(tip); - } -}; - -/** - * Save a block (not an entry) to the - * database and potentially connect the inputs. - * @param {ChainEntry} entry - * @param {Block} block - * @param {CoinView?} view - * @returns {Promise} - Returns {@link Block}. - */ - -ChainDB.prototype.saveBlock = async function saveBlock(entry, block, view) { - const hash = block.hash(); - - if (this.options.spv) - return; - - // Write actual block data (this may be - // better suited to flat files in the future). - this.put(layout.b(hash), block.toRaw()); - - if (!view) - return; - - await this.connectBlock(entry, block, view); -}; - -/** - * Remove a block (not an entry) to the database. - * Disconnect inputs. - * @param {ChainEntry} entry - * @returns {Promise} - Returns {@link Block}. - */ - -ChainDB.prototype.removeBlock = async function removeBlock(entry) { - if (this.options.spv) - return new CoinView(); - - const block = await this.getBlock(entry.hash); - - if (!block) - throw new Error('Block not found.'); - - this.del(layout.b(block.hash())); - - return await this.disconnectBlock(entry, block); -}; - -/** - * Commit coin view to database. - * @private - * @param {CoinView} view - */ - -ChainDB.prototype.saveView = function saveView(view) { - for (const [hash, coins] of view.map) { - for (const [index, coin] of coins.outputs) { - if (coin.spent) { - this.del(layout.c(hash, index)); - this.coinCache.unpush(hash + index); - continue; - } - - const raw = coin.toRaw(); - - this.put(layout.c(hash, index), raw); - this.coinCache.push(hash + index, raw); - } - } -}; - -/** - * Connect block inputs. - * @param {ChainEntry} entry - * @param {Block} block - * @param {CoinView} view - * @returns {Promise} - Returns {@link Block}. - */ - -ChainDB.prototype.connectBlock = async function connectBlock(entry, block, view) { - if (this.options.spv) - return; - - const hash = block.hash(); - - this.pending.connect(block); - - // Genesis block's coinbase is unspendable. - if (entry.isGenesis()) - return; - - // Update chain state value. - for (let i = 0; i < block.txs.length; i++) { - const tx = block.txs[i]; - - if (i > 0) { - for (const {prevout} of tx.inputs) - this.pending.spend(view.getOutput(prevout)); + if (options.indexAddress != null) { + assert(typeof options.indexAddress === 'boolean'); + this.indexAddress = options.indexAddress; } - for (const output of tx.outputs) { - if (output.script.isUnspendable()) - continue; - - this.pending.add(output); - } - - // Index the transaction if enabled. - this.indexTX(tx, view, entry, i); + return this; } - // Commit new coin state. - this.saveView(view); - - // Write undo coins (if there are any). - if (!view.undo.isEmpty()) - this.put(layout.u(hash), view.undo.commit()); - - // Prune height-288 if pruning is enabled. - await this.pruneBlock(entry); -}; - -/** - * Disconnect block inputs. - * @param {ChainEntry} entry - * @param {Block} block - * @returns {Promise} - Returns {@link CoinView}. - */ - -ChainDB.prototype.disconnectBlock = async function disconnectBlock(entry, block) { - const view = new CoinView(); - - if (this.options.spv) - return view; - - const hash = block.hash(); - const undo = await this.getUndoCoins(hash); - - this.pending.disconnect(block); - - // Disconnect all transactions. - for (let i = block.txs.length - 1; i >= 0; i--) { - const tx = block.txs[i]; - - if (i > 0) { - for (let j = tx.inputs.length - 1; j >= 0; j--) { - const {prevout} = tx.inputs[j]; - undo.apply(view, prevout); - this.pending.add(view.getOutput(prevout)); - } - } - - // Remove any created coins. - view.removeTX(tx, entry.height); - - for (let j = tx.outputs.length - 1; j >= 0; j--) { - const output = tx.outputs[j]; - - if (output.script.isUnspendable()) - continue; - - this.pending.spend(output); - } - - // Remove from transaction index. - this.unindexTX(tx, view); + static fromOptions(data) { + return new ChainFlags().fromOptions(data); } - // Undo coins should be empty. - assert(undo.isEmpty(), 'Undo coins data inconsistency.'); + toRaw() { + const bw = new StaticWriter(12); + let flags = 0; - // Commit new coin state. - this.saveView(view); + if (this.spv) + flags |= 1 << 0; - // Remove undo coins. - this.del(layout.u(hash)); + if (this.witness) + flags |= 1 << 1; - return view; -}; + if (this.prune) + flags |= 1 << 2; -/** - * Prune a block from the chain and - * add current block to the prune queue. - * @private - * @param {ChainEntry} entry - * @returns {Promise} - */ + if (this.indexTX) + flags |= 1 << 3; -ChainDB.prototype.pruneBlock = async function pruneBlock(entry) { - if (this.options.spv) - return; + if (this.indexAddress) + flags |= 1 << 4; - if (!this.options.prune) - return; + if (this.bip91) + flags |= 1 << 5; - const height = entry.height - this.network.block.keepBlocks; + if (this.bip148) + flags |= 1 << 6; - if (height <= this.network.block.pruneAfterHeight) - return; + bw.writeU32(this.network.magic); + bw.writeU32(flags); + bw.writeU32(0); - const hash = await this.getHash(height); - - if (!hash) - return; - - this.del(layout.b(hash)); - this.del(layout.u(hash)); -}; - -/** - * Save database options. - * @returns {Promise} - */ - -ChainDB.prototype.saveFlags = function saveFlags() { - const flags = ChainFlags.fromOptions(this.options); - const batch = this.db.batch(); - batch.put(layout.O, flags.toRaw()); - return batch.write(); -}; - -/** - * Index a transaction by txid and address. - * @private - * @param {TX} tx - * @param {CoinView} view - * @param {ChainEntry} entry - * @param {Number} index - */ - -ChainDB.prototype.indexTX = function indexTX(tx, view, entry, index) { - const hash = tx.hash(); - - if (this.options.indexTX) { - const meta = TXMeta.fromTX(tx, entry, index); - - this.put(layout.t(hash), meta.toRaw()); - - if (this.options.indexAddress) { - const hashes = tx.getHashes(view); - for (const addr of hashes) - this.put(layout.T(addr, hash), null); - } + return bw.render(); } - if (!this.options.indexAddress) - return; + fromRaw(data) { + const br = new BufferReader(data); - if (!tx.isCoinbase()) { - for (const {prevout} of tx.inputs) { - const addr = view.getOutput(prevout).getHash(); + this.network = Network.fromMagic(br.readU32()); - if (!addr) - continue; + const flags = br.readU32(); - this.del(layout.C(addr, prevout.hash, prevout.index)); - } + this.spv = (flags & 1) !== 0; + this.witness = (flags & 2) !== 0; + this.prune = (flags & 4) !== 0; + this.indexTX = (flags & 8) !== 0; + this.indexAddress = (flags & 16) !== 0; + this.bip91 = (flags & 32) !== 0; + this.bip148 = (flags & 64) !== 0; + + return this; } - for (let i = 0; i < tx.outputs.length; i++) { - const output = tx.outputs[i]; - const addr = output.getHash(); - - if (!addr) - continue; - - this.put(layout.C(addr, hash, i), null); + static fromRaw(data) { + return new ChainFlags().fromRaw(data); } -}; - -/** - * Remove transaction from index. - * @private - * @param {TX} tx - * @param {CoinView} view - */ - -ChainDB.prototype.unindexTX = function unindexTX(tx, view) { - const hash = tx.hash(); - - if (this.options.indexTX) { - this.del(layout.t(hash)); - if (this.options.indexAddress) { - const hashes = tx.getHashes(view); - for (const addr of hashes) - this.del(layout.T(addr, hash)); - } - } - - if (!this.options.indexAddress) - return; - - if (!tx.isCoinbase()) { - for (const {prevout} of tx.inputs) { - const addr = view.getOutput(prevout).getHash(); - - if (!addr) - continue; - - this.put(layout.C(addr, prevout.hash, prevout.index), null); - } - } - - for (let i = 0; i < tx.outputs.length; i++) { - const output = tx.outputs[i]; - const addr = output.getHash(); - - if (!addr) - continue; - - this.del(layout.C(addr, hash, i)); - } -}; - -/** - * Chain Flags - * @alias module:blockchain.ChainFlags - * @constructor - */ - -function ChainFlags(options) { - if (!(this instanceof ChainFlags)) - return new ChainFlags(options); - - this.network = Network.primary; - this.spv = false; - this.witness = true; - this.bip91 = false; - this.bip148 = false; - this.prune = false; - this.indexTX = false; - this.indexAddress = false; - - if (options) - this.fromOptions(options); } -ChainFlags.prototype.fromOptions = function fromOptions(options) { - this.network = Network.get(options.network); - - if (options.spv != null) { - assert(typeof options.spv === 'boolean'); - this.spv = options.spv; - } - - 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.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; - } - - return this; -}; - -ChainFlags.fromOptions = function fromOptions(data) { - return new ChainFlags().fromOptions(data); -}; - -ChainFlags.prototype.toRaw = function toRaw() { - const bw = new StaticWriter(12); - let flags = 0; - - if (this.spv) - flags |= 1 << 0; - - if (this.witness) - flags |= 1 << 1; - - if (this.prune) - flags |= 1 << 2; - - if (this.indexTX) - flags |= 1 << 3; - - if (this.indexAddress) - flags |= 1 << 4; - - if (this.bip91) - flags |= 1 << 5; - - if (this.bip148) - flags |= 1 << 6; - - bw.writeU32(this.network.magic); - bw.writeU32(flags); - bw.writeU32(0); - - return bw.render(); -}; - -ChainFlags.prototype.fromRaw = function fromRaw(data) { - const br = new BufferReader(data); - - this.network = Network.fromMagic(br.readU32()); - - const flags = br.readU32(); - - this.spv = (flags & 1) !== 0; - this.witness = (flags & 2) !== 0; - this.prune = (flags & 4) !== 0; - this.indexTX = (flags & 8) !== 0; - this.indexAddress = (flags & 16) !== 0; - this.bip91 = (flags & 32) !== 0; - this.bip148 = (flags & 64) !== 0; - - return this; -}; - -ChainFlags.fromRaw = function fromRaw(data) { - return new ChainFlags().fromRaw(data); -}; - /** * Chain State - * @alias module:blockchain.ChainState - * @constructor */ -function ChainState() { - this.tip = encoding.NULL_HASH; - this.tx = 0; - this.coin = 0; - this.value = 0; - this.committed = false; +class ChainState { + /** + * Create chain state. + * @alias module:blockchain.ChainState + * @constructor + */ + + constructor() { + this.tip = encoding.NULL_HASH; + this.tx = 0; + this.coin = 0; + this.value = 0; + this.committed = false; + } + + rhash() { + return encoding.revHex(this.tip); + } + + clone() { + const state = new ChainState(); + state.tip = this.tip; + state.tx = this.tx; + state.coin = this.coin; + state.value = this.value; + return state; + } + + connect(block) { + this.tx += block.txs.length; + } + + disconnect(block) { + this.tx -= block.txs.length; + } + + add(coin) { + this.coin++; + this.value += coin.value; + } + + spend(coin) { + this.coin--; + this.value -= coin.value; + } + + commit(hash) { + if (typeof hash !== 'string') + hash = hash.toString('hex'); + this.tip = hash; + this.committed = true; + return this.toRaw(); + } + + toRaw() { + const bw = new StaticWriter(56); + bw.writeHash(this.tip); + bw.writeU64(this.tx); + bw.writeU64(this.coin); + bw.writeU64(this.value); + return bw.render(); + } + + static fromRaw(data) { + const state = new ChainState(); + const br = new BufferReader(data); + state.tip = br.readHash('hex'); + state.tx = br.readU64(); + state.coin = br.readU64(); + state.value = br.readU64(); + return state; + } } -ChainState.prototype.rhash = function rhash() { - return encoding.revHex(this.tip); -}; - -ChainState.prototype.clone = function clone() { - const state = new ChainState(); - state.tip = this.tip; - state.tx = this.tx; - state.coin = this.coin; - state.value = this.value; - return state; -}; - -ChainState.prototype.connect = function connect(block) { - this.tx += block.txs.length; -}; - -ChainState.prototype.disconnect = function disconnect(block) { - this.tx -= block.txs.length; -}; - -ChainState.prototype.add = function add(coin) { - this.coin++; - this.value += coin.value; -}; - -ChainState.prototype.spend = function spend(coin) { - this.coin--; - this.value -= coin.value; -}; - -ChainState.prototype.commit = function commit(hash) { - if (typeof hash !== 'string') - hash = hash.toString('hex'); - this.tip = hash; - this.committed = true; - return this.toRaw(); -}; - -ChainState.prototype.toRaw = function toRaw() { - const bw = new StaticWriter(56); - bw.writeHash(this.tip); - bw.writeU64(this.tx); - bw.writeU64(this.coin); - bw.writeU64(this.value); - return bw.render(); -}; - -ChainState.fromRaw = function fromRaw(data) { - const state = new ChainState(); - const br = new BufferReader(data); - state.tip = br.readHash('hex'); - state.tx = br.readU64(); - state.coin = br.readU64(); - state.value = br.readU64(); - return state; -}; - /** - * StateCache - * @alias module:blockchain.StateCache - * @constructor + * State Cache */ -function StateCache(network) { - this.network = network; - this.bits = []; - this.updates = []; - this.init(); -} +class StateCache { + /** + * Create state cache. + * @alias module:blockchain.StateCache + * @constructor + */ -StateCache.prototype.init = function init() { - for (let i = 0; i < 32; i++) - this.bits.push(null); - - for (const {bit} of this.network.deploys) { - assert(!this.bits[bit]); - this.bits[bit] = new Map(); + constructor(network) { + this.network = network; + this.bits = []; + this.updates = []; + this.init(); } -}; -StateCache.prototype.set = function set(bit, entry, state) { - const cache = this.bits[bit]; + init() { + for (let i = 0; i < 32; i++) + this.bits.push(null); - assert(cache); - - if (cache.get(entry.hash) !== state) { - cache.set(entry.hash, state); - this.updates.push(new CacheUpdate(bit, entry.hash, state)); + for (const {bit} of this.network.deploys) { + assert(!this.bits[bit]); + this.bits[bit] = new Map(); + } } -}; -StateCache.prototype.get = function get(bit, entry) { - const cache = this.bits[bit]; + set(bit, entry, state) { + const cache = this.bits[bit]; - assert(cache); + assert(cache); - const state = cache.get(entry.hash); + if (cache.get(entry.hash) !== state) { + cache.set(entry.hash, state); + this.updates.push(new CacheUpdate(bit, entry.hash, state)); + } + } - if (state == null) - return -1; + get(bit, entry) { + const cache = this.bits[bit]; - return state; -}; + assert(cache); -StateCache.prototype.commit = function commit() { - this.updates.length = 0; -}; + const state = cache.get(entry.hash); -StateCache.prototype.drop = function drop() { - for (const {bit, hash} of this.updates) { + if (state == null) + return -1; + + return state; + } + + commit() { + this.updates.length = 0; + } + + drop() { + for (const {bit, hash} of this.updates) { + const cache = this.bits[bit]; + assert(cache); + cache.delete(hash); + } + + this.updates.length = 0; + } + + insert(bit, hash, state) { const cache = this.bits[bit]; assert(cache); - cache.delete(hash); + cache.set(hash, state); } - - this.updates.length = 0; -}; - -StateCache.prototype.insert = function insert(bit, hash, state) { - const cache = this.bits[bit]; - assert(cache); - cache.set(hash, state); -}; - -/** - * CacheUpdate - * @constructor - * @ignore - */ - -function CacheUpdate(bit, hash, state) { - this.bit = bit; - this.hash = hash; - this.state = state; } -CacheUpdate.prototype.toRaw = function toRaw() { - return u8(this.state); -}; +/** + * Cache Update + */ + +class CacheUpdate { + /** + * Create cache update. + * @constructor + * @ignore + */ + + constructor(bit, hash, state) { + this.bit = bit; + this.hash = hash; + this.state = state; + } + + toRaw() { + return u8(this.state); + } +} /* * Helpers diff --git a/lib/blockchain/chainentry.js b/lib/blockchain/chainentry.js index 7c410585..c6f91df7 100644 --- a/lib/blockchain/chainentry.js +++ b/lib/blockchain/chainentry.js @@ -19,18 +19,15 @@ const InvItem = require('../primitives/invitem'); const ZERO = new BN(0); /** + * Chain Entry * Represents an entry in the chain. Unlike * other bitcoin fullnodes, we store the * chainwork _with_ the entry in order to * avoid reading the entire chain index on * boot and recalculating the chainworks. * @alias module:blockchain.ChainEntry - * @constructor - * @param {Object?} options * @property {Hash} hash - * @property {Number} version - Transaction version. Note that Bcoin reads - * versions as unsigned even though they are signed at the protocol level. - * This value will never be negative. + * @property {Number} version * @property {Hash} prevBlock * @property {Hash} merkleRoot * @property {Number} time @@ -38,25 +35,331 @@ const ZERO = new BN(0); * @property {Number} nonce * @property {Number} height * @property {BN} chainwork - * @property {ReversedHash} rhash - Reversed block hash (uint256le). + * @property {ReversedHash} rhash */ -function ChainEntry(options) { - if (!(this instanceof ChainEntry)) - return new ChainEntry(options); +class ChainEntry { + /** + * Create a chain entry. + * @constructor + * @param {Object?} options + */ - this.hash = encoding.NULL_HASH; - this.version = 1; - this.prevBlock = encoding.NULL_HASH; - this.merkleRoot = encoding.NULL_HASH; - this.time = 0; - this.bits = 0; - this.nonce = 0; - this.height = 0; - this.chainwork = ZERO; + constructor(options) { + this.hash = encoding.NULL_HASH; + this.version = 1; + this.prevBlock = encoding.NULL_HASH; + this.merkleRoot = encoding.NULL_HASH; + this.time = 0; + this.bits = 0; + this.nonce = 0; + this.height = 0; + this.chainwork = ZERO; - if (options) - this.fromOptions(options); + if (options) + this.fromOptions(options); + } + + /** + * Inject properties from options. + * @private + * @param {Object} options + */ + + fromOptions(options) { + assert(options, 'Block data is required.'); + assert(typeof options.hash === 'string'); + assert((options.version >>> 0) === options.version); + assert(typeof options.prevBlock === 'string'); + assert(typeof options.merkleRoot === 'string'); + assert((options.time >>> 0) === options.time); + assert((options.bits >>> 0) === options.bits); + assert((options.nonce >>> 0) === options.nonce); + assert((options.height >>> 0) === options.height); + assert(!options.chainwork || BN.isBN(options.chainwork)); + + this.hash = options.hash; + this.version = options.version; + this.prevBlock = options.prevBlock; + this.merkleRoot = options.merkleRoot; + this.time = options.time; + this.bits = options.bits; + this.nonce = options.nonce; + this.height = options.height; + this.chainwork = options.chainwork || ZERO; + + return this; + } + + /** + * Instantiate chainentry from options. + * @param {Object} options + * @param {ChainEntry} prev - Previous entry. + * @returns {ChainEntry} + */ + + static fromOptions(options, prev) { + return new this().fromOptions(options, prev); + } + + /** + * Calculate the proof: (1 << 256) / (target + 1) + * @returns {BN} proof + */ + + getProof() { + const target = consensus.fromCompact(this.bits); + + if (target.isNeg() || target.isZero()) + return new BN(0); + + return ChainEntry.MAX_CHAINWORK.div(target.iaddn(1)); + } + + /** + * Calculate the chainwork by + * adding proof to previous chainwork. + * @returns {BN} chainwork + */ + + getChainwork(prev) { + const proof = this.getProof(); + + if (!prev) + return proof; + + return proof.iadd(prev.chainwork); + } + + /** + * Test against the genesis block. + * @returns {Boolean} + */ + + isGenesis() { + return this.height === 0; + } + + /** + * Test whether the entry contains an unknown version bit. + * @param {Network} network + * @returns {Boolean} + */ + + hasUnknown(network) { + const TOP_MASK = consensus.VERSION_TOP_MASK; + const TOP_BITS = consensus.VERSION_TOP_BITS; + const bits = (this.version & TOP_MASK) >>> 0; + + if (bits !== TOP_BITS) + return false; + + return (this.version & network.unknownBits) !== 0; + } + + /** + * Test whether the entry contains a version bit. + * @param {Number} bit + * @returns {Boolean} + */ + + hasBit(bit) { + return consensus.hasBit(this.version, bit); + } + + /** + * Get little-endian block hash. + * @returns {Hash} + */ + + rhash() { + return encoding.revHex(this.hash); + } + + /** + * Inject properties from block. + * @private + * @param {Block|MerkleBlock} block + * @param {ChainEntry} prev - Previous entry. + */ + + fromBlock(block, prev) { + this.hash = block.hash('hex'); + this.version = block.version; + this.prevBlock = block.prevBlock; + this.merkleRoot = block.merkleRoot; + this.time = block.time; + this.bits = block.bits; + this.nonce = block.nonce; + this.height = prev ? prev.height + 1: 0; + this.chainwork = this.getChainwork(prev); + return this; + } + + /** + * Instantiate chainentry from block. + * @param {Block|MerkleBlock} block + * @param {ChainEntry} prev - Previous entry. + * @returns {ChainEntry} + */ + + static fromBlock(block, prev) { + return new this().fromBlock(block, prev); + } + + /** + * Serialize the entry to internal database format. + * @returns {Buffer} + */ + + toRaw() { + const bw = new StaticWriter(116); + + bw.writeU32(this.version); + bw.writeHash(this.prevBlock); + bw.writeHash(this.merkleRoot); + bw.writeU32(this.time); + bw.writeU32(this.bits); + bw.writeU32(this.nonce); + bw.writeU32(this.height); + bw.writeBytes(this.chainwork.toArrayLike(Buffer, 'le', 32)); + + return bw.render(); + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + const br = new BufferReader(data, true); + const hash = hash256.digest(br.readBytes(80)); + + br.seek(-80); + + this.hash = hash.toString('hex'); + this.version = br.readU32(); + this.prevBlock = br.readHash('hex'); + this.merkleRoot = br.readHash('hex'); + this.time = br.readU32(); + this.bits = br.readU32(); + this.nonce = br.readU32(); + this.height = br.readU32(); + this.chainwork = new BN(br.readBytes(32), 'le'); + + return this; + } + + /** + * Deserialize the entry. + * @param {Buffer} data + * @returns {ChainEntry} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } + + /** + * Serialize the entry to an object more + * suitable for JSON serialization. + * @returns {Object} + */ + + toJSON() { + return { + hash: encoding.revHex(this.hash), + version: this.version, + prevBlock: encoding.revHex(this.prevBlock), + merkleRoot: encoding.revHex(this.merkleRoot), + time: this.time, + bits: this.bits, + nonce: this.nonce, + height: this.height, + chainwork: this.chainwork.toString('hex', 64) + }; + } + + /** + * Inject properties from json object. + * @private + * @param {Object} json + */ + + fromJSON(json) { + assert(json, 'Block data is required.'); + assert(typeof json.hash === 'string'); + assert((json.version >>> 0) === json.version); + assert(typeof json.prevBlock === 'string'); + assert(typeof json.merkleRoot === 'string'); + assert((json.time >>> 0) === json.time); + assert((json.bits >>> 0) === json.bits); + assert((json.nonce >>> 0) === json.nonce); + assert(typeof json.chainwork === 'string'); + + this.hash = encoding.revHex(json.hash); + this.version = json.version; + this.prevBlock = encoding.revHex(json.prevBlock); + this.merkleRoot = encoding.revHex(json.merkleRoot); + this.time = json.time; + this.bits = json.bits; + this.nonce = json.nonce; + this.height = json.height; + this.chainwork = new BN(json.chainwork, 'hex'); + + return this; + } + + /** + * Instantiate block from jsonified object. + * @param {Object} json + * @returns {ChainEntry} + */ + + static fromJSON(json) { + return new this().fromJSON(json); + } + + /** + * Convert the entry to a headers object. + * @returns {Headers} + */ + + toHeaders() { + return Headers.fromEntry(this); + } + + /** + * Convert the entry to an inv item. + * @returns {InvItem} + */ + + toInv() { + return new InvItem(InvItem.types.BLOCK, this.hash); + } + + /** + * Return a more user-friendly object. + * @returns {Object} + */ + + inspect() { + const json = this.toJSON(); + json.version = json.version.toString(16); + return json; + } + + /** + * Test whether an object is a {@link ChainEntry}. + * @param {Object} obj + * @returns {Boolean} + */ + + static isChainEntry(obj) { + return obj instanceof ChainEntry; + } } /** @@ -66,307 +369,6 @@ function ChainEntry(options) { ChainEntry.MAX_CHAINWORK = new BN(1).ushln(256); -/** - * Inject properties from options. - * @private - * @param {Object} options - */ - -ChainEntry.prototype.fromOptions = function fromOptions(options) { - assert(options, 'Block data is required.'); - assert(typeof options.hash === 'string'); - assert((options.version >>> 0) === options.version); - assert(typeof options.prevBlock === 'string'); - assert(typeof options.merkleRoot === 'string'); - assert((options.time >>> 0) === options.time); - assert((options.bits >>> 0) === options.bits); - assert((options.nonce >>> 0) === options.nonce); - assert((options.height >>> 0) === options.height); - assert(!options.chainwork || BN.isBN(options.chainwork)); - - this.hash = options.hash; - this.version = options.version; - this.prevBlock = options.prevBlock; - this.merkleRoot = options.merkleRoot; - this.time = options.time; - this.bits = options.bits; - this.nonce = options.nonce; - this.height = options.height; - this.chainwork = options.chainwork || ZERO; - - return this; -}; - -/** - * Instantiate chainentry from options. - * @param {Object} options - * @param {ChainEntry} prev - Previous entry. - * @returns {ChainEntry} - */ - -ChainEntry.fromOptions = function fromOptions(options, prev) { - return new ChainEntry().fromOptions(options, prev); -}; - -/** - * Calculate the proof: (1 << 256) / (target + 1) - * @returns {BN} proof - */ - -ChainEntry.prototype.getProof = function getProof() { - const target = consensus.fromCompact(this.bits); - - if (target.isNeg() || target.isZero()) - return new BN(0); - - return ChainEntry.MAX_CHAINWORK.div(target.iaddn(1)); -}; - -/** - * Calculate the chainwork by - * adding proof to previous chainwork. - * @returns {BN} chainwork - */ - -ChainEntry.prototype.getChainwork = function getChainwork(prev) { - const proof = this.getProof(); - - if (!prev) - return proof; - - return proof.iadd(prev.chainwork); -}; - -/** - * Test against the genesis block. - * @returns {Boolean} - */ - -ChainEntry.prototype.isGenesis = function isGenesis() { - return this.height === 0; -}; - -/** - * Test whether the entry contains an unknown version bit. - * @param {Network} network - * @returns {Boolean} - */ - -ChainEntry.prototype.hasUnknown = function hasUnknown(network) { - const TOP_MASK = consensus.VERSION_TOP_MASK; - const TOP_BITS = consensus.VERSION_TOP_BITS; - const bits = (this.version & TOP_MASK) >>> 0; - - if (bits !== TOP_BITS) - return false; - - return (this.version & network.unknownBits) !== 0; -}; - -/** - * Test whether the entry contains a version bit. - * @param {Number} bit - * @returns {Boolean} - */ - -ChainEntry.prototype.hasBit = function hasBit(bit) { - return consensus.hasBit(this.version, bit); -}; - -/** - * Get little-endian block hash. - * @returns {Hash} - */ - -ChainEntry.prototype.rhash = function rhash() { - return encoding.revHex(this.hash); -}; - -/** - * Inject properties from block. - * @private - * @param {Block|MerkleBlock} block - * @param {ChainEntry} prev - Previous entry. - */ - -ChainEntry.prototype.fromBlock = function fromBlock(block, prev) { - this.hash = block.hash('hex'); - this.version = block.version; - this.prevBlock = block.prevBlock; - this.merkleRoot = block.merkleRoot; - this.time = block.time; - this.bits = block.bits; - this.nonce = block.nonce; - this.height = prev ? prev.height + 1: 0; - this.chainwork = this.getChainwork(prev); - return this; -}; - -/** - * Instantiate chainentry from block. - * @param {Block|MerkleBlock} block - * @param {ChainEntry} prev - Previous entry. - * @returns {ChainEntry} - */ - -ChainEntry.fromBlock = function fromBlock(block, prev) { - return new ChainEntry().fromBlock(block, prev); -}; - -/** - * Serialize the entry to internal database format. - * @returns {Buffer} - */ - -ChainEntry.prototype.toRaw = function toRaw() { - const bw = new StaticWriter(116); - - bw.writeU32(this.version); - bw.writeHash(this.prevBlock); - bw.writeHash(this.merkleRoot); - bw.writeU32(this.time); - bw.writeU32(this.bits); - bw.writeU32(this.nonce); - bw.writeU32(this.height); - bw.writeBytes(this.chainwork.toArrayLike(Buffer, 'le', 32)); - - return bw.render(); -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -ChainEntry.prototype.fromRaw = function fromRaw(data) { - const br = new BufferReader(data, true); - const hash = hash256.digest(br.readBytes(80)); - - br.seek(-80); - - this.hash = hash.toString('hex'); - this.version = br.readU32(); - this.prevBlock = br.readHash('hex'); - this.merkleRoot = br.readHash('hex'); - this.time = br.readU32(); - this.bits = br.readU32(); - this.nonce = br.readU32(); - this.height = br.readU32(); - this.chainwork = new BN(br.readBytes(32), 'le'); - - return this; -}; - -/** - * Deserialize the entry. - * @param {Buffer} data - * @returns {ChainEntry} - */ - -ChainEntry.fromRaw = function fromRaw(data) { - return new ChainEntry().fromRaw(data); -}; - -/** - * Serialize the entry to an object more - * suitable for JSON serialization. - * @returns {Object} - */ - -ChainEntry.prototype.toJSON = function toJSON() { - return { - hash: encoding.revHex(this.hash), - version: this.version, - prevBlock: encoding.revHex(this.prevBlock), - merkleRoot: encoding.revHex(this.merkleRoot), - time: this.time, - bits: this.bits, - nonce: this.nonce, - height: this.height, - chainwork: this.chainwork.toString('hex', 64) - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - */ - -ChainEntry.prototype.fromJSON = function fromJSON(json) { - assert(json, 'Block data is required.'); - assert(typeof json.hash === 'string'); - assert((json.version >>> 0) === json.version); - assert(typeof json.prevBlock === 'string'); - assert(typeof json.merkleRoot === 'string'); - assert((json.time >>> 0) === json.time); - assert((json.bits >>> 0) === json.bits); - assert((json.nonce >>> 0) === json.nonce); - assert(typeof json.chainwork === 'string'); - - this.hash = encoding.revHex(json.hash); - this.version = json.version; - this.prevBlock = encoding.revHex(json.prevBlock); - this.merkleRoot = encoding.revHex(json.merkleRoot); - this.time = json.time; - this.bits = json.bits; - this.nonce = json.nonce; - this.height = json.height; - this.chainwork = new BN(json.chainwork, 'hex'); - - return this; -}; - -/** - * Instantiate block from jsonified object. - * @param {Object} json - * @returns {ChainEntry} - */ - -ChainEntry.fromJSON = function fromJSON(json) { - return new ChainEntry().fromJSON(json); -}; - -/** - * Convert the entry to a headers object. - * @returns {Headers} - */ - -ChainEntry.prototype.toHeaders = function toHeaders() { - return Headers.fromEntry(this); -}; - -/** - * Convert the entry to an inv item. - * @returns {InvItem} - */ - -ChainEntry.prototype.toInv = function toInv() { - return new InvItem(InvItem.types.BLOCK, this.hash); -}; - -/** - * Return a more user-friendly object. - * @returns {Object} - */ - -ChainEntry.prototype.inspect = function inspect() { - const json = this.toJSON(); - json.version = json.version.toString(16); - return json; -}; - -/** - * Test whether an object is a {@link ChainEntry}. - * @param {Object} obj - * @returns {Boolean} - */ - -ChainEntry.isChainEntry = function isChainEntry(obj) { - return obj instanceof ChainEntry; -}; - /* * Expose */ diff --git a/lib/coins/coinentry.js b/lib/coins/coinentry.js index 3d74e32b..aecfb1c3 100644 --- a/lib/coins/coinentry.js +++ b/lib/coins/coinentry.js @@ -22,9 +22,9 @@ const NUM_FLAGS = 1; const MAX_HEIGHT = ((1 << (32 - NUM_FLAGS)) >>> 0) - 1; /** + * Coin Entry * Represents an unspent output. * @alias module:coins.CoinEntry - * @constructor * @property {Number} version - Transaction version. * @property {Number} height - Transaction height (-1 if unconfirmed). * @property {Boolean} coinbase - Whether the containing @@ -34,241 +34,245 @@ const MAX_HEIGHT = ((1 << (32 - NUM_FLAGS)) >>> 0) - 1; * @property {Buffer} raw */ -function CoinEntry() { - if (!(this instanceof CoinEntry)) - return new CoinEntry(); +class CoinEntry { + /** + * Create a coin entry. + * @constructor + */ - this.version = 1; - this.height = -1; - this.coinbase = false; - this.output = new Output(); - this.spent = false; - this.raw = null; -} + constructor() { + this.version = 1; + this.height = -1; + this.coinbase = false; + this.output = new Output(); + this.spent = false; + this.raw = null; + } -/** - * Convert coin entry to an output. - * @returns {Output} - */ + /** + * Convert coin entry to an output. + * @returns {Output} + */ -CoinEntry.prototype.toOutput = function toOutput() { - return this.output; -}; + toOutput() { + return this.output; + } -/** - * Convert coin entry to a coin. - * @param {Outpoint} prevout - * @returns {Coin} - */ + /** + * Convert coin entry to a coin. + * @param {Outpoint} prevout + * @returns {Coin} + */ -CoinEntry.prototype.toCoin = function toCoin(prevout) { - const coin = new Coin(); - coin.version = this.version; - coin.height = this.height; - coin.coinbase = this.coinbase; - coin.script = this.output.script; - coin.value = this.output.value; - coin.hash = prevout.hash; - coin.index = prevout.index; - return coin; -}; + toCoin(prevout) { + const coin = new Coin(); + coin.version = this.version; + coin.height = this.height; + coin.coinbase = this.coinbase; + coin.script = this.output.script; + coin.value = this.output.value; + coin.hash = prevout.hash; + coin.index = prevout.index; + return coin; + } -/** - * Inject properties from TX. - * @param {TX} tx - * @param {Number} index - */ + /** + * Inject properties from TX. + * @param {TX} tx + * @param {Number} index + */ -CoinEntry.prototype.fromOutput = function fromOutput(output) { - this.output = output; - return this; -}; + fromOutput(output) { + this.output = output; + return this; + } -/** - * Instantiate a coin from a TX - * @param {TX} tx - * @param {Number} index - Output index. - * @returns {CoinEntry} - */ + /** + * Instantiate a coin from a TX + * @param {TX} tx + * @param {Number} index - Output index. + * @returns {CoinEntry} + */ -CoinEntry.fromOutput = function fromOutput(output) { - return new CoinEntry().fromOutput(output); -}; + static fromOutput(output) { + return new this().fromOutput(output); + } -/** - * Inject properties from TX. - * @param {TX} tx - * @param {Number} index - */ + /** + * Inject properties from TX. + * @param {TX} tx + * @param {Number} index + */ -CoinEntry.prototype.fromCoin = function fromCoin(coin) { - this.version = coin.version; - this.height = coin.height; - this.coinbase = coin.coinbase; - this.output.script = coin.script; - this.output.value = coin.value; - return this; -}; + fromCoin(coin) { + this.version = coin.version; + this.height = coin.height; + this.coinbase = coin.coinbase; + this.output.script = coin.script; + this.output.value = coin.value; + return this; + } -/** - * Instantiate a coin from a TX - * @param {TX} tx - * @param {Number} index - Output index. - * @returns {CoinEntry} - */ + /** + * Instantiate a coin from a TX + * @param {TX} tx + * @param {Number} index - Output index. + * @returns {CoinEntry} + */ -CoinEntry.fromCoin = function fromCoin(coin) { - return new CoinEntry().fromCoin(coin); -}; + static fromCoin(coin) { + return new this().fromCoin(coin); + } -/** - * Inject properties from TX. - * @param {TX} tx - * @param {Number} index - */ + /** + * Inject properties from TX. + * @param {TX} tx + * @param {Number} index + */ -CoinEntry.prototype.fromTX = function fromTX(tx, index, height) { - assert(typeof index === 'number'); - assert(typeof height === 'number'); - assert(index >= 0 && index < tx.outputs.length); - this.version = tx.version; - this.height = height; - this.coinbase = tx.isCoinbase(); - this.output = tx.outputs[index]; - return this; -}; + fromTX(tx, index, height) { + assert(typeof index === 'number'); + assert(typeof height === 'number'); + assert(index >= 0 && index < tx.outputs.length); + this.version = tx.version; + this.height = height; + this.coinbase = tx.isCoinbase(); + this.output = tx.outputs[index]; + return this; + } -/** - * Instantiate a coin from a TX - * @param {TX} tx - * @param {Number} index - Output index. - * @returns {CoinEntry} - */ + /** + * Instantiate a coin from a TX + * @param {TX} tx + * @param {Number} index - Output index. + * @returns {CoinEntry} + */ -CoinEntry.fromTX = function fromTX(tx, index, height) { - return new CoinEntry().fromTX(tx, index, height); -}; + static fromTX(tx, index, height) { + return new this().fromTX(tx, index, height); + } -/** - * Calculate size of coin. - * @returns {Number} - */ + /** + * Calculate size of coin. + * @returns {Number} + */ -CoinEntry.prototype.getSize = function getSize() { - if (this.raw) - return this.raw.length; + getSize() { + if (this.raw) + return this.raw.length; - let size = 0; - size += encoding.sizeVarint(this.version); - size += 4; - size += compress.size(this.output); + let size = 0; + size += encoding.sizeVarint(this.version); + size += 4; + size += compress.size(this.output); - return size; -}; + return size; + } -/** - * Write the coin to a buffer writer. - * @param {BufferWriter} bw - */ + /** + * Write the coin to a buffer writer. + * @param {BufferWriter} bw + */ + + toWriter(bw) { + if (this.raw) { + bw.writeBytes(this.raw); + return bw; + } + + let height = this.height; + let field = 0; + + if (this.coinbase) + field |= 1; + + if (height === -1) + height = MAX_HEIGHT; + + field |= height << NUM_FLAGS; + + bw.writeVarint(this.version); + bw.writeU32(field); + compress.pack(this.output, bw); -CoinEntry.prototype.toWriter = function toWriter(bw) { - if (this.raw) { - bw.writeBytes(this.raw); return bw; } - let height = this.height; - let field = 0; + /** + * Serialize the coin. + * @returns {Buffer} + */ - if (this.coinbase) - field |= 1; + toRaw() { + if (this.raw) + return this.raw; - if (height === -1) - height = MAX_HEIGHT; + const size = this.getSize(); + const bw = new StaticWriter(size); - field |= height << NUM_FLAGS; + this.toWriter(bw); - bw.writeVarint(this.version); - bw.writeU32(field); - compress.pack(this.output, bw); + this.raw = bw.render(); - return bw; -}; - -/** - * Serialize the coin. - * @returns {Buffer} - */ - -CoinEntry.prototype.toRaw = function toRaw() { - if (this.raw) return this.raw; + } - const size = this.getSize(); - const bw = new StaticWriter(size); + /** + * Inject properties from serialized buffer writer. + * @private + * @param {BufferReader} br + */ - this.toWriter(bw); + fromReader(br) { + const version = br.readVarint(); + const field = br.readU32(); - this.raw = bw.render(); + let height = field >>> NUM_FLAGS; - return this.raw; -}; + if (height === MAX_HEIGHT) + height = -1; -/** - * Inject properties from serialized buffer writer. - * @private - * @param {BufferReader} br - */ + this.version = version; + this.coinbase = (field & 1) !== 0; + this.height = height; -CoinEntry.prototype.fromReader = function fromReader(br) { - const version = br.readVarint(); - const field = br.readU32(); + compress.unpack(this.output, br); - let height = field >>> NUM_FLAGS; + return this; + } - if (height === MAX_HEIGHT) - height = -1; + /** + * Instantiate a coin from a serialized Buffer. + * @param {Buffer} data + * @returns {CoinEntry} + */ - this.version = version; - this.coinbase = (field & 1) !== 0; - this.height = height; + static fromReader(data) { + return new this().fromReader(data); + } - compress.unpack(this.output, br); + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ - return this; -}; + fromRaw(data) { + this.fromReader(new BufferReader(data)); + this.raw = data; + return this; + } -/** - * Instantiate a coin from a serialized Buffer. - * @param {Buffer} data - * @returns {CoinEntry} - */ + /** + * Instantiate a coin from a serialized Buffer. + * @param {Buffer} data + * @returns {CoinEntry} + */ -CoinEntry.fromReader = function fromReader(data) { - return new CoinEntry().fromReader(data); -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -CoinEntry.prototype.fromRaw = function fromRaw(data) { - this.fromReader(new BufferReader(data)); - this.raw = data; - return this; -}; - -/** - * Instantiate a coin from a serialized Buffer. - * @param {Buffer} data - * @returns {CoinEntry} - */ - -CoinEntry.fromRaw = function fromRaw(data) { - return new CoinEntry().fromRaw(data); -}; + static fromRaw(data) { + return new this().fromRaw(data); + } +} /* * Expose diff --git a/lib/coins/coins.js b/lib/coins/coins.js index 1eb2affc..1671efc2 100644 --- a/lib/coins/coins.js +++ b/lib/coins/coins.js @@ -10,209 +10,213 @@ const assert = require('assert'); const CoinEntry = require('./coinentry'); /** + * Coins * Represents the outputs for a single transaction. * @alias module:coins.Coins - * @constructor * @property {Map[]} outputs - Coins. */ -function Coins() { - if (!(this instanceof Coins)) - return new Coins(); +class Coins { + /** + * Create coins. + * @constructor + */ - this.outputs = new Map(); -} - -/** - * Add a single entry to the collection. - * @param {Number} index - * @param {CoinEntry} coin - * @returns {CoinEntry} - */ - -Coins.prototype.add = function add(index, coin) { - assert((index >>> 0) === index); - assert(coin); - this.outputs.set(index, coin); - return coin; -}; - -/** - * Add a single output to the collection. - * @param {Number} index - * @param {Output} output - * @returns {CoinEntry} - */ - -Coins.prototype.addOutput = function addOutput(index, output) { - return this.add(index, CoinEntry.fromOutput(output)); -}; - -/** - * Add an output to the collection by output index. - * @param {TX} tx - * @param {Number} index - * @param {Number} height - * @returns {CoinEntry} - */ - -Coins.prototype.addIndex = function addIndex(tx, index, height) { - return this.add(index, CoinEntry.fromTX(tx, index, height)); -}; - -/** - * Add a single coin to the collection. - * @param {Coin} coin - * @returns {CoinEntry} - */ - -Coins.prototype.addCoin = function addCoin(coin) { - return this.add(coin.index, CoinEntry.fromCoin(coin)); -}; - -/** - * Test whether the collection has a coin. - * @param {Number} index - * @returns {Boolean} - */ - -Coins.prototype.has = function has(index) { - return this.outputs.has(index); -}; - -/** - * Test whether the collection has an unspent coin. - * @param {Number} index - * @returns {Boolean} - */ - -Coins.prototype.isUnspent = function isUnspent(index) { - const coin = this.outputs.get(index); - - if (!coin || coin.spent) - return false; - - return true; -}; - -/** - * Get a coin entry. - * @param {Number} index - * @returns {CoinEntry|null} - */ - -Coins.prototype.get = function get(index) { - return this.outputs.get(index) || null; -}; - -/** - * Get an output. - * @param {Number} index - * @returns {Output|null} - */ - -Coins.prototype.getOutput = function getOutput(index) { - const coin = this.outputs.get(index); - - if (!coin) - return null; - - return coin.output; -}; - -/** - * Get a coin. - * @param {Outpoint} prevout - * @returns {Coin|null} - */ - -Coins.prototype.getCoin = function getCoin(prevout) { - const coin = this.outputs.get(prevout.index); - - if (!coin) - return null; - - return coin.toCoin(prevout); -}; - -/** - * Spend a coin entry and return it. - * @param {Number} index - * @returns {CoinEntry|null} - */ - -Coins.prototype.spend = function spend(index) { - const coin = this.get(index); - - if (!coin || coin.spent) - return null; - - coin.spent = true; - - return coin; -}; - -/** - * Remove a coin entry and return it. - * @param {Number} index - * @returns {CoinEntry|null} - */ - -Coins.prototype.remove = function remove(index) { - const coin = this.get(index); - - if (!coin) - return null; - - this.outputs.delete(index); - - return coin; -}; - -/** - * Test whether the coins are fully spent. - * @returns {Boolean} - */ - -Coins.prototype.isEmpty = function isEmpty() { - return this.outputs.size === 0; -}; - -/** - * Inject properties from tx. - * @private - * @param {TX} tx - * @param {Number} height - * @returns {Coins} - */ - -Coins.prototype.fromTX = function fromTX(tx, height) { - assert(typeof height === 'number'); - - for (let i = 0; i < tx.outputs.length; i++) { - const output = tx.outputs[i]; - - if (output.script.isUnspendable()) - continue; - - const entry = CoinEntry.fromTX(tx, i, height); - - this.outputs.set(i, entry); + constructor() { + this.outputs = new Map(); } - return this; -}; + /** + * Add a single entry to the collection. + * @param {Number} index + * @param {CoinEntry} coin + * @returns {CoinEntry} + */ -/** - * Instantiate a coins object from a transaction. - * @param {TX} tx - * @param {Number} height - * @returns {Coins} - */ + add(index, coin) { + assert((index >>> 0) === index); + assert(coin); + this.outputs.set(index, coin); + return coin; + } -Coins.fromTX = function fromTX(tx, height) { - return new Coins().fromTX(tx, height); -}; + /** + * Add a single output to the collection. + * @param {Number} index + * @param {Output} output + * @returns {CoinEntry} + */ + + addOutput(index, output) { + return this.add(index, CoinEntry.fromOutput(output)); + } + + /** + * Add an output to the collection by output index. + * @param {TX} tx + * @param {Number} index + * @param {Number} height + * @returns {CoinEntry} + */ + + addIndex(tx, index, height) { + return this.add(index, CoinEntry.fromTX(tx, index, height)); + } + + /** + * Add a single coin to the collection. + * @param {Coin} coin + * @returns {CoinEntry} + */ + + addCoin(coin) { + return this.add(coin.index, CoinEntry.fromCoin(coin)); + } + + /** + * Test whether the collection has a coin. + * @param {Number} index + * @returns {Boolean} + */ + + has(index) { + return this.outputs.has(index); + } + + /** + * Test whether the collection has an unspent coin. + * @param {Number} index + * @returns {Boolean} + */ + + isUnspent(index) { + const coin = this.outputs.get(index); + + if (!coin || coin.spent) + return false; + + return true; + } + + /** + * Get a coin entry. + * @param {Number} index + * @returns {CoinEntry|null} + */ + + get(index) { + return this.outputs.get(index) || null; + } + + /** + * Get an output. + * @param {Number} index + * @returns {Output|null} + */ + + getOutput(index) { + const coin = this.outputs.get(index); + + if (!coin) + return null; + + return coin.output; + } + + /** + * Get a coin. + * @param {Outpoint} prevout + * @returns {Coin|null} + */ + + getCoin(prevout) { + const coin = this.outputs.get(prevout.index); + + if (!coin) + return null; + + return coin.toCoin(prevout); + } + + /** + * Spend a coin entry and return it. + * @param {Number} index + * @returns {CoinEntry|null} + */ + + spend(index) { + const coin = this.get(index); + + if (!coin || coin.spent) + return null; + + coin.spent = true; + + return coin; + } + + /** + * Remove a coin entry and return it. + * @param {Number} index + * @returns {CoinEntry|null} + */ + + remove(index) { + const coin = this.get(index); + + if (!coin) + return null; + + this.outputs.delete(index); + + return coin; + } + + /** + * Test whether the coins are fully spent. + * @returns {Boolean} + */ + + isEmpty() { + return this.outputs.size === 0; + } + + /** + * Inject properties from tx. + * @private + * @param {TX} tx + * @param {Number} height + * @returns {Coins} + */ + + fromTX(tx, height) { + assert(typeof height === 'number'); + + for (let i = 0; i < tx.outputs.length; i++) { + const output = tx.outputs[i]; + + if (output.script.isUnspendable()) + continue; + + const entry = CoinEntry.fromTX(tx, i, height); + + this.outputs.set(i, entry); + } + + return this; + } + + /** + * Instantiate a coins object from a transaction. + * @param {TX} tx + * @param {Number} height + * @returns {Coins} + */ + + static fromTX(tx, height) { + return new this().fromTX(tx, height); + } +} /* * Expose diff --git a/lib/coins/coinview.js b/lib/coins/coinview.js index 38898585..83a8cf1a 100644 --- a/lib/coins/coinview.js +++ b/lib/coins/coinview.js @@ -11,537 +11,541 @@ const UndoCoins = require('./undocoins'); const CoinEntry = require('./coinentry'); /** + * Coin View * Represents a coin viewpoint: * a snapshot of {@link Coins} objects. * @alias module:coins.CoinView - * @constructor * @property {Object} map * @property {UndoCoins} undo */ -function CoinView() { - if (!(this instanceof CoinView)) - return new CoinView(); +class CoinView { + /** + * Create a coin view. + * @constructor + */ - this.map = new Map(); - this.undo = new UndoCoins(); -} + constructor() { + this.map = new Map(); + this.undo = new UndoCoins(); + } -/** - * Get coins. - * @param {Hash} hash - * @returns {Coins} coins - */ + /** + * Get coins. + * @param {Hash} hash + * @returns {Coins} coins + */ -CoinView.prototype.get = function get(hash) { - return this.map.get(hash); -}; + get(hash) { + return this.map.get(hash); + } -/** - * Test whether the view has an entry. - * @param {Hash} hash - * @returns {Boolean} - */ + /** + * Test whether the view has an entry. + * @param {Hash} hash + * @returns {Boolean} + */ -CoinView.prototype.has = function has(hash) { - return this.map.has(hash); -}; + has(hash) { + return this.map.has(hash); + } -/** - * Add coins to the collection. - * @param {Hash} hash - * @param {Coins} coins - * @returns {Coins} - */ + /** + * Add coins to the collection. + * @param {Hash} hash + * @param {Coins} coins + * @returns {Coins} + */ -CoinView.prototype.add = function add(hash, coins) { - this.map.set(hash, coins); - return coins; -}; - -/** - * Ensure existence of coins object in the collection. - * @param {Hash} hash - * @returns {Coins} - */ - -CoinView.prototype.ensure = function ensure(hash) { - const coins = this.map.get(hash); - - if (coins) + add(hash, coins) { + this.map.set(hash, coins); return coins; - - return this.add(hash, new Coins()); -}; - -/** - * Remove coins from the collection. - * @param {Coins} coins - * @returns {Coins|null} - */ - -CoinView.prototype.remove = function remove(hash) { - const coins = this.map.get(hash); - - if (!coins) - return null; - - this.map.delete(hash); - - return coins; -}; - -/** - * Add a tx to the collection. - * @param {TX} tx - * @param {Number} height - * @returns {Coins} - */ - -CoinView.prototype.addTX = function addTX(tx, height) { - const hash = tx.hash('hex'); - const coins = Coins.fromTX(tx, height); - return this.add(hash, coins); -}; - -/** - * Remove a tx from the collection. - * @param {TX} tx - * @param {Number} height - * @returns {Coins} - */ - -CoinView.prototype.removeTX = function removeTX(tx, height) { - const hash = tx.hash('hex'); - const coins = Coins.fromTX(tx, height); - - for (const coin of coins.outputs.values()) - coin.spent = true; - - return this.add(hash, coins); -}; - -/** - * Add an entry to the collection. - * @param {Outpoint} prevout - * @param {CoinEntry} coin - * @returns {CoinEntry|null} - */ - -CoinView.prototype.addEntry = function addEntry(prevout, coin) { - const {hash, index} = prevout; - const coins = this.ensure(hash); - return coins.add(index, coin); -}; - -/** - * Add a coin to the collection. - * @param {Coin} coin - * @returns {CoinEntry|null} - */ - -CoinView.prototype.addCoin = function addCoin(coin) { - const coins = this.ensure(coin.hash); - return coins.addCoin(coin); -}; - -/** - * Add an output to the collection. - * @param {Outpoint} prevout - * @param {Output} output - * @returns {CoinEntry|null} - */ - -CoinView.prototype.addOutput = function addOutput(prevout, output) { - const {hash, index} = prevout; - const coins = this.ensure(hash); - return coins.addOutput(index, output); -}; - -/** - * Add an output to the collection by output index. - * @param {TX} tx - * @param {Number} index - * @param {Number} height - * @returns {CoinEntry|null} - */ - -CoinView.prototype.addIndex = function addIndex(tx, index, height) { - const hash = tx.hash('hex'); - const coins = this.ensure(hash); - return coins.addIndex(tx, index, height); -}; - -/** - * Spend an output. - * @param {Outpoint} prevout - * @returns {CoinEntry|null} - */ - -CoinView.prototype.spendEntry = function spendEntry(prevout) { - const {hash, index} = prevout; - const coins = this.get(hash); - - if (!coins) - return null; - - const coin = coins.spend(index); - - if (!coin) - return null; - - this.undo.push(coin); - - return coin; -}; - -/** - * Remove an output. - * @param {Outpoint} prevout - * @returns {CoinEntry|null} - */ - -CoinView.prototype.removeEntry = function removeEntry(prevout) { - const {hash, index} = prevout; - const coins = this.get(hash); - - if (!coins) - return null; - - return coins.remove(index); -}; - -/** - * Test whether the view has an entry by prevout. - * @param {Outpoint} prevout - * @returns {Boolean} - */ - -CoinView.prototype.hasEntry = function hasEntry(prevout) { - const {hash, index} = prevout; - const coins = this.get(hash); - - if (!coins) - return false; - - return coins.has(index); -}; - -/** - * Get a single entry by prevout. - * @param {Outpoint} prevout - * @returns {CoinEntry|null} - */ - -CoinView.prototype.getEntry = function getEntry(prevout) { - const {hash, index} = prevout; - const coins = this.get(hash); - - if (!coins) - return null; - - return coins.get(index); -}; - -/** - * Test whether an entry has been spent by prevout. - * @param {Outpoint} prevout - * @returns {Boolean} - */ - -CoinView.prototype.isUnspent = function isUnspent(prevout) { - const {hash, index} = prevout; - const coins = this.get(hash); - - if (!coins) - return false; - - return coins.isUnspent(index); -}; - -/** - * Get a single coin by prevout. - * @param {Outpoint} prevout - * @returns {Coin|null} - */ - -CoinView.prototype.getCoin = function getCoin(prevout) { - const coins = this.get(prevout.hash); - - if (!coins) - return null; - - return coins.getCoin(prevout); -}; - -/** - * Get a single output by prevout. - * @param {Outpoint} prevout - * @returns {Output|null} - */ - -CoinView.prototype.getOutput = function getOutput(prevout) { - const {hash, index} = prevout; - const coins = this.get(hash); - - if (!coins) - return null; - - return coins.getOutput(index); -}; - -/** - * Get coins height by prevout. - * @param {Outpoint} prevout - * @returns {Number} - */ - -CoinView.prototype.getHeight = function getHeight(prevout) { - const coin = this.getEntry(prevout); - - if (!coin) - return -1; - - return coin.height; -}; - -/** - * Get coins coinbase flag by prevout. - * @param {Outpoint} prevout - * @returns {Boolean} - */ - -CoinView.prototype.isCoinbase = function isCoinbase(prevout) { - const coin = this.getEntry(prevout); - - if (!coin) - return false; - - return coin.coinbase; -}; - -/** - * Test whether the view has an entry by input. - * @param {Input} input - * @returns {Boolean} - */ - -CoinView.prototype.hasEntryFor = function hasEntryFor(input) { - return this.hasEntry(input.prevout); -}; - -/** - * Get a single entry by input. - * @param {Input} input - * @returns {CoinEntry|null} - */ - -CoinView.prototype.getEntryFor = function getEntryFor(input) { - return this.getEntry(input.prevout); -}; - -/** - * Test whether an entry has been spent by input. - * @param {Input} input - * @returns {Boolean} - */ - -CoinView.prototype.isUnspentFor = function isUnspentFor(input) { - return this.isUnspent(input.prevout); -}; - -/** - * Get a single coin by input. - * @param {Input} input - * @returns {Coin|null} - */ - -CoinView.prototype.getCoinFor = function getCoinFor(input) { - return this.getCoin(input.prevout); -}; - -/** - * Get a single output by input. - * @param {Input} input - * @returns {Output|null} - */ - -CoinView.prototype.getOutputFor = function getOutputFor(input) { - return this.getOutput(input.prevout); -}; - -/** - * Get coins height by input. - * @param {Input} input - * @returns {Number} - */ - -CoinView.prototype.getHeightFor = function getHeightFor(input) { - return this.getHeight(input.prevout); -}; - -/** - * Get coins coinbase flag by input. - * @param {Input} input - * @returns {Boolean} - */ - -CoinView.prototype.isCoinbaseFor = function isCoinbaseFor(input) { - return this.isCoinbase(input.prevout); -}; - -/** - * Retrieve coins from database. - * @method - * @param {ChainDB} db - * @param {Outpoint} prevout - * @returns {Promise} - Returns {@link CoinEntry}. - */ - -CoinView.prototype.readCoin = async function readCoin(db, prevout) { - const cache = this.getEntry(prevout); - - if (cache) - return cache; - - const coin = await db.readCoin(prevout); - - if (!coin) - return null; - - return this.addEntry(prevout, coin); -}; - -/** - * Read all input coins into unspent map. - * @method - * @param {ChainDB} db - * @param {TX} tx - * @returns {Promise} - Returns {Boolean}. - */ - -CoinView.prototype.readInputs = async function readInputs(db, tx) { - let found = true; - - for (const {prevout} of tx.inputs) { - if (!await this.readCoin(db, prevout)) - found = false; } - return found; -}; + /** + * Ensure existence of coins object in the collection. + * @param {Hash} hash + * @returns {Coins} + */ -/** - * Spend coins for transaction. - * @method - * @param {ChainDB} db - * @param {TX} tx - * @returns {Promise} - Returns {Boolean}. - */ + ensure(hash) { + const coins = this.map.get(hash); -CoinView.prototype.spendInputs = async function spendInputs(db, tx) { - let i = 0; + if (coins) + return coins; - while (i < tx.inputs.length) { - const len = Math.min(i + 4, tx.inputs.length); - const jobs = []; + return this.add(hash, new Coins()); + } - for (; i < len; i++) { - const {prevout} = tx.inputs[i]; - jobs.push(this.readCoin(db, prevout)); - } + /** + * Remove coins from the collection. + * @param {Coins} coins + * @returns {Coins|null} + */ - const coins = await Promise.all(jobs); + remove(hash) { + const coins = this.map.get(hash); - for (const coin of coins) { - if (!coin || coin.spent) - return false; + if (!coins) + return null; + this.map.delete(hash); + + return coins; + } + + /** + * Add a tx to the collection. + * @param {TX} tx + * @param {Number} height + * @returns {Coins} + */ + + addTX(tx, height) { + const hash = tx.hash('hex'); + const coins = Coins.fromTX(tx, height); + return this.add(hash, coins); + } + + /** + * Remove a tx from the collection. + * @param {TX} tx + * @param {Number} height + * @returns {Coins} + */ + + removeTX(tx, height) { + const hash = tx.hash('hex'); + const coins = Coins.fromTX(tx, height); + + for (const coin of coins.outputs.values()) coin.spent = true; - this.undo.push(coin); - } + + return this.add(hash, coins); } - return true; -}; + /** + * Add an entry to the collection. + * @param {Outpoint} prevout + * @param {CoinEntry} coin + * @returns {CoinEntry|null} + */ -/** - * Calculate serialization size. - * @returns {Number} - */ + addEntry(prevout, coin) { + const {hash, index} = prevout; + const coins = this.ensure(hash); + return coins.add(index, coin); + } -CoinView.prototype.getSize = function getSize(tx) { - let size = 0; + /** + * Add a coin to the collection. + * @param {Coin} coin + * @returns {CoinEntry|null} + */ - size += tx.inputs.length; + addCoin(coin) { + const coins = this.ensure(coin.hash); + return coins.addCoin(coin); + } - for (const {prevout} of tx.inputs) { + /** + * Add an output to the collection. + * @param {Outpoint} prevout + * @param {Output} output + * @returns {CoinEntry|null} + */ + + addOutput(prevout, output) { + const {hash, index} = prevout; + const coins = this.ensure(hash); + return coins.addOutput(index, output); + } + + /** + * Add an output to the collection by output index. + * @param {TX} tx + * @param {Number} index + * @param {Number} height + * @returns {CoinEntry|null} + */ + + addIndex(tx, index, height) { + const hash = tx.hash('hex'); + const coins = this.ensure(hash); + return coins.addIndex(tx, index, height); + } + + /** + * Spend an output. + * @param {Outpoint} prevout + * @returns {CoinEntry|null} + */ + + spendEntry(prevout) { + const {hash, index} = prevout; + const coins = this.get(hash); + + if (!coins) + return null; + + const coin = coins.spend(index); + + if (!coin) + return null; + + this.undo.push(coin); + + return coin; + } + + /** + * Remove an output. + * @param {Outpoint} prevout + * @returns {CoinEntry|null} + */ + + removeEntry(prevout) { + const {hash, index} = prevout; + const coins = this.get(hash); + + if (!coins) + return null; + + return coins.remove(index); + } + + /** + * Test whether the view has an entry by prevout. + * @param {Outpoint} prevout + * @returns {Boolean} + */ + + hasEntry(prevout) { + const {hash, index} = prevout; + const coins = this.get(hash); + + if (!coins) + return false; + + return coins.has(index); + } + + /** + * Get a single entry by prevout. + * @param {Outpoint} prevout + * @returns {CoinEntry|null} + */ + + getEntry(prevout) { + const {hash, index} = prevout; + const coins = this.get(hash); + + if (!coins) + return null; + + return coins.get(index); + } + + /** + * Test whether an entry has been spent by prevout. + * @param {Outpoint} prevout + * @returns {Boolean} + */ + + isUnspent(prevout) { + const {hash, index} = prevout; + const coins = this.get(hash); + + if (!coins) + return false; + + return coins.isUnspent(index); + } + + /** + * Get a single coin by prevout. + * @param {Outpoint} prevout + * @returns {Coin|null} + */ + + getCoin(prevout) { + const coins = this.get(prevout.hash); + + if (!coins) + return null; + + return coins.getCoin(prevout); + } + + /** + * Get a single output by prevout. + * @param {Outpoint} prevout + * @returns {Output|null} + */ + + getOutput(prevout) { + const {hash, index} = prevout; + const coins = this.get(hash); + + if (!coins) + return null; + + return coins.getOutput(index); + } + + /** + * Get coins height by prevout. + * @param {Outpoint} prevout + * @returns {Number} + */ + + getHeight(prevout) { const coin = this.getEntry(prevout); if (!coin) - continue; + return -1; - size += coin.getSize(); + return coin.height; } - return size; -}; + /** + * Get coins coinbase flag by prevout. + * @param {Outpoint} prevout + * @returns {Boolean} + */ -/** - * Write coin data to buffer writer - * as it pertains to a transaction. - * @param {BufferWriter} bw - * @param {TX} tx - */ - -CoinView.prototype.toWriter = function toWriter(bw, tx) { - for (const {prevout} of tx.inputs) { + isCoinbase(prevout) { const coin = this.getEntry(prevout); - if (!coin) { - bw.writeU8(0); - continue; + if (!coin) + return false; + + return coin.coinbase; + } + + /** + * Test whether the view has an entry by input. + * @param {Input} input + * @returns {Boolean} + */ + + hasEntryFor(input) { + return this.hasEntry(input.prevout); + } + + /** + * Get a single entry by input. + * @param {Input} input + * @returns {CoinEntry|null} + */ + + getEntryFor(input) { + return this.getEntry(input.prevout); + } + + /** + * Test whether an entry has been spent by input. + * @param {Input} input + * @returns {Boolean} + */ + + isUnspentFor(input) { + return this.isUnspent(input.prevout); + } + + /** + * Get a single coin by input. + * @param {Input} input + * @returns {Coin|null} + */ + + getCoinFor(input) { + return this.getCoin(input.prevout); + } + + /** + * Get a single output by input. + * @param {Input} input + * @returns {Output|null} + */ + + getOutputFor(input) { + return this.getOutput(input.prevout); + } + + /** + * Get coins height by input. + * @param {Input} input + * @returns {Number} + */ + + getHeightFor(input) { + return this.getHeight(input.prevout); + } + + /** + * Get coins coinbase flag by input. + * @param {Input} input + * @returns {Boolean} + */ + + isCoinbaseFor(input) { + return this.isCoinbase(input.prevout); + } + + /** + * Retrieve coins from database. + * @method + * @param {ChainDB} db + * @param {Outpoint} prevout + * @returns {Promise} - Returns {@link CoinEntry}. + */ + + async readCoin(db, prevout) { + const cache = this.getEntry(prevout); + + if (cache) + return cache; + + const coin = await db.readCoin(prevout); + + if (!coin) + return null; + + return this.addEntry(prevout, coin); + } + + /** + * Read all input coins into unspent map. + * @method + * @param {ChainDB} db + * @param {TX} tx + * @returns {Promise} - Returns {Boolean}. + */ + + async readInputs(db, tx) { + let found = true; + + for (const {prevout} of tx.inputs) { + if (!await this.readCoin(db, prevout)) + found = false; } - bw.writeU8(1); - coin.toWriter(bw); + return found; } - return bw; -}; + /** + * Spend coins for transaction. + * @method + * @param {ChainDB} db + * @param {TX} tx + * @returns {Promise} - Returns {Boolean}. + */ -/** - * Read serialized view data from a buffer - * reader as it pertains to a transaction. - * @private - * @param {BufferReader} br - * @param {TX} tx - */ + async spendInputs(db, tx) { + let i = 0; -CoinView.prototype.fromReader = function fromReader(br, tx) { - for (const {prevout} of tx.inputs) { - if (br.readU8() === 0) - continue; + while (i < tx.inputs.length) { + const len = Math.min(i + 4, tx.inputs.length); + const jobs = []; - const coin = CoinEntry.fromReader(br); + for (; i < len; i++) { + const {prevout} = tx.inputs[i]; + jobs.push(this.readCoin(db, prevout)); + } - this.addEntry(prevout, coin); + const coins = await Promise.all(jobs); + + for (const coin of coins) { + if (!coin || coin.spent) + return false; + + coin.spent = true; + this.undo.push(coin); + } + } + + return true; } - return this; -}; + /** + * Calculate serialization size. + * @returns {Number} + */ -/** - * Read serialized view data from a buffer - * reader as it pertains to a transaction. - * @param {BufferReader} br - * @param {TX} tx - * @returns {CoinView} - */ + getSize(tx) { + let size = 0; -CoinView.fromReader = function fromReader(br, tx) { - return new CoinView().fromReader(br, tx); -}; + size += tx.inputs.length; + + for (const {prevout} of tx.inputs) { + const coin = this.getEntry(prevout); + + if (!coin) + continue; + + size += coin.getSize(); + } + + return size; + } + + /** + * Write coin data to buffer writer + * as it pertains to a transaction. + * @param {BufferWriter} bw + * @param {TX} tx + */ + + toWriter(bw, tx) { + for (const {prevout} of tx.inputs) { + const coin = this.getEntry(prevout); + + if (!coin) { + bw.writeU8(0); + continue; + } + + bw.writeU8(1); + coin.toWriter(bw); + } + + return bw; + } + + /** + * Read serialized view data from a buffer + * reader as it pertains to a transaction. + * @private + * @param {BufferReader} br + * @param {TX} tx + */ + + fromReader(br, tx) { + for (const {prevout} of tx.inputs) { + if (br.readU8() === 0) + continue; + + const coin = CoinEntry.fromReader(br); + + this.addEntry(prevout, coin); + } + + return this; + } + + /** + * Read serialized view data from a buffer + * reader as it pertains to a transaction. + * @param {BufferReader} br + * @param {TX} tx + * @returns {CoinView} + */ + + static fromReader(br, tx) { + return new this().fromReader(br, tx); + } +} /* * Expose diff --git a/lib/coins/undocoins.js b/lib/coins/undocoins.js index 8b862ee5..bf55ea3b 100644 --- a/lib/coins/undocoins.js +++ b/lib/coins/undocoins.js @@ -12,127 +12,130 @@ const StaticWriter = require('bufio/lib/staticwriter'); const CoinEntry = require('../coins/coinentry'); /** - * UndoCoins + * Undo Coins * Coins need to be resurrected from somewhere * during a reorg. The undo coins store all * spent coins in a single record per block * (in a compressed format). * @alias module:coins.UndoCoins - * @constructor * @property {UndoCoin[]} items */ -function UndoCoins() { - if (!(this instanceof UndoCoins)) - return new UndoCoins(); +class UndoCoins { + /** + * Create undo coins. + * @constructor + */ - this.items = []; + constructor() { + this.items = []; + } + + /** + * Push coin entry onto undo coin array. + * @param {CoinEntry} + * @returns {Number} + */ + + push(coin) { + return this.items.push(coin); + } + + /** + * Calculate undo coins size. + * @returns {Number} + */ + + getSize() { + let size = 0; + + size += 4; + + for (const coin of this.items) + size += coin.getSize(); + + return size; + } + + /** + * Serialize all undo coins. + * @returns {Buffer} + */ + + toRaw() { + const size = this.getSize(); + const bw = new StaticWriter(size); + + bw.writeU32(this.items.length); + + for (const coin of this.items) + coin.toWriter(bw); + + return bw.render(); + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @returns {UndoCoins} + */ + + fromRaw(data) { + const br = new BufferReader(data); + const count = br.readU32(); + + for (let i = 0; i < count; i++) + this.items.push(CoinEntry.fromReader(br)); + + return this; + } + + /** + * Instantiate undo coins from serialized data. + * @param {Buffer} data + * @returns {UndoCoins} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } + + /** + * Test whether the undo coins have any members. + * @returns {Boolean} + */ + + isEmpty() { + return this.items.length === 0; + } + + /** + * Render the undo coins. + * @returns {Buffer} + */ + + commit() { + const raw = this.toRaw(); + this.items.length = 0; + return raw; + } + + /** + * Re-apply undo coins to a view, effectively unspending them. + * @param {CoinView} view + * @param {Outpoint} prevout + */ + + apply(view, prevout) { + const undo = this.items.pop(); + + assert(undo); + + view.addEntry(prevout, undo); + } } -/** - * Push coin entry onto undo coin array. - * @param {CoinEntry} - * @returns {Number} - */ - -UndoCoins.prototype.push = function push(coin) { - return this.items.push(coin); -}; - -/** - * Calculate undo coins size. - * @returns {Number} - */ - -UndoCoins.prototype.getSize = function getSize() { - let size = 0; - - size += 4; - - for (const coin of this.items) - size += coin.getSize(); - - return size; -}; - -/** - * Serialize all undo coins. - * @returns {Buffer} - */ - -UndoCoins.prototype.toRaw = function toRaw() { - const size = this.getSize(); - const bw = new StaticWriter(size); - - bw.writeU32(this.items.length); - - for (const coin of this.items) - coin.toWriter(bw); - - return bw.render(); -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - * @returns {UndoCoins} - */ - -UndoCoins.prototype.fromRaw = function fromRaw(data) { - const br = new BufferReader(data); - const count = br.readU32(); - - for (let i = 0; i < count; i++) - this.items.push(CoinEntry.fromReader(br)); - - return this; -}; - -/** - * Instantiate undo coins from serialized data. - * @param {Buffer} data - * @returns {UndoCoins} - */ - -UndoCoins.fromRaw = function fromRaw(data) { - return new UndoCoins().fromRaw(data); -}; - -/** - * Test whether the undo coins have any members. - * @returns {Boolean} - */ - -UndoCoins.prototype.isEmpty = function isEmpty() { - return this.items.length === 0; -}; - -/** - * Render the undo coins. - * @returns {Buffer} - */ - -UndoCoins.prototype.commit = function commit() { - const raw = this.toRaw(); - this.items.length = 0; - return raw; -}; - -/** - * Re-apply undo coins to a view, effectively unspending them. - * @param {CoinView} view - * @param {Outpoint} prevout - */ - -UndoCoins.prototype.apply = function apply(view, prevout) { - const undo = this.items.pop(); - - assert(undo); - - view.addEntry(prevout, undo); -}; - /* * Expose */ diff --git a/lib/mining/miner.js b/lib/mining/miner.js index 80f0619b..7b6a7f84 100644 --- a/lib/mining/miner.js +++ b/lib/mining/miner.js @@ -221,7 +221,7 @@ Miner.prototype.mineBlock = function mineBlock(tip, address) { */ Miner.prototype.addAddress = function addAddress(address) { - this.addresses.push(Address(address)); + this.addresses.push(new Address(address)); }; /** diff --git a/lib/mining/template.js b/lib/mining/template.js index 5eb1dcfd..7a7a4bd2 100644 --- a/lib/mining/template.js +++ b/lib/mining/template.js @@ -512,7 +512,7 @@ BlockTemplate.prototype.getDifficulty = function getDifficulty() { */ BlockTemplate.prototype.setAddress = function setAddress(address) { - this.address = Address(address); + this.address = new Address(address); this.refresh(); }; diff --git a/lib/net/bip152.js b/lib/net/bip152.js index 79088f2d..d7ecda65 100644 --- a/lib/net/bip152.js +++ b/lib/net/bip152.js @@ -24,11 +24,10 @@ const Block = require('../primitives/block'); const common = require('./common'); /** + * Compact Block * Represents a compact block (bip152): `cmpctblock` packet. * @see https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki - * @constructor * @extends AbstractBlock - * @param {Object} options * @property {Buffer|null} keyNonce - Nonce for siphash key. * @property {Number[]} ids - Short IDs. * @property {Object[]} ptx - Prefilled transactions. @@ -38,914 +37,925 @@ const common = require('./common'); * @property {Buffer|null} sipKey - Siphash key. */ -function CompactBlock(options) { - if (!(this instanceof CompactBlock)) - return new CompactBlock(options); +class CompactBlock extends AbstractBlock { + /** + * Create a compact block. + * @constructor + * @param {Object?} options + */ - AbstractBlock.call(this); + constructor(options) { + super(); - this.keyNonce = null; - this.ids = []; - this.ptx = []; + this.keyNonce = null; + this.ids = []; + this.ptx = []; - this.available = []; - this.idMap = new Map(); - this.count = 0; - this.sipKey = null; - this.totalTX = 0; - this.now = 0; + this.available = []; + this.idMap = new Map(); + this.count = 0; + this.sipKey = null; + this.totalTX = 0; + this.now = 0; - if (options) - this.fromOptions(options); -} - -Object.setPrototypeOf(CompactBlock.prototype, AbstractBlock.prototype); - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -CompactBlock.prototype.fromOptions = function fromOptions(options) { - this.parseOptions(options); - - assert(Buffer.isBuffer(options.keyNonce)); - assert(Array.isArray(options.ids)); - assert(Array.isArray(options.ptx)); - - this.keyNonce = options.keyNonce; - this.ids = options.ids; - this.ptx = options.ptx; - - if (options.available) - this.available = options.available; - - if (options.idMap) - this.idMap = options.idMap; - - if (options.count) - this.count = options.count; - - if (options.totalTX != null) - this.totalTX = options.totalTX; - - this.sipKey = this.getKey(); - - return this; -}; - -/** - * Instantiate compact block from options. - * @param {Object} options - * @returns {CompactBlock} - */ - -CompactBlock.fromOptions = function fromOptions(options) { - return new CompactBlock().fromOptions(options); -}; - -/** - * Verify the block. - * @returns {Boolean} - */ - -CompactBlock.prototype.verifyBody = function verifyBody() { - return true; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -CompactBlock.prototype.fromRaw = function fromRaw(data) { - const br = new BufferReader(data); - - this.readHead(br); - - this.keyNonce = br.readBytes(8); - this.sipKey = this.getKey(); - - const idCount = br.readVarint(); - - this.totalTX += idCount; - - for (let i = 0; i < idCount; i++) { - const lo = br.readU32(); - const hi = br.readU16(); - this.ids.push(hi * 0x100000000 + lo); + if (options) + this.fromOptions(options); } - const txCount = br.readVarint(); + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ - this.totalTX += txCount; + fromOptions(options) { + this.parseOptions(options); - for (let i = 0; i < txCount; i++) { - const index = br.readVarint(); + assert(Buffer.isBuffer(options.keyNonce)); + assert(Array.isArray(options.ids)); + assert(Array.isArray(options.ptx)); - assert(index <= 0xffff); - assert(index < this.totalTX); + this.keyNonce = options.keyNonce; + this.ids = options.ids; + this.ptx = options.ptx; - const tx = TX.fromReader(br); + if (options.available) + this.available = options.available; - this.ptx.push([index, tx]); + if (options.idMap) + this.idMap = options.idMap; + + if (options.count) + this.count = options.count; + + if (options.totalTX != null) + this.totalTX = options.totalTX; + + this.sipKey = this.getKey(); + + return this; } - return this; -}; + /** + * Instantiate compact block from options. + * @param {Object} options + * @returns {CompactBlock} + */ -/** - * Instantiate a block from serialized data. - * @param {Buffer} data - * @param {String?} enc - * @returns {CompactBlock} - */ - -CompactBlock.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - return new CompactBlock().fromRaw(data); -}; - -/** - * Serialize compact block with witness data. - * @returns {Buffer} - */ - -CompactBlock.prototype.toRaw = function toRaw() { - return this.frameRaw(true); -}; - -/** - * Serialize compact block without witness data. - * @returns {Buffer} - */ - -CompactBlock.prototype.toNormal = function toNormal() { - return this.frameRaw(false); -}; - -/** - * Write serialized block to a buffer - * writer (includes witness data). - * @param {BufferWriter} bw - */ - -CompactBlock.prototype.toWriter = function toWriter(bw) { - return this.writeRaw(bw, true); -}; - -/** - * Write serialized block to a buffer - * writer (excludes witness data). - * @param {BufferWriter} bw - */ - -CompactBlock.prototype.toNormalWriter = function toNormalWriter(bw) { - return this.writeRaw(bw, false); -}; - -/** - * Serialize compact block. - * @private - * @param {Boolean} witness - * @returns {Buffer} - */ - -CompactBlock.prototype.frameRaw = function frameRaw(witness) { - const size = this.getSize(witness); - return this.writeRaw(new StaticWriter(size), witness).render(); -}; - -/** - * Calculate block serialization size. - * @param {Boolean} witness - * @returns {Number} - */ - -CompactBlock.prototype.getSize = function getSize(witness) { - let size = 0; - - size += 80; - size += 8; - size += encoding.sizeVarint(this.ids.length); - size += this.ids.length * 6; - size += encoding.sizeVarint(this.ptx.length); - - for (const [index, tx] of this.ptx) { - size += encoding.sizeVarint(index); - - if (witness) - size += tx.getSize(); - else - size += tx.getBaseSize(); + static fromOptions(options) { + return new this().fromOptions(options); } - return size; -}; + /** + * Verify the block. + * @returns {Boolean} + */ -/** - * Serialize block to buffer writer. - * @private - * @param {BufferWriter} bw - * @param {Boolean} witness - */ - -CompactBlock.prototype.writeRaw = function writeRaw(bw, witness) { - this.writeHead(bw); - - bw.writeBytes(this.keyNonce); - - bw.writeVarint(this.ids.length); - - for (const id of this.ids) { - const lo = id % 0x100000000; - const hi = (id - lo) / 0x100000000; - assert(hi <= 0xffff); - bw.writeU32(lo); - bw.writeU16(hi); - } - - bw.writeVarint(this.ptx.length); - - for (const [index, tx] of this.ptx) { - bw.writeVarint(index); - - if (witness) - tx.toWriter(bw); - else - tx.toNormalWriter(bw); - } - - return bw; -}; - -/** - * Convert block to a TXRequest - * containing missing indexes. - * @returns {TXRequest} - */ - -CompactBlock.prototype.toRequest = function toRequest() { - return TXRequest.fromCompact(this); -}; - -/** - * Attempt to fill missing transactions from mempool. - * @param {Boolean} witness - * @param {Mempool} mempool - * @returns {Boolean} - */ - -CompactBlock.prototype.fillMempool = function fillMempool(witness, mempool) { - if (this.count === this.totalTX) + verifyBody() { return true; + } - const set = new Set(); + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ - for (const {tx} of mempool.map.values()) { - let hash = tx.hash(); + fromRaw(data) { + const br = new BufferReader(data); - if (witness) - hash = tx.witnessHash(); + this.readHead(br); - const id = this.sid(hash); - const index = this.idMap.get(id); + this.keyNonce = br.readBytes(8); + this.sipKey = this.getKey(); - if (index == null) - continue; + const idCount = br.readVarint(); - if (set.has(index)) { - // Siphash collision, just request it. - this.available[index] = null; - this.count--; - continue; + this.totalTX += idCount; + + for (let i = 0; i < idCount; i++) { + const lo = br.readU32(); + const hi = br.readU16(); + this.ids.push(hi * 0x100000000 + lo); } - this.available[index] = tx; - set.add(index); - this.count++; + const txCount = br.readVarint(); - // We actually may have a siphash collision - // here, but exit early anyway for perf. + this.totalTX += txCount; + + for (let i = 0; i < txCount; i++) { + const index = br.readVarint(); + + assert(index <= 0xffff); + assert(index < this.totalTX); + + const tx = TX.fromReader(br); + + this.ptx.push([index, tx]); + } + + return this; + } + + /** + * Instantiate a block from serialized data. + * @param {Buffer} data + * @param {String?} enc + * @returns {CompactBlock} + */ + + static fromRaw(data, enc) { + if (typeof data === 'string') + data = Buffer.from(data, enc); + return new this().fromRaw(data); + } + + /** + * Serialize compact block with witness data. + * @returns {Buffer} + */ + + toRaw() { + return this.frameRaw(true); + } + + /** + * Serialize compact block without witness data. + * @returns {Buffer} + */ + + toNormal() { + return this.frameRaw(false); + } + + /** + * Write serialized block to a buffer + * writer (includes witness data). + * @param {BufferWriter} bw + */ + + toWriter(bw) { + return this.writeRaw(bw, true); + } + + /** + * Write serialized block to a buffer + * writer (excludes witness data). + * @param {BufferWriter} bw + */ + + toNormalWriter(bw) { + return this.writeRaw(bw, false); + } + + /** + * Serialize compact block. + * @private + * @param {Boolean} witness + * @returns {Buffer} + */ + + frameRaw(witness) { + const size = this.getSize(witness); + return this.writeRaw(new StaticWriter(size), witness).render(); + } + + /** + * Calculate block serialization size. + * @param {Boolean} witness + * @returns {Number} + */ + + getSize(witness) { + let size = 0; + + size += 80; + size += 8; + size += encoding.sizeVarint(this.ids.length); + size += this.ids.length * 6; + size += encoding.sizeVarint(this.ptx.length); + + for (const [index, tx] of this.ptx) { + size += encoding.sizeVarint(index); + + if (witness) + size += tx.getSize(); + else + size += tx.getBaseSize(); + } + + return size; + } + + /** + * Serialize block to buffer writer. + * @private + * @param {BufferWriter} bw + * @param {Boolean} witness + */ + + writeRaw(bw, witness) { + this.writeHead(bw); + + bw.writeBytes(this.keyNonce); + + bw.writeVarint(this.ids.length); + + for (const id of this.ids) { + const lo = id % 0x100000000; + const hi = (id - lo) / 0x100000000; + assert(hi <= 0xffff); + bw.writeU32(lo); + bw.writeU16(hi); + } + + bw.writeVarint(this.ptx.length); + + for (const [index, tx] of this.ptx) { + bw.writeVarint(index); + + if (witness) + tx.toWriter(bw); + else + tx.toNormalWriter(bw); + } + + return bw; + } + + /** + * Convert block to a TXRequest + * containing missing indexes. + * @returns {TXRequest} + */ + + toRequest() { + return TXRequest.fromCompact(this); + } + + /** + * Attempt to fill missing transactions from mempool. + * @param {Boolean} witness + * @param {Mempool} mempool + * @returns {Boolean} + */ + + fillMempool(witness, mempool) { if (this.count === this.totalTX) return true; + + const set = new Set(); + + for (const {tx} of mempool.map.values()) { + let hash = tx.hash(); + + if (witness) + hash = tx.witnessHash(); + + const id = this.sid(hash); + const index = this.idMap.get(id); + + if (index == null) + continue; + + if (set.has(index)) { + // Siphash collision, just request it. + this.available[index] = null; + this.count -= 1; + continue; + } + + this.available[index] = tx; + set.add(index); + this.count += 1; + + // We actually may have a siphash collision + // here, but exit early anyway for perf. + if (this.count === this.totalTX) + return true; + } + + return false; } - return false; -}; + /** + * Attempt to fill missing transactions from TXResponse. + * @param {TXResponse} res + * @returns {Boolean} + */ -/** - * Attempt to fill missing transactions from TXResponse. - * @param {TXResponse} res - * @returns {Boolean} - */ + fillMissing(res) { + let offset = 0; -CompactBlock.prototype.fillMissing = function fillMissing(res) { - let offset = 0; + for (let i = 0; i < this.available.length; i++) { + if (this.available[i]) + continue; - for (let i = 0; i < this.available.length; i++) { - if (this.available[i]) - continue; + if (offset >= res.txs.length) + return false; - if (offset >= res.txs.length) - return false; + this.available[i] = res.txs[offset++]; + } - this.available[i] = res.txs[offset++]; + return offset === res.txs.length; } - return offset === res.txs.length; -}; + /** + * Calculate a transaction short ID. + * @param {Hash} hash + * @returns {Number} + */ -/** - * Calculate a transaction short ID. - * @param {Hash} hash - * @returns {Number} - */ + sid(hash) { + if (typeof hash === 'string') + hash = Buffer.from(hash, 'hex'); -CompactBlock.prototype.sid = function sid(hash) { - if (typeof hash === 'string') - hash = Buffer.from(hash, 'hex'); + const [hi, lo] = siphash256(hash, this.sipKey); - const [hi, lo] = siphash256(hash, this.sipKey); - - return (hi & 0xffff) * 0x100000000 + (lo >>> 0); -}; - -/** - * Test whether an index is available. - * @param {Number} index - * @returns {Boolean} - */ - -CompactBlock.prototype.hasIndex = function hasIndex(index) { - return this.available[index] != null; -}; - -/** - * Initialize the siphash key. - * @private - * @returns {Buffer} - */ - -CompactBlock.prototype.getKey = function getKey() { - const data = Buffer.concat([this.toHead(), this.keyNonce]); - const hash = sha256.digest(data); - return hash.slice(0, 16); -}; - -/** - * Initialize compact block and short id map. - * @private - */ - -CompactBlock.prototype.init = function init() { - if (this.totalTX === 0) - throw new Error('Empty vectors.'); - - if (this.totalTX > consensus.MAX_BLOCK_SIZE / 10) - throw new Error('Compact block too big.'); - - // Custom limit to avoid a hashdos. - // Min valid tx size: (4 + 1 + 41 + 1 + 9 + 4) = 60 - // Min block header size: 81 - // Max number of transactions: (1000000 - 81) / 60 = 16665 - if (this.totalTX > (consensus.MAX_BLOCK_SIZE - 81) / 60) - throw new Error('Compact block too big.'); - - // No sparse arrays here, v8. - for (let i = 0; i < this.totalTX; i++) - this.available.push(null); - - let last = -1; - let offset = 0; - - for (let i = 0; i < this.ptx.length; i++) { - const [index, tx] = this.ptx[i]; - last += index + 1; - assert(last <= 0xffff); - assert(last <= this.ids.length + i); - this.available[last] = tx; - this.count++; + return (hi & 0xffff) * 0x100000000 + (lo >>> 0); } - for (let i = 0; i < this.ids.length; i++) { - const id = this.ids[i]; + /** + * Test whether an index is available. + * @param {Number} index + * @returns {Boolean} + */ - while (this.available[i + offset]) - offset++; - - // Fails on siphash collision. - if (this.idMap.has(id)) - return false; - - this.idMap.set(id, i + offset); + hasIndex(index) { + return this.available[index] != null; } - return true; -}; + /** + * Initialize the siphash key. + * @private + * @returns {Buffer} + */ -/** - * Convert completely filled compact - * block to a regular block. - * @returns {Block} - */ - -CompactBlock.prototype.toBlock = function toBlock() { - const block = new Block(); - - block.version = this.version; - block.prevBlock = this.prevBlock; - block.merkleRoot = this.merkleRoot; - block.time = this.time; - block.bits = this.bits; - block.nonce = this.nonce; - block._hash = this._hash; - block._hhash = this._hhash; - - for (const tx of this.available) { - assert(tx, 'Compact block is not full.'); - block.txs.push(tx); + getKey() { + const data = Buffer.concat([this.toHead(), this.keyNonce]); + const hash = sha256.digest(data); + return hash.slice(0, 16); } - return block; -}; + /** + * Initialize compact block and short id map. + * @private + */ -/** - * Inject properties from block. - * @private - * @param {Block} block - * @param {Boolean} witness - * @param {Buffer?} nonce - * @returns {CompactBlock} - */ + init() { + if (this.totalTX === 0) + throw new Error('Empty vectors.'); -CompactBlock.prototype.fromBlock = function fromBlock(block, witness, nonce) { - this.version = block.version; - this.prevBlock = block.prevBlock; - this.merkleRoot = block.merkleRoot; - this.time = block.time; - this.bits = block.bits; - this.nonce = block.nonce; - this.totalTX = block.txs.length; - this._hash = block._hash; - this._hhash = block._hhash; + if (this.totalTX > consensus.MAX_BLOCK_SIZE / 10) + throw new Error('Compact block too big.'); - if (!nonce) - nonce = common.nonce(); + // Custom limit to avoid a hashdos. + // Min valid tx size: (4 + 1 + 41 + 1 + 9 + 4) = 60 + // Min block header size: 81 + // Max number of transactions: (1000000 - 81) / 60 = 16665 + if (this.totalTX > (consensus.MAX_BLOCK_SIZE - 81) / 60) + throw new Error('Compact block too big.'); - this.keyNonce = nonce; - this.sipKey = this.getKey(); + // No sparse arrays here, v8. + for (let i = 0; i < this.totalTX; i++) + this.available.push(null); - for (let i = 1; i < block.txs.length; i++) { - const tx = block.txs[i]; - let hash = tx.hash(); + let last = -1; + let offset = 0; - if (witness) - hash = tx.witnessHash(); + for (let i = 0; i < this.ptx.length; i++) { + const [index, tx] = this.ptx[i]; + last += index + 1; + assert(last <= 0xffff); + assert(last <= this.ids.length + i); + this.available[last] = tx; + this.count += 1; + } - const id = this.sid(hash); + for (let i = 0; i < this.ids.length; i++) { + const id = this.ids[i]; - this.ids.push(id); + while (this.available[i + offset]) + offset += 1; + + // Fails on siphash collision. + if (this.idMap.has(id)) + return false; + + this.idMap.set(id, i + offset); + } + + return true; } - this.ptx.push([0, block.txs[0]]); + /** + * Convert completely filled compact + * block to a regular block. + * @returns {Block} + */ - return this; -}; - -/** - * Instantiate compact block from a block. - * @param {Block} block - * @param {Boolean} witness - * @param {Buffer?} nonce - * @returns {CompactBlock} - */ - -CompactBlock.fromBlock = function fromBlock(block, witness, nonce) { - return new CompactBlock().fromBlock(block, witness, nonce); -}; - -/** - * Convert block to headers. - * @returns {Headers} - */ - -CompactBlock.prototype.toHeaders = function toHeaders() { - return Headers.fromBlock(this); -}; + toBlock() { + const block = new Block(); + + block.version = this.version; + block.prevBlock = this.prevBlock; + block.merkleRoot = this.merkleRoot; + block.time = this.time; + block.bits = this.bits; + block.nonce = this.nonce; + block._hash = this._hash; + block._hhash = this._hhash; + + for (const tx of this.available) { + assert(tx, 'Compact block is not full.'); + block.txs.push(tx); + } + + return block; + } + + /** + * Inject properties from block. + * @private + * @param {Block} block + * @param {Boolean} witness + * @param {Buffer?} nonce + * @returns {CompactBlock} + */ + + fromBlock(block, witness, nonce) { + this.version = block.version; + this.prevBlock = block.prevBlock; + this.merkleRoot = block.merkleRoot; + this.time = block.time; + this.bits = block.bits; + this.nonce = block.nonce; + this.totalTX = block.txs.length; + this._hash = block._hash; + this._hhash = block._hhash; + + if (!nonce) + nonce = common.nonce(); + + this.keyNonce = nonce; + this.sipKey = this.getKey(); + + for (let i = 1; i < block.txs.length; i++) { + const tx = block.txs[i]; + let hash = tx.hash(); + + if (witness) + hash = tx.witnessHash(); + + const id = this.sid(hash); + + this.ids.push(id); + } + + this.ptx.push([0, block.txs[0]]); + + return this; + } + + /** + * Instantiate compact block from a block. + * @param {Block} block + * @param {Boolean} witness + * @param {Buffer?} nonce + * @returns {CompactBlock} + */ + + static fromBlock(block, witness, nonce) { + return new this().fromBlock(block, witness, nonce); + } + + /** + * Convert block to headers. + * @returns {Headers} + */ + + toHeaders() { + return Headers.fromBlock(this); + } +} /** + * TX Request * Represents a BlockTransactionsRequest (bip152): `getblocktxn` packet. * @see https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki - * @constructor - * @param {Object} options * @property {Hash} hash * @property {Number[]} indexes */ -function TXRequest(options) { - if (!(this instanceof TXRequest)) - return new TXRequest(options); +class TXRequest { + /** + * TX Request + * @constructor + * @param {Object?} options + */ - this.hash = encoding.NULL_HASH; - this.indexes = []; + constructor(options) { + this.hash = encoding.NULL_HASH; + this.indexes = []; - if (options) - this.fromOptions(options); + if (options) + this.fromOptions(options); + } + + /** + * Inject properties from options. + * @private + * @param {Object} options + * @returns {TXRequest} + */ + + fromOptions(options) { + this.hash = options.hash; + + if (options.indexes) + this.indexes = options.indexes; + + return this; + } + + /** + * Instantiate request from options. + * @param {Object} options + * @returns {TXRequest} + */ + + static fromOptions(options) { + return new this().fromOptions(options); + } + + /** + * Inject properties from compact block. + * @private + * @param {CompactBlock} block + * @returns {TXRequest} + */ + + fromCompact(block) { + this.hash = block.hash('hex'); + + for (let i = 0; i < block.available.length; i++) { + if (!block.available[i]) + this.indexes.push(i); + } + + return this; + } + + /** + * Instantiate request from compact block. + * @param {CompactBlock} block + * @returns {TXRequest} + */ + + static fromCompact(block) { + return new this().fromCompact(block); + } + + /** + * Inject properties from buffer reader. + * @private + * @param {BufferReader} br + * @returns {TXRequest} + */ + + fromReader(br) { + this.hash = br.readHash('hex'); + + const count = br.readVarint(); + + for (let i = 0; i < count; i++) { + const index = br.readVarint(); + assert(index <= 0xffff); + this.indexes.push(index); + } + + let offset = 0; + + for (let i = 0; i < count; i++) { + let index = this.indexes[i]; + index += offset; + assert(index <= 0xffff); + this.indexes[i] = index; + offset = index + 1; + } + + return this; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @returns {TXRequest} + */ + + fromRaw(data) { + return this.fromReader(new BufferReader(data)); + } + + /** + * Instantiate request from buffer reader. + * @param {BufferReader} br + * @returns {TXRequest} + */ + + static fromReader(br) { + return new this().fromReader(br); + } + + /** + * Instantiate request from serialized data. + * @param {Buffer} data + * @returns {TXRequest} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } + + /** + * Calculate request serialization size. + * @returns {Number} + */ + + getSize() { + let size = 0; + + size += 32; + size += encoding.sizeVarint(this.indexes.length); + + for (let i = 0; i < this.indexes.length; i++) { + let index = this.indexes[i]; + + if (i > 0) + index -= this.indexes[i - 1] + 1; + + size += encoding.sizeVarint(index); + } + + return size; + } + + /** + * Write serialized request to buffer writer. + * @param {BufferWriter} bw + */ + + toWriter(bw) { + bw.writeHash(this.hash); + + bw.writeVarint(this.indexes.length); + + for (let i = 0; i < this.indexes.length; i++) { + let index = this.indexes[i]; + + if (i > 0) + index -= this.indexes[i - 1] + 1; + + bw.writeVarint(index); + } + + return bw; + } + + /** + * Serialize request. + * @returns {Buffer} + */ + + toRaw() { + const size = this.getSize(); + return this.toWriter(new StaticWriter(size)).render(); + } } /** - * Inject properties from options. - * @private - * @param {Object} options - * @returns {TXRequest} - */ - -TXRequest.prototype.fromOptions = function fromOptions(options) { - this.hash = options.hash; - - if (options.indexes) - this.indexes = options.indexes; - - return this; -}; - -/** - * Instantiate request from options. - * @param {Object} options - * @returns {TXRequest} - */ - -TXRequest.fromOptions = function fromOptions(options) { - return new TXRequest().fromOptions(options); -}; - -/** - * Inject properties from compact block. - * @private - * @param {CompactBlock} block - * @returns {TXRequest} - */ - -TXRequest.prototype.fromCompact = function fromCompact(block) { - this.hash = block.hash('hex'); - - for (let i = 0; i < block.available.length; i++) { - if (!block.available[i]) - this.indexes.push(i); - } - - return this; -}; - -/** - * Instantiate request from compact block. - * @param {CompactBlock} block - * @returns {TXRequest} - */ - -TXRequest.fromCompact = function fromCompact(block) { - return new TXRequest().fromCompact(block); -}; - -/** - * Inject properties from buffer reader. - * @private - * @param {BufferReader} br - * @returns {TXRequest} - */ - -TXRequest.prototype.fromReader = function fromReader(br) { - this.hash = br.readHash('hex'); - - const count = br.readVarint(); - - for (let i = 0; i < count; i++) { - const index = br.readVarint(); - assert(index <= 0xffff); - this.indexes.push(index); - } - - let offset = 0; - - for (let i = 0; i < count; i++) { - let index = this.indexes[i]; - index += offset; - assert(index <= 0xffff); - this.indexes[i] = index; - offset = index + 1; - } - - return this; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - * @returns {TXRequest} - */ - -TXRequest.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(new BufferReader(data)); -}; - -/** - * Instantiate request from buffer reader. - * @param {BufferReader} br - * @returns {TXRequest} - */ - -TXRequest.fromReader = function fromReader(br) { - return new TXRequest().fromReader(br); -}; - -/** - * Instantiate request from serialized data. - * @param {Buffer} data - * @returns {TXRequest} - */ - -TXRequest.fromRaw = function fromRaw(data) { - return new TXRequest().fromRaw(data); -}; - -/** - * Calculate request serialization size. - * @returns {Number} - */ - -TXRequest.prototype.getSize = function getSize() { - let size = 0; - - size += 32; - size += encoding.sizeVarint(this.indexes.length); - - for (let i = 0; i < this.indexes.length; i++) { - let index = this.indexes[i]; - - if (i > 0) - index -= this.indexes[i - 1] + 1; - - size += encoding.sizeVarint(index); - } - - return size; -}; - -/** - * Write serialized request to buffer writer. - * @param {BufferWriter} bw - */ - -TXRequest.prototype.toWriter = function toWriter(bw) { - bw.writeHash(this.hash); - - bw.writeVarint(this.indexes.length); - - for (let i = 0; i < this.indexes.length; i++) { - let index = this.indexes[i]; - - if (i > 0) - index -= this.indexes[i - 1] + 1; - - bw.writeVarint(index); - } - - return bw; -}; - -/** - * Serialize request. - * @returns {Buffer} - */ - -TXRequest.prototype.toRaw = function toRaw() { - const size = this.getSize(); - return this.toWriter(new StaticWriter(size)).render(); -}; - -/** + * TX Response * Represents BlockTransactions (bip152): `blocktxn` packet. * @see https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki - * @constructor - * @param {Object} options * @property {Hash} hash * @property {TX[]} txs */ -function TXResponse(options) { - if (!(this instanceof TXResponse)) - return new TXResponse(options); +class TXResponse { + /** + * Create a tx response. + * @constructor + * @param {Object?} options + */ - this.hash = encoding.NULL_HASH; - this.txs = []; + constructor(options) { + this.hash = encoding.NULL_HASH; + this.txs = []; - if (options) - this.fromOptions(options); + if (options) + this.fromOptions(options); + } + + /** + * Inject properties from options. + * @private + * @param {Object} options + * @returns {TXResponse} + */ + + fromOptions(options) { + this.hash = options.hash; + + if (options.txs) + this.txs = options.txs; + + return this; + } + + /** + * Instantiate response from options. + * @param {Object} options + * @returns {TXResponse} + */ + + static fromOptions(options) { + return new this().fromOptions(options); + } + + /** + * Inject properties from buffer reader. + * @private + * @param {BufferReader} br + * @returns {TXResponse} + */ + + fromReader(br) { + this.hash = br.readHash('hex'); + + const count = br.readVarint(); + + for (let i = 0; i < count; i++) + this.txs.push(TX.fromReader(br)); + + return this; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @returns {TXResponse} + */ + + fromRaw(data) { + return this.fromReader(new BufferReader(data)); + } + + /** + * Instantiate response from buffer reader. + * @param {BufferReader} br + * @returns {TXResponse} + */ + + static fromReader(br) { + return new this().fromReader(br); + } + + /** + * Instantiate response from serialized data. + * @param {Buffer} data + * @returns {TXResponse} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } + + /** + * Inject properties from block. + * @private + * @param {Block} block + * @returns {TXResponse} + */ + + fromBlock(block, req) { + this.hash = req.hash; + + for (const index of req.indexes) { + if (index >= block.txs.length) + break; + + this.txs.push(block.txs[index]); + } + + return this; + } + + /** + * Instantiate response from block. + * @param {Block} block + * @returns {TXResponse} + */ + + static fromBlock(block, req) { + return new this().fromBlock(block, req); + } + + /** + * Serialize response with witness data. + * @returns {Buffer} + */ + + toRaw() { + return this.frameRaw(true); + } + + /** + * Serialize response without witness data. + * @returns {Buffer} + */ + + toNormal() { + return this.frameRaw(false); + } + + /** + * Write serialized response to a buffer + * writer (includes witness data). + * @param {BufferWriter} bw + */ + + toWriter(bw) { + return this.writeRaw(bw, true); + } + + /** + * Write serialized response to a buffer + * writer (excludes witness data). + * @param {BufferWriter} bw + */ + + toNormalWriter(bw) { + return this.writeRaw(bw, false); + } + + /** + * Calculate request serialization size. + * @returns {Number} + */ + + getSize(witness) { + let size = 0; + + size += 32; + size += encoding.sizeVarint(this.txs.length); + + for (const tx of this.txs) { + if (witness) + size += tx.getSize(); + else + size += tx.getBaseSize(); + } + + return size; + } + + /** + * Write serialized response to buffer writer. + * @private + * @param {BufferWriter} bw + * @param {Boolean} witness + */ + + writeRaw(bw, witness) { + bw.writeHash(this.hash); + + bw.writeVarint(this.txs.length); + + for (const tx of this.txs) { + if (witness) + tx.toWriter(bw); + else + tx.toNormalWriter(bw); + } + + return bw; + } + + /** + * Serialize response with witness data. + * @private + * @param {Boolean} witness + * @returns {Buffer} + */ + + frameRaw(witness) { + const size = this.getSize(witness); + return this.writeRaw(new StaticWriter(size), witness).render(); + } } -/** - * Inject properties from options. - * @private - * @param {Object} options - * @returns {TXResponse} - */ - -TXResponse.prototype.fromOptions = function fromOptions(options) { - this.hash = options.hash; - - if (options.txs) - this.txs = options.txs; - - return this; -}; - -/** - * Instantiate response from options. - * @param {Object} options - * @returns {TXResponse} - */ - -TXResponse.fromOptions = function fromOptions(options) { - return new TXResponse().fromOptions(options); -}; - -/** - * Inject properties from buffer reader. - * @private - * @param {BufferReader} br - * @returns {TXResponse} - */ - -TXResponse.prototype.fromReader = function fromReader(br) { - this.hash = br.readHash('hex'); - - const count = br.readVarint(); - - for (let i = 0; i < count; i++) - this.txs.push(TX.fromReader(br)); - - return this; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - * @returns {TXResponse} - */ - -TXResponse.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(new BufferReader(data)); -}; - -/** - * Instantiate response from buffer reader. - * @param {BufferReader} br - * @returns {TXResponse} - */ - -TXResponse.fromReader = function fromReader(br) { - return new TXResponse().fromReader(br); -}; - -/** - * Instantiate response from serialized data. - * @param {Buffer} data - * @returns {TXResponse} - */ - -TXResponse.fromRaw = function fromRaw(data) { - return new TXResponse().fromRaw(data); -}; - -/** - * Inject properties from block. - * @private - * @param {Block} block - * @returns {TXResponse} - */ - -TXResponse.prototype.fromBlock = function fromBlock(block, req) { - this.hash = req.hash; - - for (const index of req.indexes) { - if (index >= block.txs.length) - break; - - this.txs.push(block.txs[index]); - } - - return this; -}; - -/** - * Instantiate response from block. - * @param {Block} block - * @returns {TXResponse} - */ - -TXResponse.fromBlock = function fromBlock(block, req) { - return new TXResponse().fromBlock(block, req); -}; - -/** - * Serialize response with witness data. - * @returns {Buffer} - */ - -TXResponse.prototype.toRaw = function toRaw() { - return this.frameRaw(true); -}; - -/** - * Serialize response without witness data. - * @returns {Buffer} - */ - -TXResponse.prototype.toNormal = function toNormal() { - return this.frameRaw(false); -}; - -/** - * Write serialized response to a buffer - * writer (includes witness data). - * @param {BufferWriter} bw - */ - -TXResponse.prototype.toWriter = function toWriter(bw) { - return this.writeRaw(bw, true); -}; - -/** - * Write serialized response to a buffer - * writer (excludes witness data). - * @param {BufferWriter} bw - */ - -TXResponse.prototype.toNormalWriter = function toNormalWriter(bw) { - return this.writeRaw(bw, false); -}; - -/** - * Calculate request serialization size. - * @returns {Number} - */ - -TXResponse.prototype.getSize = function getSize(witness) { - let size = 0; - - size += 32; - size += encoding.sizeVarint(this.txs.length); - - for (const tx of this.txs) { - if (witness) - size += tx.getSize(); - else - size += tx.getBaseSize(); - } - - return size; -}; - -/** - * Write serialized response to buffer writer. - * @private - * @param {BufferWriter} bw - * @param {Boolean} witness - */ - -TXResponse.prototype.writeRaw = function writeRaw(bw, witness) { - bw.writeHash(this.hash); - - bw.writeVarint(this.txs.length); - - for (const tx of this.txs) { - if (witness) - tx.toWriter(bw); - else - tx.toNormalWriter(bw); - } - - return bw; -}; - -/** - * Serialize response with witness data. - * @private - * @param {Boolean} witness - * @returns {Buffer} - */ - -TXResponse.prototype.frameRaw = function frameRaw(witness) { - const size = this.getSize(witness); - return this.writeRaw(new StaticWriter(size), witness).render(); -}; - /* * Expose */ diff --git a/lib/primitives/abstractblock.js b/lib/primitives/abstractblock.js index d9a543bf..3bc2efb3 100644 --- a/lib/primitives/abstractblock.js +++ b/lib/primitives/abstractblock.js @@ -11,249 +11,252 @@ const assert = require('assert'); const hash256 = require('bcrypto/lib/hash256'); const BufferReader = require('bufio/lib/reader'); const StaticWriter = require('bufio/lib/staticwriter'); -const InvItem = require('./invitem'); const encoding = require('bufio/lib/encoding'); +const InvItem = require('./invitem'); const consensus = require('../protocol/consensus'); /** + * Abstract Block * The class which all block-like objects inherit from. * @alias module:primitives.AbstractBlock - * @constructor * @abstract - * @property {Number} version - Block version. Note - * that Bcoin reads versions as unsigned despite - * them being signed on the protocol level. This - * number will never be negative. - * @property {Hash} prevBlock - Previous block hash. - * @property {Hash} merkleRoot - Merkle root hash. - * @property {Number} time - Timestamp. + * @property {Number} version + * @property {Hash} prevBlock + * @property {Hash} merkleRoot + * @property {Number} time * @property {Number} bits * @property {Number} nonce */ -function AbstractBlock() { - if (!(this instanceof AbstractBlock)) - return new AbstractBlock(); +class AbstractBlock { + /** + * Create an abstract block. + * @constructor + */ - this.version = 1; - this.prevBlock = encoding.NULL_HASH; - this.merkleRoot = encoding.NULL_HASH; - this.time = 0; - this.bits = 0; - this.nonce = 0; + constructor() { + this.version = 1; + this.prevBlock = encoding.NULL_HASH; + this.merkleRoot = encoding.NULL_HASH; + this.time = 0; + this.bits = 0; + this.nonce = 0; - this.mutable = false; + this.mutable = false; - this._hash = null; - this._hhash = null; -} - -/** - * Inject properties from options object. - * @private - * @param {NakedBlock} options - */ - -AbstractBlock.prototype.parseOptions = function parseOptions(options) { - assert(options, 'Block data is required.'); - assert((options.version >>> 0) === options.version); - assert(typeof options.prevBlock === 'string'); - assert(typeof options.merkleRoot === 'string'); - assert((options.time >>> 0) === options.time); - assert((options.bits >>> 0) === options.bits); - assert((options.nonce >>> 0) === options.nonce); - - this.version = options.version; - this.prevBlock = options.prevBlock; - this.merkleRoot = options.merkleRoot; - this.time = options.time; - this.bits = options.bits; - this.nonce = options.nonce; - - if (options.mutable != null) - this.mutable = Boolean(options.mutable); - - return this; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - */ - -AbstractBlock.prototype.parseJSON = function parseJSON(json) { - assert(json, 'Block data is required.'); - assert((json.version >>> 0) === json.version); - assert(typeof json.prevBlock === 'string'); - assert(typeof json.merkleRoot === 'string'); - assert((json.time >>> 0) === json.time); - assert((json.bits >>> 0) === json.bits); - assert((json.nonce >>> 0) === json.nonce); - - this.version = json.version; - this.prevBlock = encoding.revHex(json.prevBlock); - this.merkleRoot = encoding.revHex(json.merkleRoot); - this.time = json.time; - this.bits = json.bits; - this.nonce = json.nonce; - - return this; -}; - -/** - * Test whether the block is a memblock. - * @returns {Boolean} - */ - -AbstractBlock.prototype.isMemory = function isMemory() { - return false; -}; - -/** - * Clear any cached values (abstract). - */ - -AbstractBlock.prototype._refresh = function _refresh() { - this._hash = null; - this._hhash = null; -}; - -/** - * Clear any cached values. - */ - -AbstractBlock.prototype.refresh = function refresh() { - return this._refresh(); -}; - -/** - * Hash the block headers. - * @param {String?} enc - Can be `'hex'` or `null`. - * @returns {Hash|Buffer} hash - */ - -AbstractBlock.prototype.hash = function hash(enc) { - let h = this._hash; - - if (!h) { - h = hash256.digest(this.toHead()); - if (!this.mutable) - this._hash = h; + this._hash = null; + this._hhash = null; } - if (enc === 'hex') { - let hex = this._hhash; - if (!hex) { - hex = h.toString('hex'); - if (!this.mutable) - this._hhash = hex; + /** + * Inject properties from options object. + * @private + * @param {NakedBlock} options + */ + + parseOptions(options) { + assert(options, 'Block data is required.'); + assert((options.version >>> 0) === options.version); + assert(typeof options.prevBlock === 'string'); + assert(typeof options.merkleRoot === 'string'); + assert((options.time >>> 0) === options.time); + assert((options.bits >>> 0) === options.bits); + assert((options.nonce >>> 0) === options.nonce); + + this.version = options.version; + this.prevBlock = options.prevBlock; + this.merkleRoot = options.merkleRoot; + this.time = options.time; + this.bits = options.bits; + this.nonce = options.nonce; + + if (options.mutable != null) { + assert(typeof options.mutable === 'boolean'); + this.mutable = options.mutable; } - h = hex; + + return this; } - return h; -}; + /** + * Inject properties from json object. + * @private + * @param {Object} json + */ -/** - * Serialize the block headers. - * @returns {Buffer} - */ + parseJSON(json) { + assert(json, 'Block data is required.'); + assert((json.version >>> 0) === json.version); + assert(typeof json.prevBlock === 'string'); + assert(typeof json.merkleRoot === 'string'); + assert((json.time >>> 0) === json.time); + assert((json.bits >>> 0) === json.bits); + assert((json.nonce >>> 0) === json.nonce); -AbstractBlock.prototype.toHead = function toHead() { - return this.writeHead(new StaticWriter(80)).render(); -}; + this.version = json.version; + this.prevBlock = encoding.revHex(json.prevBlock); + this.merkleRoot = encoding.revHex(json.merkleRoot); + this.time = json.time; + this.bits = json.bits; + this.nonce = json.nonce; -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ + return this; + } -AbstractBlock.prototype.fromHead = function fromHead(data) { - return this.readHead(new BufferReader(data)); -}; + /** + * Test whether the block is a memblock. + * @returns {Boolean} + */ -/** - * Serialize the block headers. - * @param {BufferWriter} bw - */ - -AbstractBlock.prototype.writeHead = function writeHead(bw) { - bw.writeU32(this.version); - bw.writeHash(this.prevBlock); - bw.writeHash(this.merkleRoot); - bw.writeU32(this.time); - bw.writeU32(this.bits); - bw.writeU32(this.nonce); - return bw; -}; - -/** - * Parse the block headers. - * @param {BufferReader} br - */ - -AbstractBlock.prototype.readHead = function readHead(br) { - this.version = br.readU32(); - this.prevBlock = br.readHash('hex'); - this.merkleRoot = br.readHash('hex'); - this.time = br.readU32(); - this.bits = br.readU32(); - this.nonce = br.readU32(); - return this; -}; - -/** - * Verify the block. - * @returns {Boolean} - */ - -AbstractBlock.prototype.verify = function verify() { - if (!this.verifyPOW()) + isMemory() { return false; + } - if (!this.verifyBody()) - return false; + /** + * Clear any cached values (abstract). + */ - return true; -}; + _refresh() { + this._hash = null; + this._hhash = null; + } -/** - * Verify proof-of-work. - * @returns {Boolean} - */ + /** + * Clear any cached values. + */ -AbstractBlock.prototype.verifyPOW = function verifyPOW() { - return consensus.verifyPOW(this.hash(), this.bits); -}; + refresh() { + return this._refresh(); + } -/** - * Verify the block. - * @returns {Boolean} - */ + /** + * Hash the block headers. + * @param {String?} enc - Can be `'hex'` or `null`. + * @returns {Hash|Buffer} hash + */ -AbstractBlock.prototype.verifyBody = function verifyBody() { - throw new Error('Abstract method.'); -}; + hash(enc) { + let h = this._hash; -/** - * Get little-endian block hash. - * @returns {Hash} - */ + if (!h) { + h = hash256.digest(this.toHead()); + if (!this.mutable) + this._hash = h; + } -AbstractBlock.prototype.rhash = function rhash() { - return encoding.revHex(this.hash('hex')); -}; + if (enc === 'hex') { + let hex = this._hhash; + if (!hex) { + hex = h.toString('hex'); + if (!this.mutable) + this._hhash = hex; + } + h = hex; + } -/** - * Convert the block to an inv item. - * @returns {InvItem} - */ + return h; + } -AbstractBlock.prototype.toInv = function toInv() { - return new InvItem(InvItem.types.BLOCK, this.hash('hex')); -}; + /** + * Serialize the block headers. + * @returns {Buffer} + */ + + toHead() { + return this.writeHead(new StaticWriter(80)).render(); + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromHead(data) { + return this.readHead(new BufferReader(data)); + } + + /** + * Serialize the block headers. + * @param {BufferWriter} bw + */ + + writeHead(bw) { + bw.writeU32(this.version); + bw.writeHash(this.prevBlock); + bw.writeHash(this.merkleRoot); + bw.writeU32(this.time); + bw.writeU32(this.bits); + bw.writeU32(this.nonce); + return bw; + } + + /** + * Parse the block headers. + * @param {BufferReader} br + */ + + readHead(br) { + this.version = br.readU32(); + this.prevBlock = br.readHash('hex'); + this.merkleRoot = br.readHash('hex'); + this.time = br.readU32(); + this.bits = br.readU32(); + this.nonce = br.readU32(); + return this; + } + + /** + * Verify the block. + * @returns {Boolean} + */ + + verify() { + if (!this.verifyPOW()) + return false; + + if (!this.verifyBody()) + return false; + + return true; + } + + /** + * Verify proof-of-work. + * @returns {Boolean} + */ + + verifyPOW() { + return consensus.verifyPOW(this.hash(), this.bits); + } + + /** + * Verify the block. + * @returns {Boolean} + */ + + verifyBody() { + throw new Error('Abstract method.'); + } + + /** + * Get little-endian block hash. + * @returns {Hash} + */ + + rhash() { + return encoding.revHex(this.hash('hex')); + } + + /** + * Convert the block to an inv item. + * @returns {InvItem} + */ + + toInv() { + return new InvItem(InvItem.types.BLOCK, this.hash('hex')); + } +} /* * Expose diff --git a/lib/primitives/address.js b/lib/primitives/address.js index d621a674..415b3d55 100644 --- a/lib/primitives/address.js +++ b/lib/primitives/address.js @@ -8,36 +8,854 @@ 'use strict'; const assert = require('assert'); -const Network = require('../protocol/network'); +const {base58, bech32} = require('bstring'); const encoding = require('bufio/lib/encoding'); const sha256 = require('bcrypto/lib/sha256'); const hash160 = require('bcrypto/lib/hash160'); const hash256 = require('bcrypto/lib/hash256'); const BufferReader = require('bufio/lib/reader'); const StaticWriter = require('bufio/lib/staticwriter'); -const base58 = require('bstring/lib/base58'); -const bech32 = require('bstring/lib/bech32'); +const Network = require('../protocol/network'); /** + * Address * Represents an address. * @alias module:primitives.Address - * @constructor - * @param {Object?} options * @property {Buffer} hash * @property {AddressPrefix} type * @property {Number} version */ -function Address(options, network) { - if (!(this instanceof Address)) - return new Address(options, network); +class Address { + /** + * Create an address. + * @constructor + * @param {Object?} options + */ - this.type = Address.types.PUBKEYHASH; - this.version = -1; - this.hash = encoding.ZERO_HASH160; + constructor(options, network) { + this.type = Address.types.PUBKEYHASH; + this.version = -1; + this.hash = encoding.ZERO_HASH160; - if (options) - this.fromOptions(options, network); + if (options) + this.fromOptions(options, network); + } + + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ + + fromOptions(options, network) { + if (typeof options === 'string') + return this.fromString(options, network); + + assert(options); + + const {hash, type, version} = options; + + return this.fromHash(hash, type, version); + } + + /** + * Insantiate address from options. + * @param {Object} options + * @returns {Address} + */ + + static fromOptions(options, network) { + return new this().fromOptions(options, network); + } + + /** + * Get the address hash. + * @param {String?} enc - Can be `"hex"` or `null`. + * @returns {Hash|Buffer} + */ + + getHash(enc) { + if (enc === 'hex') + return this.hash.toString(enc); + return this.hash; + } + + /** + * Test whether the address is null. + * @returns {Boolean} + */ + + isNull() { + if (this.hash.length === 20) + return this.hash.equals(encoding.ZERO_HASH160); + + if (this.hash.length === 32) + return this.hash.equals(encoding.ZERO_HASH); + + for (let i = 0; i < this.hash.length; i++) { + if (this.hash[i] !== 0) + return false; + } + + return true; + } + + /** + * Test equality against another address. + * @param {Address} addr + * @returns {Boolean} + */ + + equals(addr) { + assert(addr instanceof Address); + + return this.type === addr.type + && this.version === addr.version + && this.hash.equals(addr.hash); + } + + /** + * Get the address type as a string. + * @returns {String} + */ + + getType() { + return Address.typesByVal[this.type].toLowerCase(); + } + + /** + * Get a network address prefix for the address. + * @param {Network?} network + * @returns {Number} + */ + + getPrefix(network) { + network = Network.get(network); + + const prefixes = network.addressPrefix; + + switch (this.type) { + case Address.types.PUBKEYHASH: + return prefixes.pubkeyhash; + case Address.types.SCRIPTHASH: + return prefixes.scripthash; + case Address.types.WITNESS: + if (this.hash.length === 20) + return prefixes.witnesspubkeyhash; + + if (this.hash.length === 32) + return prefixes.witnessscripthash; + + break; + } + + return -1; + } + + /** + * Calculate size of serialized address. + * @returns {Number} + */ + + getSize() { + let size = 5 + this.hash.length; + + if (this.version !== -1) + size += 2; + + return size; + } + + /** + * Compile the address object to its raw serialization. + * @param {{NetworkType|Network)?} network + * @returns {Buffer} + * @throws Error on bad hash/prefix. + */ + + toRaw(network) { + const size = this.getSize(); + const bw = new StaticWriter(size); + const prefix = this.getPrefix(network); + + assert(prefix !== -1, 'Not a valid address prefix.'); + + bw.writeU8(prefix); + + if (this.version !== -1) { + bw.writeU8(this.version); + bw.writeU8(0); + } + + bw.writeBytes(this.hash); + bw.writeChecksum(hash256.digest); + + return bw.render(); + } + + /** + * Compile the address object to a base58 address. + * @param {{NetworkType|Network)?} network + * @returns {Base58Address} + * @throws Error on bad hash/prefix. + */ + + toBase58(network) { + return base58.encode(this.toRaw(network)); + } + + /** + * Compile the address object to a bech32 address. + * @param {{NetworkType|Network)?} network + * @returns {String} + * @throws Error on bad hash/prefix. + */ + + toBech32(network) { + const version = this.version; + const hash = this.hash; + + assert(version !== -1, + 'Cannot convert non-program address to bech32.'); + + network = Network.get(network); + + const hrp = network.addressPrefix.bech32; + + return bech32.encode(hrp, version, hash); + } + + /** + * Inject properties from string. + * @private + * @param {String} addr + * @param {(Network|NetworkType)?} network + * @returns {Address} + */ + + fromString(addr, network) { + assert(typeof addr === 'string'); + assert(addr.length > 0); + assert(addr.length <= 100); + + // If the address is mixed case, + // it can only ever be base58. + if (isMixedCase(addr)) + return this.fromBase58(addr, network); + + // Otherwise, it's most likely bech32. + try { + return this.fromBech32(addr, network); + } catch (e) { + return this.fromBase58(addr, network); + } + } + + /** + * Instantiate address from string. + * @param {String} addr + * @param {(Network|NetworkType)?} network + * @returns {Address} + */ + + static fromString(addr, network) { + return new this().fromString(addr, network); + } + + /** + * Convert the Address to a string. + * @param {(Network|NetworkType)?} network + * @returns {Base58Address} + */ + + toString(network) { + if (this.version !== -1) + return this.toBech32(network); + return this.toBase58(network); + } + + /** + * Inspect the Address. + * @returns {Object} + */ + + inspect() { + return ''; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @throws Parse error + */ + + fromRaw(data, network) { + const br = new BufferReader(data, true); + const prefix = br.readU8(); + + network = Network.fromAddress(prefix, network); + + const type = Address.getType(prefix, network); + + let version = -1; + if (type === Address.types.WITNESS) { + if (data.length > 38) + throw new Error('Address is too long.'); + + version = br.readU8(); + + if (br.readU8() !== 0) + throw new Error('Address version padding is non-zero.'); + } else { + if (data.length !== 25) + throw new Error('Address is too long.'); + } + + const hash = br.readBytes(br.left() - 4); + + br.verifyChecksum(hash256.digest); + + return this.fromHash(hash, type, version); + } + + /** + * Create an address object from a serialized address. + * @param {Buffer} data + * @returns {Address} + * @throws Parse error. + */ + + static fromRaw(data, network) { + return new this().fromRaw(data, network); + } + + /** + * Inject properties from base58 address. + * @private + * @param {Base58Address} data + * @param {Network?} network + * @throws Parse error + */ + + fromBase58(data, network) { + assert(typeof data === 'string'); + + if (data.length > 55) + throw new Error('Address is too long.'); + + return this.fromRaw(base58.decode(data), network); + } + + /** + * Create an address object from a base58 address. + * @param {Base58Address} data + * @param {Network?} network + * @returns {Address} + * @throws Parse error. + */ + + static fromBase58(data, network) { + return new this().fromBase58(data, network); + } + + /** + * Inject properties from bech32 address. + * @private + * @param {String} data + * @param {Network?} network + * @throws Parse error + */ + + fromBech32(data, network) { + const type = Address.types.WITNESS; + + assert(typeof data === 'string'); + + const addr = bech32.decode(data); + + Network.fromBech32(addr.hrp, network); + + return this.fromHash(addr.hash, type, addr.version); + } + + /** + * Create an address object from a bech32 address. + * @param {String} data + * @param {Network?} network + * @returns {Address} + * @throws Parse error. + */ + + static fromBech32(data, network) { + return new this().fromBech32(data, network); + } + + /** + * Inject properties from output script. + * @private + * @param {Script} script + */ + + fromScript(script) { + const pk = script.getPubkey(); + + if (pk) { + this.hash = hash160.digest(pk); + this.type = Address.types.PUBKEYHASH; + this.version = -1; + return this; + } + + const pkh = script.getPubkeyhash(); + + if (pkh) { + this.hash = pkh; + this.type = Address.types.PUBKEYHASH; + this.version = -1; + return this; + } + + const sh = script.getScripthash(); + + if (sh) { + this.hash = sh; + this.type = Address.types.SCRIPTHASH; + this.version = -1; + return this; + } + + const program = script.getProgram(); + + if (program && !program.isMalformed()) { + this.hash = program.data; + this.type = Address.types.WITNESS; + this.version = program.version; + return this; + } + + // Put this last: it's the slowest to check. + if (script.isMultisig()) { + this.hash = script.hash160(); + this.type = Address.types.SCRIPTHASH; + this.version = -1; + return this; + } + + return null; + } + + /** + * Inject properties from witness. + * @private + * @param {Witness} witness + */ + + fromWitness(witness) { + const [, pk] = witness.getPubkeyhashInput(); + + // We're pretty much screwed here + // since we can't get the version. + if (pk) { + this.hash = hash160.digest(pk); + this.type = Address.types.WITNESS; + this.version = 0; + return this; + } + + const redeem = witness.getScripthashInput(); + + if (redeem) { + this.hash = sha256.digest(redeem); + this.type = Address.types.WITNESS; + this.version = 0; + return this; + } + + return null; + } + + /** + * Inject properties from input script. + * @private + * @param {Script} script + */ + + fromInputScript(script) { + const [, pk] = script.getPubkeyhashInput(); + + if (pk) { + this.hash = hash160.digest(pk); + this.type = Address.types.PUBKEYHASH; + this.version = -1; + return this; + } + + const redeem = script.getScripthashInput(); + + if (redeem) { + this.hash = hash160.digest(redeem); + this.type = Address.types.SCRIPTHASH; + this.version = -1; + return this; + } + + return null; + } + + /** + * Create an Address from a witness. + * Attempt to extract address + * properties from a witness. + * @param {Witness} + * @returns {Address|null} + */ + + static fromWitness(witness) { + return new this().fromWitness(witness); + } + + /** + * Create an Address from an input script. + * Attempt to extract address + * properties from an input script. + * @param {Script} + * @returns {Address|null} + */ + + static fromInputScript(script) { + return new this().fromInputScript(script); + } + + /** + * Create an Address from an output script. + * Parse an output script and extract address + * properties. Converts pubkey and multisig + * scripts to pubkeyhash and scripthash addresses. + * @param {Script} + * @returns {Address|null} + */ + + static fromScript(script) { + return new this().fromScript(script); + } + + /** + * Inject properties from a hash. + * @private + * @param {Buffer|Hash} hash + * @param {AddressPrefix} type + * @param {Number} [version=-1] + * @throws on bad hash size + */ + + fromHash(hash, type, version) { + if (typeof hash === 'string') + hash = Buffer.from(hash, 'hex'); + + if (typeof type === 'string') { + type = Address.types[type.toUpperCase()]; + assert(type != null, 'Not a valid address type.'); + } + + if (type == null) + type = Address.types.PUBKEYHASH; + + if (version == null) + version = -1; + + assert(Buffer.isBuffer(hash)); + assert((type >>> 0) === type); + assert((version | 0) === version); + + assert(type >= Address.types.PUBKEYHASH && type <= Address.types.WITNESS, + 'Not a valid address type.'); + + if (version === -1) { + assert(type !== Address.types.WITNESS, 'Wrong version (witness)'); + assert(hash.length === 20, 'Hash is the wrong size.'); + } else { + assert(type === Address.types.WITNESS, 'Wrong version (non-witness).'); + assert(version >= 0 && version <= 16, 'Bad program version.'); + if (version === 0 && type === Address.types.WITNESS) { + assert(hash.length === 20 || hash.length === 32, + 'Witness program hash is the wrong size.'); + } + assert(hash.length >= 2 && hash.length <= 40, 'Hash is the wrong size.'); + } + + this.hash = hash; + this.type = type; + this.version = version; + + return this; + } + + /** + * Create a naked address from hash/type/version. + * @param {Hash} hash + * @param {AddressPrefix} type + * @param {Number} [version=-1] + * @returns {Address} + * @throws on bad hash size + */ + + static fromHash(hash, type, version) { + return new this().fromHash(hash, type, version); + } + + /** + * Inject properties from pubkeyhash. + * @private + * @param {Buffer} hash + * @returns {Address} + */ + + fromPubkeyhash(hash) { + const type = Address.types.PUBKEYHASH; + assert(hash.length === 20, 'P2PKH must be 20 bytes.'); + return this.fromHash(hash, type, -1); + } + + /** + * Instantiate address from pubkeyhash. + * @param {Buffer} hash + * @returns {Address} + */ + + static fromPubkeyhash(hash) { + return new this().fromPubkeyhash(hash); + } + + /** + * Inject properties from scripthash. + * @private + * @param {Buffer} hash + * @returns {Address} + */ + + fromScripthash(hash) { + const type = Address.types.SCRIPTHASH; + assert(hash && hash.length === 20, 'P2SH must be 20 bytes.'); + return this.fromHash(hash, type, -1); + } + + /** + * Instantiate address from scripthash. + * @param {Buffer} hash + * @returns {Address} + */ + + static fromScripthash(hash) { + return new this().fromScripthash(hash); + } + + /** + * Inject properties from witness pubkeyhash. + * @private + * @param {Buffer} hash + * @returns {Address} + */ + + fromWitnessPubkeyhash(hash) { + const type = Address.types.WITNESS; + assert(hash && hash.length === 20, 'P2WPKH must be 20 bytes.'); + return this.fromHash(hash, type, 0); + } + + /** + * Instantiate address from witness pubkeyhash. + * @param {Buffer} hash + * @returns {Address} + */ + + static fromWitnessPubkeyhash(hash) { + return new this().fromWitnessPubkeyhash(hash); + } + + /** + * Inject properties from witness scripthash. + * @private + * @param {Buffer} hash + * @returns {Address} + */ + + fromWitnessScripthash(hash) { + const type = Address.types.WITNESS; + assert(hash && hash.length === 32, 'P2WPKH must be 32 bytes.'); + return this.fromHash(hash, type, 0); + } + + /** + * Instantiate address from witness scripthash. + * @param {Buffer} hash + * @returns {Address} + */ + + static fromWitnessScripthash(hash) { + return new this().fromWitnessScripthash(hash); + } + + /** + * Inject properties from witness program. + * @private + * @param {Number} version + * @param {Buffer} hash + * @returns {Address} + */ + + fromProgram(version, hash) { + const type = Address.types.WITNESS; + + assert(version >= 0, 'Bad version for witness program.'); + + if (typeof hash === 'string') + hash = Buffer.from(hash, 'hex'); + + return this.fromHash(hash, type, version); + } + + /** + * Instantiate address from witness program. + * @param {Number} version + * @param {Buffer} hash + * @returns {Address} + */ + + static fromProgram(version, hash) { + return new this().fromProgram(version, hash); + } + + /** + * Test whether the address is pubkeyhash. + * @returns {Boolean} + */ + + isPubkeyhash() { + return this.type === Address.types.PUBKEYHASH; + } + + /** + * Test whether the address is scripthash. + * @returns {Boolean} + */ + + isScripthash() { + return this.type === Address.types.SCRIPTHASH; + } + + /** + * Test whether the address is witness pubkeyhash. + * @returns {Boolean} + */ + + isWitnessPubkeyhash() { + return this.version === 0 && this.hash.length === 20; + } + + /** + * Test whether the address is witness scripthash. + * @returns {Boolean} + */ + + isWitnessScripthash() { + return this.version === 0 && this.hash.length === 32; + } + + /** + * Test whether the address is witness masthash. + * @returns {Boolean} + */ + + isWitnessMasthash() { + return this.version === 1 && this.hash.length === 32; + } + + /** + * Test whether the address is a witness program. + * @returns {Boolean} + */ + + isProgram() { + return this.version !== -1; + } + + /** + * Test whether the address is an unknown witness program. + * @returns {Boolean} + */ + + isUnknown() { + if (this.version === -1) + return false; + + if (this.version > 0) + return true; + + return this.hash.length !== 20 && this.hash.length !== 32; + } + + /** + * Get the hash of a base58 address or address-related object. + * @param {String|Address|Hash} data + * @param {String?} enc + * @param {Network?} network + * @returns {Hash} + */ + + static getHash(data, enc, network) { + if (!data) + throw new Error('Object is not an address.'); + + let hash; + + if (typeof data === 'string') { + if (data.length === 40 || data.length === 64) + return enc === 'hex' ? data : Buffer.from(data, 'hex'); + + hash = Address.fromString(data, network).hash; + } else if (Buffer.isBuffer(data)) { + if (data.length !== 20 && data.length !== 32) + throw new Error('Object is not an address.'); + hash = data; + } else if (data instanceof Address) { + hash = data.hash; + } else { + throw new Error('Object is not an address.'); + } + + return enc === 'hex' + ? hash.toString('hex') + : hash; + } + + /** + * Get an address type for a specified network address prefix. + * @param {Number} prefix + * @param {Network} network + * @returns {AddressType} + */ + + static getType(prefix, network) { + const prefixes = network.addressPrefix; + + switch (prefix) { + case prefixes.pubkeyhash: + return Address.types.PUBKEYHASH; + case prefixes.scripthash: + return Address.types.SCRIPTHASH; + case prefixes.witnesspubkeyhash: + case prefixes.witnessscripthash: + return Address.types.WITNESS; + default: + throw new Error('Unknown address prefix.'); + } + } } /** @@ -64,816 +882,6 @@ Address.typesByVal = [ 'WITNESS' ]; -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -Address.prototype.fromOptions = function fromOptions(options, network) { - if (typeof options === 'string') - return this.fromString(options, network); - - return this.fromHash(options.hash, options.type, options.version); -}; - -/** - * Insantiate address from options. - * @param {Object} options - * @returns {Address} - */ - -Address.fromOptions = function fromOptions(options, network) { - return new Address().fromOptions(options, network); -}; - -/** - * Get the address hash. - * @param {String?} enc - Can be `"hex"` or `null`. - * @returns {Hash|Buffer} - */ - -Address.prototype.getHash = function getHash(enc) { - if (enc === 'hex') - return this.hash.toString(enc); - return this.hash; -}; - -/** - * Test whether the address is null. - * @returns {Boolean} - */ - -Address.prototype.isNull = function isNull() { - if (this.hash.length === 20) - return this.hash.equals(encoding.ZERO_HASH160); - - if (this.hash.length === 32) - return this.hash.equals(encoding.ZERO_HASH); - - for (let i = 0; i < this.hash.length; i++) { - if (this.hash[i] !== 0) - return false; - } - - return true; -}; - -/** - * Test equality against another address. - * @param {Address} addr - * @returns {Boolean} - */ - -Address.prototype.equals = function equals(addr) { - assert(addr instanceof Address); - - return this.type === addr.type - && this.version === addr.version - && this.hash.equals(addr.hash); -}; - -/** - * Get the address type as a string. - * @returns {String} - */ - -Address.prototype.getType = function getType() { - return Address.typesByVal[this.type].toLowerCase(); -}; - -/** - * Get a network address prefix for the address. - * @param {Network?} network - * @returns {Number} - */ - -Address.prototype.getPrefix = function getPrefix(network) { - network = Network.get(network); - - const prefixes = network.addressPrefix; - - switch (this.type) { - case Address.types.PUBKEYHASH: - return prefixes.pubkeyhash; - case Address.types.SCRIPTHASH: - return prefixes.scripthash; - case Address.types.WITNESS: - if (this.hash.length === 20) - return prefixes.witnesspubkeyhash; - - if (this.hash.length === 32) - return prefixes.witnessscripthash; - - break; - } - - return -1; -}; - -/** - * Calculate size of serialized address. - * @returns {Number} - */ - -Address.prototype.getSize = function getSize() { - let size = 5 + this.hash.length; - - if (this.version !== -1) - size += 2; - - return size; -}; - -/** - * Compile the address object to its raw serialization. - * @param {{NetworkType|Network)?} network - * @returns {Buffer} - * @throws Error on bad hash/prefix. - */ - -Address.prototype.toRaw = function toRaw(network) { - const size = this.getSize(); - const bw = new StaticWriter(size); - const prefix = this.getPrefix(network); - - assert(prefix !== -1, 'Not a valid address prefix.'); - - bw.writeU8(prefix); - - if (this.version !== -1) { - bw.writeU8(this.version); - bw.writeU8(0); - } - - bw.writeBytes(this.hash); - bw.writeChecksum(hash256.digest); - - return bw.render(); -}; - -/** - * Compile the address object to a base58 address. - * @param {{NetworkType|Network)?} network - * @returns {Base58Address} - * @throws Error on bad hash/prefix. - */ - -Address.prototype.toBase58 = function toBase58(network) { - return base58.encode(this.toRaw(network)); -}; - -/** - * Compile the address object to a bech32 address. - * @param {{NetworkType|Network)?} network - * @returns {String} - * @throws Error on bad hash/prefix. - */ - -Address.prototype.toBech32 = function toBech32(network) { - const version = this.version; - const hash = this.hash; - - assert(version !== -1, - 'Cannot convert non-program address to bech32.'); - - network = Network.get(network); - - const hrp = network.addressPrefix.bech32; - - return bech32.encode(hrp, version, hash); -}; - -/** - * Inject properties from string. - * @private - * @param {String} addr - * @param {(Network|NetworkType)?} network - * @returns {Address} - */ - -Address.prototype.fromString = function fromString(addr, network) { - assert(typeof addr === 'string'); - assert(addr.length > 0); - assert(addr.length <= 100); - - // If the address is mixed case, - // it can only ever be base58. - if (isMixedCase(addr)) - return this.fromBase58(addr, network); - - // Otherwise, it's most likely bech32. - try { - return this.fromBech32(addr, network); - } catch (e) { - return this.fromBase58(addr, network); - } -}; - -/** - * Instantiate address from string. - * @param {String} addr - * @param {(Network|NetworkType)?} network - * @returns {Address} - */ - -Address.fromString = function fromString(addr, network) { - return new Address().fromString(addr, network); -}; - -/** - * Convert the Address to a string. - * @param {(Network|NetworkType)?} network - * @returns {Base58Address} - */ - -Address.prototype.toString = function toString(network) { - if (this.version !== -1) - return this.toBech32(network); - return this.toBase58(network); -}; - -/** - * Inspect the Address. - * @returns {Object} - */ - -Address.prototype.inspect = function inspect() { - return ''; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - * @throws Parse error - */ - -Address.prototype.fromRaw = function fromRaw(data, network) { - const br = new BufferReader(data, true); - const prefix = br.readU8(); - - network = Network.fromAddress(prefix, network); - - const type = Address.getType(prefix, network); - - let version = -1; - if (type === Address.types.WITNESS) { - if (data.length > 38) - throw new Error('Address is too long.'); - - version = br.readU8(); - - if (br.readU8() !== 0) - throw new Error('Address version padding is non-zero.'); - } else { - if (data.length !== 25) - throw new Error('Address is too long.'); - } - - const hash = br.readBytes(br.left() - 4); - - br.verifyChecksum(hash256.digest); - - return this.fromHash(hash, type, version); -}; - -/** - * Create an address object from a serialized address. - * @param {Buffer} data - * @returns {Address} - * @throws Parse error. - */ - -Address.fromRaw = function fromRaw(data, network) { - return new Address().fromRaw(data, network); -}; - -/** - * Inject properties from base58 address. - * @private - * @param {Base58Address} data - * @param {Network?} network - * @throws Parse error - */ - -Address.prototype.fromBase58 = function fromBase58(data, network) { - assert(typeof data === 'string'); - - if (data.length > 55) - throw new Error('Address is too long.'); - - return this.fromRaw(base58.decode(data), network); -}; - -/** - * Create an address object from a base58 address. - * @param {Base58Address} data - * @param {Network?} network - * @returns {Address} - * @throws Parse error. - */ - -Address.fromBase58 = function fromBase58(data, network) { - return new Address().fromBase58(data, network); -}; - -/** - * Inject properties from bech32 address. - * @private - * @param {String} data - * @param {Network?} network - * @throws Parse error - */ - -Address.prototype.fromBech32 = function fromBech32(data, network) { - const type = Address.types.WITNESS; - - assert(typeof data === 'string'); - - const addr = bech32.decode(data); - - Network.fromBech32(addr.hrp, network); - - return this.fromHash(addr.hash, type, addr.version); -}; - -/** - * Create an address object from a bech32 address. - * @param {String} data - * @param {Network?} network - * @returns {Address} - * @throws Parse error. - */ - -Address.fromBech32 = function fromBech32(data, network) { - return new Address().fromBech32(data, network); -}; - -/** - * Inject properties from output script. - * @private - * @param {Script} script - */ - -Address.prototype.fromScript = function fromScript(script) { - const pk = script.getPubkey(); - - if (pk) { - this.hash = hash160.digest(pk); - this.type = Address.types.PUBKEYHASH; - this.version = -1; - return this; - } - - const pkh = script.getPubkeyhash(); - - if (pkh) { - this.hash = pkh; - this.type = Address.types.PUBKEYHASH; - this.version = -1; - return this; - } - - const sh = script.getScripthash(); - - if (sh) { - this.hash = sh; - this.type = Address.types.SCRIPTHASH; - this.version = -1; - return this; - } - - const program = script.getProgram(); - - if (program && !program.isMalformed()) { - this.hash = program.data; - this.type = Address.types.WITNESS; - this.version = program.version; - return this; - } - - // Put this last: it's the slowest to check. - if (script.isMultisig()) { - this.hash = script.hash160(); - this.type = Address.types.SCRIPTHASH; - this.version = -1; - return this; - } - - return null; -}; - -/** - * Inject properties from witness. - * @private - * @param {Witness} witness - */ - -Address.prototype.fromWitness = function fromWitness(witness) { - const [, pk] = witness.getPubkeyhashInput(); - - // We're pretty much screwed here - // since we can't get the version. - if (pk) { - this.hash = hash160.digest(pk); - this.type = Address.types.WITNESS; - this.version = 0; - return this; - } - - const redeem = witness.getScripthashInput(); - - if (redeem) { - this.hash = sha256.digest(redeem); - this.type = Address.types.WITNESS; - this.version = 0; - return this; - } - - return null; -}; - -/** - * Inject properties from input script. - * @private - * @param {Script} script - */ - -Address.prototype.fromInputScript = function fromInputScript(script) { - const [, pk] = script.getPubkeyhashInput(); - - if (pk) { - this.hash = hash160.digest(pk); - this.type = Address.types.PUBKEYHASH; - this.version = -1; - return this; - } - - const redeem = script.getScripthashInput(); - - if (redeem) { - this.hash = hash160.digest(redeem); - this.type = Address.types.SCRIPTHASH; - this.version = -1; - return this; - } - - return null; -}; - -/** - * Create an Address from a witness. - * Attempt to extract address - * properties from a witness. - * @param {Witness} - * @returns {Address|null} - */ - -Address.fromWitness = function fromWitness(witness) { - return new Address().fromWitness(witness); -}; - -/** - * Create an Address from an input script. - * Attempt to extract address - * properties from an input script. - * @param {Script} - * @returns {Address|null} - */ - -Address.fromInputScript = function fromInputScript(script) { - return new Address().fromInputScript(script); -}; - -/** - * Create an Address from an output script. - * Parse an output script and extract address - * properties. Converts pubkey and multisig - * scripts to pubkeyhash and scripthash addresses. - * @param {Script} - * @returns {Address|null} - */ - -Address.fromScript = function fromScript(script) { - return new Address().fromScript(script); -}; - -/** - * Inject properties from a hash. - * @private - * @param {Buffer|Hash} hash - * @param {AddressPrefix} type - * @param {Number} [version=-1] - * @throws on bad hash size - */ - -Address.prototype.fromHash = function fromHash(hash, type, version) { - if (typeof hash === 'string') - hash = Buffer.from(hash, 'hex'); - - if (typeof type === 'string') { - type = Address.types[type.toUpperCase()]; - assert(type != null, 'Not a valid address type.'); - } - - if (type == null) - type = Address.types.PUBKEYHASH; - - if (version == null) - version = -1; - - assert(Buffer.isBuffer(hash)); - assert((type >>> 0) === type); - assert((version | 0) === version); - - assert(type >= Address.types.PUBKEYHASH && type <= Address.types.WITNESS, - 'Not a valid address type.'); - - if (version === -1) { - assert(type !== Address.types.WITNESS, 'Wrong version (witness)'); - assert(hash.length === 20, 'Hash is the wrong size.'); - } else { - assert(type === Address.types.WITNESS, 'Wrong version (non-witness).'); - assert(version >= 0 && version <= 16, 'Bad program version.'); - if (version === 0 && type === Address.types.WITNESS) { - assert(hash.length === 20 || hash.length === 32, - 'Witness program hash is the wrong size.'); - } - assert(hash.length >= 2 && hash.length <= 40, 'Hash is the wrong size.'); - } - - this.hash = hash; - this.type = type; - this.version = version; - - return this; -}; - -/** - * Create a naked address from hash/type/version. - * @param {Hash} hash - * @param {AddressPrefix} type - * @param {Number} [version=-1] - * @returns {Address} - * @throws on bad hash size - */ - -Address.fromHash = function fromHash(hash, type, version) { - return new Address().fromHash(hash, type, version); -}; - -/** - * Inject properties from pubkeyhash. - * @private - * @param {Buffer} hash - * @returns {Address} - */ - -Address.prototype.fromPubkeyhash = function fromPubkeyhash(hash) { - const type = Address.types.PUBKEYHASH; - assert(hash.length === 20, 'P2PKH must be 20 bytes.'); - return this.fromHash(hash, type, -1); -}; - -/** - * Instantiate address from pubkeyhash. - * @param {Buffer} hash - * @returns {Address} - */ - -Address.fromPubkeyhash = function fromPubkeyhash(hash) { - return new Address().fromPubkeyhash(hash); -}; - -/** - * Inject properties from scripthash. - * @private - * @param {Buffer} hash - * @returns {Address} - */ - -Address.prototype.fromScripthash = function fromScripthash(hash) { - const type = Address.types.SCRIPTHASH; - assert(hash && hash.length === 20, 'P2SH must be 20 bytes.'); - return this.fromHash(hash, type, -1); -}; - -/** - * Instantiate address from scripthash. - * @param {Buffer} hash - * @returns {Address} - */ - -Address.fromScripthash = function fromScripthash(hash) { - return new Address().fromScripthash(hash); -}; - -/** - * Inject properties from witness pubkeyhash. - * @private - * @param {Buffer} hash - * @returns {Address} - */ - -Address.prototype.fromWitnessPubkeyhash = function fromWitnessPubkeyhash(hash) { - const type = Address.types.WITNESS; - assert(hash && hash.length === 20, 'P2WPKH must be 20 bytes.'); - return this.fromHash(hash, type, 0); -}; - -/** - * Instantiate address from witness pubkeyhash. - * @param {Buffer} hash - * @returns {Address} - */ - -Address.fromWitnessPubkeyhash = function fromWitnessPubkeyhash(hash) { - return new Address().fromWitnessPubkeyhash(hash); -}; - -/** - * Inject properties from witness scripthash. - * @private - * @param {Buffer} hash - * @returns {Address} - */ - -Address.prototype.fromWitnessScripthash = function fromWitnessScripthash(hash) { - const type = Address.types.WITNESS; - assert(hash && hash.length === 32, 'P2WPKH must be 32 bytes.'); - return this.fromHash(hash, type, 0); -}; - -/** - * Instantiate address from witness scripthash. - * @param {Buffer} hash - * @returns {Address} - */ - -Address.fromWitnessScripthash = function fromWitnessScripthash(hash) { - return new Address().fromWitnessScripthash(hash); -}; - -/** - * Inject properties from witness program. - * @private - * @param {Number} version - * @param {Buffer} hash - * @returns {Address} - */ - -Address.prototype.fromProgram = function fromProgram(version, hash) { - const type = Address.types.WITNESS; - - assert(version >= 0, 'Bad version for witness program.'); - - if (typeof hash === 'string') - hash = Buffer.from(hash, 'hex'); - - return this.fromHash(hash, type, version); -}; - -/** - * Instantiate address from witness program. - * @param {Number} version - * @param {Buffer} hash - * @returns {Address} - */ - -Address.fromProgram = function fromProgram(version, hash) { - return new Address().fromProgram(version, hash); -}; - -/** - * Test whether the address is pubkeyhash. - * @returns {Boolean} - */ - -Address.prototype.isPubkeyhash = function isPubkeyhash() { - return this.type === Address.types.PUBKEYHASH; -}; - -/** - * Test whether the address is scripthash. - * @returns {Boolean} - */ - -Address.prototype.isScripthash = function isScripthash() { - return this.type === Address.types.SCRIPTHASH; -}; - -/** - * Test whether the address is witness pubkeyhash. - * @returns {Boolean} - */ - -Address.prototype.isWitnessPubkeyhash = function isWitnessPubkeyhash() { - return this.version === 0 && this.hash.length === 20; -}; - -/** - * Test whether the address is witness scripthash. - * @returns {Boolean} - */ - -Address.prototype.isWitnessScripthash = function isWitnessScripthash() { - return this.version === 0 && this.hash.length === 32; -}; - -/** - * Test whether the address is witness masthash. - * @returns {Boolean} - */ - -Address.prototype.isWitnessMasthash = function isWitnessMasthash() { - return this.version === 1 && this.hash.length === 32; -}; - -/** - * Test whether the address is a witness program. - * @returns {Boolean} - */ - -Address.prototype.isProgram = function isProgram() { - return this.version !== -1; -}; - -/** - * Test whether the address is an unknown witness program. - * @returns {Boolean} - */ - -Address.prototype.isUnknown = function isUnknown() { - if (this.version === -1) - return false; - - if (this.version > 0) - return true; - - return this.hash.length !== 20 && this.hash.length !== 32; -}; - -/** - * Get the hash of a base58 address or address-related object. - * @param {String|Address|Hash} data - * @param {String?} enc - * @param {Network?} network - * @returns {Hash} - */ - -Address.getHash = function getHash(data, enc, network) { - if (!data) - throw new Error('Object is not an address.'); - - let hash; - - if (typeof data === 'string') { - if (data.length === 40 || data.length === 64) - return enc === 'hex' ? data : Buffer.from(data, 'hex'); - - hash = Address.fromString(data, network).hash; - } else if (Buffer.isBuffer(data)) { - if (data.length !== 20 && data.length !== 32) - throw new Error('Object is not an address.'); - hash = data; - } else if (data instanceof Address) { - hash = data.hash; - } else { - throw new Error('Object is not an address.'); - } - - return enc === 'hex' - ? hash.toString('hex') - : hash; -}; - -/** - * Get an address type for a specified network address prefix. - * @param {Number} prefix - * @param {Network} network - * @returns {AddressType} - */ - -Address.getType = function getType(prefix, network) { - const prefixes = network.addressPrefix; - switch (prefix) { - case prefixes.pubkeyhash: - return Address.types.PUBKEYHASH; - case prefixes.scripthash: - return Address.types.SCRIPTHASH; - case prefixes.witnesspubkeyhash: - case prefixes.witnessscripthash: - return Address.types.WITNESS; - default: - throw new Error('Unknown address prefix.'); - } -}; - /* * Helpers */ diff --git a/lib/primitives/block.js b/lib/primitives/block.js index 8489c6e1..7c9f8a1d 100644 --- a/lib/primitives/block.js +++ b/lib/primitives/block.js @@ -22,815 +22,819 @@ const Headers = require('./headers'); const Network = require('../protocol/network'); /** + * Block * Represents a full block. * @alias module:primitives.Block - * @constructor * @extends AbstractBlock - * @param {NakedBlock} options */ -function Block(options) { - if (!(this instanceof Block)) - return new Block(options); +class Block extends AbstractBlock { + /** + * Create a block. + * @constructor + * @param {Object} options + */ - AbstractBlock.call(this); + constructor(options) { + super(); - this.txs = []; + this.txs = []; - this._raw = null; - this._size = -1; - this._witness = -1; + this._raw = null; + this._size = -1; + this._witness = -1; - if (options) - this.fromOptions(options); -} + if (options) + this.fromOptions(options); + } -Object.setPrototypeOf(Block.prototype, AbstractBlock.prototype); + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ + fromOptions(options) { + this.parseOptions(options); -Block.prototype.fromOptions = function fromOptions(options) { - this.parseOptions(options); - - if (options.txs) { - assert(Array.isArray(options.txs)); - for (const tx of options.txs) { - assert(tx instanceof TX); - this.txs.push(tx); + if (options.txs) { + assert(Array.isArray(options.txs)); + for (const tx of options.txs) { + assert(tx instanceof TX); + this.txs.push(tx); + } } } -}; -/** - * Instantiate block from options. - * @param {Object} options - * @returns {Block} - */ + /** + * Instantiate block from options. + * @param {Object} options + * @returns {Block} + */ -Block.fromOptions = function fromOptions(options) { - return new Block().fromOptions(options); -}; + static fromOptions(options) { + return new this().fromOptions(options); + } -/** - * Clear any cached values. - * @param {Boolean?} all - Clear transactions. - */ + /** + * Clear any cached values. + * @param {Boolean?} all - Clear transactions. + */ -Block.prototype.refresh = function refresh(all) { - this._refresh(); + refresh(all) { + this._refresh(); - this._raw = null; - this._size = -1; - this._witness = -1; + this._raw = null; + this._size = -1; + this._witness = -1; - if (!all) - return; + if (!all) + return; - for (const tx of this.txs) - tx.refresh(); -}; + for (const tx of this.txs) + tx.refresh(); + } -/** - * Serialize the block. Include witnesses if present. - * @returns {Buffer} - */ + /** + * Serialize the block. Include witnesses if present. + * @returns {Buffer} + */ -Block.prototype.toRaw = function toRaw() { - return this.frame().data; -}; + toRaw() { + return this.frame().data; + } -/** - * Serialize the block, do not include witnesses. - * @returns {Buffer} - */ + /** + * Serialize the block, do not include witnesses. + * @returns {Buffer} + */ -Block.prototype.toNormal = function toNormal() { - if (this.hasWitness()) - return this.frameNormal().data; - return this.toRaw(); -}; + toNormal() { + if (this.hasWitness()) + return this.frameNormal().data; + return this.toRaw(); + } -/** - * Serialize the block. Include witnesses if present. - * @param {BufferWriter} bw - */ + /** + * Serialize the block. Include witnesses if present. + * @param {BufferWriter} bw + */ -Block.prototype.toWriter = function toWriter(bw) { - if (this.mutable) - return this.writeWitness(bw); + toWriter(bw) { + if (this.mutable) + return this.writeWitness(bw); - const raw = this.frame(); - bw.writeBytes(raw.data); + const raw = this.frame(); + bw.writeBytes(raw.data); - return bw; -}; - -/** - * Serialize the block, do not include witnesses. - * @param {BufferWriter} bw - */ - -Block.prototype.toNormalWriter = function toNormalWriter(bw) { - if (this.hasWitness()) { - this.writeNormal(bw); return bw; } - return this.toWriter(bw); -}; -/** - * Get the raw block serialization. - * Include witnesses if present. - * @private - * @returns {RawBlock} - */ + /** + * Serialize the block, do not include witnesses. + * @param {BufferWriter} bw + */ -Block.prototype.frame = function frame() { - if (this.mutable) { - assert(!this._raw); - return this.frameWitness(); + toNormalWriter(bw) { + if (this.hasWitness()) { + this.writeNormal(bw); + return bw; + } + return this.toWriter(bw); } - if (this._raw) { - assert(this._size >= 0); - assert(this._witness >= 0); - const raw = new RawBlock(this._size, this._witness); - raw.data = this._raw; + /** + * Get the raw block serialization. + * Include witnesses if present. + * @private + * @returns {RawBlock} + */ + + frame() { + if (this.mutable) { + assert(!this._raw); + return this.frameWitness(); + } + + if (this._raw) { + assert(this._size >= 0); + assert(this._witness >= 0); + const raw = new RawBlock(this._size, this._witness); + raw.data = this._raw; + return raw; + } + + const raw = this.frameWitness(); + + this._raw = raw.data; + this._size = raw.size; + this._witness = raw.witness; + return raw; } - const raw = this.frameWitness(); + /** + * Calculate real size and size of the witness bytes. + * @returns {Object} Contains `size` and `witness`. + */ - this._raw = raw.data; - this._size = raw.size; - this._witness = raw.witness; - - return raw; -}; - -/** - * Calculate real size and size of the witness bytes. - * @returns {Object} Contains `size` and `witness`. - */ - -Block.prototype.getSizes = function getSizes() { - if (this.mutable) - return this.getWitnessSizes(); - return this.frame(); -}; - -/** - * Calculate virtual block size. - * @returns {Number} Virtual size. - */ - -Block.prototype.getVirtualSize = function getVirtualSize() { - const scale = consensus.WITNESS_SCALE_FACTOR; - return (this.getWeight() + scale - 1) / scale | 0; -}; - -/** - * Calculate block weight. - * @returns {Number} weight - */ - -Block.prototype.getWeight = function getWeight() { - const raw = this.getSizes(); - const base = raw.size - raw.witness; - return base * (consensus.WITNESS_SCALE_FACTOR - 1) + raw.size; -}; - -/** - * Get real block size. - * @returns {Number} size - */ - -Block.prototype.getSize = function getSize() { - return this.getSizes().size; -}; - -/** - * Get base block size (without witness). - * @returns {Number} size - */ - -Block.prototype.getBaseSize = function getBaseSize() { - const raw = this.getSizes(); - return raw.size - raw.witness; -}; - -/** - * Test whether the block contains a - * transaction with a non-empty witness. - * @returns {Boolean} - */ - -Block.prototype.hasWitness = function hasWitness() { - if (this._witness !== -1) - return this._witness !== 0; - - for (const tx of this.txs) { - if (tx.hasWitness()) - return true; + getSizes() { + if (this.mutable) + return this.getWitnessSizes(); + return this.frame(); } - return false; -}; + /** + * Calculate virtual block size. + * @returns {Number} Virtual size. + */ -/** - * Test the block's transaction vector against a hash. - * @param {Hash} hash - * @returns {Boolean} - */ - -Block.prototype.hasTX = function hasTX(hash) { - return this.indexOf(hash) !== -1; -}; - -/** - * Find the index of a transaction in the block. - * @param {Hash} hash - * @returns {Number} index (-1 if not present). - */ - -Block.prototype.indexOf = function indexOf(hash) { - for (let i = 0; i < this.txs.length; i++) { - const tx = this.txs[i]; - if (tx.hash('hex') === hash) - return i; + getVirtualSize() { + const scale = consensus.WITNESS_SCALE_FACTOR; + return (this.getWeight() + scale - 1) / scale | 0; } - return -1; -}; + /** + * Calculate block weight. + * @returns {Number} weight + */ -/** - * Calculate merkle root. Returns null - * if merkle tree has been malleated. - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {Hash|null} - */ - -Block.prototype.createMerkleRoot = function createMerkleRoot(enc) { - const leaves = []; - - for (const tx of this.txs) - leaves.push(tx.hash()); - - const [root, malleated] = merkle.createRoot(hash256, leaves); - - if (malleated) - return null; - - return enc === 'hex' ? root.toString('hex') : root; -}; - -/** - * Create a witness nonce (for mining). - * @returns {Buffer} - */ - -Block.prototype.createWitnessNonce = function createWitnessNonce() { - return Buffer.from(encoding.ZERO_HASH); -}; - -/** - * Calculate commitment hash (the root of the - * witness merkle tree hashed with the witnessNonce). - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {Hash} - */ - -Block.prototype.createCommitmentHash = function createCommitmentHash(enc) { - const nonce = this.getWitnessNonce(); - const leaves = []; - - assert(nonce, 'No witness nonce present.'); - - leaves.push(encoding.ZERO_HASH); - - for (let i = 1; i < this.txs.length; i++) { - const tx = this.txs[i]; - leaves.push(tx.witnessHash()); + getWeight() { + const raw = this.getSizes(); + const base = raw.size - raw.witness; + return base * (consensus.WITNESS_SCALE_FACTOR - 1) + raw.size; } - const [root] = merkle.createRoot(hash256, leaves); + /** + * Get real block size. + * @returns {Number} size + */ - // Note: malleation check ignored here. - // assert(!malleated); + getSize() { + return this.getSizes().size; + } - const hash = hash256.root(root, nonce); + /** + * Get base block size (without witness). + * @returns {Number} size + */ - return enc === 'hex' - ? hash.toString('hex') - : hash; -}; + getBaseSize() { + const raw = this.getSizes(); + return raw.size - raw.witness; + } -/** - * Retrieve the merkle root from the block header. - * @param {String?} enc - * @returns {Hash} - */ + /** + * Test whether the block contains a + * transaction with a non-empty witness. + * @returns {Boolean} + */ -Block.prototype.getMerkleRoot = function getMerkleRoot(enc) { - if (enc === 'hex') - return this.merkleRoot; - return Buffer.from(this.merkleRoot, 'hex'); -}; + hasWitness() { + if (this._witness !== -1) + return this._witness !== 0; -/** - * Retrieve the witness nonce from the - * coinbase's witness vector (if present). - * @returns {Buffer|null} - */ - -Block.prototype.getWitnessNonce = function getWitnessNonce() { - if (this.txs.length === 0) - return null; - - const coinbase = this.txs[0]; - - if (coinbase.inputs.length !== 1) - return null; - - const input = coinbase.inputs[0]; - - if (input.witness.items.length !== 1) - return null; - - if (input.witness.items[0].length !== 32) - return null; - - return input.witness.items[0]; -}; - -/** - * Retrieve the commitment hash - * from the coinbase's outputs. - * @param {String?} enc - * @returns {Hash|null} - */ - -Block.prototype.getCommitmentHash = function getCommitmentHash(enc) { - if (this.txs.length === 0) - return null; - - const coinbase = this.txs[0]; - let hash; - - for (let i = coinbase.outputs.length - 1; i >= 0; i--) { - const output = coinbase.outputs[i]; - if (output.script.isCommitment()) { - hash = output.script.getCommitment(); - break; + for (const tx of this.txs) { + if (tx.hasWitness()) + return true; } + + return false; } - if (!hash) - return null; + /** + * Test the block's transaction vector against a hash. + * @param {Hash} hash + * @returns {Boolean} + */ - return enc === 'hex' - ? hash.toString('hex') - : hash; -}; - -/** - * Do non-contextual verification on the block. Including checking the block - * size, the coinbase and the merkle root. This is consensus-critical. - * @returns {Boolean} - */ - -Block.prototype.verifyBody = function verifyBody() { - const [valid] = this.checkBody(); - return valid; -}; - -/** - * Do non-contextual verification on the block. Including checking the block - * size, the coinbase and the merkle root. This is consensus-critical. - * @returns {Array} [valid, reason, score] - */ - -Block.prototype.checkBody = function checkBody() { - // Check merkle root. - const root = this.createMerkleRoot('hex'); - - // If the merkle is mutated, - // we have duplicate txs. - if (!root) - return [false, 'bad-txns-duplicate', 100]; - - if (this.merkleRoot !== root) - return [false, 'bad-txnmrklroot', 100]; - - // Check base size. - if (this.txs.length === 0 - || this.txs.length > consensus.MAX_BLOCK_SIZE - || this.getBaseSize() > consensus.MAX_BLOCK_SIZE) { - return [false, 'bad-blk-length', 100]; + hasTX(hash) { + return this.indexOf(hash) !== -1; } - // First TX must be a coinbase. - if (this.txs.length === 0 || !this.txs[0].isCoinbase()) - return [false, 'bad-cb-missing', 100]; + /** + * Find the index of a transaction in the block. + * @param {Hash} hash + * @returns {Number} index (-1 if not present). + */ - // Test all transactions. - const scale = consensus.WITNESS_SCALE_FACTOR; - let sigops = 0; + indexOf(hash) { + for (let i = 0; i < this.txs.length; i++) { + const tx = this.txs[i]; + if (tx.hash('hex') === hash) + return i; + } - for (let i = 0; i < this.txs.length; i++) { - const tx = this.txs[i]; - - // The rest of the txs must not be coinbases. - if (i > 0 && tx.isCoinbase()) - return [false, 'bad-cb-multiple', 100]; - - // Sanity checks. - const [valid, reason, score] = tx.checkSanity(); - - if (!valid) - return [valid, reason, score]; - - // Count legacy sigops (do not count scripthash or witness). - sigops += tx.getLegacySigops(); - if (sigops * scale > consensus.MAX_BLOCK_SIGOPS_COST) - return [false, 'bad-blk-sigops', 100]; - } - - return [true, 'valid', 0]; -}; - -/** - * Retrieve the coinbase height from the coinbase input script. - * @returns {Number} height (-1 if not present). - */ - -Block.prototype.getCoinbaseHeight = function getCoinbaseHeight() { - if (this.version < 2) return -1; - - if (this.txs.length === 0) - return -1; - - const coinbase = this.txs[0]; - - if (coinbase.inputs.length === 0) - return -1; - - return coinbase.inputs[0].script.getCoinbaseHeight(); -}; - -/** - * Get the "claimed" reward by the coinbase. - * @returns {Amount} claimed - */ - -Block.prototype.getClaimed = function getClaimed() { - assert(this.txs.length > 0); - assert(this.txs[0].isCoinbase()); - return this.txs[0].getOutputValue(); -}; - -/** - * Get all unique outpoint hashes in the - * block. Coinbases are ignored. - * @returns {Hash[]} Outpoint hashes. - */ - -Block.prototype.getPrevout = function getPrevout() { - const prevout = Object.create(null); - - for (let i = 1; i < this.txs.length; i++) { - const tx = this.txs[i]; - - for (const input of tx.inputs) - prevout[input.prevout.hash] = true; } - return Object.keys(prevout); -}; + /** + * Calculate merkle root. Returns null + * if merkle tree has been malleated. + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {Hash|null} + */ -/** - * Inspect the block and return a more - * user-friendly representation of the data. - * @returns {Object} - */ + createMerkleRoot(enc) { + const leaves = []; -Block.prototype.inspect = function inspect() { - return this.format(); -}; + for (const tx of this.txs) + leaves.push(tx.hash()); -/** - * Inspect the block and return a more - * user-friendly representation of the data. - * @param {CoinView} view - * @param {Number} height - * @returns {Object} - */ + const [root, malleated] = merkle.createRoot(hash256, leaves); -Block.prototype.format = function format(view, height) { - const commitmentHash = this.getCommitmentHash('hex'); - return { - hash: this.rhash(), - height: height != null ? height : -1, - size: this.getSize(), - virtualSize: this.getVirtualSize(), - date: util.date(this.time), - version: this.version.toString(16), - prevBlock: encoding.revHex(this.prevBlock), - merkleRoot: encoding.revHex(this.merkleRoot), - commitmentHash: commitmentHash - ? encoding.revHex(commitmentHash) - : null, - time: this.time, - bits: this.bits, - nonce: this.nonce, - txs: this.txs.map((tx, i) => { - return tx.format(view, null, i); - }) - }; -}; + if (malleated) + return null; -/** - * Convert the block to an object suitable - * for JSON serialization. - * @returns {Object} - */ - -Block.prototype.toJSON = function toJSON() { - return this.getJSON(); -}; - -/** - * Convert the block to an object suitable - * for JSON serialization. Note that the hashes - * will be reversed to abide by bitcoind's legacy - * of little-endian uint256s. - * @param {Network} network - * @param {CoinView} view - * @param {Number} height - * @param {Number} depth - * @returns {Object} - */ - -Block.prototype.getJSON = function getJSON(network, view, height, depth) { - network = Network.get(network); - return { - hash: this.rhash(), - height: height, - depth: depth, - version: this.version, - prevBlock: encoding.revHex(this.prevBlock), - merkleRoot: encoding.revHex(this.merkleRoot), - time: this.time, - bits: this.bits, - nonce: this.nonce, - txs: this.txs.map((tx, i) => { - return tx.getJSON(network, view, null, i); - }) - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - */ - -Block.prototype.fromJSON = function fromJSON(json) { - assert(json, 'Block data is required.'); - assert(Array.isArray(json.txs)); - - this.parseJSON(json); - - for (const tx of json.txs) - this.txs.push(TX.fromJSON(tx)); - - return this; -}; - -/** - * Instantiate a block from a jsonified block object. - * @param {Object} json - The jsonified block object. - * @returns {Block} - */ - -Block.fromJSON = function fromJSON(json) { - return new Block().fromJSON(json); -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -Block.prototype.fromReader = function fromReader(br) { - br.start(); - - this.readHead(br); - - const count = br.readVarint(); - let witness = 0; - - for (let i = 0; i < count; i++) { - const tx = TX.fromReader(br); - witness += tx._witness; - this.txs.push(tx); + return enc === 'hex' ? root.toString('hex') : root; } - if (!this.mutable) { - this._raw = br.endData(); - this._size = this._raw.length; - this._witness = witness; + /** + * Create a witness nonce (for mining). + * @returns {Buffer} + */ + + createWitnessNonce() { + return Buffer.from(encoding.ZERO_HASH); } - return this; -}; + /** + * Calculate commitment hash (the root of the + * witness merkle tree hashed with the witnessNonce). + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {Hash} + */ -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ + createCommitmentHash(enc) { + const nonce = this.getWitnessNonce(); + const leaves = []; -Block.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(new BufferReader(data)); -}; + assert(nonce, 'No witness nonce present.'); -/** - * Instantiate a block from a serialized Buffer. - * @param {Buffer} data - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {Block} - */ + leaves.push(encoding.ZERO_HASH); -Block.fromReader = function fromReader(data) { - return new Block().fromReader(data); -}; + for (let i = 1; i < this.txs.length; i++) { + const tx = this.txs[i]; + leaves.push(tx.witnessHash()); + } -/** - * Instantiate a block from a serialized Buffer. - * @param {Buffer} data - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {Block} - */ + const [root] = merkle.createRoot(hash256, leaves); -Block.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - return new Block().fromRaw(data); -}; + // Note: malleation check ignored here. + // assert(!malleated); -/** - * Convert the Block to a MerkleBlock. - * @param {Bloom} filter - Bloom filter for transactions - * to match. The merkle block will contain only the - * matched transactions. - * @returns {MerkleBlock} - */ + const hash = hash256.root(root, nonce); -Block.prototype.toMerkle = function toMerkle(filter) { - return MerkleBlock.fromBlock(this, filter); -}; - -/** - * Serialze block with or without witness data. - * @private - * @param {Boolean} witness - * @param {BufferWriter?} writer - * @returns {Buffer} - */ - -Block.prototype.writeNormal = function writeNormal(bw) { - this.writeHead(bw); - - bw.writeVarint(this.txs.length); - - for (const tx of this.txs) - tx.toNormalWriter(bw); - - return bw; -}; - -/** - * Serialze block with or without witness data. - * @private - * @param {Boolean} witness - * @param {BufferWriter?} writer - * @returns {Buffer} - */ - -Block.prototype.writeWitness = function writeWitness(bw) { - this.writeHead(bw); - - bw.writeVarint(this.txs.length); - - for (const tx of this.txs) - tx.toWriter(bw); - - return bw; -}; - -/** - * Serialze block with or without witness data. - * @private - * @param {Boolean} witness - * @param {BufferWriter?} writer - * @returns {Buffer} - */ - -Block.prototype.frameNormal = function frameNormal() { - const raw = this.getNormalSizes(); - const bw = new StaticWriter(raw.size); - this.writeNormal(bw); - raw.data = bw.render(); - return raw; -}; - -/** - * Serialze block without witness data. - * @private - * @param {BufferWriter?} writer - * @returns {Buffer} - */ - -Block.prototype.frameWitness = function frameWitness() { - const raw = this.getWitnessSizes(); - const bw = new StaticWriter(raw.size); - this.writeWitness(bw); - raw.data = bw.render(); - return raw; -}; - -/** - * Convert the block to a headers object. - * @returns {Headers} - */ - -Block.prototype.toHeaders = function toHeaders() { - return Headers.fromBlock(this); -}; - -/** - * Get real block size without witness. - * @returns {RawBlock} - */ - -Block.prototype.getNormalSizes = function getNormalSizes() { - let size = 0; - - size += 80; - size += encoding.sizeVarint(this.txs.length); - - for (const tx of this.txs) - size += tx.getBaseSize(); - - return new RawBlock(size, 0); -}; - -/** - * Get real block size with witness. - * @returns {RawBlock} - */ - -Block.prototype.getWitnessSizes = function getWitnessSizes() { - let size = 0; - let witness = 0; - - size += 80; - size += encoding.sizeVarint(this.txs.length); - - for (const tx of this.txs) { - const raw = tx.getSizes(); - size += raw.size; - witness += raw.witness; + return enc === 'hex' + ? hash.toString('hex') + : hash; } - return new RawBlock(size, witness); -}; + /** + * Retrieve the merkle root from the block header. + * @param {String?} enc + * @returns {Hash} + */ -/** - * Test whether an object is a Block. - * @param {Object} obj - * @returns {Boolean} - */ + getMerkleRoot(enc) { + if (enc === 'hex') + return this.merkleRoot; + return Buffer.from(this.merkleRoot, 'hex'); + } -Block.isBlock = function isBlock(obj) { - return obj instanceof Block; -}; + /** + * Retrieve the witness nonce from the + * coinbase's witness vector (if present). + * @returns {Buffer|null} + */ + + getWitnessNonce() { + if (this.txs.length === 0) + return null; + + const coinbase = this.txs[0]; + + if (coinbase.inputs.length !== 1) + return null; + + const input = coinbase.inputs[0]; + + if (input.witness.items.length !== 1) + return null; + + if (input.witness.items[0].length !== 32) + return null; + + return input.witness.items[0]; + } + + /** + * Retrieve the commitment hash + * from the coinbase's outputs. + * @param {String?} enc + * @returns {Hash|null} + */ + + getCommitmentHash(enc) { + if (this.txs.length === 0) + return null; + + const coinbase = this.txs[0]; + let hash; + + for (let i = coinbase.outputs.length - 1; i >= 0; i--) { + const output = coinbase.outputs[i]; + if (output.script.isCommitment()) { + hash = output.script.getCommitment(); + break; + } + } + + if (!hash) + return null; + + return enc === 'hex' + ? hash.toString('hex') + : hash; + } + + /** + * Do non-contextual verification on the block. Including checking the block + * size, the coinbase and the merkle root. This is consensus-critical. + * @returns {Boolean} + */ + + verifyBody() { + const [valid] = this.checkBody(); + return valid; + } + + /** + * Do non-contextual verification on the block. Including checking the block + * size, the coinbase and the merkle root. This is consensus-critical. + * @returns {Array} [valid, reason, score] + */ + + checkBody() { + // Check merkle root. + const root = this.createMerkleRoot('hex'); + + // If the merkle is mutated, + // we have duplicate txs. + if (!root) + return [false, 'bad-txns-duplicate', 100]; + + if (this.merkleRoot !== root) + return [false, 'bad-txnmrklroot', 100]; + + // Check base size. + if (this.txs.length === 0 + || this.txs.length > consensus.MAX_BLOCK_SIZE + || this.getBaseSize() > consensus.MAX_BLOCK_SIZE) { + return [false, 'bad-blk-length', 100]; + } + + // First TX must be a coinbase. + if (this.txs.length === 0 || !this.txs[0].isCoinbase()) + return [false, 'bad-cb-missing', 100]; + + // Test all transactions. + const scale = consensus.WITNESS_SCALE_FACTOR; + let sigops = 0; + + for (let i = 0; i < this.txs.length; i++) { + const tx = this.txs[i]; + + // The rest of the txs must not be coinbases. + if (i > 0 && tx.isCoinbase()) + return [false, 'bad-cb-multiple', 100]; + + // Sanity checks. + const [valid, reason, score] = tx.checkSanity(); + + if (!valid) + return [valid, reason, score]; + + // Count legacy sigops (do not count scripthash or witness). + sigops += tx.getLegacySigops(); + if (sigops * scale > consensus.MAX_BLOCK_SIGOPS_COST) + return [false, 'bad-blk-sigops', 100]; + } + + return [true, 'valid', 0]; + } + + /** + * Retrieve the coinbase height from the coinbase input script. + * @returns {Number} height (-1 if not present). + */ + + getCoinbaseHeight() { + if (this.version < 2) + return -1; + + if (this.txs.length === 0) + return -1; + + const coinbase = this.txs[0]; + + if (coinbase.inputs.length === 0) + return -1; + + return coinbase.inputs[0].script.getCoinbaseHeight(); + } + + /** + * Get the "claimed" reward by the coinbase. + * @returns {Amount} claimed + */ + + getClaimed() { + assert(this.txs.length > 0); + assert(this.txs[0].isCoinbase()); + return this.txs[0].getOutputValue(); + } + + /** + * Get all unique outpoint hashes in the + * block. Coinbases are ignored. + * @returns {Hash[]} Outpoint hashes. + */ + + getPrevout() { + const prevout = Object.create(null); + + for (let i = 1; i < this.txs.length; i++) { + const tx = this.txs[i]; + + for (const input of tx.inputs) + prevout[input.prevout.hash] = true; + } + + return Object.keys(prevout); + } + + /** + * Inspect the block and return a more + * user-friendly representation of the data. + * @returns {Object} + */ + + inspect() { + return this.format(); + } + + /** + * Inspect the block and return a more + * user-friendly representation of the data. + * @param {CoinView} view + * @param {Number} height + * @returns {Object} + */ + + format(view, height) { + const commitmentHash = this.getCommitmentHash('hex'); + return { + hash: this.rhash(), + height: height != null ? height : -1, + size: this.getSize(), + virtualSize: this.getVirtualSize(), + date: util.date(this.time), + version: this.version.toString(16), + prevBlock: encoding.revHex(this.prevBlock), + merkleRoot: encoding.revHex(this.merkleRoot), + commitmentHash: commitmentHash + ? encoding.revHex(commitmentHash) + : null, + time: this.time, + bits: this.bits, + nonce: this.nonce, + txs: this.txs.map((tx, i) => { + return tx.format(view, null, i); + }) + }; + } + + /** + * Convert the block to an object suitable + * for JSON serialization. + * @returns {Object} + */ + + toJSON() { + return this.getJSON(); + } + + /** + * Convert the block to an object suitable + * for JSON serialization. Note that the hashes + * will be reversed to abide by bitcoind's legacy + * of little-endian uint256s. + * @param {Network} network + * @param {CoinView} view + * @param {Number} height + * @param {Number} depth + * @returns {Object} + */ + + getJSON(network, view, height, depth) { + network = Network.get(network); + return { + hash: this.rhash(), + height: height, + depth: depth, + version: this.version, + prevBlock: encoding.revHex(this.prevBlock), + merkleRoot: encoding.revHex(this.merkleRoot), + time: this.time, + bits: this.bits, + nonce: this.nonce, + txs: this.txs.map((tx, i) => { + return tx.getJSON(network, view, null, i); + }) + }; + } + + /** + * Inject properties from json object. + * @private + * @param {Object} json + */ + + fromJSON(json) { + assert(json, 'Block data is required.'); + assert(Array.isArray(json.txs)); + + this.parseJSON(json); + + for (const tx of json.txs) + this.txs.push(TX.fromJSON(tx)); + + return this; + } + + /** + * Instantiate a block from a jsonified block object. + * @param {Object} json - The jsonified block object. + * @returns {Block} + */ + + static fromJSON(json) { + return new this().fromJSON(json); + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromReader(br) { + br.start(); + + this.readHead(br); + + const count = br.readVarint(); + let witness = 0; + + for (let i = 0; i < count; i++) { + const tx = TX.fromReader(br); + witness += tx._witness; + this.txs.push(tx); + } + + if (!this.mutable) { + this._raw = br.endData(); + this._size = this._raw.length; + this._witness = witness; + } + + return this; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + return this.fromReader(new BufferReader(data)); + } + + /** + * Instantiate a block from a serialized Buffer. + * @param {Buffer} data + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {Block} + */ + + static fromReader(data) { + return new this().fromReader(data); + } + + /** + * Instantiate a block from a serialized Buffer. + * @param {Buffer} data + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {Block} + */ + + static fromRaw(data, enc) { + if (typeof data === 'string') + data = Buffer.from(data, enc); + return new this().fromRaw(data); + } + + /** + * Convert the Block to a MerkleBlock. + * @param {Bloom} filter - Bloom filter for transactions + * to match. The merkle block will contain only the + * matched transactions. + * @returns {MerkleBlock} + */ + + toMerkle(filter) { + return MerkleBlock.fromBlock(this, filter); + } + + /** + * Serialze block with or without witness data. + * @private + * @param {Boolean} witness + * @param {BufferWriter?} writer + * @returns {Buffer} + */ + + writeNormal(bw) { + this.writeHead(bw); + + bw.writeVarint(this.txs.length); + + for (const tx of this.txs) + tx.toNormalWriter(bw); + + return bw; + } + + /** + * Serialze block with or without witness data. + * @private + * @param {Boolean} witness + * @param {BufferWriter?} writer + * @returns {Buffer} + */ + + writeWitness(bw) { + this.writeHead(bw); + + bw.writeVarint(this.txs.length); + + for (const tx of this.txs) + tx.toWriter(bw); + + return bw; + } + + /** + * Serialze block with or without witness data. + * @private + * @param {Boolean} witness + * @param {BufferWriter?} writer + * @returns {Buffer} + */ + + frameNormal() { + const raw = this.getNormalSizes(); + const bw = new StaticWriter(raw.size); + this.writeNormal(bw); + raw.data = bw.render(); + return raw; + } + + /** + * Serialze block without witness data. + * @private + * @param {BufferWriter?} writer + * @returns {Buffer} + */ + + frameWitness() { + const raw = this.getWitnessSizes(); + const bw = new StaticWriter(raw.size); + this.writeWitness(bw); + raw.data = bw.render(); + return raw; + } + + /** + * Convert the block to a headers object. + * @returns {Headers} + */ + + toHeaders() { + return Headers.fromBlock(this); + } + + /** + * Get real block size without witness. + * @returns {RawBlock} + */ + + getNormalSizes() { + let size = 0; + + size += 80; + size += encoding.sizeVarint(this.txs.length); + + for (const tx of this.txs) + size += tx.getBaseSize(); + + return new RawBlock(size, 0); + } + + /** + * Get real block size with witness. + * @returns {RawBlock} + */ + + getWitnessSizes() { + let size = 0; + let witness = 0; + + size += 80; + size += encoding.sizeVarint(this.txs.length); + + for (const tx of this.txs) { + const raw = tx.getSizes(); + size += raw.size; + witness += raw.witness; + } + + return new RawBlock(size, witness); + } + + /** + * Test whether an object is a Block. + * @param {Object} obj + * @returns {Boolean} + */ + + static isBlock(obj) { + return obj instanceof Block; + } +} /* * Helpers */ -function RawBlock(size, witness) { - this.data = null; - this.size = size; - this.witness = witness; +class RawBlock { + constructor(size, witness) { + this.data = null; + this.size = size; + this.witness = witness; + } } /* diff --git a/lib/primitives/coin.js b/lib/primitives/coin.js index 9e6f8a24..a0d6406c 100644 --- a/lib/primitives/coin.js +++ b/lib/primitives/coin.js @@ -8,427 +8,429 @@ 'use strict'; const assert = require('assert'); +const BufferReader = require('bufio/lib/reader'); +const StaticWriter = require('bufio/lib/staticwriter'); +const encoding = require('bufio/lib/encoding'); const Amount = require('../btc/amount'); const Output = require('./output'); const Script = require('../script/script'); const Network = require('../protocol/network'); -const BufferReader = require('bufio/lib/reader'); -const StaticWriter = require('bufio/lib/staticwriter'); -const encoding = require('bufio/lib/encoding'); /** + * Coin * Represents an unspent output. * @alias module:primitives.Coin - * @constructor * @extends Output - * @param {NakedCoin|Coin} options - * @property {Number} version - Transaction version. - * @property {Number} height - Transaction height (-1 if unconfirmed). - * @property {Amount} value - Output value in satoshis. - * @property {Script} script - Output script. - * @property {Boolean} coinbase - Whether the containing - * transaction is a coinbase. - * @property {Hash} hash - Transaction hash. - * @property {Number} index - Output index. + * @property {Number} version + * @property {Number} height + * @property {Amount} value + * @property {Script} script + * @property {Boolean} coinbase + * @property {Hash} hash + * @property {Number} index */ -function Coin(options) { - if (!(this instanceof Coin)) - return new Coin(options); +class Coin extends Output { + /** + * Create a coin. + * @constructor + * @param {Object} options + */ - this.version = 1; - this.height = -1; - this.value = 0; - this.script = new Script(); - this.coinbase = false; - this.hash = encoding.NULL_HASH; - this.index = 0; + constructor(options) { + super(); - if (options) - this.fromOptions(options); -} - -Object.setPrototypeOf(Coin.prototype, Output.prototype); - -/** - * Inject options into coin. - * @private - * @param {Object} options - */ - -Coin.prototype.fromOptions = function fromOptions(options) { - assert(options, 'Coin data is required.'); - - if (options.version != null) { - assert((options.version >>> 0) === options.version, - 'Version must be a uint32.'); - this.version = options.version; - } - - if (options.height != null) { - if (options.height !== -1) { - assert((options.height >>> 0) === options.height, - 'Height must be a uint32.'); - this.height = options.height; - } else { - this.height = -1; - } - } - - if (options.value != null) { - assert(Number.isSafeInteger(options.value) && options.value >= 0, - 'Value must be a uint64.'); - this.value = options.value; - } - - if (options.script) - this.script.fromOptions(options.script); - - if (options.coinbase != null) { - assert(typeof options.coinbase === 'boolean', - 'Coinbase must be a boolean.'); - this.coinbase = options.coinbase; - } - - if (options.hash != null) { - assert(typeof options.hash === 'string', 'Hash must be a string.'); - this.hash = options.hash; - } - - if (options.index != null) { - assert((options.index >>> 0) === options.index, 'Index must be a uint32.'); - this.index = options.index; - } - - return this; -}; - -/** - * Instantiate Coin from options object. - * @private - * @param {Object} options - */ - -Coin.fromOptions = function fromOptions(options) { - return new Coin().fromOptions(options); -}; - -/** - * Clone the coin. - * @private - * @returns {Coin} - */ - -Coin.prototype.clone = function clone() { - assert(false, 'Coins are not cloneable.'); -}; - -/** - * Calculate number of confirmations since coin was created. - * @param {Number?} height - Current chain height. Network - * height is used if not passed in. - * @return {Number} - */ - -Coin.prototype.getDepth = function getDepth(height) { - assert(typeof height === 'number', 'Must pass a height.'); - - if (this.height === -1) - return 0; - - if (height === -1) - return 0; - - if (height < this.height) - return 0; - - return height - this.height + 1; -}; - -/** - * Serialize coin to a key - * suitable for a hash table. - * @returns {String} - */ - -Coin.prototype.toKey = function toKey() { - return this.hash + this.index; -}; - -/** - * Inject properties from hash table key. - * @private - * @param {String} key - * @returns {Coin} - */ - -Coin.prototype.fromKey = function fromKey(key) { - assert(key.length > 64); - this.hash = key.slice(0, 64); - this.index = parseInt(key.slice(64), 10); - return this; -}; - -/** - * Instantiate coin from hash table key. - * @param {String} key - * @returns {Coin} - */ - -Coin.fromKey = function fromKey(key) { - return new Coin().fromKey(key); -}; - -/** - * Get little-endian hash. - * @returns {Hash} - */ - -Coin.prototype.rhash = function rhash() { - return encoding.revHex(this.hash); -}; - -/** - * Get little-endian hash. - * @returns {Hash} - */ - -Coin.prototype.txid = function txid() { - return this.rhash(); -}; - -/** - * Convert the coin to a more user-friendly object. - * @returns {Object} - */ - -Coin.prototype.inspect = function inspect() { - return { - type: this.getType(), - version: this.version, - height: this.height, - value: Amount.btc(this.value), - script: this.script, - coinbase: this.coinbase, - hash: this.hash ? encoding.revHex(this.hash) : null, - index: this.index, - address: this.getAddress() - }; -}; - -/** - * Convert the coin to an object suitable - * for JSON serialization. - * @returns {Object} - */ - -Coin.prototype.toJSON = function toJSON() { - return this.getJSON(); -}; - -/** - * Convert the coin to an object suitable - * for JSON serialization. Note that the hash - * will be reversed to abide by bitcoind's legacy - * of little-endian uint256s. - * @param {Network} network - * @param {Boolean} minimal - * @returns {Object} - */ - -Coin.prototype.getJSON = function getJSON(network, minimal) { - let addr = this.getAddress(); - - network = Network.get(network); - - if (addr) - addr = addr.toString(network); - - return { - version: this.version, - height: this.height, - value: this.value, - script: this.script.toJSON(), - address: addr, - coinbase: this.coinbase, - hash: !minimal ? this.rhash() : undefined, - index: !minimal ? this.index : undefined - }; -}; - -/** - * Inject JSON properties into coin. - * @private - * @param {Object} json - */ - -Coin.prototype.fromJSON = function fromJSON(json) { - assert(json, 'Coin data required.'); - assert((json.version >>> 0) === json.version, 'Version must be a uint32.'); - assert(json.height === -1 || (json.height >>> 0) === json.height, - 'Height must be a uint32.'); - assert(Number.isSafeInteger(json.value) && json.value >= 0, - 'Value must be a uint64.'); - assert(typeof json.coinbase === 'boolean', 'Coinbase must be a boolean.'); - - this.version = json.version; - this.height = json.height; - this.value = json.value; - this.script.fromJSON(json.script); - this.coinbase = json.coinbase; - - if (json.hash != null) { - assert(typeof json.hash === 'string', 'Hash must be a string.'); - assert(json.hash.length === 64, 'Hash must be a string.'); - assert((json.index >>> 0) === json.index, 'Index must be a uint32.'); - this.hash = encoding.revHex(json.hash); - this.index = json.index; - } - - return this; -}; - -/** - * Instantiate an Coin from a jsonified coin object. - * @param {Object} json - The jsonified coin object. - * @returns {Coin} - */ - -Coin.fromJSON = function fromJSON(json) { - return new Coin().fromJSON(json); -}; - -/** - * Calculate size of coin. - * @returns {Number} - */ - -Coin.prototype.getSize = function getSize() { - return 17 + this.script.getVarSize(); -}; - -/** - * Write the coin to a buffer writer. - * @param {BufferWriter} bw - */ - -Coin.prototype.toWriter = function toWriter(bw) { - let height = this.height; - - if (height === -1) - height = 0x7fffffff; - - bw.writeU32(this.version); - bw.writeU32(height); - bw.writeI64(this.value); - bw.writeVarBytes(this.script.toRaw()); - bw.writeU8(this.coinbase ? 1 : 0); - - return bw; -}; - -/** - * Serialize the coin. - * @returns {Buffer|String} - */ - -Coin.prototype.toRaw = function toRaw() { - const size = this.getSize(); - return this.toWriter(new StaticWriter(size)).render(); -}; - -/** - * Inject properties from serialized buffer writer. - * @private - * @param {BufferReader} br - */ - -Coin.prototype.fromReader = function fromReader(br) { - this.version = br.readU32(); - this.height = br.readU32(); - this.value = br.readI64(); - this.script.fromRaw(br.readVarBytes()); - this.coinbase = br.readU8() === 1; - - if (this.height === 0x7fffffff) + this.version = 1; this.height = -1; + this.coinbase = false; + this.hash = encoding.NULL_HASH; + this.index = 0; - return this; -}; + if (options) + this.fromOptions(options); + } -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ + /** + * Inject options into coin. + * @private + * @param {Object} options + */ -Coin.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(new BufferReader(data)); -}; + fromOptions(options) { + assert(options, 'Coin data is required.'); -/** - * Instantiate a coin from a buffer reader. - * @param {BufferReader} br - * @returns {Coin} - */ + if (options.version != null) { + assert((options.version >>> 0) === options.version, + 'Version must be a uint32.'); + this.version = options.version; + } -Coin.fromReader = function fromReader(br) { - return new Coin().fromReader(br); -}; + if (options.height != null) { + if (options.height !== -1) { + assert((options.height >>> 0) === options.height, + 'Height must be a uint32.'); + this.height = options.height; + } else { + this.height = -1; + } + } -/** - * Instantiate a coin from a serialized Buffer. - * @param {Buffer} data - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {Coin} - */ + if (options.value != null) { + assert(Number.isSafeInteger(options.value) && options.value >= 0, + 'Value must be a uint64.'); + this.value = options.value; + } -Coin.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - return new Coin().fromRaw(data); -}; + if (options.script) + this.script.fromOptions(options.script); -/** - * Inject properties from TX. - * @param {TX} tx - * @param {Number} index - */ + if (options.coinbase != null) { + assert(typeof options.coinbase === 'boolean', + 'Coinbase must be a boolean.'); + this.coinbase = options.coinbase; + } -Coin.prototype.fromTX = function fromTX(tx, index, height) { - assert(typeof index === 'number'); - assert(typeof height === 'number'); - assert(index >= 0 && index < tx.outputs.length); - this.version = tx.version; - this.height = height; - this.value = tx.outputs[index].value; - this.script = tx.outputs[index].script; - this.coinbase = tx.isCoinbase(); - this.hash = tx.hash('hex'); - this.index = index; - return this; -}; + if (options.hash != null) { + assert(typeof options.hash === 'string', 'Hash must be a string.'); + this.hash = options.hash; + } -/** - * Instantiate a coin from a TX - * @param {TX} tx - * @param {Number} index - Output index. - * @returns {Coin} - */ + if (options.index != null) { + assert((options.index >>> 0) === options.index, + 'Index must be a uint32.'); + this.index = options.index; + } -Coin.fromTX = function fromTX(tx, index, height) { - return new Coin().fromTX(tx, index, height); -}; + return this; + } -/** - * Test an object to see if it is a Coin. - * @param {Object} obj - * @returns {Boolean} - */ + /** + * Instantiate Coin from options object. + * @private + * @param {Object} options + */ -Coin.isCoin = function isCoin(obj) { - return obj instanceof Coin; -}; + static fromOptions(options) { + return new this().fromOptions(options); + } + + /** + * Clone the coin. + * @private + * @returns {Coin} + */ + + clone() { + assert(false, 'Coins are not cloneable.'); + } + + /** + * Calculate number of confirmations since coin was created. + * @param {Number?} height - Current chain height. Network + * height is used if not passed in. + * @return {Number} + */ + + getDepth(height) { + assert(typeof height === 'number', 'Must pass a height.'); + + if (this.height === -1) + return 0; + + if (height === -1) + return 0; + + if (height < this.height) + return 0; + + return height - this.height + 1; + } + + /** + * Serialize coin to a key + * suitable for a hash table. + * @returns {String} + */ + + toKey() { + return this.hash + this.index; + } + + /** + * Inject properties from hash table key. + * @private + * @param {String} key + * @returns {Coin} + */ + + fromKey(key) { + assert(key.length > 64); + this.hash = key.slice(0, 64); + this.index = parseInt(key.slice(64), 10); + return this; + } + + /** + * Instantiate coin from hash table key. + * @param {String} key + * @returns {Coin} + */ + + static fromKey(key) { + return new this().fromKey(key); + } + + /** + * Get little-endian hash. + * @returns {Hash} + */ + + rhash() { + return encoding.revHex(this.hash); + } + + /** + * Get little-endian hash. + * @returns {Hash} + */ + + txid() { + return this.rhash(); + } + + /** + * Convert the coin to a more user-friendly object. + * @returns {Object} + */ + + inspect() { + return { + type: this.getType(), + version: this.version, + height: this.height, + value: Amount.btc(this.value), + script: this.script, + coinbase: this.coinbase, + hash: this.hash ? encoding.revHex(this.hash) : null, + index: this.index, + address: this.getAddress() + }; + } + + /** + * Convert the coin to an object suitable + * for JSON serialization. + * @returns {Object} + */ + + toJSON() { + return this.getJSON(); + } + + /** + * Convert the coin to an object suitable + * for JSON serialization. Note that the hash + * will be reversed to abide by bitcoind's legacy + * of little-endian uint256s. + * @param {Network} network + * @param {Boolean} minimal + * @returns {Object} + */ + + getJSON(network, minimal) { + let addr = this.getAddress(); + + network = Network.get(network); + + if (addr) + addr = addr.toString(network); + + return { + version: this.version, + height: this.height, + value: this.value, + script: this.script.toJSON(), + address: addr, + coinbase: this.coinbase, + hash: !minimal ? this.rhash() : undefined, + index: !minimal ? this.index : undefined + }; + } + + /** + * Inject JSON properties into coin. + * @private + * @param {Object} json + */ + + fromJSON(json) { + assert(json, 'Coin data required.'); + assert((json.version >>> 0) === json.version, 'Version must be a uint32.'); + assert(json.height === -1 || (json.height >>> 0) === json.height, + 'Height must be a uint32.'); + assert(Number.isSafeInteger(json.value) && json.value >= 0, + 'Value must be a uint64.'); + assert(typeof json.coinbase === 'boolean', 'Coinbase must be a boolean.'); + + this.version = json.version; + this.height = json.height; + this.value = json.value; + this.script.fromJSON(json.script); + this.coinbase = json.coinbase; + + if (json.hash != null) { + assert(typeof json.hash === 'string', 'Hash must be a string.'); + assert(json.hash.length === 64, 'Hash must be a string.'); + assert((json.index >>> 0) === json.index, 'Index must be a uint32.'); + this.hash = encoding.revHex(json.hash); + this.index = json.index; + } + + return this; + } + + /** + * Instantiate an Coin from a jsonified coin object. + * @param {Object} json - The jsonified coin object. + * @returns {Coin} + */ + + static fromJSON(json) { + return new this().fromJSON(json); + } + + /** + * Calculate size of coin. + * @returns {Number} + */ + + getSize() { + return 17 + this.script.getVarSize(); + } + + /** + * Write the coin to a buffer writer. + * @param {BufferWriter} bw + */ + + toWriter(bw) { + let height = this.height; + + if (height === -1) + height = 0x7fffffff; + + bw.writeU32(this.version); + bw.writeU32(height); + bw.writeI64(this.value); + bw.writeVarBytes(this.script.toRaw()); + bw.writeU8(this.coinbase ? 1 : 0); + + return bw; + } + + /** + * Serialize the coin. + * @returns {Buffer|String} + */ + + toRaw() { + const size = this.getSize(); + return this.toWriter(new StaticWriter(size)).render(); + } + + /** + * Inject properties from serialized buffer writer. + * @private + * @param {BufferReader} br + */ + + fromReader(br) { + this.version = br.readU32(); + this.height = br.readU32(); + this.value = br.readI64(); + this.script.fromRaw(br.readVarBytes()); + this.coinbase = br.readU8() === 1; + + if (this.height === 0x7fffffff) + this.height = -1; + + return this; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + return this.fromReader(new BufferReader(data)); + } + + /** + * Instantiate a coin from a buffer reader. + * @param {BufferReader} br + * @returns {Coin} + */ + + static fromReader(br) { + return new this().fromReader(br); + } + + /** + * Instantiate a coin from a serialized Buffer. + * @param {Buffer} data + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {Coin} + */ + + static fromRaw(data, enc) { + if (typeof data === 'string') + data = Buffer.from(data, enc); + return new this().fromRaw(data); + } + + /** + * Inject properties from TX. + * @param {TX} tx + * @param {Number} index + */ + + fromTX(tx, index, height) { + assert(typeof index === 'number'); + assert(typeof height === 'number'); + assert(index >= 0 && index < tx.outputs.length); + this.version = tx.version; + this.height = height; + this.value = tx.outputs[index].value; + this.script = tx.outputs[index].script; + this.coinbase = tx.isCoinbase(); + this.hash = tx.hash('hex'); + this.index = index; + return this; + } + + /** + * Instantiate a coin from a TX + * @param {TX} tx + * @param {Number} index - Output index. + * @returns {Coin} + */ + + static fromTX(tx, index, height) { + return new this().fromTX(tx, index, height); + } + + /** + * Test an object to see if it is a Coin. + * @param {Object} obj + * @returns {Boolean} + */ + + static isCoin(obj) { + return obj instanceof Coin; + } +} /* * Expose diff --git a/lib/primitives/headers.js b/lib/primitives/headers.js index c37878b2..a2c799d8 100644 --- a/lib/primitives/headers.js +++ b/lib/primitives/headers.js @@ -14,262 +14,264 @@ const BufferReader = require('bufio/lib/reader'); const encoding = require('bufio/lib/encoding'); /** - * Represents block headers obtained from the network via `headers`. + * Headers + * Represents block headers obtained + * from the network via `headers`. * @alias module:primitives.Headers - * @constructor * @extends AbstractBlock - * @param {NakedBlock} options */ -function Headers(options) { - if (!(this instanceof Headers)) - return new Headers(options); +class Headers extends AbstractBlock { + /** + * Create headers. + * @constructor + * @param {Object} options + */ - AbstractBlock.call(this); + constructor(options) { + super(); - if (options) - this.parseOptions(options); + if (options) + this.parseOptions(options); + } + + /** + * Perform non-contextual + * verification on the headers. + * @returns {Boolean} + */ + + verifyBody() { + return true; + } + + /** + * Get size of the headers. + * @returns {Number} + */ + + getSize() { + return 81; + } + + /** + * Serialize the headers to a buffer writer. + * @param {BufferWriter} bw + */ + + toWriter(bw) { + this.writeHead(bw); + bw.writeVarint(0); + return bw; + } + + /** + * Serialize the headers. + * @returns {Buffer|String} + */ + + toRaw() { + const size = this.getSize(); + return this.toWriter(new StaticWriter(size)).render(); + } + + /** + * Inject properties from buffer reader. + * @private + * @param {Buffer} data + */ + + fromReader(br) { + this.readHead(br); + br.readVarint(); + return this; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + return this.fromReader(new BufferReader(data)); + } + + /** + * Instantiate headers from buffer reader. + * @param {BufferReader} br + * @returns {Headers} + */ + + static fromReader(br) { + return new this().fromReader(br); + } + + /** + * Instantiate headers from serialized data. + * @param {Buffer} data + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {Headers} + */ + + static fromRaw(data, enc) { + if (typeof data === 'string') + data = Buffer.from(data, enc); + return new this().fromRaw(data); + } + + /** + * Instantiate headers from serialized data. + * @param {Buffer} data + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {Headers} + */ + + static fromHead(data, enc) { + if (typeof data === 'string') + data = Buffer.from(data, enc); + return new this().fromHead(data); + } + + /** + * Instantiate headers from a chain entry. + * @param {ChainEntry} entry + * @returns {Headers} + */ + + static fromEntry(entry) { + const headers = new this(); + headers.version = entry.version; + headers.prevBlock = entry.prevBlock; + headers.merkleRoot = entry.merkleRoot; + headers.time = entry.time; + headers.bits = entry.bits; + headers.nonce = entry.nonce; + headers._hash = Buffer.from(entry.hash, 'hex'); + headers._hhash = entry.hash; + return headers; + } + + /** + * Convert the block to a headers object. + * @returns {Headers} + */ + + toHeaders() { + return this; + } + + /** + * Convert the block to a headers object. + * @param {Block|MerkleBlock} block + * @returns {Headers} + */ + + static fromBlock(block) { + const headers = new this(block); + headers._hash = block._hash; + headers._hhash = block._hhash; + return headers; + } + + /** + * Convert the block to an object suitable + * for JSON serialization. + * @returns {Object} + */ + + toJSON() { + return this.getJSON(); + } + + /** + * Convert the block to an object suitable + * for JSON serialization. Note that the hashes + * will be reversed to abide by bitcoind's legacy + * of little-endian uint256s. + * @param {Network} network + * @param {CoinView} view + * @param {Number} height + * @returns {Object} + */ + + getJSON(network, view, height) { + return { + hash: this.rhash(), + height: height, + version: this.version, + prevBlock: encoding.revHex(this.prevBlock), + merkleRoot: encoding.revHex(this.merkleRoot), + time: this.time, + bits: this.bits, + nonce: this.nonce + }; + } + + /** + * Inject properties from json object. + * @private + * @param {Object} json + */ + + fromJSON(json) { + this.parseJSON(json); + return this; + } + + /** + * Instantiate a merkle block from a jsonified block object. + * @param {Object} json - The jsonified block object. + * @returns {Headers} + */ + + static fromJSON(json) { + return new this().fromJSON(json); + } + + /** + * Inspect the headers and return a more + * user-friendly representation of the data. + * @returns {Object} + */ + + inspect() { + return this.format(); + } + + /** + * Inspect the headers and return a more + * user-friendly representation of the data. + * @param {CoinView} view + * @param {Number} height + * @returns {Object} + */ + + format(view, height) { + return { + hash: this.rhash(), + height: height != null ? height : -1, + date: util.date(this.time), + version: this.version.toString(16), + prevBlock: encoding.revHex(this.prevBlock), + merkleRoot: encoding.revHex(this.merkleRoot), + time: this.time, + bits: this.bits, + nonce: this.nonce + }; + } + + /** + * Test an object to see if it is a Headers object. + * @param {Object} obj + * @returns {Boolean} + */ + + static isHeaders(obj) { + return obj instanceof Headers; + } } -Object.setPrototypeOf(Headers.prototype, AbstractBlock.prototype); - -/** - * Do non-contextual verification on the headers. - * @param {Object?} ret - Return object, may be - * set with properties `reason` and `score`. - * @returns {Boolean} - */ - -Headers.prototype.verifyBody = function verifyBody(ret) { - return true; -}; - -/** - * Get size of the headers. - * @returns {Number} - */ - -Headers.prototype.getSize = function getSize() { - return 81; -}; - -/** - * Serialize the headers to a buffer writer. - * @param {BufferWriter} bw - */ - -Headers.prototype.toWriter = function toWriter(bw) { - this.writeHead(bw); - bw.writeVarint(0); - return bw; -}; - -/** - * Serialize the headers. - * @returns {Buffer|String} - */ - -Headers.prototype.toRaw = function toRaw() { - const size = this.getSize(); - return this.toWriter(new StaticWriter(size)).render(); -}; - -/** - * Inject properties from buffer reader. - * @private - * @param {Buffer} data - */ - -Headers.prototype.fromReader = function fromReader(br) { - this.readHead(br); - br.readVarint(); - return this; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -Headers.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(new BufferReader(data)); -}; - -/** - * Instantiate headers from buffer reader. - * @param {BufferReader} br - * @returns {Headers} - */ - -Headers.fromReader = function fromReader(br) { - return new Headers().fromReader(br); -}; - -/** - * Instantiate headers from serialized data. - * @param {Buffer} data - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {Headers} - */ - -Headers.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - return new Headers().fromRaw(data); -}; - -/** - * Instantiate headers from serialized data. - * @param {Buffer} data - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {Headers} - */ - -Headers.fromHead = function fromHead(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - return new Headers().fromHead(data); -}; - -/** - * Instantiate headers from a chain entry. - * @param {ChainEntry} entry - * @returns {Headers} - */ - -Headers.fromEntry = function fromEntry(entry) { - const headers = new Headers(); - headers.version = entry.version; - headers.prevBlock = entry.prevBlock; - headers.merkleRoot = entry.merkleRoot; - headers.time = entry.time; - headers.bits = entry.bits; - headers.nonce = entry.nonce; - headers._hash = Buffer.from(entry.hash, 'hex'); - headers._hhash = entry.hash; - return headers; -}; - -/** - * Convert the block to a headers object. - * @returns {Headers} - */ - -Headers.prototype.toHeaders = function toHeaders() { - return this; -}; - -/** - * Convert the block to a headers object. - * @param {Block|MerkleBlock} block - * @returns {Headers} - */ - -Headers.fromBlock = function fromBlock(block) { - const headers = new Headers(block); - headers._hash = block._hash; - headers._hhash = block._hhash; - return headers; -}; - -/** - * Convert the block to an object suitable - * for JSON serialization. - * @returns {Object} - */ - -Headers.prototype.toJSON = function toJSON() { - return this.getJSON(); -}; - -/** - * Convert the block to an object suitable - * for JSON serialization. Note that the hashes - * will be reversed to abide by bitcoind's legacy - * of little-endian uint256s. - * @param {Network} network - * @param {CoinView} view - * @param {Number} height - * @returns {Object} - */ - -Headers.prototype.getJSON = function getJSON(network, view, height) { - return { - hash: this.rhash(), - height: height, - version: this.version, - prevBlock: encoding.revHex(this.prevBlock), - merkleRoot: encoding.revHex(this.merkleRoot), - time: this.time, - bits: this.bits, - nonce: this.nonce - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - */ - -Headers.prototype.fromJSON = function fromJSON(json) { - this.parseJSON(json); - return this; -}; - -/** - * Instantiate a merkle block from a jsonified block object. - * @param {Object} json - The jsonified block object. - * @returns {Headers} - */ - -Headers.fromJSON = function fromJSON(json) { - return new Headers().fromJSON(json); -}; - -/** - * Inspect the headers and return a more - * user-friendly representation of the data. - * @returns {Object} - */ - -Headers.prototype.inspect = function inspect() { - return this.format(); -}; - -/** - * Inspect the headers and return a more - * user-friendly representation of the data. - * @param {CoinView} view - * @param {Number} height - * @returns {Object} - */ - -Headers.prototype.format = function format(view, height) { - return { - hash: this.rhash(), - height: height != null ? height : -1, - date: util.date(this.time), - version: this.version.toString(16), - prevBlock: encoding.revHex(this.prevBlock), - merkleRoot: encoding.revHex(this.merkleRoot), - time: this.time, - bits: this.bits, - nonce: this.nonce - }; -}; - -/** - * Test an object to see if it is a Headers object. - * @param {Object} obj - * @returns {Boolean} - */ - -Headers.isHeaders = function isHeaders(obj) { - return obj instanceof Headers; -}; - /* * Expose */ diff --git a/lib/primitives/input.js b/lib/primitives/input.js index b0ec233b..a51846f4 100644 --- a/lib/primitives/input.js +++ b/lib/primitives/input.js @@ -16,502 +16,507 @@ const StaticWriter = require('bufio/lib/staticwriter'); const BufferReader = require('bufio/lib/reader'); /** + * Input * Represents a transaction input. * @alias module:primitives.Input - * @constructor - * @param {NakedInput} options * @property {Outpoint} prevout - Outpoint. * @property {Script} script - Input script / scriptSig. * @property {Number} sequence - nSequence. * @property {Witness} witness - Witness (empty if not present). */ -function Input(options) { - if (!(this instanceof Input)) - return new Input(options); +class Input { + /** + * Create transaction input. + * @constructor + * @param {Object} options + */ - this.prevout = new Outpoint(); - this.script = new Script(); - this.sequence = 0xffffffff; - this.witness = new Witness(); + constructor(options) { + this.prevout = new Outpoint(); + this.script = new Script(); + this.sequence = 0xffffffff; + this.witness = new Witness(); - if (options) - this.fromOptions(options); -} + if (options) + this.fromOptions(options); + } -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ -Input.prototype.fromOptions = function fromOptions(options) { - assert(options, 'Input data is required.'); + fromOptions(options) { + assert(options, 'Input data is required.'); - this.prevout.fromOptions(options.prevout); + this.prevout.fromOptions(options.prevout); - if (options.script) - this.script.fromOptions(options.script); + if (options.script) + this.script.fromOptions(options.script); - if (options.sequence != null) { - assert((options.sequence >>> 0) === options.sequence, + if (options.sequence != null) { + assert((options.sequence >>> 0) === options.sequence, + 'Sequence must be a uint32.'); + this.sequence = options.sequence; + } + + if (options.witness) + this.witness.fromOptions(options.witness); + + return this; + } + + /** + * Instantiate an Input from options object. + * @param {NakedInput} options + * @returns {Input} + */ + + static fromOptions(options) { + return new this().fromOptions(options); + } + + /** + * Clone the input. + * @returns {Input} + */ + + clone() { + const input = new this.constructor(); + input.prevout = this.prevout; + input.script.inject(this.script); + input.sequence = this.sequence; + input.witness.inject(this.witness); + return input; + } + + /** + * Test equality against another input. + * @param {Input} input + * @returns {Boolean} + */ + + equals(input) { + assert(Input.isInput(input)); + return this.prevout.equals(input.prevout); + } + + /** + * Compare against another input (BIP69). + * @param {Input} input + * @returns {Number} + */ + + compare(input) { + assert(Input.isInput(input)); + return this.prevout.compare(input.prevout); + } + + /** + * Get the previous output script type as a string. + * Will "guess" based on the input script and/or + * witness if coin is not available. + * @param {Coin?} coin + * @returns {ScriptType} type + */ + + getType(coin) { + if (this.isCoinbase()) + return 'coinbase'; + + if (coin) + return coin.getType(); + + let type; + + if (this.witness.items.length > 0) + type = this.witness.getInputType(); + else + type = this.script.getInputType(); + + return Script.typesByVal[type].toLowerCase(); + } + + /** + * Get the redeem script. Will attempt to resolve nested + * redeem scripts if witnessscripthash is behind a scripthash. + * @param {Coin?} coin + * @returns {Script?} Redeem script. + */ + + getRedeem(coin) { + if (this.isCoinbase()) + return null; + + if (!coin) { + if (this.witness.isScripthashInput()) + return this.witness.getRedeem(); + + if (this.script.isScripthashInput()) + return this.script.getRedeem(); + + return null; + } + + let prev = coin.script; + let redeem = null; + + if (prev.isScripthash()) { + prev = this.script.getRedeem(); + redeem = prev; + } + + if (prev && prev.isWitnessScripthash()) { + prev = this.witness.getRedeem(); + redeem = prev; + } + + return redeem; + } + + /** + * Get the redeem script type. + * @param {Coin?} coin + * @returns {String} subtype + */ + + getSubtype(coin) { + if (this.isCoinbase()) + return null; + + const redeem = this.getRedeem(coin); + + if (!redeem) + return null; + + const type = redeem.getType(); + + return Script.typesByVal[type].toLowerCase(); + } + + /** + * Get the previous output script's address. Will "guess" + * based on the input script and/or witness if coin + * is not available. + * @param {Coin?} coin + * @returns {Address?} addr + */ + + getAddress(coin) { + if (this.isCoinbase()) + return null; + + if (coin) + return coin.getAddress(); + + if (this.witness.items.length > 0) + return this.witness.getInputAddress(); + + return this.script.getInputAddress(); + } + + /** + * Get the address hash. + * @param {String?} enc + * @returns {Hash} hash + */ + + getHash(enc) { + const addr = this.getAddress(); + + if (!addr) + return null; + + return addr.getHash(enc); + } + + /** + * Test to see if nSequence is equal to uint32max. + * @returns {Boolean} + */ + + isFinal() { + return this.sequence === 0xffffffff; + } + + /** + * Test to see if nSequence is less than 0xfffffffe. + * @returns {Boolean} + */ + + isRBF() { + return this.sequence < 0xfffffffe; + } + + /** + * Test to see if outpoint is null. + * @returns {Boolean} + */ + + isCoinbase() { + return this.prevout.isNull(); + } + + /** + * Convert the input to a more user-friendly object. + * @returns {Object} + */ + + inspect() { + return this.format(); + } + + /** + * Convert the input to a more user-friendly object. + * @param {Coin?} coin + * @returns {Object} + */ + + format(coin) { + return { + type: this.getType(coin), + subtype: this.getSubtype(coin), + address: this.getAddress(coin), + script: this.script, + witness: this.witness, + redeem: this.getRedeem(coin), + sequence: this.sequence, + prevout: this.prevout, + coin: coin || null + }; + } + + /** + * Convert the input to an object suitable + * for JSON serialization. + * @returns {Object} + */ + + toJSON(network, coin) { + return this.getJSON(); + } + + /** + * Convert the input to an object suitable + * for JSON serialization. Note that the hashes + * will be reversed to abide by bitcoind's legacy + * of little-endian uint256s. + * @param {Network} network + * @param {Coin} coin + * @returns {Object} + */ + + getJSON(network, coin) { + network = Network.get(network); + + let addr; + if (!coin) { + addr = this.getAddress(); + if (addr) + addr = addr.toString(network); + } + + return { + prevout: this.prevout.toJSON(), + script: this.script.toJSON(), + witness: this.witness.toJSON(), + sequence: this.sequence, + address: addr, + coin: coin ? coin.getJSON(network, true) : undefined + }; + } + + /** + * Inject properties from a JSON object. + * @private + * @param {Object} json + */ + + fromJSON(json) { + assert(json, 'Input data is required.'); + assert((json.sequence >>> 0) === json.sequence, 'Sequence must be a uint32.'); - this.sequence = options.sequence; + this.prevout.fromJSON(json.prevout); + this.script.fromJSON(json.script); + this.witness.fromJSON(json.witness); + this.sequence = json.sequence; + return this; } - if (options.witness) - this.witness.fromOptions(options.witness); + /** + * Instantiate an Input from a jsonified input object. + * @param {Object} json - The jsonified input object. + * @returns {Input} + */ - return this; -}; - -/** - * Instantiate an Input from options object. - * @param {NakedInput} options - * @returns {Input} - */ - -Input.fromOptions = function fromOptions(options) { - return new Input().fromOptions(options); -}; - -/** - * Clone the input. - * @returns {Input} - */ - -Input.prototype.clone = function clone() { - const input = new Input(); - input.prevout = this.prevout; - input.script.inject(this.script); - input.sequence = this.sequence; - input.witness.inject(this.witness); - return input; -}; - -/** - * Test equality against another input. - * @param {Input} input - * @returns {Boolean} - */ - -Input.prototype.equals = function equals(input) { - assert(Input.isInput(input)); - return this.prevout.equals(input.prevout); -}; - -/** - * Compare against another input (BIP69). - * @param {Input} input - * @returns {Number} - */ - -Input.prototype.compare = function compare(input) { - assert(Input.isInput(input)); - return this.prevout.compare(input.prevout); -}; - -/** - * Get the previous output script type as a string. - * Will "guess" based on the input script and/or - * witness if coin is not available. - * @param {Coin?} coin - * @returns {ScriptType} type - */ - -Input.prototype.getType = function getType(coin) { - if (this.isCoinbase()) - return 'coinbase'; - - if (coin) - return coin.getType(); - - let type; - - if (this.witness.items.length > 0) - type = this.witness.getInputType(); - else - type = this.script.getInputType(); - - return Script.typesByVal[type].toLowerCase(); -}; - -/** - * Get the redeem script. Will attempt to resolve nested - * redeem scripts if witnessscripthash is behind a scripthash. - * @param {Coin?} coin - * @returns {Script?} Redeem script. - */ - -Input.prototype.getRedeem = function getRedeem(coin) { - if (this.isCoinbase()) - return null; - - if (!coin) { - if (this.witness.isScripthashInput()) - return this.witness.getRedeem(); - - if (this.script.isScripthashInput()) - return this.script.getRedeem(); - - return null; + static fromJSON(json) { + return new this().fromJSON(json); } - let prev = coin.script; - let redeem = null; + /** + * Calculate size of serialized input. + * @returns {Number} + */ - if (prev.isScripthash()) { - prev = this.script.getRedeem(); - redeem = prev; + getSize() { + return 40 + this.script.getVarSize(); } - if (prev && prev.isWitnessScripthash()) { - prev = this.witness.getRedeem(); - redeem = prev; + /** + * Serialize the input. + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {Buffer|String} + */ + + toRaw() { + const size = this.getSize(); + return this.toWriter(new StaticWriter(size)).render(); } - return redeem; -}; + /** + * Write the input to a buffer writer. + * @param {BufferWriter} bw + */ -/** - * Get the redeem script type. - * @param {Coin?} coin - * @returns {String} subtype - */ - -Input.prototype.getSubtype = function getSubtype(coin) { - if (this.isCoinbase()) - return null; - - const redeem = this.getRedeem(coin); - - if (!redeem) - return null; - - const type = redeem.getType(); - - return Script.typesByVal[type].toLowerCase(); -}; - -/** - * Get the previous output script's address. Will "guess" - * based on the input script and/or witness if coin - * is not available. - * @param {Coin?} coin - * @returns {Address?} addr - */ - -Input.prototype.getAddress = function getAddress(coin) { - if (this.isCoinbase()) - return null; - - if (coin) - return coin.getAddress(); - - if (this.witness.items.length > 0) - return this.witness.getInputAddress(); - - return this.script.getInputAddress(); -}; - -/** - * Get the address hash. - * @param {String?} enc - * @returns {Hash} hash - */ - -Input.prototype.getHash = function getHash(enc) { - const addr = this.getAddress(); - - if (!addr) - return null; - - return addr.getHash(enc); -}; - -/** - * Test to see if nSequence is equal to uint32max. - * @returns {Boolean} - */ - -Input.prototype.isFinal = function isFinal() { - return this.sequence === 0xffffffff; -}; - -/** - * Test to see if nSequence is less than 0xfffffffe. - * @returns {Boolean} - */ - -Input.prototype.isRBF = function isRBF() { - return this.sequence < 0xfffffffe; -}; - -/** - * Test to see if outpoint is null. - * @returns {Boolean} - */ - -Input.prototype.isCoinbase = function isCoinbase() { - return this.prevout.isNull(); -}; - -/** - * Convert the input to a more user-friendly object. - * @returns {Object} - */ - -Input.prototype.inspect = function inspect() { - return this.format(); -}; - -/** - * Convert the input to a more user-friendly object. - * @param {Coin?} coin - * @returns {Object} - */ - -Input.prototype.format = function format(coin) { - return { - type: this.getType(coin), - subtype: this.getSubtype(coin), - address: this.getAddress(coin), - script: this.script, - witness: this.witness, - redeem: this.getRedeem(coin), - sequence: this.sequence, - prevout: this.prevout, - coin: coin || null - }; -}; - -/** - * Convert the input to an object suitable - * for JSON serialization. - * @returns {Object} - */ - -Input.prototype.toJSON = function toJSON(network, coin) { - return this.getJSON(); -}; - -/** - * Convert the input to an object suitable - * for JSON serialization. Note that the hashes - * will be reversed to abide by bitcoind's legacy - * of little-endian uint256s. - * @param {Network} network - * @param {Coin} coin - * @returns {Object} - */ - -Input.prototype.getJSON = function getJSON(network, coin) { - network = Network.get(network); - - let addr; - if (!coin) { - addr = this.getAddress(); - if (addr) - addr = addr.toString(network); + toWriter(bw) { + this.prevout.toWriter(bw); + bw.writeVarBytes(this.script.toRaw()); + bw.writeU32(this.sequence); + return bw; } - return { - prevout: this.prevout.toJSON(), - script: this.script.toJSON(), - witness: this.witness.toJSON(), - sequence: this.sequence, - address: addr, - coin: coin ? coin.getJSON(network, true) : undefined - }; -}; + /** + * Inject properties from buffer reader. + * @private + * @param {BufferReader} br + */ -/** - * Inject properties from a JSON object. - * @private - * @param {Object} json - */ + fromReader(br) { + this.prevout.fromReader(br); + this.script.fromRaw(br.readVarBytes()); + this.sequence = br.readU32(); + return this; + } -Input.prototype.fromJSON = function fromJSON(json) { - assert(json, 'Input data is required.'); - assert((json.sequence >>> 0) === json.sequence, 'Sequence must be a uint32.'); - this.prevout.fromJSON(json.prevout); - this.script.fromJSON(json.script); - this.witness.fromJSON(json.witness); - this.sequence = json.sequence; - return this; -}; + /** + * Inject properties from serialized data. + * @param {Buffer} data + */ -/** - * Instantiate an Input from a jsonified input object. - * @param {Object} json - The jsonified input object. - * @returns {Input} - */ + fromRaw(data) { + return this.fromReader(new BufferReader(data)); + } -Input.fromJSON = function fromJSON(json) { - return new Input().fromJSON(json); -}; + /** + * Instantiate an input from a buffer reader. + * @param {BufferReader} br + * @returns {Input} + */ -/** - * Calculate size of serialized input. - * @returns {Number} - */ + static fromReader(br) { + return new this().fromReader(br); + } -Input.prototype.getSize = function getSize() { - return 40 + this.script.getVarSize(); -}; + /** + * Instantiate an input from a serialized Buffer. + * @param {Buffer} data + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {Input} + */ -/** - * Serialize the input. - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {Buffer|String} - */ + static fromRaw(data, enc) { + if (typeof data === 'string') + data = Buffer.from(data, enc); + return new this().fromRaw(data); + } -Input.prototype.toRaw = function toRaw() { - const size = this.getSize(); - return this.toWriter(new StaticWriter(size)).render(); -}; + /** + * Inject properties from outpoint. + * @private + * @param {Outpoint} outpoint + */ -/** - * Write the input to a buffer writer. - * @param {BufferWriter} bw - */ + fromOutpoint(outpoint) { + assert(typeof outpoint.hash === 'string'); + assert(typeof outpoint.index === 'number'); + this.prevout.hash = outpoint.hash; + this.prevout.index = outpoint.index; + return this; + } -Input.prototype.toWriter = function toWriter(bw) { - this.prevout.toWriter(bw); - bw.writeVarBytes(this.script.toRaw()); - bw.writeU32(this.sequence); - return bw; -}; + /** + * Instantiate input from outpoint. + * @param {Outpoint} + * @returns {Input} + */ -/** - * Inject properties from buffer reader. - * @private - * @param {BufferReader} br - */ + static fromOutpoint(outpoint) { + return new this().fromOutpoint(outpoint); + } -Input.prototype.fromReader = function fromReader(br) { - this.prevout.fromReader(br); - this.script.fromRaw(br.readVarBytes()); - this.sequence = br.readU32(); - return this; -}; + /** + * Inject properties from coin. + * @private + * @param {Coin} coin + */ -/** - * Inject properties from serialized data. - * @param {Buffer} data - */ + fromCoin(coin) { + assert(typeof coin.hash === 'string'); + assert(typeof coin.index === 'number'); + this.prevout.hash = coin.hash; + this.prevout.index = coin.index; + return this; + } -Input.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(new BufferReader(data)); -}; + /** + * Instantiate input from coin. + * @param {Coin} + * @returns {Input} + */ -/** - * Instantiate an input from a buffer reader. - * @param {BufferReader} br - * @returns {Input} - */ + static fromCoin(coin) { + return new this().fromCoin(coin); + } -Input.fromReader = function fromReader(br) { - return new Input().fromReader(br); -}; + /** + * Inject properties from transaction. + * @private + * @param {TX} tx + * @param {Number} index + */ -/** - * Instantiate an input from a serialized Buffer. - * @param {Buffer} data - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {Input} - */ + fromTX(tx, index) { + assert(tx); + assert(typeof index === 'number'); + assert(index >= 0 && index < tx.outputs.length); + this.prevout.hash = tx.hash('hex'); + this.prevout.index = index; + return this; + } -Input.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - return new Input().fromRaw(data); -}; + /** + * Instantiate input from tx. + * @param {TX} tx + * @param {Number} index + * @returns {Input} + */ -/** - * Inject properties from outpoint. - * @private - * @param {Outpoint} outpoint - */ + static fromTX(tx, index) { + return new this().fromTX(tx, index); + } -Input.prototype.fromOutpoint = function fromOutpoint(outpoint) { - assert(typeof outpoint.hash === 'string'); - assert(typeof outpoint.index === 'number'); - this.prevout.hash = outpoint.hash; - this.prevout.index = outpoint.index; - return this; -}; + /** + * Test an object to see if it is an Input. + * @param {Object} obj + * @returns {Boolean} + */ -/** - * Instantiate input from outpoint. - * @param {Outpoint} - * @returns {Input} - */ - -Input.fromOutpoint = function fromOutpoint(outpoint) { - return new Input().fromOutpoint(outpoint); -}; - -/** - * Inject properties from coin. - * @private - * @param {Coin} coin - */ - -Input.prototype.fromCoin = function fromCoin(coin) { - assert(typeof coin.hash === 'string'); - assert(typeof coin.index === 'number'); - this.prevout.hash = coin.hash; - this.prevout.index = coin.index; - return this; -}; - -/** - * Instantiate input from coin. - * @param {Coin} - * @returns {Input} - */ - -Input.fromCoin = function fromCoin(coin) { - return new Input().fromCoin(coin); -}; - -/** - * Inject properties from transaction. - * @private - * @param {TX} tx - * @param {Number} index - */ - -Input.prototype.fromTX = function fromTX(tx, index) { - assert(tx); - assert(typeof index === 'number'); - assert(index >= 0 && index < tx.outputs.length); - this.prevout.hash = tx.hash('hex'); - this.prevout.index = index; - return this; -}; - -/** - * Instantiate input from tx. - * @param {TX} tx - * @param {Number} index - * @returns {Input} - */ - -Input.fromTX = function fromTX(tx, index) { - return new Input().fromTX(tx, index); -}; - -/** - * Test an object to see if it is an Input. - * @param {Object} obj - * @returns {Boolean} - */ - -Input.isInput = function isInput(obj) { - return obj instanceof Input; -}; + static isInput(obj) { + return obj instanceof Input; + } +} /* * Expose diff --git a/lib/primitives/invitem.js b/lib/primitives/invitem.js index fcdb532d..7ca42cfc 100644 --- a/lib/primitives/invitem.js +++ b/lib/primitives/invitem.js @@ -15,18 +15,146 @@ const encoding = require('bufio/lib/encoding'); * Inv Item * @alias module:primitives.InvItem * @constructor - * @param {Number} type - * @param {Hash} hash * @property {InvType} type * @property {Hash} hash */ -function InvItem(type, hash) { - if (!(this instanceof InvItem)) - return new InvItem(type, hash); +class InvItem { + /** + * Create an inv item. + * @constructor + * @param {Number} type + * @param {Hash} hash + */ - this.type = type; - this.hash = hash; + constructor(type, hash) { + this.type = type; + this.hash = hash; + } + + /** + * Write inv item to buffer writer. + * @param {BufferWriter} bw + */ + + getSize() { + return 36; + } + + /** + * Write inv item to buffer writer. + * @param {BufferWriter} bw + */ + + toWriter(bw) { + bw.writeU32(this.type); + bw.writeHash(this.hash); + return bw; + } + + /** + * Serialize inv item. + * @returns {Buffer} + */ + + toRaw() { + return this.toWriter(new StaticWriter(36)).render(); + } + + /** + * Inject properties from buffer reader. + * @private + * @param {BufferReader} br + */ + + fromReader(br) { + this.type = br.readU32(); + this.hash = br.readHash('hex'); + return this; + } + + /** + * Inject properties from serialized data. + * @param {Buffer} data + */ + + fromRaw(data) { + return this.fromReader(new BufferReader(data)); + } + + /** + * Instantiate inv item from buffer reader. + * @param {BufferReader} br + * @returns {InvItem} + */ + + static fromReader(br) { + return new this().fromReader(br); + } + + /** + * Instantiate inv item from serialized data. + * @param {Buffer} data + * @param {String?} enc + * @returns {InvItem} + */ + + static fromRaw(data, enc) { + if (typeof data === 'string') + data = Buffer.from(data, enc); + return new this().fromRaw(data); + } + + /** + * Test whether the inv item is a block. + * @returns {Boolean} + */ + + isBlock() { + switch (this.type) { + case InvItem.types.BLOCK: + case InvItem.types.WITNESS_BLOCK: + case InvItem.types.FILTERED_BLOCK: + case InvItem.types.WITNESS_FILTERED_BLOCK: + case InvItem.types.CMPCT_BLOCK: + return true; + default: + return false; + } + } + + /** + * Test whether the inv item is a tx. + * @returns {Boolean} + */ + + isTX() { + switch (this.type) { + case InvItem.types.TX: + case InvItem.types.WITNESS_TX: + return true; + default: + return false; + } + } + + /** + * Test whether the inv item has the witness bit set. + * @returns {Boolean} + */ + + hasWitness() { + return (this.type & InvItem.WITNESS_FLAG) !== 0; + } + + /** + * Get little-endian hash. + * @returns {Hash} + */ + + rhash() { + return encoding.revHex(this.hash); + } } /** @@ -36,7 +164,6 @@ function InvItem(type, hash) { */ InvItem.types = { - ERROR: 0, TX: 1, BLOCK: 2, FILTERED_BLOCK: 3, @@ -52,7 +179,6 @@ InvItem.types = { */ InvItem.typesByVal = { - 0: 'ERROR', 1: 'TX', 2: 'BLOCK', 3: 'FILTERED_BLOCK', @@ -70,130 +196,6 @@ InvItem.typesByVal = { InvItem.WITNESS_FLAG = 1 << 30; -/** - * Write inv item to buffer writer. - * @param {BufferWriter} bw - */ - -InvItem.prototype.getSize = function getSize() { - return 36; -}; - -/** - * Write inv item to buffer writer. - * @param {BufferWriter} bw - */ - -InvItem.prototype.toWriter = function toWriter(bw) { - bw.writeU32(this.type); - bw.writeHash(this.hash); - return bw; -}; - -/** - * Serialize inv item. - * @returns {Buffer} - */ - -InvItem.prototype.toRaw = function toRaw() { - return this.toWriter(new StaticWriter(36)).render(); -}; - -/** - * Inject properties from buffer reader. - * @private - * @param {BufferReader} br - */ - -InvItem.prototype.fromReader = function fromReader(br) { - this.type = br.readU32(); - this.hash = br.readHash('hex'); - return this; -}; - -/** - * Inject properties from serialized data. - * @param {Buffer} data - */ - -InvItem.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(new BufferReader(data)); -}; - -/** - * Instantiate inv item from buffer reader. - * @param {BufferReader} br - * @returns {InvItem} - */ - -InvItem.fromReader = function fromReader(br) { - return new InvItem().fromReader(br); -}; - -/** - * Instantiate inv item from serialized data. - * @param {Buffer} data - * @param {String?} enc - * @returns {InvItem} - */ - -InvItem.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - return new InvItem().fromRaw(data); -}; - -/** - * Test whether the inv item is a block. - * @returns {Boolean} - */ - -InvItem.prototype.isBlock = function isBlock() { - switch (this.type) { - case InvItem.types.BLOCK: - case InvItem.types.WITNESS_BLOCK: - case InvItem.types.FILTERED_BLOCK: - case InvItem.types.WITNESS_FILTERED_BLOCK: - case InvItem.types.CMPCT_BLOCK: - return true; - default: - return false; - } -}; - -/** - * Test whether the inv item is a tx. - * @returns {Boolean} - */ - -InvItem.prototype.isTX = function isTX() { - switch (this.type) { - case InvItem.types.TX: - case InvItem.types.WITNESS_TX: - return true; - default: - return false; - } -}; - -/** - * Test whether the inv item has the witness bit set. - * @returns {Boolean} - */ - -InvItem.prototype.hasWitness = function hasWitness() { - return (this.type & InvItem.WITNESS_FLAG) !== 0; -}; - -/** - * Get little-endian hash. - * @returns {Hash} - */ - -InvItem.prototype.rhash = function rhash() { - return encoding.revHex(this.hash); -}; - /* * Expose */ diff --git a/lib/primitives/keyring.js b/lib/primitives/keyring.js index a82667be..dd342c0b 100644 --- a/lib/primitives/keyring.js +++ b/lib/primitives/keyring.js @@ -21,890 +21,894 @@ const Output = require('./output'); const secp256k1 = require('bcrypto/lib/secp256k1'); /** + * Key Ring * Represents a key ring which amounts to an address. * @alias module:primitives.KeyRing - * @constructor - * @param {Object} options */ -function KeyRing(options) { - if (!(this instanceof KeyRing)) - return new KeyRing(options); +class KeyRing { + /** + * Create a key ring. + * @constructor + * @param {Object} options + */ - this.witness = false; - this.nested = false; - this.publicKey = encoding.ZERO_KEY; - this.privateKey = null; - this.script = null; + constructor(options) { + this.witness = false; + this.nested = false; + this.publicKey = encoding.ZERO_KEY; + this.privateKey = null; + this.script = null; - this._keyHash = null; - this._keyAddress = null; - this._program = null; - this._nestedHash = null; - this._nestedAddress = null; - this._scriptHash160 = null; - this._scriptHash256 = null; - this._scriptAddress = null; + this._keyHash = null; + this._keyAddress = null; + this._program = null; + this._nestedHash = null; + this._nestedAddress = null; + this._scriptHash160 = null; + this._scriptHash256 = null; + this._scriptAddress = null; - if (options) - this.fromOptions(options); -} - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -KeyRing.prototype.fromOptions = function fromOptions(options) { - let key = toKey(options); - - if (options.witness != null) { - assert(typeof options.witness === 'boolean'); - this.witness = options.witness; + if (options) + this.fromOptions(options); } - if (options.nested != null) { - assert(typeof options.nested === 'boolean'); - this.nested = options.nested; - } - - if (Buffer.isBuffer(key)) - return this.fromKey(key); - - key = toKey(options.key); - - if (options.publicKey) - key = toKey(options.publicKey); - - if (options.privateKey) - key = toKey(options.privateKey); - - const script = options.script; - const compress = options.compressed; - - if (script) - return this.fromScript(key, script, compress); - - return this.fromKey(key, compress); -}; - -/** - * Instantiate key ring from options. - * @param {Object} options - * @returns {KeyRing} - */ - -KeyRing.fromOptions = function fromOptions(options) { - return new KeyRing().fromOptions(options); -}; - -/** - * Clear cached key/script hashes. - */ - -KeyRing.prototype.refresh = function refresh() { - this._keyHash = null; - this._keyAddress = null; - this._program = null; - this._nestedHash = null; - this._nestedAddress = null; - this._scriptHash160 = null; - this._scriptHash256 = null; - this._scriptAddress = null; -}; - -/** - * Inject data from private key. - * @private - * @param {Buffer} key - * @param {Boolean?} compress - */ - -KeyRing.prototype.fromPrivate = function fromPrivate(key, compress) { - assert(Buffer.isBuffer(key), 'Private key must be a buffer.'); - assert(secp256k1.privateKeyVerify(key), 'Not a valid private key.'); - - this.privateKey = key; - this.publicKey = secp256k1.publicKeyCreate(key, compress !== false); - - return this; -}; - -/** - * Instantiate keyring from a private key. - * @param {Buffer} key - * @param {Boolean?} compress - * @returns {KeyRing} - */ - -KeyRing.fromPrivate = function fromPrivate(key, compress) { - return new KeyRing().fromPrivate(key, compress); -}; - -/** - * Inject data from public key. - * @private - * @param {Buffer} key - */ - -KeyRing.prototype.fromPublic = function fromPublic(key) { - assert(Buffer.isBuffer(key), 'Public key must be a buffer.'); - assert(secp256k1.publicKeyVerify(key), 'Not a valid public key.'); - this.publicKey = key; - return this; -}; - -/** - * Generate a keyring. - * @private - * @param {Boolean?} compress - * @returns {KeyRing} - */ - -KeyRing.prototype.generate = function generate(compress) { - const key = secp256k1.generatePrivateKey(); - return this.fromKey(key, compress); -}; - -/** - * Generate a keyring. - * @param {Boolean?} compress - * @returns {KeyRing} - */ - -KeyRing.generate = function generate(compress) { - return new KeyRing().generate(compress); -}; - -/** - * Instantiate keyring from a public key. - * @param {Buffer} publicKey - * @returns {KeyRing} - */ - -KeyRing.fromPublic = function fromPublic(key) { - return new KeyRing().fromPublic(key); -}; - -/** - * Inject data from public key. - * @private - * @param {Buffer} privateKey - * @param {Boolean?} compress - */ - -KeyRing.prototype.fromKey = function fromKey(key, compress) { - assert(Buffer.isBuffer(key), 'Key must be a buffer.'); - - if (key.length === 32) - return this.fromPrivate(key, compress !== false); - - return this.fromPublic(key); -}; - -/** - * Instantiate keyring from a public key. - * @param {Buffer} publicKey - * @param {Boolean?} compress - * @returns {KeyRing} - */ - -KeyRing.fromKey = function fromKey(key, compress) { - return new KeyRing().fromKey(key, compress); -}; - -/** - * Inject data from script. - * @private - * @param {Buffer} key - * @param {Script} script - * @param {Boolean?} compress - */ - -KeyRing.prototype.fromScript = function fromScript(key, script, compress) { - assert(script instanceof Script, 'Non-script passed into KeyRing.'); - - this.fromKey(key, compress); - this.script = script; - - return this; -}; - -/** - * Instantiate keyring from script. - * @param {Buffer} key - * @param {Script} script - * @param {Boolean?} compress - * @returns {KeyRing} - */ - -KeyRing.fromScript = function fromScript(key, script, compress) { - return new KeyRing().fromScript(key, script, compress); -}; - -/** - * Calculate WIF serialization size. - * @returns {Number} - */ - -KeyRing.prototype.getSecretSize = function getSecretSize() { - let size = 0; - - size += 1; - size += this.privateKey.length; - - if (this.publicKey.length === 33) - size += 1; - - size += 4; - - return size; -}; - -/** - * Convert key to a CBitcoinSecret. - * @param {(Network|NetworkType)?} network - * @returns {Base58String} - */ - -KeyRing.prototype.toSecret = function toSecret(network) { - const size = this.getSecretSize(); - const bw = new StaticWriter(size); - - assert(this.privateKey, 'Cannot serialize without private key.'); - - network = Network.get(network); - - bw.writeU8(network.keyPrefix.privkey); - bw.writeBytes(this.privateKey); - - if (this.publicKey.length === 33) - bw.writeU8(1); - - bw.writeChecksum(hash256.digest); - - return base58.encode(bw.render()); -}; - -/** - * Inject properties from serialized CBitcoinSecret. - * @private - * @param {Base58String} secret - * @param {(Network|NetworkType)?} network - */ - -KeyRing.prototype.fromSecret = function fromSecret(data, network) { - const br = new BufferReader(base58.decode(data), true); - - const version = br.readU8(); - - Network.fromWIF(version, network); - - const key = br.readBytes(32); - - let compress = false; - - if (br.left() > 4) { - assert(br.readU8() === 1, 'Bad compression flag.'); - compress = true; - } - - br.verifyChecksum(hash256.digest); - - return this.fromPrivate(key, compress); -}; - -/** - * Instantiate a keyring from a serialized CBitcoinSecret. - * @param {Base58String} secret - * @param {(Network|NetworkType)?} network - * @returns {KeyRing} - */ - -KeyRing.fromSecret = function fromSecret(data, network) { - return new KeyRing().fromSecret(data, network); -}; - -/** - * Get private key. - * @param {String?} enc - Can be `"hex"`, `"base58"`, or `null`. - * @returns {Buffer} Private key. - */ - -KeyRing.prototype.getPrivateKey = function getPrivateKey(enc, network) { - if (!this.privateKey) - return null; - - if (enc === 'base58') - return this.toSecret(network); - - if (enc === 'hex') - return this.privateKey.toString('hex'); - - return this.privateKey; -}; - -/** - * Get public key. - * @param {String?} enc - `"hex"` or `null`. - * @returns {Buffer} - */ - -KeyRing.prototype.getPublicKey = function getPublicKey(enc) { - if (enc === 'base58') - return base58.encode(this.publicKey); - - if (enc === 'hex') - return this.publicKey.toString('hex'); - - return this.publicKey; -}; - -/** - * Get redeem script. - * @returns {Script} - */ - -KeyRing.prototype.getScript = function getScript() { - return this.script; -}; - -/** - * Get witness program. - * @returns {Buffer} - */ - -KeyRing.prototype.getProgram = function getProgram() { - if (!this.witness) - return null; - - if (!this._program) { - let program; - if (!this.script) { - const hash = hash160.digest(this.publicKey); - program = Script.fromProgram(0, hash); - } else { - const hash = this.script.sha256(); - program = Script.fromProgram(0, hash); + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ + + fromOptions(options) { + let key = toKey(options); + + if (options.witness != null) { + assert(typeof options.witness === 'boolean'); + this.witness = options.witness; } - this._program = program; - } - return this._program; -}; - -/** - * Get address' ripemd160 program scripthash - * (for witness programs behind a scripthash). - * @param {String?} enc - `"hex"` or `null`. - * @returns {Buffer} - */ - -KeyRing.prototype.getNestedHash = function getNestedHash(enc) { - if (!this.witness) - return null; - - if (!this._nestedHash) - this._nestedHash = this.getProgram().hash160(); - - return enc === 'hex' - ? this._nestedHash.toString('hex') - : this._nestedHash; -}; - -/** - * Get address' scripthash address for witness program. - * @param {String?} enc - `"base58"` or `null`. - * @returns {Address|Base58Address} - */ - -KeyRing.prototype.getNestedAddress = function getNestedAddress(enc, network) { - if (!this.witness) - return null; - - if (!this._nestedAddress) { - const hash = this.getNestedHash(); - const addr = Address.fromScripthash(hash); - this._nestedAddress = addr; - } - - if (enc === 'base58') - return this._nestedAddress.toBase58(network); - - if (enc === 'string') - return this._nestedAddress.toString(network); - - return this._nestedAddress; -}; - -/** - * Get scripthash. - * @param {String?} enc - `"hex"` or `null`. - * @returns {Buffer} - */ - -KeyRing.prototype.getScriptHash = function getScriptHash(enc) { - if (this.witness) - return this.getScriptHash256(enc); - return this.getScriptHash160(enc); -}; - -/** - * Get ripemd160 scripthash. - * @param {String?} enc - `"hex"` or `null`. - * @returns {Buffer} - */ - -KeyRing.prototype.getScriptHash160 = function getScriptHash160(enc) { - if (!this.script) - return null; - - if (!this._scriptHash160) - this._scriptHash160 = this.script.hash160(); - - return enc === 'hex' - ? this._scriptHash160.toString('hex') - : this._scriptHash160; -}; - -/** - * Get sha256 scripthash. - * @param {String?} enc - `"hex"` or `null`. - * @returns {Buffer} - */ - -KeyRing.prototype.getScriptHash256 = function getScriptHash256(enc) { - if (!this.script) - return null; - - if (!this._scriptHash256) - this._scriptHash256 = this.script.sha256(); - - return enc === 'hex' - ? this._scriptHash256.toString('hex') - : this._scriptHash256; -}; - -/** - * Get scripthash address. - * @param {String?} enc - `"base58"` or `null`. - * @returns {Address|Base58Address} - */ - -KeyRing.prototype.getScriptAddress = function getScriptAddress(enc, network) { - if (!this.script) - return null; - - if (!this._scriptAddress) { - let addr; - if (this.witness) { - const hash = this.getScriptHash256(); - addr = Address.fromWitnessScripthash(hash); - } else { - const hash = this.getScriptHash160(); - addr = Address.fromScripthash(hash); + if (options.nested != null) { + assert(typeof options.nested === 'boolean'); + this.nested = options.nested; } - this._scriptAddress = addr; + + if (Buffer.isBuffer(key)) + return this.fromKey(key); + + key = toKey(options.key); + + if (options.publicKey) + key = toKey(options.publicKey); + + if (options.privateKey) + key = toKey(options.privateKey); + + const script = options.script; + const compress = options.compressed; + + if (script) + return this.fromScript(key, script, compress); + + return this.fromKey(key, compress); } - if (enc === 'base58') - return this._scriptAddress.toBase58(network); + /** + * Instantiate key ring from options. + * @param {Object} options + * @returns {KeyRing} + */ - if (enc === 'string') - return this._scriptAddress.toString(network); - - return this._scriptAddress; -}; - -/** - * Get public key hash. - * @param {String?} enc - `"hex"` or `null`. - * @returns {Buffer} - */ - -KeyRing.prototype.getKeyHash = function getKeyHash(enc) { - if (!this._keyHash) - this._keyHash = hash160.digest(this.publicKey); - - return enc === 'hex' - ? this._keyHash.toString('hex') - : this._keyHash; -}; - -/** - * Get pubkeyhash address. - * @param {String?} enc - `"base58"` or `null`. - * @returns {Address|Base58Address} - */ - -KeyRing.prototype.getKeyAddress = function getKeyAddress(enc, network) { - if (!this._keyAddress) { - const hash = this.getKeyHash(); - - let addr; - if (this.witness) - addr = Address.fromWitnessPubkeyhash(hash); - else - addr = Address.fromPubkeyhash(hash); - - this._keyAddress = addr; + static fromOptions(options) { + return new this().fromOptions(options); } - if (enc === 'base58') - return this._keyAddress.toBase58(network); + /** + * Clear cached key/script hashes. + */ - if (enc === 'string') - return this._keyAddress.toString(network); - - return this._keyAddress; -}; - -/** - * Get hash. - * @param {String?} enc - `"hex"` or `null`. - * @returns {Buffer} - */ - -KeyRing.prototype.getHash = function getHash(enc) { - if (this.nested) - return this.getNestedHash(enc); - - if (this.script) - return this.getScriptHash(enc); - - return this.getKeyHash(enc); -}; - -/** - * Get base58 address. - * @param {String?} enc - `"base58"` or `null`. - * @returns {Address|Base58Address} - */ - -KeyRing.prototype.getAddress = function getAddress(enc, network) { - if (this.nested) - return this.getNestedAddress(enc, network); - - if (this.script) - return this.getScriptAddress(enc, network); - - return this.getKeyAddress(enc, network); -}; - -/** - * Test an address hash against hash and program hash. - * @param {Buffer} hash - * @returns {Boolean} - */ - -KeyRing.prototype.ownHash = function ownHash(hash) { - if (!hash) - return false; - - if (hash.equals(this.getKeyHash())) - return true; - - if (this.script) { - if (hash.equals(this.getScriptHash())) - return true; + refresh() { + this._keyHash = null; + this._keyAddress = null; + this._program = null; + this._nestedHash = null; + this._nestedAddress = null; + this._scriptHash160 = null; + this._scriptHash256 = null; + this._scriptAddress = null; } - if (this.witness) { - if (hash.equals(this.getNestedHash())) - return true; - } + /** + * Inject data from private key. + * @private + * @param {Buffer} key + * @param {Boolean?} compress + */ - return false; -}; + fromPrivate(key, compress) { + assert(Buffer.isBuffer(key), 'Private key must be a buffer.'); + assert(secp256k1.privateKeyVerify(key), 'Not a valid private key.'); -/** - * Check whether transaction output belongs to this address. - * @param {TX|Output} tx - Transaction or Output. - * @param {Number?} index - Output index. - * @returns {Boolean} - */ - -KeyRing.prototype.ownOutput = function ownOutput(tx, index) { - let output; - - if (tx instanceof Output) { - output = tx; - } else { - output = tx.outputs[index]; - assert(output, 'Output does not exist.'); - } - - return this.ownHash(output.getHash()); -}; - -/** - * Test a hash against script hashes to - * find the correct redeem script, if any. - * @param {Buffer} hash - * @returns {Script|null} - */ - -KeyRing.prototype.getRedeem = function getRedeem(hash) { - if (this.witness) { - if (hash.equals(this.getNestedHash())) - return this.getProgram(); - } - - if (this.script) { - if (hash.equals(this.getScriptHash160())) - return this.script; - - if (hash.equals(this.getScriptHash256())) - return this.script; - } - - return null; -}; - -/** - * Sign a message. - * @param {Buffer} msg - * @returns {Buffer} Signature in DER format. - */ - -KeyRing.prototype.sign = function sign(msg) { - assert(this.privateKey, 'Cannot sign without private key.'); - return secp256k1.sign(msg, this.privateKey); -}; - -/** - * Verify a message. - * @param {Buffer} msg - * @param {Buffer} sig - Signature in DER format. - * @returns {Boolean} - */ - -KeyRing.prototype.verify = function verify(msg, sig) { - return secp256k1.verify(msg, sig, this.publicKey); -}; - -/** - * Get witness program version. - * @returns {Number} - */ - -KeyRing.prototype.getVersion = function getVersion() { - if (!this.witness) - return -1; - - if (this.nested) - return -1; - - return 0; -}; - -/** - * Get address type. - * @returns {ScriptType} - */ - -KeyRing.prototype.getType = function getType() { - if (this.nested) - return Address.types.SCRIPTHASH; - - if (this.witness) - return Address.types.WITNESS; - - if (this.script) - return Address.types.SCRIPTHASH; - - return Address.types.PUBKEYHASH; -}; - -/** - * Inspect keyring. - * @returns {Object} - */ - -KeyRing.prototype.inspect = function inspect() { - return this.toJSON(); -}; - -/** - * Convert an KeyRing to a more json-friendly object. - * @returns {Object} - */ - -KeyRing.prototype.toJSON = function toJSON(network) { - return { - witness: this.witness, - nested: this.nested, - publicKey: this.publicKey.toString('hex'), - script: this.script ? this.script.toRaw().toString('hex') : null, - program: this.witness ? this.getProgram().toRaw().toString('hex') : null, - type: Address.typesByVal[this.getType()].toLowerCase(), - address: this.getAddress('string', network) - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - */ - -KeyRing.prototype.fromJSON = function fromJSON(json) { - assert(json); - assert(typeof json.witness === 'boolean'); - assert(typeof json.nested === 'boolean'); - assert(typeof json.publicKey === 'string'); - assert(!json.script || typeof json.script === 'string'); - - this.witness = json.witness; - this.nested = json.nested; - this.publicKey = Buffer.from(json.publicKey, 'hex'); - - if (json.script) - this.script = Buffer.from(json.script, 'hex'); - - return this; -}; - -/** - * Instantiate an KeyRing from a jsonified transaction object. - * @param {Object} json - The jsonified transaction object. - * @returns {KeyRing} - */ - -KeyRing.fromJSON = function fromJSON(json) { - return new KeyRing().fromJSON(json); -}; - -/** - * Calculate serialization size. - * @returns {Number} - */ - -KeyRing.prototype.getSize = function getSize() { - let size = 0; - size += 1; - if (this.privateKey) { - size += encoding.sizeVarBytes(this.privateKey); - size += 1; - } else { - size += encoding.sizeVarBytes(this.publicKey); - } - size += this.script ? this.script.getVarSize() : 1; - return size; -}; - -/** - * Write the keyring to a buffer writer. - * @param {BufferWriter} bw - */ - -KeyRing.prototype.toWriter = function toWriter(bw) { - let field = 0; - - if (this.witness) - field |= 1; - - if (this.nested) - field |= 2; - - bw.writeU8(field); - - if (this.privateKey) { - bw.writeVarBytes(this.privateKey); - bw.writeU8(this.publicKey.length === 33); - } else { - bw.writeVarBytes(this.publicKey); - } - - if (this.script) - bw.writeVarBytes(this.script.toRaw()); - else - bw.writeVarint(0); - - return bw; -}; - -/** - * Serialize the keyring. - * @returns {Buffer} - */ - -KeyRing.prototype.toRaw = function toRaw() { - const size = this.getSize(); - return this.toWriter(new StaticWriter(size)).render(); -}; - -/** - * Inject properties from buffer reader. - * @private - * @param {BufferReader} br - */ - -KeyRing.prototype.fromReader = function fromReader(br) { - const field = br.readU8(); - - this.witness = (field & 1) !== 0; - this.nested = (field & 2) !== 0; - - const key = br.readVarBytes(); - - if (key.length === 32) { - const compress = br.readU8() === 1; this.privateKey = key; - this.publicKey = secp256k1.publicKeyCreate(key, compress); - } else { - this.publicKey = key; - assert(secp256k1.publicKeyVerify(key), 'Invalid public key.'); + this.publicKey = secp256k1.publicKeyCreate(key, compress !== false); + + return this; } - const script = br.readVarBytes(); + /** + * Instantiate keyring from a private key. + * @param {Buffer} key + * @param {Boolean?} compress + * @returns {KeyRing} + */ - if (script.length > 0) - this.script = Script.fromRaw(script); + static fromPrivate(key, compress) { + return new this().fromPrivate(key, compress); + } - return this; -}; + /** + * Inject data from public key. + * @private + * @param {Buffer} key + */ -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ + fromPublic(key) { + assert(Buffer.isBuffer(key), 'Public key must be a buffer.'); + assert(secp256k1.publicKeyVerify(key), 'Not a valid public key.'); + this.publicKey = key; + return this; + } -KeyRing.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(new BufferReader(data)); -}; + /** + * Generate a keyring. + * @private + * @param {Boolean?} compress + * @returns {KeyRing} + */ -/** - * Instantiate a keyring from buffer reader. - * @param {BufferReader} br - * @returns {KeyRing} - */ + generate(compress) { + const key = secp256k1.generatePrivateKey(); + return this.fromKey(key, compress); + } -KeyRing.fromReader = function fromReader(br) { - return new KeyRing().fromReader(br); -}; + /** + * Generate a keyring. + * @param {Boolean?} compress + * @returns {KeyRing} + */ -/** - * Instantiate a keyring from serialized data. - * @param {Buffer} data - * @returns {KeyRing} - */ + static generate(compress) { + return new this().generate(compress); + } -KeyRing.fromRaw = function fromRaw(data) { - return new KeyRing().fromRaw(data); -}; + /** + * Instantiate keyring from a public key. + * @param {Buffer} publicKey + * @returns {KeyRing} + */ -/** - * Test whether an object is a KeyRing. - * @param {Object} obj - * @returns {Boolean} - */ + static fromPublic(key) { + return new this().fromPublic(key); + } -KeyRing.isKeyRing = function isKeyRing(obj) { - return obj instanceof KeyRing; -}; + /** + * Inject data from public key. + * @private + * @param {Buffer} privateKey + * @param {Boolean?} compress + */ + + fromKey(key, compress) { + assert(Buffer.isBuffer(key), 'Key must be a buffer.'); + + if (key.length === 32) + return this.fromPrivate(key, compress !== false); + + return this.fromPublic(key); + } + + /** + * Instantiate keyring from a public key. + * @param {Buffer} publicKey + * @param {Boolean?} compress + * @returns {KeyRing} + */ + + static fromKey(key, compress) { + return new this().fromKey(key, compress); + } + + /** + * Inject data from script. + * @private + * @param {Buffer} key + * @param {Script} script + * @param {Boolean?} compress + */ + + fromScript(key, script, compress) { + assert(script instanceof Script, 'Non-script passed into KeyRing.'); + + this.fromKey(key, compress); + this.script = script; + + return this; + } + + /** + * Instantiate keyring from script. + * @param {Buffer} key + * @param {Script} script + * @param {Boolean?} compress + * @returns {KeyRing} + */ + + static fromScript(key, script, compress) { + return new this().fromScript(key, script, compress); + } + + /** + * Calculate WIF serialization size. + * @returns {Number} + */ + + getSecretSize() { + let size = 0; + + size += 1; + size += this.privateKey.length; + + if (this.publicKey.length === 33) + size += 1; + + size += 4; + + return size; + } + + /** + * Convert key to a CBitcoinSecret. + * @param {(Network|NetworkType)?} network + * @returns {Base58String} + */ + + toSecret(network) { + const size = this.getSecretSize(); + const bw = new StaticWriter(size); + + assert(this.privateKey, 'Cannot serialize without private key.'); + + network = Network.get(network); + + bw.writeU8(network.keyPrefix.privkey); + bw.writeBytes(this.privateKey); + + if (this.publicKey.length === 33) + bw.writeU8(1); + + bw.writeChecksum(hash256.digest); + + return base58.encode(bw.render()); + } + + /** + * Inject properties from serialized CBitcoinSecret. + * @private + * @param {Base58String} secret + * @param {(Network|NetworkType)?} network + */ + + fromSecret(data, network) { + const br = new BufferReader(base58.decode(data), true); + + const version = br.readU8(); + + Network.fromWIF(version, network); + + const key = br.readBytes(32); + + let compress = false; + + if (br.left() > 4) { + assert(br.readU8() === 1, 'Bad compression flag.'); + compress = true; + } + + br.verifyChecksum(hash256.digest); + + return this.fromPrivate(key, compress); + } + + /** + * Instantiate a keyring from a serialized CBitcoinSecret. + * @param {Base58String} secret + * @param {(Network|NetworkType)?} network + * @returns {KeyRing} + */ + + static fromSecret(data, network) { + return new this().fromSecret(data, network); + } + + /** + * Get private key. + * @param {String?} enc - Can be `"hex"`, `"base58"`, or `null`. + * @returns {Buffer} Private key. + */ + + getPrivateKey(enc, network) { + if (!this.privateKey) + return null; + + if (enc === 'base58') + return this.toSecret(network); + + if (enc === 'hex') + return this.privateKey.toString('hex'); + + return this.privateKey; + } + + /** + * Get public key. + * @param {String?} enc - `"hex"` or `null`. + * @returns {Buffer} + */ + + getPublicKey(enc) { + if (enc === 'base58') + return base58.encode(this.publicKey); + + if (enc === 'hex') + return this.publicKey.toString('hex'); + + return this.publicKey; + } + + /** + * Get redeem script. + * @returns {Script} + */ + + getScript() { + return this.script; + } + + /** + * Get witness program. + * @returns {Buffer} + */ + + getProgram() { + if (!this.witness) + return null; + + if (!this._program) { + let program; + if (!this.script) { + const hash = hash160.digest(this.publicKey); + program = Script.fromProgram(0, hash); + } else { + const hash = this.script.sha256(); + program = Script.fromProgram(0, hash); + } + this._program = program; + } + + return this._program; + } + + /** + * Get address' ripemd160 program scripthash + * (for witness programs behind a scripthash). + * @param {String?} enc - `"hex"` or `null`. + * @returns {Buffer} + */ + + getNestedHash(enc) { + if (!this.witness) + return null; + + if (!this._nestedHash) + this._nestedHash = this.getProgram().hash160(); + + return enc === 'hex' + ? this._nestedHash.toString('hex') + : this._nestedHash; + } + + /** + * Get address' scripthash address for witness program. + * @param {String?} enc - `"base58"` or `null`. + * @returns {Address|Base58Address} + */ + + getNestedAddress(enc, network) { + if (!this.witness) + return null; + + if (!this._nestedAddress) { + const hash = this.getNestedHash(); + const addr = Address.fromScripthash(hash); + this._nestedAddress = addr; + } + + if (enc === 'base58') + return this._nestedAddress.toBase58(network); + + if (enc === 'string') + return this._nestedAddress.toString(network); + + return this._nestedAddress; + } + + /** + * Get scripthash. + * @param {String?} enc - `"hex"` or `null`. + * @returns {Buffer} + */ + + getScriptHash(enc) { + if (this.witness) + return this.getScriptHash256(enc); + return this.getScriptHash160(enc); + } + + /** + * Get ripemd160 scripthash. + * @param {String?} enc - `"hex"` or `null`. + * @returns {Buffer} + */ + + getScriptHash160(enc) { + if (!this.script) + return null; + + if (!this._scriptHash160) + this._scriptHash160 = this.script.hash160(); + + return enc === 'hex' + ? this._scriptHash160.toString('hex') + : this._scriptHash160; + } + + /** + * Get sha256 scripthash. + * @param {String?} enc - `"hex"` or `null`. + * @returns {Buffer} + */ + + getScriptHash256(enc) { + if (!this.script) + return null; + + if (!this._scriptHash256) + this._scriptHash256 = this.script.sha256(); + + return enc === 'hex' + ? this._scriptHash256.toString('hex') + : this._scriptHash256; + } + + /** + * Get scripthash address. + * @param {String?} enc - `"base58"` or `null`. + * @returns {Address|Base58Address} + */ + + getScriptAddress(enc, network) { + if (!this.script) + return null; + + if (!this._scriptAddress) { + let addr; + if (this.witness) { + const hash = this.getScriptHash256(); + addr = Address.fromWitnessScripthash(hash); + } else { + const hash = this.getScriptHash160(); + addr = Address.fromScripthash(hash); + } + this._scriptAddress = addr; + } + + if (enc === 'base58') + return this._scriptAddress.toBase58(network); + + if (enc === 'string') + return this._scriptAddress.toString(network); + + return this._scriptAddress; + } + + /** + * Get public key hash. + * @param {String?} enc - `"hex"` or `null`. + * @returns {Buffer} + */ + + getKeyHash(enc) { + if (!this._keyHash) + this._keyHash = hash160.digest(this.publicKey); + + return enc === 'hex' + ? this._keyHash.toString('hex') + : this._keyHash; + } + + /** + * Get pubkeyhash address. + * @param {String?} enc - `"base58"` or `null`. + * @returns {Address|Base58Address} + */ + + getKeyAddress(enc, network) { + if (!this._keyAddress) { + const hash = this.getKeyHash(); + + let addr; + if (this.witness) + addr = Address.fromWitnessPubkeyhash(hash); + else + addr = Address.fromPubkeyhash(hash); + + this._keyAddress = addr; + } + + if (enc === 'base58') + return this._keyAddress.toBase58(network); + + if (enc === 'string') + return this._keyAddress.toString(network); + + return this._keyAddress; + } + + /** + * Get hash. + * @param {String?} enc - `"hex"` or `null`. + * @returns {Buffer} + */ + + getHash(enc) { + if (this.nested) + return this.getNestedHash(enc); + + if (this.script) + return this.getScriptHash(enc); + + return this.getKeyHash(enc); + } + + /** + * Get base58 address. + * @param {String?} enc - `"base58"` or `null`. + * @returns {Address|Base58Address} + */ + + getAddress(enc, network) { + if (this.nested) + return this.getNestedAddress(enc, network); + + if (this.script) + return this.getScriptAddress(enc, network); + + return this.getKeyAddress(enc, network); + } + + /** + * Test an address hash against hash and program hash. + * @param {Buffer} hash + * @returns {Boolean} + */ + + ownHash(hash) { + if (!hash) + return false; + + if (hash.equals(this.getKeyHash())) + return true; + + if (this.script) { + if (hash.equals(this.getScriptHash())) + return true; + } + + if (this.witness) { + if (hash.equals(this.getNestedHash())) + return true; + } + + return false; + } + + /** + * Check whether transaction output belongs to this address. + * @param {TX|Output} tx - Transaction or Output. + * @param {Number?} index - Output index. + * @returns {Boolean} + */ + + ownOutput(tx, index) { + let output; + + if (tx instanceof Output) { + output = tx; + } else { + output = tx.outputs[index]; + assert(output, 'Output does not exist.'); + } + + return this.ownHash(output.getHash()); + } + + /** + * Test a hash against script hashes to + * find the correct redeem script, if any. + * @param {Buffer} hash + * @returns {Script|null} + */ + + getRedeem(hash) { + if (this.witness) { + if (hash.equals(this.getNestedHash())) + return this.getProgram(); + } + + if (this.script) { + if (hash.equals(this.getScriptHash160())) + return this.script; + + if (hash.equals(this.getScriptHash256())) + return this.script; + } + + return null; + } + + /** + * Sign a message. + * @param {Buffer} msg + * @returns {Buffer} Signature in DER format. + */ + + sign(msg) { + assert(this.privateKey, 'Cannot sign without private key.'); + return secp256k1.sign(msg, this.privateKey); + } + + /** + * Verify a message. + * @param {Buffer} msg + * @param {Buffer} sig - Signature in DER format. + * @returns {Boolean} + */ + + verify(msg, sig) { + return secp256k1.verify(msg, sig, this.publicKey); + } + + /** + * Get witness program version. + * @returns {Number} + */ + + getVersion() { + if (!this.witness) + return -1; + + if (this.nested) + return -1; + + return 0; + } + + /** + * Get address type. + * @returns {ScriptType} + */ + + getType() { + if (this.nested) + return Address.types.SCRIPTHASH; + + if (this.witness) + return Address.types.WITNESS; + + if (this.script) + return Address.types.SCRIPTHASH; + + return Address.types.PUBKEYHASH; + } + + /** + * Inspect keyring. + * @returns {Object} + */ + + inspect() { + return this.toJSON(); + } + + /** + * Convert an KeyRing to a more json-friendly object. + * @returns {Object} + */ + + toJSON(network) { + return { + witness: this.witness, + nested: this.nested, + publicKey: this.publicKey.toString('hex'), + script: this.script ? this.script.toRaw().toString('hex') : null, + program: this.witness ? this.getProgram().toRaw().toString('hex') : null, + type: Address.typesByVal[this.getType()].toLowerCase(), + address: this.getAddress('string', network) + }; + } + + /** + * Inject properties from json object. + * @private + * @param {Object} json + */ + + fromJSON(json) { + assert(json); + assert(typeof json.witness === 'boolean'); + assert(typeof json.nested === 'boolean'); + assert(typeof json.publicKey === 'string'); + assert(!json.script || typeof json.script === 'string'); + + this.witness = json.witness; + this.nested = json.nested; + this.publicKey = Buffer.from(json.publicKey, 'hex'); + + if (json.script) + this.script = Buffer.from(json.script, 'hex'); + + return this; + } + + /** + * Instantiate an KeyRing from a jsonified transaction object. + * @param {Object} json - The jsonified transaction object. + * @returns {KeyRing} + */ + + static fromJSON(json) { + return new this().fromJSON(json); + } + + /** + * Calculate serialization size. + * @returns {Number} + */ + + getSize() { + let size = 0; + size += 1; + if (this.privateKey) { + size += encoding.sizeVarBytes(this.privateKey); + size += 1; + } else { + size += encoding.sizeVarBytes(this.publicKey); + } + size += this.script ? this.script.getVarSize() : 1; + return size; + } + + /** + * Write the keyring to a buffer writer. + * @param {BufferWriter} bw + */ + + toWriter(bw) { + let field = 0; + + if (this.witness) + field |= 1; + + if (this.nested) + field |= 2; + + bw.writeU8(field); + + if (this.privateKey) { + bw.writeVarBytes(this.privateKey); + bw.writeU8(this.publicKey.length === 33); + } else { + bw.writeVarBytes(this.publicKey); + } + + if (this.script) + bw.writeVarBytes(this.script.toRaw()); + else + bw.writeVarint(0); + + return bw; + } + + /** + * Serialize the keyring. + * @returns {Buffer} + */ + + toRaw() { + const size = this.getSize(); + return this.toWriter(new StaticWriter(size)).render(); + } + + /** + * Inject properties from buffer reader. + * @private + * @param {BufferReader} br + */ + + fromReader(br) { + const field = br.readU8(); + + this.witness = (field & 1) !== 0; + this.nested = (field & 2) !== 0; + + const key = br.readVarBytes(); + + if (key.length === 32) { + const compress = br.readU8() === 1; + this.privateKey = key; + this.publicKey = secp256k1.publicKeyCreate(key, compress); + } else { + this.publicKey = key; + assert(secp256k1.publicKeyVerify(key), 'Invalid public key.'); + } + + const script = br.readVarBytes(); + + if (script.length > 0) + this.script = Script.fromRaw(script); + + return this; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + return this.fromReader(new BufferReader(data)); + } + + /** + * Instantiate a keyring from buffer reader. + * @param {BufferReader} br + * @returns {KeyRing} + */ + + static fromReader(br) { + return new this().fromReader(br); + } + + /** + * Instantiate a keyring from serialized data. + * @param {Buffer} data + * @returns {KeyRing} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } + + /** + * Test whether an object is a KeyRing. + * @param {Object} obj + * @returns {Boolean} + */ + + static isKeyRing(obj) { + return obj instanceof KeyRing; + } +} /* * Helpers diff --git a/lib/primitives/memblock.js b/lib/primitives/memblock.js index 87628eaf..00f36083 100644 --- a/lib/primitives/memblock.js +++ b/lib/primitives/memblock.js @@ -15,6 +15,7 @@ const BufferReader = require('bufio/lib/reader'); const DUMMY = Buffer.alloc(0); /** + * Mem Block * A block object which is essentially a "placeholder" * for a full {@link Block} object. The v8 garbage * collector's head will explode if there is too much @@ -31,189 +32,190 @@ const DUMMY = Buffer.alloc(0); * 500mb of blocks on the js heap would not be a good * thing. * @alias module:primitives.MemBlock - * @constructor - * @param {NakedBlock} options + * @extends AbstractBlock */ -function MemBlock() { - if (!(this instanceof MemBlock)) - return new MemBlock(); +class MemBlock extends AbstractBlock { + /** + * Create a mem block. + * @constructor + */ - AbstractBlock.call(this); + constructor() { + super(); - this._raw = DUMMY; + this._raw = DUMMY; + } + + /** + * Test whether the block is a memblock. + * @returns {Boolean} + */ + + isMemory() { + return true; + } + + /** + * Serialize the block headers. + * @returns {Buffer} + */ + + toHead() { + return this._raw.slice(0, 80); + } + + /** + * Get the full block size. + * @returns {Number} + */ + + getSize() { + return this._raw.length; + } + + /** + * Verify the block. + * @returns {Boolean} + */ + + verifyBody() { + return true; + } + + /** + * Retrieve the coinbase height + * from the coinbase input script. + * @returns {Number} height (-1 if not present). + */ + + getCoinbaseHeight() { + if (this.version < 2) + return -1; + + try { + return this.parseCoinbaseHeight(); + } catch (e) { + return -1; + } + } + + /** + * Parse the coinbase height + * from the coinbase input script. + * @private + * @returns {Number} height (-1 if not present). + */ + + parseCoinbaseHeight() { + const br = new BufferReader(this._raw, true); + + br.seek(80); + + const txCount = br.readVarint(); + + if (txCount === 0) + return -1; + + br.seek(4); + + let inCount = br.readVarint(); + + if (inCount === 0) { + if (br.readU8() !== 0) + inCount = br.readVarint(); + } + + if (inCount === 0) + return -1; + + br.seek(36); + + const script = br.readVarBytes(); + + return Script.getCoinbaseHeight(script); + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + const br = new BufferReader(data, true); + + this.readHead(br); + + this._raw = br.data; + + return this; + } + + /** + * Insantiate a memblock from serialized data. + * @param {Buffer} data + * @returns {MemBlock} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } + + /** + * Return serialized block data. + * @returns {Buffer} + */ + + toRaw() { + return this._raw; + } + + /** + * Return serialized block data. + * @returns {Buffer} + */ + + toNormal() { + return this._raw; + } + + /** + * Parse the serialized block data + * and create an actual {@link Block}. + * @returns {Block} + * @throws Parse error + */ + + toBlock() { + const block = Block.fromRaw(this._raw); + + block._hash = this._hash; + block._hhash = this._hhash; + + return block; + } + + /** + * Convert the block to a headers object. + * @returns {Headers} + */ + + toHeaders() { + return Headers.fromBlock(this); + } + + /** + * Test whether an object is a MemBlock. + * @param {Object} obj + * @returns {Boolean} + */ + + static isMemBlock(obj) { + return obj instanceof MemBlock; + } } -Object.setPrototypeOf(MemBlock.prototype, AbstractBlock.prototype); - -/** - * Test whether the block is a memblock. - * @returns {Boolean} - */ - -MemBlock.prototype.isMemory = function isMemory() { - return true; -}; - -/** - * Serialize the block headers. - * @returns {Buffer} - */ - -MemBlock.prototype.toHead = function toHead() { - return this._raw.slice(0, 80); -}; - -/** - * Get the full block size. - * @returns {Number} - */ - -MemBlock.prototype.getSize = function getSize() { - return this._raw.length; -}; - -/** - * Verify the block. - * @returns {Boolean} - */ - -MemBlock.prototype.verifyBody = function verifyBody() { - return true; -}; - -/** - * Retrieve the coinbase height - * from the coinbase input script. - * @returns {Number} height (-1 if not present). - */ - -MemBlock.prototype.getCoinbaseHeight = function getCoinbaseHeight() { - if (this.version < 2) - return -1; - - try { - return this.parseCoinbaseHeight(); - } catch (e) { - return -1; - } -}; - -/** - * Parse the coinbase height - * from the coinbase input script. - * @private - * @returns {Number} height (-1 if not present). - */ - -MemBlock.prototype.parseCoinbaseHeight = function parseCoinbaseHeight() { - const br = new BufferReader(this._raw, true); - - br.seek(80); - - const txCount = br.readVarint(); - - if (txCount === 0) - return -1; - - br.seek(4); - - let inCount = br.readVarint(); - - if (inCount === 0) { - if (br.readU8() !== 0) - inCount = br.readVarint(); - } - - if (inCount === 0) - return -1; - - br.seek(36); - - const script = br.readVarBytes(); - - return Script.getCoinbaseHeight(script); -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -MemBlock.prototype.fromRaw = function fromRaw(data) { - const br = new BufferReader(data, true); - - this.readHead(br); - - this._raw = br.data; - - return this; -}; - -/** - * Insantiate a memblock from serialized data. - * @param {Buffer} data - * @returns {MemBlock} - */ - -MemBlock.fromRaw = function fromRaw(data) { - return new MemBlock().fromRaw(data); -}; - -/** - * Return serialized block data. - * @returns {Buffer} - */ - -MemBlock.prototype.toRaw = function toRaw() { - return this._raw; -}; - -/** - * Return serialized block data. - * @returns {Buffer} - */ - -MemBlock.prototype.toNormal = function toNormal() { - return this._raw; -}; - -/** - * Parse the serialized block data - * and create an actual {@link Block}. - * @returns {Block} - * @throws Parse error - */ - -MemBlock.prototype.toBlock = function toBlock() { - const block = Block.fromRaw(this._raw); - - block._hash = this._hash; - block._hhash = this._hhash; - - return block; -}; - -/** - * Convert the block to a headers object. - * @returns {Headers} - */ - -MemBlock.prototype.toHeaders = function toHeaders() { - return Headers.fromBlock(this); -}; - -/** - * Test whether an object is a MemBlock. - * @param {Object} obj - * @returns {Boolean} - */ - -MemBlock.isMemBlock = function isMemBlock(obj) { - return obj instanceof MemBlock; -}; - /* * Expose */ diff --git a/lib/primitives/merkleblock.js b/lib/primitives/merkleblock.js index 388f96a2..844a6b19 100644 --- a/lib/primitives/merkleblock.js +++ b/lib/primitives/merkleblock.js @@ -19,645 +19,652 @@ const Headers = require('./headers'); const DUMMY = Buffer.from([0]); /** + * Merkle Block * Represents a merkle (filtered) block. * @alias module:primitives.MerkleBlock - * @constructor * @extends AbstractBlock - * @param {NakedBlock} options */ -function MerkleBlock(options) { - if (!(this instanceof MerkleBlock)) - return new MerkleBlock(options); +class MerkleBlock extends AbstractBlock { + /** + * Create a merkle block. + * @constructor + * @param {Object} options + */ - AbstractBlock.call(this); + constructor(options) { + super(); - this.txs = []; - this.hashes = []; - this.flags = DUMMY; + this.txs = []; + this.hashes = []; + this.flags = DUMMY; - this.totalTX = 0; - this._tree = null; + this.totalTX = 0; + this._tree = null; - if (options) - this.fromOptions(options); -} - -Object.setPrototypeOf(MerkleBlock.prototype, AbstractBlock.prototype); - -/** - * Inject properties from options object. - * @private - * @param {NakedBlock} options - */ - -MerkleBlock.prototype.fromOptions = function fromOptions(options) { - this.parseOptions(options); - - assert(options, 'MerkleBlock data is required.'); - assert(Array.isArray(options.hashes)); - assert(Buffer.isBuffer(options.flags)); - assert((options.totalTX >>> 0) === options.totalTX); - - if (options.hashes) { - for (let hash of options.hashes) { - if (typeof hash === 'string') - hash = Buffer.from(hash, 'hex'); - assert(Buffer.isBuffer(hash)); - this.hashes.push(hash); - } + if (options) + this.fromOptions(options); } - if (options.flags) { + /** + * Inject properties from options object. + * @private + * @param {NakedBlock} options + */ + + fromOptions(options) { + this.parseOptions(options); + + assert(options, 'MerkleBlock data is required.'); + assert(Array.isArray(options.hashes)); assert(Buffer.isBuffer(options.flags)); - this.flags = options.flags; - } - - if (options.totalTX != null) { assert((options.totalTX >>> 0) === options.totalTX); - this.totalTX = options.totalTX; - } - return this; -}; - -/** - * Instantiate merkle block from options object. - * @param {NakedBlock} options - * @returns {MerkleBlock} - */ - -MerkleBlock.fromOptions = function fromOptions(data) { - return new MerkleBlock().fromOptions(data); -}; - -/** - * Clear any cached values. - * @param {Boolean?} all - Clear transactions. - */ - -MerkleBlock.prototype.refresh = function refresh(all) { - this._refresh(); - this._tree = null; - - if (!all) - return; - - for (const tx of this.txs) - tx.refresh(); -}; - -/** - * Test the block's _matched_ transaction vector against a hash. - * @param {Hash} hash - * @returns {Boolean} - */ - -MerkleBlock.prototype.hasTX = function hasTX(hash) { - return this.indexOf(hash) !== -1; -}; - -/** - * Test the block's _matched_ transaction vector against a hash. - * @param {Hash} hash - * @returns {Number} Index. - */ - -MerkleBlock.prototype.indexOf = function indexOf(hash) { - const tree = this.getTree(); - const index = tree.map.get(hash); - - if (index == null) - return -1; - - return index; -}; - -/** - * Verify the partial merkletree. - * @private - * @returns {Boolean} - */ - -MerkleBlock.prototype.verifyBody = function verifyBody() { - const [valid] = this.checkBody(); - return valid; -}; - -/** - * Verify the partial merkletree. - * @private - * @returns {Array} [valid, reason, score] - */ - -MerkleBlock.prototype.checkBody = function checkBody() { - const tree = this.getTree(); - - if (tree.root !== this.merkleRoot) - return [false, 'bad-txnmrklroot', 100]; - - return [true, 'valid', 0]; -}; - -/** - * Extract the matches from partial merkle - * tree and calculate merkle root. - * @returns {Object} - */ - -MerkleBlock.prototype.getTree = function getTree() { - if (!this._tree) { - try { - this._tree = this.extractTree(); - } catch (e) { - this._tree = new PartialTree(); - } - } - return this._tree; -}; - -/** - * Extract the matches from partial merkle - * tree and calculate merkle root. - * @private - * @returns {Object} - */ - -MerkleBlock.prototype.extractTree = function extractTree() { - const matches = []; - const indexes = []; - const map = new Map(); - const hashes = this.hashes; - const flags = this.flags; - const totalTX = this.totalTX; - let bitsUsed = 0; - let hashUsed = 0; - let failed = false; - let height = 0; - - const width = (height) => { - return (totalTX + (1 << height) - 1) >>> height; - }; - - const traverse = (height, pos) => { - if (bitsUsed >= flags.length * 8) { - failed = true; - return encoding.ZERO_HASH; + if (options.hashes) { + for (let hash of options.hashes) { + if (typeof hash === 'string') + hash = Buffer.from(hash, 'hex'); + assert(Buffer.isBuffer(hash)); + this.hashes.push(hash); + } } - const parent = (flags[bitsUsed / 8 | 0] >>> (bitsUsed % 8)) & 1; + if (options.flags) { + assert(Buffer.isBuffer(options.flags)); + this.flags = options.flags; + } - bitsUsed++; + if (options.totalTX != null) { + assert((options.totalTX >>> 0) === options.totalTX); + this.totalTX = options.totalTX; + } - if (height === 0 || !parent) { - if (hashUsed >= hashes.length) { + return this; + } + + /** + * Instantiate merkle block from options object. + * @param {NakedBlock} options + * @returns {MerkleBlock} + */ + + static fromOptions(data) { + return new this().fromOptions(data); + } + + /** + * Clear any cached values. + * @param {Boolean?} all - Clear transactions. + */ + + refresh(all) { + this._refresh(); + this._tree = null; + + if (!all) + return; + + for (const tx of this.txs) + tx.refresh(); + } + + /** + * Test the block's _matched_ transaction vector against a hash. + * @param {Hash} hash + * @returns {Boolean} + */ + + hasTX(hash) { + return this.indexOf(hash) !== -1; + } + + /** + * Test the block's _matched_ transaction vector against a hash. + * @param {Hash} hash + * @returns {Number} Index. + */ + + indexOf(hash) { + const tree = this.getTree(); + const index = tree.map.get(hash); + + if (index == null) + return -1; + + return index; + } + + /** + * Verify the partial merkletree. + * @private + * @returns {Boolean} + */ + + verifyBody() { + const [valid] = this.checkBody(); + return valid; + } + + /** + * Verify the partial merkletree. + * @private + * @returns {Array} [valid, reason, score] + */ + + checkBody() { + const tree = this.getTree(); + + if (tree.root !== this.merkleRoot) + return [false, 'bad-txnmrklroot', 100]; + + return [true, 'valid', 0]; + } + + /** + * Extract the matches from partial merkle + * tree and calculate merkle root. + * @returns {Object} + */ + + getTree() { + if (!this._tree) { + try { + this._tree = this.extractTree(); + } catch (e) { + this._tree = new PartialTree(); + } + } + return this._tree; + } + + /** + * Extract the matches from partial merkle + * tree and calculate merkle root. + * @private + * @returns {Object} + */ + + extractTree() { + const matches = []; + const indexes = []; + const map = new Map(); + const hashes = this.hashes; + const flags = this.flags; + const totalTX = this.totalTX; + + let bitsUsed = 0; + let hashUsed = 0; + let failed = false; + let height = 0; + + const width = (height) => { + return (totalTX + (1 << height) - 1) >>> height; + }; + + const traverse = (height, pos) => { + if (bitsUsed >= flags.length * 8) { failed = true; return encoding.ZERO_HASH; } - const hash = hashes[hashUsed++]; + const parent = (flags[bitsUsed / 8 | 0] >>> (bitsUsed % 8)) & 1; - if (height === 0 && parent) { - const txid = hash.toString('hex'); - matches.push(hash); - indexes.push(pos); - map.set(txid, pos); + bitsUsed += 1; + + if (height === 0 || !parent) { + if (hashUsed >= hashes.length) { + failed = true; + return encoding.ZERO_HASH; + } + + const hash = hashes[hashUsed]; + + hashUsed += 1; + + if (height === 0 && parent) { + const txid = hash.toString('hex'); + matches.push(hash); + indexes.push(pos); + map.set(txid, pos); + } + + return hash; } - return hash; + const left = traverse(height - 1, pos * 2); + let right; + + if (pos * 2 + 1 < width(height - 1)) { + right = traverse(height - 1, pos * 2 + 1); + if (right.equals(left)) + failed = true; + } else { + right = left; + } + + return hash256.root(left, right); + }; + + if (totalTX === 0) + throw new Error('Zero transactions.'); + + if (totalTX > consensus.MAX_BLOCK_SIZE / 60) + throw new Error('Too many transactions.'); + + if (hashes.length > totalTX) + throw new Error('Too many hashes.'); + + if (flags.length * 8 < hashes.length) + throw new Error('Flags too small.'); + + while (width(height) > 1) + height += 1; + + const root = traverse(height, 0); + + if (failed) + throw new Error('Mutated merkle tree.'); + + if (((bitsUsed + 7) / 8 | 0) !== flags.length) + throw new Error('Too many flag bits.'); + + if (hashUsed !== hashes.length) + throw new Error('Incorrect number of hashes.'); + + return new PartialTree(root, matches, indexes, map); + } + + /** + * Extract the coinbase height (always -1). + * @returns {Number} + */ + + getCoinbaseHeight() { + return -1; + } + + /** + * Inspect the block and return a more + * user-friendly representation of the data. + * @returns {Object} + */ + + inspect() { + return this.format(); + } + + /** + * Inspect the block and return a more + * user-friendly representation of the data. + * @param {CoinView} view + * @param {Number} height + * @returns {Object} + */ + + format(view, height) { + return { + hash: this.rhash(), + height: height != null ? height : -1, + date: util.date(this.time), + version: this.version.toString(16), + prevBlock: encoding.revHex(this.prevBlock), + merkleRoot: encoding.revHex(this.merkleRoot), + time: this.time, + bits: this.bits, + nonce: this.nonce, + totalTX: this.totalTX, + hashes: this.hashes.map((hash) => { + return hash.toString('hex'); + }), + flags: this.flags, + map: this.getTree().map, + txs: this.txs.length + }; + } + + /** + * Get merkleblock size. + * @returns {Number} Size. + */ + + getSize() { + let size = 0; + size += 80; + size += 4; + size += encoding.sizeVarint(this.hashes.length); + size += this.hashes.length * 32; + size += encoding.sizeVarint(this.flags.length); + size += this.flags.length; + return size; + } + + /** + * Write the merkleblock to a buffer writer. + * @param {BufferWriter} bw + */ + + toWriter(bw) { + this.writeHead(bw); + + bw.writeU32(this.totalTX); + + bw.writeVarint(this.hashes.length); + + for (const hash of this.hashes) + bw.writeHash(hash); + + bw.writeVarBytes(this.flags); + + return bw; + } + + /** + * Serialize the merkleblock. + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {Buffer|String} + */ + + toRaw() { + const size = this.getSize(); + return this.toWriter(new StaticWriter(size)).render(); + } + + /** + * Inject properties from buffer reader. + * @private + * @param {BufferReader} br + */ + + fromReader(br) { + this.readHead(br); + + this.totalTX = br.readU32(); + + const count = br.readVarint(); + + for (let i = 0; i < count; i++) + this.hashes.push(br.readHash()); + + this.flags = br.readVarBytes(); + + return this; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + return this.fromReader(new BufferReader(data)); + } + + /** + * Instantiate a merkleblock from a buffer reader. + * @param {BufferReader} br + * @returns {MerkleBlock} + */ + + static fromReader(br) { + return new this().fromReader(br); + } + + /** + * Instantiate a merkleblock from a serialized data. + * @param {Buffer} data + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {MerkleBlock} + */ + + static fromRaw(data, enc) { + if (typeof data === 'string') + data = Buffer.from(data, enc); + return new this().fromRaw(data); + } + + /** + * Convert the block to an object suitable + * for JSON serialization. + * @returns {Object} + */ + + toJSON() { + return this.getJSON(); + } + + /** + * Convert the block to an object suitable + * for JSON serialization. Note that the hashes + * will be reversed to abide by bitcoind's legacy + * of little-endian uint256s. + * @param {Network} network + * @param {CoinView} view + * @param {Number} height + * @returns {Object} + */ + + getJSON(network, view, height) { + return { + hash: this.rhash(), + height: height, + version: this.version, + prevBlock: encoding.revHex(this.prevBlock), + merkleRoot: encoding.revHex(this.merkleRoot), + time: this.time, + bits: this.bits, + nonce: this.nonce, + totalTX: this.totalTX, + hashes: this.hashes.map((hash) => { + return encoding.revHex(hash.toString('hex')); + }), + flags: this.flags.toString('hex') + }; + } + + /** + * Inject properties from json object. + * @private + * @param {Object} json + */ + + fromJSON(json) { + assert(json, 'MerkleBlock data is required.'); + assert(Array.isArray(json.hashes)); + assert(typeof json.flags === 'string'); + assert((json.totalTX >>> 0) === json.totalTX); + + this.parseJSON(json); + + for (let hash of json.hashes) { + hash = encoding.revHex(hash); + this.hashes.push(Buffer.from(hash, 'hex')); } - const left = traverse(height - 1, pos * 2); - let right; + this.flags = Buffer.from(json.flags, 'hex'); - if (pos * 2 + 1 < width(height - 1)) { - right = traverse(height - 1, pos * 2 + 1); - if (right.equals(left)) - failed = true; - } else { - right = left; + this.totalTX = json.totalTX; + + return this; + } + + /** + * Instantiate a merkle block from a jsonified block object. + * @param {Object} json - The jsonified block object. + * @returns {MerkleBlock} + */ + + static fromJSON(json) { + return new this().fromJSON(json); + } + + /** + * Create a merkleblock from a {@link Block} object, passing + * it through a filter first. This will build the partial + * merkle tree. + * @param {Block} block + * @param {Bloom} filter + * @returns {MerkleBlock} + */ + + static fromBlock(block, filter) { + const matches = []; + + for (const tx of block.txs) + matches.push(tx.isWatched(filter) ? 1 : 0); + + return this.fromMatches(block, matches); + } + + /** + * Create a merkleblock from an array of txids. + * This will build the partial merkle tree. + * @param {Block} block + * @param {Hash[]} hashes + * @returns {MerkleBlock} + */ + + static fromHashes(block, hashes) { + const filter = new Set(); + + for (let hash of hashes) { + if (Buffer.isBuffer(hash)) + hash = hash.toString('hex'); + filter.add(hash); } - return hash256.root(left, right); - }; + const matches = []; - if (totalTX === 0) - throw new Error('Zero transactions.'); - - if (totalTX > consensus.MAX_BLOCK_SIZE / 60) - throw new Error('Too many transactions.'); - - if (hashes.length > totalTX) - throw new Error('Too many hashes.'); - - if (flags.length * 8 < hashes.length) - throw new Error('Flags too small.'); - - while (width(height) > 1) - height++; - - const root = traverse(height, 0); - - if (failed) - throw new Error('Mutated merkle tree.'); - - if (((bitsUsed + 7) / 8 | 0) !== flags.length) - throw new Error('Too many flag bits.'); - - if (hashUsed !== hashes.length) - throw new Error('Incorrect number of hashes.'); - - return new PartialTree(root, matches, indexes, map); -}; - -/** - * Extract the coinbase height (always -1). - * @returns {Number} - */ - -MerkleBlock.prototype.getCoinbaseHeight = function getCoinbaseHeight() { - return -1; -}; - -/** - * Inspect the block and return a more - * user-friendly representation of the data. - * @returns {Object} - */ - -MerkleBlock.prototype.inspect = function inspect() { - return this.format(); -}; - -/** - * Inspect the block and return a more - * user-friendly representation of the data. - * @param {CoinView} view - * @param {Number} height - * @returns {Object} - */ - -MerkleBlock.prototype.format = function format(view, height) { - return { - hash: this.rhash(), - height: height != null ? height : -1, - date: util.date(this.time), - version: this.version.toString(16), - prevBlock: encoding.revHex(this.prevBlock), - merkleRoot: encoding.revHex(this.merkleRoot), - time: this.time, - bits: this.bits, - nonce: this.nonce, - totalTX: this.totalTX, - hashes: this.hashes.map((hash) => { - return hash.toString('hex'); - }), - flags: this.flags, - map: this.getTree().map, - txs: this.txs.length - }; -}; - -/** - * Get merkleblock size. - * @returns {Number} Size. - */ - -MerkleBlock.prototype.getSize = function getSize() { - let size = 0; - size += 80; - size += 4; - size += encoding.sizeVarint(this.hashes.length); - size += this.hashes.length * 32; - size += encoding.sizeVarint(this.flags.length); - size += this.flags.length; - return size; -}; - -/** - * Write the merkleblock to a buffer writer. - * @param {BufferWriter} bw - */ - -MerkleBlock.prototype.toWriter = function toWriter(bw) { - this.writeHead(bw); - - bw.writeU32(this.totalTX); - - bw.writeVarint(this.hashes.length); - - for (const hash of this.hashes) - bw.writeHash(hash); - - bw.writeVarBytes(this.flags); - - return bw; -}; - -/** - * Serialize the merkleblock. - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {Buffer|String} - */ - -MerkleBlock.prototype.toRaw = function toRaw() { - const size = this.getSize(); - return this.toWriter(new StaticWriter(size)).render(); -}; - -/** - * Inject properties from buffer reader. - * @private - * @param {BufferReader} br - */ - -MerkleBlock.prototype.fromReader = function fromReader(br) { - this.readHead(br); - - this.totalTX = br.readU32(); - - const count = br.readVarint(); - - for (let i = 0; i < count; i++) - this.hashes.push(br.readHash()); - - this.flags = br.readVarBytes(); - - return this; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -MerkleBlock.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(new BufferReader(data)); -}; - -/** - * Instantiate a merkleblock from a buffer reader. - * @param {BufferReader} br - * @returns {MerkleBlock} - */ - -MerkleBlock.fromReader = function fromReader(br) { - return new MerkleBlock().fromReader(br); -}; - -/** - * Instantiate a merkleblock from a serialized data. - * @param {Buffer} data - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {MerkleBlock} - */ - -MerkleBlock.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - return new MerkleBlock().fromRaw(data); -}; - -/** - * Convert the block to an object suitable - * for JSON serialization. - * @returns {Object} - */ - -MerkleBlock.prototype.toJSON = function toJSON() { - return this.getJSON(); -}; - -/** - * Convert the block to an object suitable - * for JSON serialization. Note that the hashes - * will be reversed to abide by bitcoind's legacy - * of little-endian uint256s. - * @param {Network} network - * @param {CoinView} view - * @param {Number} height - * @returns {Object} - */ - -MerkleBlock.prototype.getJSON = function getJSON(network, view, height) { - return { - hash: this.rhash(), - height: height, - version: this.version, - prevBlock: encoding.revHex(this.prevBlock), - merkleRoot: encoding.revHex(this.merkleRoot), - time: this.time, - bits: this.bits, - nonce: this.nonce, - totalTX: this.totalTX, - hashes: this.hashes.map((hash) => { - return encoding.revHex(hash.toString('hex')); - }), - flags: this.flags.toString('hex') - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - */ - -MerkleBlock.prototype.fromJSON = function fromJSON(json) { - assert(json, 'MerkleBlock data is required.'); - assert(Array.isArray(json.hashes)); - assert(typeof json.flags === 'string'); - assert((json.totalTX >>> 0) === json.totalTX); - - this.parseJSON(json); - - for (let hash of json.hashes) { - hash = encoding.revHex(hash); - this.hashes.push(Buffer.from(hash, 'hex')); - } - - this.flags = Buffer.from(json.flags, 'hex'); - - this.totalTX = json.totalTX; - - return this; -}; - -/** - * Instantiate a merkle block from a jsonified block object. - * @param {Object} json - The jsonified block object. - * @returns {MerkleBlock} - */ - -MerkleBlock.fromJSON = function fromJSON(json) { - return new MerkleBlock().fromJSON(json); -}; - -/** - * Create a merkleblock from a {@link Block} object, passing - * it through a filter first. This will build the partial - * merkle tree. - * @param {Block} block - * @param {Bloom} filter - * @returns {MerkleBlock} - */ - -MerkleBlock.fromBlock = function fromBlock(block, filter) { - const matches = []; - - for (const tx of block.txs) - matches.push(tx.isWatched(filter) ? 1 : 0); - - return MerkleBlock.fromMatches(block, matches); -}; - -/** - * Create a merkleblock from an array of txids. - * This will build the partial merkle tree. - * @param {Block} block - * @param {Hash[]} hashes - * @returns {MerkleBlock} - */ - -MerkleBlock.fromHashes = function fromHashes(block, hashes) { - const filter = new Set(); - - for (let hash of hashes) { - if (Buffer.isBuffer(hash)) - hash = hash.toString('hex'); - filter.add(hash); - } - - const matches = []; - - for (const tx of block.txs) { - const hash = tx.hash('hex'); - matches.push(filter.has(hash) ? 1 : 0); - } - - return MerkleBlock.fromMatches(block, matches); -}; - -/** - * Create a merkleblock from an array of matches. - * This will build the partial merkle tree. - * @param {Block} block - * @param {Number[]} matches - * @returns {MerkleBlock} - */ - -MerkleBlock.fromMatches = function fromMatches(block, matches) { - const txs = []; - const leaves = []; - const bits = []; - const hashes = []; - const totalTX = block.txs.length; - let height = 0; - - const width = (height) => { - return (totalTX + (1 << height) - 1) >>> height; - }; - - const hash = (height, pos, leaves) => { - if (height === 0) - return leaves[pos]; - - const left = hash(height - 1, pos * 2, leaves); - let right; - - if (pos * 2 + 1 < width(height - 1)) - right = hash(height - 1, pos * 2 + 1, leaves); - else - right = left; - - return hash256.root(left, right); - }; - - const traverse = (height, pos, leaves, matches) => { - let parent = 0; - - for (let p = (pos << height); p < ((pos + 1) << height) && p < totalTX; p++) - parent |= matches[p]; - - bits.push(parent); - - if (height === 0 || !parent) { - hashes.push(hash(height, pos, leaves)); - return; + for (const tx of block.txs) { + const hash = tx.hash('hex'); + matches.push(filter.has(hash) ? 1 : 0); } - traverse(height - 1, pos * 2, leaves, matches); - - if (pos * 2 + 1 < width(height - 1)) - traverse(height - 1, pos * 2 + 1, leaves, matches); - }; - - for (let i = 0; i < block.txs.length; i++) { - const tx = block.txs[i]; - - if (matches[i]) - txs.push(tx); - - leaves.push(tx.hash()); + return this.fromMatches(block, matches); } - while (width(height) > 1) - height++; + /** + * Create a merkleblock from an array of matches. + * This will build the partial merkle tree. + * @param {Block} block + * @param {Number[]} matches + * @returns {MerkleBlock} + */ - traverse(height, 0, leaves, matches); + static fromMatches(block, matches) { + const txs = []; + const leaves = []; + const bits = []; + const hashes = []; + const totalTX = block.txs.length; + let height = 0; - const flags = Buffer.allocUnsafe((bits.length + 7) / 8 | 0); - flags.fill(0); + const width = (height) => { + return (totalTX + (1 << height) - 1) >>> height; + }; - for (let p = 0; p < bits.length; p++) - flags[p / 8 | 0] |= bits[p] << (p % 8); + const hash = (height, pos, leaves) => { + if (height === 0) + return leaves[pos]; - const merkle = new MerkleBlock(); - merkle._hash = block._hash; - merkle._hhash = block._hhash; - merkle.version = block.version; - merkle.prevBlock = block.prevBlock; - merkle.merkleRoot = block.merkleRoot; - merkle.time = block.time; - merkle.bits = block.bits; - merkle.nonce = block.nonce; - merkle.totalTX = totalTX; - merkle.hashes = hashes; - merkle.flags = flags; - merkle.txs = txs; + const left = hash(height - 1, pos * 2, leaves); + let right; - return merkle; -}; + if (pos * 2 + 1 < width(height - 1)) + right = hash(height - 1, pos * 2 + 1, leaves); + else + right = left; -/** - * Test whether an object is a MerkleBlock. - * @param {Object} obj - * @returns {Boolean} - */ + return hash256.root(left, right); + }; -MerkleBlock.isMerkleBlock = function isMerkleBlock(obj) { - return obj instanceof MerkleBlock; -}; + const traverse = (height, pos, leaves, matches) => { + let parent = 0; -/** - * Convert the block to a headers object. - * @returns {Headers} - */ + for (let p = pos << height; p < ((pos + 1) << height) && p < totalTX; p++) + parent |= matches[p]; -MerkleBlock.prototype.toHeaders = function toHeaders() { - return Headers.fromBlock(this); -}; + bits.push(parent); + + if (height === 0 || !parent) { + hashes.push(hash(height, pos, leaves)); + return; + } + + traverse(height - 1, pos * 2, leaves, matches); + + if (pos * 2 + 1 < width(height - 1)) + traverse(height - 1, pos * 2 + 1, leaves, matches); + }; + + for (let i = 0; i < block.txs.length; i++) { + const tx = block.txs[i]; + + if (matches[i]) + txs.push(tx); + + leaves.push(tx.hash()); + } + + while (width(height) > 1) + height += 1; + + traverse(height, 0, leaves, matches); + + const flags = Buffer.allocUnsafe((bits.length + 7) / 8 | 0); + flags.fill(0); + + for (let p = 0; p < bits.length; p++) + flags[p / 8 | 0] |= bits[p] << (p % 8); + + const merkle = new this(); + merkle._hash = block._hash; + merkle._hhash = block._hhash; + merkle.version = block.version; + merkle.prevBlock = block.prevBlock; + merkle.merkleRoot = block.merkleRoot; + merkle.time = block.time; + merkle.bits = block.bits; + merkle.nonce = block.nonce; + merkle.totalTX = totalTX; + merkle.hashes = hashes; + merkle.flags = flags; + merkle.txs = txs; + + return merkle; + } + + /** + * Test whether an object is a MerkleBlock. + * @param {Object} obj + * @returns {Boolean} + */ + + static isMerkleBlock(obj) { + return obj instanceof MerkleBlock; + } + + /** + * Convert the block to a headers object. + * @returns {Headers} + */ + + toHeaders() { + return Headers.fromBlock(this); + } +} /* * Helpers */ -function PartialTree(root, matches, indexes, map) { - this.root = root ? root.toString('hex') : encoding.NULL_HASH; - this.matches = matches || []; - this.indexes = indexes || []; - this.map = map || new Map(); +class PartialTree { + constructor(root, matches, indexes, map) { + this.root = root ? root.toString('hex') : encoding.NULL_HASH; + this.matches = matches || []; + this.indexes = indexes || []; + this.map = map || new Map(); + } } /* diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index 12c9bbb7..788af26f 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -23,1558 +23,1853 @@ const Amount = require('../btc/amount'); const Stack = require('../script/stack'); /** + * MTX * A mutable transaction object. * @alias module:primitives.MTX * @extends TX - * @constructor - * @param {Object} options - * @param {Number?} options.version - * @param {Number?} options.changeIndex - * @param {Input[]?} options.inputs - * @param {Output[]?} options.outputs - * @property {Number} version - Transaction version. - * @property {Number} flag - Flag field for segregated witness. - * Always non-zero (1 if not present). - * @property {Input[]} inputs - * @property {Output[]} outputs - * @property {Number} locktime - nLockTime + * @property {Number} changeIndex * @property {CoinView} view */ -function MTX(options) { - if (!(this instanceof MTX)) - return new MTX(options); +class MTX extends TX { + /** + * Create a mutable transaction. + * @alias module:primitives.MTX + * @constructor + * @param {Object} options + */ - TX.call(this); + constructor(options) { + super(); - this.mutable = true; - this.changeIndex = -1; - this.view = new CoinView(); + this.mutable = true; + this.changeIndex = -1; + this.view = new CoinView(); - if (options) - this.fromOptions(options); -} - -Object.setPrototypeOf(MTX.prototype, TX.prototype); - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -MTX.prototype.fromOptions = function fromOptions(options) { - if (options.version != null) { - assert((options.version >>> 0) === options.version, - 'Version must a be uint32.'); - this.version = options.version; + if (options) + this.fromOptions(options); } - if (options.inputs) { - assert(Array.isArray(options.inputs), 'Inputs must be an array.'); - for (const input of options.inputs) - this.addInput(input); - } + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ - if (options.outputs) { - assert(Array.isArray(options.outputs), 'Outputs must be an array.'); - for (const output of options.outputs) - this.addOutput(output); - } - - if (options.locktime != null) { - assert((options.locktime >>> 0) === options.locktime, - 'Locktime must be a uint32.'); - this.locktime = options.locktime; - } - - if (options.changeIndex != null) { - if (options.changeIndex !== -1) { - assert((options.changeIndex >>> 0) === options.changeIndex, - 'Change index must be a uint32.'); - this.changeIndex = options.changeIndex; - } else { - this.changeIndex = -1; + fromOptions(options) { + if (options.version != null) { + assert((options.version >>> 0) === options.version, + 'Version must a be uint32.'); + this.version = options.version; } + + if (options.inputs) { + assert(Array.isArray(options.inputs), 'Inputs must be an array.'); + for (const input of options.inputs) + this.addInput(input); + } + + if (options.outputs) { + assert(Array.isArray(options.outputs), 'Outputs must be an array.'); + for (const output of options.outputs) + this.addOutput(output); + } + + if (options.locktime != null) { + assert((options.locktime >>> 0) === options.locktime, + 'Locktime must be a uint32.'); + this.locktime = options.locktime; + } + + if (options.changeIndex != null) { + if (options.changeIndex !== -1) { + assert((options.changeIndex >>> 0) === options.changeIndex, + 'Change index must be a uint32.'); + this.changeIndex = options.changeIndex; + } else { + this.changeIndex = -1; + } + } + + return this; } - return this; -}; + /** + * Instantiate MTX from options. + * @param {Object} options + * @returns {MTX} + */ -/** - * Instantiate MTX from options. - * @param {Object} options - * @returns {MTX} - */ - -MTX.fromOptions = function fromOptions(options) { - return new MTX().fromOptions(options); -}; - -/** - * Clone the transaction. Note that - * this will not carry over the view. - * @returns {MTX} - */ - -MTX.prototype.clone = function clone() { - const mtx = new MTX(); - mtx.inject(this); - mtx.changeIndex = this.changeIndex; - return mtx; -}; - -/** - * Add an input to the transaction. - * @param {Input|Object} options - * @returns {Input} - * - * @example - * mtx.addInput({ prevout: { hash: ... }, script: ... }); - * mtx.addInput(new Input()); - */ - -MTX.prototype.addInput = function addInput(options) { - const input = Input.fromOptions(options); - this.inputs.push(input); - return input; -}; - -/** - * Add an outpoint as an input. - * @param {Outpoint|Object} outpoint - * @returns {Input} - * - * @example - * mtx.addOutpoint({ hash: ..., index: 0 }); - * mtx.addOutpoint(new Outpoint(hash, index)); - */ - -MTX.prototype.addOutpoint = function addOutpoint(outpoint) { - const prevout = Outpoint.fromOptions(outpoint); - const input = Input.fromOutpoint(prevout); - this.inputs.push(input); - return input; -}; - -/** - * Add a coin as an input. Note that this will - * add the coin to the internal coin viewpoint. - * @param {Coin} coin - * @returns {Input} - * - * @example - * mtx.addCoin(Coin.fromTX(tx, 0, -1)); - */ - -MTX.prototype.addCoin = function addCoin(coin) { - assert(coin instanceof Coin, 'Cannot add non-coin.'); - - const input = Input.fromCoin(coin); - - this.inputs.push(input); - this.view.addCoin(coin); - - return input; -}; - -/** - * Add a transaction as an input. Note that - * this will add the coin to the internal - * coin viewpoint. - * @param {TX} tx - * @param {Number} index - * @param {Number?} height - * @returns {Input} - * - * @example - * mtx.addTX(tx, 0); - */ - -MTX.prototype.addTX = function addTX(tx, index, height) { - assert(tx instanceof TX, 'Cannot add non-transaction.'); - - if (height == null) - height = -1; - - const input = Input.fromTX(tx, index); - - this.inputs.push(input); - - this.view.addIndex(tx, index, height); - - return input; -}; - -/** - * Add an output. - * @param {Address|Script|Output|Object} script - Script or output options. - * @param {Amount?} value - * @returns {Output} - * - * @example - * mtx.addOutput(new Output()); - * mtx.addOutput({ address: ..., value: 100000 }); - * mtx.addOutput(address, 100000); - * mtx.addOutput(script, 100000); - */ - -MTX.prototype.addOutput = function addOutput(script, value) { - let output; - - if (value != null) - output = Output.fromScript(script, value); - else - output = Output.fromOptions(script); - - this.outputs.push(output); - - return output; -}; - -/** - * Verify all transaction inputs. - * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] - * @returns {Boolean} Whether the inputs are valid. - * @throws {ScriptError} on invalid inputs - */ - -MTX.prototype.check = function check(flags) { - return TX.prototype.check.call(this, this.view, flags); -}; - -/** - * Verify the transaction inputs on the worker pool - * (if workers are enabled). - * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] - * @param {WorkerPool?} pool - * @returns {Promise} - */ - -MTX.prototype.checkAsync = function checkAsync(flags, pool) { - return TX.prototype.checkAsync.call(this, this.view, flags, pool); -}; - -/** - * Verify all transaction inputs. - * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] - * @returns {Boolean} Whether the inputs are valid. - */ - -MTX.prototype.verify = function verify(flags) { - try { - this.check(flags); - } catch (e) { - if (e.type === 'ScriptError') - return false; - throw e; + static fromOptions(options) { + return new this().fromOptions(options); } - return true; -}; -/** - * Verify the transaction inputs on the worker pool - * (if workers are enabled). - * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] - * @param {WorkerPool?} pool - * @returns {Promise} - */ + /** + * Clone the transaction. Note that + * this will not carry over the view. + * @returns {MTX} + */ -MTX.prototype.verifyAsync = async function verifyAsync(flags, pool) { - try { - await this.checkAsync(flags, pool); - } catch (e) { - if (e.type === 'ScriptError') - return false; - throw e; + clone() { + const mtx = new this.constructor(); + mtx.inject(this); + mtx.changeIndex = this.changeIndex; + return mtx; } - return true; -}; -/** - * Calculate the fee for the transaction. - * @returns {Amount} fee (zero if not all coins are available). - */ + /** + * Add an input to the transaction. + * @param {Input|Object} options + * @returns {Input} + * + * @example + * mtx.addInput({ prevout: { hash: ... }, script: ... }); + * mtx.addInput(new Input()); + */ -MTX.prototype.getFee = function getFee() { - return TX.prototype.getFee.call(this, this.view); -}; + addInput(options) { + const input = Input.fromOptions(options); + this.inputs.push(input); + return input; + } -/** - * Calculate the total input value. - * @returns {Amount} value - */ + /** + * Add an outpoint as an input. + * @param {Outpoint|Object} outpoint + * @returns {Input} + * + * @example + * mtx.addOutpoint({ hash: ..., index: 0 }); + * mtx.addOutpoint(new Outpoint(hash, index)); + */ -MTX.prototype.getInputValue = function getInputValue() { - return TX.prototype.getInputValue.call(this, this.view); -}; + addOutpoint(outpoint) { + const prevout = Outpoint.fromOptions(outpoint); + const input = Input.fromOutpoint(prevout); + this.inputs.push(input); + return input; + } -/** - * Get all input addresses. - * @returns {Address[]} addresses - */ + /** + * Add a coin as an input. Note that this will + * add the coin to the internal coin viewpoint. + * @param {Coin} coin + * @returns {Input} + * + * @example + * mtx.addCoin(Coin.fromTX(tx, 0, -1)); + */ -MTX.prototype.getInputAddresses = function getInputAddresses() { - return TX.prototype.getInputAddresses.call(this, this.view); -}; + addCoin(coin) { + assert(coin instanceof Coin, 'Cannot add non-coin.'); -/** - * Get all addresses. - * @returns {Address[]} addresses - */ + const input = Input.fromCoin(coin); -MTX.prototype.getAddresses = function getAddresses() { - return TX.prototype.getAddresses.call(this, this.view); -}; + this.inputs.push(input); + this.view.addCoin(coin); -/** - * Get all input address hashes. - * @returns {Hash[]} hashes - */ + return input; + } -MTX.prototype.getInputHashes = function getInputHashes(enc) { - return TX.prototype.getInputHashes.call(this, this.view, enc); -}; + /** + * Add a transaction as an input. Note that + * this will add the coin to the internal + * coin viewpoint. + * @param {TX} tx + * @param {Number} index + * @param {Number?} height + * @returns {Input} + * + * @example + * mtx.addTX(tx, 0); + */ -/** - * Get all address hashes. - * @returns {Hash[]} hashes - */ + addTX(tx, index, height) { + assert(tx instanceof TX, 'Cannot add non-transaction.'); -MTX.prototype.getHashes = function getHashes(enc) { - return TX.prototype.getHashes.call(this, this.view, enc); -}; + if (height == null) + height = -1; -/** - * Test whether the transaction has - * all coins available/filled. - * @returns {Boolean} - */ + const input = Input.fromTX(tx, index); -MTX.prototype.hasCoins = function hasCoins() { - return TX.prototype.hasCoins.call(this, this.view); -}; + this.inputs.push(input); -/** - * Calculate virtual sigop count. - * @param {VerifyFlags?} flags - * @returns {Number} sigop count - */ + this.view.addIndex(tx, index, height); -MTX.prototype.getSigops = function getSigops(flags) { - return TX.prototype.getSigops.call(this, this.view, flags); -}; + return input; + } -/** - * Calculate sigops weight, taking into account witness programs. - * @param {VerifyFlags?} flags - * @returns {Number} sigop weight - */ + /** + * Add an output. + * @param {Address|Script|Output|Object} script - Script or output options. + * @param {Amount?} value + * @returns {Output} + * + * @example + * mtx.addOutput(new Output()); + * mtx.addOutput({ address: ..., value: 100000 }); + * mtx.addOutput(address, 100000); + * mtx.addOutput(script, 100000); + */ -MTX.prototype.getSigopsCost = function getSigopsCost(flags) { - return TX.prototype.getSigopsCost.call(this, this.view, flags); -}; + addOutput(script, value) { + let output; -/** - * Calculate the virtual size of the transaction - * (weighted against bytes per sigop cost). - * @returns {Number} vsize - */ + if (value != null) + output = Output.fromScript(script, value); + else + output = Output.fromOptions(script); -MTX.prototype.getSigopsSize = function getSigopsSize() { - return TX.prototype.getSigopsSize.call(this, this.getSigopsCost()); -}; + this.outputs.push(output); -/** - * Perform contextual checks to verify input, output, - * and fee values, as well as coinbase spend maturity - * (coinbases can only be spent 100 blocks or more - * after they're created). Note that this function is - * consensus critical. - * @param {Number} height - Height at which the - * transaction is being spent. In the mempool this is - * the chain height plus one at the time it entered the pool. - * @returns {Boolean} - */ + return output; + } -MTX.prototype.verifyInputs = function verifyInputs(height) { - const [fee] = this.checkInputs(height); - return fee !== -1; -}; + /** + * Verify all transaction inputs. + * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] + * @returns {Boolean} Whether the inputs are valid. + * @throws {ScriptError} on invalid inputs + */ -/** - * Perform contextual checks to verify input, output, - * and fee values, as well as coinbase spend maturity - * (coinbases can only be spent 100 blocks or more - * after they're created). Note that this function is - * consensus critical. - * @param {Number} height - Height at which the - * transaction is being spent. In the mempool this is - * the chain height plus one at the time it entered the pool. - * @returns {Array} [fee, reason, score] - */ + check(flags) { + return super.check(this.view, flags); + } -MTX.prototype.checkInputs = function checkInputs(height) { - return TX.prototype.checkInputs.call(this, this.view, height); -}; + /** + * Verify the transaction inputs on the worker pool + * (if workers are enabled). + * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] + * @param {WorkerPool?} pool + * @returns {Promise} + */ -/** - * Build input script (or witness) templates (with - * OP_0 in place of signatures). - * @param {Number} index - Input index. - * @param {Coin|Output} coin - * @param {KeyRing} ring - * @returns {Boolean} Whether the script was able to be built. - */ + checkAsync(flags, pool) { + return super.checkAsync(this.view, flags, pool); + } -MTX.prototype.scriptInput = function scriptInput(index, coin, ring) { - const input = this.inputs[index]; + /** + * Verify all transaction inputs. + * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] + * @returns {Boolean} Whether the inputs are valid. + */ - assert(input, 'Input does not exist.'); - assert(coin, 'No coin passed.'); - - // Don't bother with any below calculation - // if the output is already templated. - if (input.script.raw.length !== 0 - || input.witness.items.length !== 0) { + verify(flags) { + try { + this.check(flags); + } catch (e) { + if (e.type === 'ScriptError') + return false; + throw e; + } return true; } - // Get the previous output's script - const prev = coin.script; + /** + * Verify the transaction inputs on the worker pool + * (if workers are enabled). + * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] + * @param {WorkerPool?} pool + * @returns {Promise} + */ - // This is easily the hardest part about - // building a transaction with segwit: - // figuring out where the redeem script - // and witness redeem scripts go. - const sh = prev.getScripthash(); + async verifyAsync(flags, pool) { + try { + await this.checkAsync(flags, pool); + } catch (e) { + if (e.type === 'ScriptError') + return false; + throw e; + } + return true; + } - if (sh) { - const redeem = ring.getRedeem(sh); + /** + * Calculate the fee for the transaction. + * @returns {Amount} fee (zero if not all coins are available). + */ - if (!redeem) - return false; + getFee() { + return super.getFee(this.view); + } - // Witness program nested in regular P2SH. - if (redeem.isProgram()) { - // P2WSH nested within pay-to-scripthash. - const wsh = redeem.getWitnessScripthash(); + /** + * Calculate the total input value. + * @returns {Amount} value + */ + + getInputValue() { + return super.getInputValue(this.view); + } + + /** + * Get all input addresses. + * @returns {Address[]} addresses + */ + + getInputAddresses() { + return super.getInputAddresses(this.view); + } + + /** + * Get all addresses. + * @returns {Address[]} addresses + */ + + getAddresses() { + return super.getAddresses(this.view); + } + + /** + * Get all input address hashes. + * @returns {Hash[]} hashes + */ + + getInputHashes(enc) { + return super.getInputHashes(this.view, enc); + } + + /** + * Get all address hashes. + * @returns {Hash[]} hashes + */ + + getHashes(enc) { + return super.getHashes(this.view, enc); + } + + /** + * Test whether the transaction has + * all coins available/filled. + * @returns {Boolean} + */ + + hasCoins() { + return super.hasCoins(this.view); + } + + /** + * Calculate virtual sigop count. + * @param {VerifyFlags?} flags + * @returns {Number} sigop count + */ + + getSigops(flags) { + return super.getSigops(this.view, flags); + } + + /** + * Calculate sigops weight, taking into account witness programs. + * @param {VerifyFlags?} flags + * @returns {Number} sigop weight + */ + + getSigopsCost(flags) { + return super.getSigopsCost(this.view, flags); + } + + /** + * Calculate the virtual size of the transaction + * (weighted against bytes per sigop cost). + * @returns {Number} vsize + */ + + getSigopsSize() { + return super.getSigopsSize(this.getSigopsCost()); + } + + /** + * Perform contextual checks to verify input, output, + * and fee values, as well as coinbase spend maturity + * (coinbases can only be spent 100 blocks or more + * after they're created). Note that this function is + * consensus critical. + * @param {Number} height - Height at which the + * transaction is being spent. In the mempool this is + * the chain height plus one at the time it entered the pool. + * @returns {Boolean} + */ + + verifyInputs(height) { + const [fee] = this.checkInputs(height); + return fee !== -1; + } + + /** + * Perform contextual checks to verify input, output, + * and fee values, as well as coinbase spend maturity + * (coinbases can only be spent 100 blocks or more + * after they're created). Note that this function is + * consensus critical. + * @param {Number} height - Height at which the + * transaction is being spent. In the mempool this is + * the chain height plus one at the time it entered the pool. + * @returns {Array} [fee, reason, score] + */ + + checkInputs(height) { + return super.checkInputs(this.view, height); + } + + /** + * Build input script (or witness) templates (with + * OP_0 in place of signatures). + * @param {Number} index - Input index. + * @param {Coin|Output} coin + * @param {KeyRing} ring + * @returns {Boolean} Whether the script was able to be built. + */ + + scriptInput(index, coin, ring) { + const input = this.inputs[index]; + + assert(input, 'Input does not exist.'); + assert(coin, 'No coin passed.'); + + // Don't bother with any below calculation + // if the output is already templated. + if (input.script.raw.length !== 0 + || input.witness.items.length !== 0) { + return true; + } + + // Get the previous output's script + const prev = coin.script; + + // This is easily the hardest part about + // building a transaction with segwit: + // figuring out where the redeem script + // and witness redeem scripts go. + const sh = prev.getScripthash(); + + if (sh) { + const redeem = ring.getRedeem(sh); + + if (!redeem) + return false; + + // Witness program nested in regular P2SH. + if (redeem.isProgram()) { + // P2WSH nested within pay-to-scripthash. + const wsh = redeem.getWitnessScripthash(); + if (wsh) { + const wredeem = ring.getRedeem(wsh); + + if (!wredeem) + return false; + + const witness = this.scriptVector(wredeem, ring); + + if (!witness) + return false; + + witness.push(wredeem.toRaw()); + + input.witness.fromStack(witness); + input.script.fromItems([redeem.toRaw()]); + + return true; + } + + // P2WPKH nested within pay-to-scripthash. + const wpkh = redeem.getWitnessPubkeyhash(); + if (wpkh) { + const pkh = Script.fromPubkeyhash(wpkh); + const witness = this.scriptVector(pkh, ring); + + if (!witness) + return false; + + input.witness.fromStack(witness); + input.script.fromItems([redeem.toRaw()]); + + return true; + } + + // Unknown witness program. + return false; + } + + // Regular P2SH. + const vector = this.scriptVector(redeem, ring); + + if (!vector) + return false; + + vector.push(redeem.toRaw()); + + input.script.fromStack(vector); + + return true; + } + + // Witness program. + if (prev.isProgram()) { + // Bare P2WSH. + const wsh = prev.getWitnessScripthash(); if (wsh) { const wredeem = ring.getRedeem(wsh); if (!wredeem) return false; - const witness = this.scriptVector(wredeem, ring); + const vector = this.scriptVector(wredeem, ring); - if (!witness) + if (!vector) return false; - witness.push(wredeem.toRaw()); + vector.push(wredeem.toRaw()); - input.witness.fromStack(witness); - input.script.fromItems([redeem.toRaw()]); + input.witness.fromStack(vector); return true; } - // P2WPKH nested within pay-to-scripthash. - const wpkh = redeem.getWitnessPubkeyhash(); + // Bare P2WPKH. + const wpkh = prev.getWitnessPubkeyhash(); if (wpkh) { const pkh = Script.fromPubkeyhash(wpkh); - const witness = this.scriptVector(pkh, ring); + const vector = this.scriptVector(pkh, ring); - if (!witness) + if (!vector) return false; - input.witness.fromStack(witness); - input.script.fromItems([redeem.toRaw()]); + input.witness.fromStack(vector); return true; } - // Unknown witness program. + // Bare... who knows? return false; } - // Regular P2SH. - const vector = this.scriptVector(redeem, ring); + // Wow, a normal output! Praise be to Jengus and Gord. + const vector = this.scriptVector(prev, ring); if (!vector) return false; - vector.push(redeem.toRaw()); - input.script.fromStack(vector); return true; } - // Witness program. - if (prev.isProgram()) { - // Bare P2WSH. - const wsh = prev.getWitnessScripthash(); - if (wsh) { - const wredeem = ring.getRedeem(wsh); + /** + * Build script for a single vector + * based on a previous script. + * @param {Script} prev + * @param {Buffer} ring + * @return {Boolean} + */ - if (!wredeem) - return false; + scriptVector(prev, ring) { + // P2PK + const pk = prev.getPubkey(); + if (pk) { + if (!pk.equals(ring.publicKey)) + return null; - const vector = this.scriptVector(wredeem, ring); + const stack = new Stack(); - if (!vector) - return false; - - vector.push(wredeem.toRaw()); - - input.witness.fromStack(vector); - - return true; - } - - // Bare P2WPKH. - const wpkh = prev.getWitnessPubkeyhash(); - if (wpkh) { - const pkh = Script.fromPubkeyhash(wpkh); - const vector = this.scriptVector(pkh, ring); - - if (!vector) - return false; - - input.witness.fromStack(vector); - - return true; - } - - // Bare... who knows? - return false; - } - - // Wow, a normal output! Praise be to Jengus and Gord. - const vector = this.scriptVector(prev, ring); - - if (!vector) - return false; - - input.script.fromStack(vector); - - return true; -}; - -/** - * Build script for a single vector - * based on a previous script. - * @param {Script} prev - * @param {Buffer} ring - * @return {Boolean} - */ - -MTX.prototype.scriptVector = function scriptVector(prev, ring) { - // P2PK - const pk = prev.getPubkey(); - if (pk) { - if (!pk.equals(ring.publicKey)) - return null; - - const stack = new Stack(); - - stack.pushInt(0); - - return stack; - } - - // P2PKH - const pkh = prev.getPubkeyhash(); - if (pkh) { - if (!pkh.equals(ring.getKeyHash())) - return null; - - const stack = new Stack(); - - stack.pushInt(0); - stack.pushData(ring.publicKey); - - return stack; - } - - // Multisig - const [, n] = prev.getMultisig(); - if (n !== -1) { - if (prev.indexOf(ring.publicKey) === -1) - return null; - - // Technically we should create m signature slots, - // but we create n signature slots so we can order - // the signatures properly. - const stack = new Stack(); - - stack.pushInt(0); - - // Fill script with `n` signature slots. - for (let i = 0; i < n; i++) stack.pushInt(0); - return stack; - } - - return null; -}; - -/** - * Sign a transaction input on the worker pool - * (if workers are enabled). - * @param {Number} index - * @param {Coin|Output} coin - * @param {KeyRing} ring - * @param {SighashType?} type - * @param {WorkerPool?} pool - * @returns {Promise} - */ - -MTX.prototype.signInputAsync = async function signInputAsync(index, coin, ring, type, pool) { - if (!pool) - return this.signInput(index, coin, ring, type); - - return await pool.signInput(this, index, coin, ring, type, pool); -}; - -/** - * Sign an input. - * @param {Number} index - Index of input being signed. - * @param {Coin|Output} coin - * @param {KeyRing} ring - Private key. - * @param {SighashType} type - * @returns {Boolean} Whether the input was able to be signed. - */ - -MTX.prototype.signInput = function signInput(index, coin, ring, type) { - const input = this.inputs[index]; - const key = ring.privateKey; - - assert(input, 'Input does not exist.'); - assert(coin, 'No coin passed.'); - - // Get the previous output's script - const value = coin.value; - let prev = coin.script; - let vector = input.script; - let version = 0; - let redeem = false; - - // Grab regular p2sh redeem script. - if (prev.isScripthash()) { - prev = input.script.getRedeem(); - if (!prev) - throw new Error('Input has not been templated.'); - redeem = true; - } - - // If the output script is a witness program, - // we have to switch the vector to the witness - // and potentially alter the length. Note that - // witnesses are stack items, so the `dummy` - // _has_ to be an empty buffer (what OP_0 - // pushes onto the stack). - if (prev.isWitnessScripthash()) { - prev = input.witness.getRedeem(); - if (!prev) - throw new Error('Input has not been templated.'); - vector = input.witness; - redeem = true; - version = 1; - } else { - const wpkh = prev.getWitnessPubkeyhash(); - if (wpkh) { - prev = Script.fromPubkeyhash(wpkh); - vector = input.witness; - redeem = false; - version = 1; + return stack; } + + // P2PKH + const pkh = prev.getPubkeyhash(); + if (pkh) { + if (!pkh.equals(ring.getKeyHash())) + return null; + + const stack = new Stack(); + + stack.pushInt(0); + stack.pushData(ring.publicKey); + + return stack; + } + + // Multisig + const [, n] = prev.getMultisig(); + if (n !== -1) { + if (prev.indexOf(ring.publicKey) === -1) + return null; + + // Technically we should create m signature slots, + // but we create n signature slots so we can order + // the signatures properly. + const stack = new Stack(); + + stack.pushInt(0); + + // Fill script with `n` signature slots. + for (let i = 0; i < n; i++) + stack.pushInt(0); + + return stack; + } + + return null; } - // Create our signature. - const sig = this.signature(index, prev, value, key, type, version); + /** + * Sign a transaction input on the worker pool + * (if workers are enabled). + * @param {Number} index + * @param {Coin|Output} coin + * @param {KeyRing} ring + * @param {SighashType?} type + * @param {WorkerPool?} pool + * @returns {Promise} + */ + + async signInputAsync(index, coin, ring, type, pool) { + if (!pool) + return this.signInput(index, coin, ring, type); + + return await pool.signInput(this, index, coin, ring, type, pool); + } + + /** + * Sign an input. + * @param {Number} index - Index of input being signed. + * @param {Coin|Output} coin + * @param {KeyRing} ring - Private key. + * @param {SighashType} type + * @returns {Boolean} Whether the input was able to be signed. + */ + + signInput(index, coin, ring, type) { + const input = this.inputs[index]; + const key = ring.privateKey; + + assert(input, 'Input does not exist.'); + assert(coin, 'No coin passed.'); + + // Get the previous output's script + const value = coin.value; + let prev = coin.script; + let vector = input.script; + let version = 0; + let redeem = false; + + // Grab regular p2sh redeem script. + if (prev.isScripthash()) { + prev = input.script.getRedeem(); + if (!prev) + throw new Error('Input has not been templated.'); + redeem = true; + } + + // If the output script is a witness program, + // we have to switch the vector to the witness + // and potentially alter the length. Note that + // witnesses are stack items, so the `dummy` + // _has_ to be an empty buffer (what OP_0 + // pushes onto the stack). + if (prev.isWitnessScripthash()) { + prev = input.witness.getRedeem(); + if (!prev) + throw new Error('Input has not been templated.'); + vector = input.witness; + redeem = true; + version = 1; + } else { + const wpkh = prev.getWitnessPubkeyhash(); + if (wpkh) { + prev = Script.fromPubkeyhash(wpkh); + vector = input.witness; + redeem = false; + version = 1; + } + } + + // Create our signature. + const sig = this.signature(index, prev, value, key, type, version); + + if (redeem) { + const stack = vector.toStack(); + const redeem = stack.pop(); + + const result = this.signVector(prev, stack, sig, ring); + + if (!result) + return false; + + result.push(redeem); + + vector.fromStack(result); + + return true; + } - if (redeem) { const stack = vector.toStack(); - const redeem = stack.pop(); - const result = this.signVector(prev, stack, sig, ring); if (!result) return false; - result.push(redeem); - vector.fromStack(result); return true; } - const stack = vector.toStack(); - const result = this.signVector(prev, stack, sig, ring); + /** + * Add a signature to a vector + * based on a previous script. + * @param {Script} prev + * @param {Stack} vector + * @param {Buffer} sig + * @param {KeyRing} ring + * @return {Boolean} + */ - if (!result) - return false; + signVector(prev, vector, sig, ring) { + // P2PK + const pk = prev.getPubkey(); + if (pk) { + // Make sure the pubkey is ours. + if (!ring.publicKey.equals(pk)) + return null; - vector.fromStack(result); + if (vector.length === 0) + throw new Error('Input has not been templated.'); - return true; -}; + // Already signed. + if (vector.get(0).length > 0) + return vector; -/** - * Add a signature to a vector - * based on a previous script. - * @param {Script} prev - * @param {Stack} vector - * @param {Buffer} sig - * @param {KeyRing} ring - * @return {Boolean} - */ + vector.set(0, sig); -MTX.prototype.signVector = function signVector(prev, vector, sig, ring) { - // P2PK - const pk = prev.getPubkey(); - if (pk) { - // Make sure the pubkey is ours. - if (!ring.publicKey.equals(pk)) - return null; - - if (vector.length === 0) - throw new Error('Input has not been templated.'); - - // Already signed. - if (vector.get(0).length > 0) return vector; + } - vector.set(0, sig); + // P2PKH + const pkh = prev.getPubkeyhash(); + if (pkh) { + // Make sure the pubkey hash is ours. + if (!ring.getKeyHash().equals(pkh)) + return null; - return vector; - } + if (vector.length !== 2) + throw new Error('Input has not been templated.'); - // P2PKH - const pkh = prev.getPubkeyhash(); - if (pkh) { - // Make sure the pubkey hash is ours. - if (!ring.getKeyHash().equals(pkh)) - return null; + if (vector.get(1).length === 0) + throw new Error('Input has not been templated.'); - if (vector.length !== 2) - throw new Error('Input has not been templated.'); + // Already signed. + if (vector.get(0).length > 0) + return vector; - if (vector.get(1).length === 0) - throw new Error('Input has not been templated.'); + vector.set(0, sig); - // Already signed. - if (vector.get(0).length > 0) return vector; - - vector.set(0, sig); - - return vector; - } - - // Multisig - const [m, n] = prev.getMultisig(); - if (m !== -1) { - if (vector.length < 2) - throw new Error('Input has not been templated.'); - - if (vector.get(0).length !== 0) - throw new Error('Input has not been templated.'); - - // Too many signature slots. Abort. - if (vector.length - 1 > n) - throw new Error('Input has not been templated.'); - - // Count the number of current signatures. - let total = 0; - for (let i = 1; i < vector.length; i++) { - const item = vector.get(i); - if (item.length > 0) - total += 1; } - // Signatures are already finalized. - if (total === m && vector.length - 1 === m) - return vector; + // Multisig + const [m, n] = prev.getMultisig(); + if (m !== -1) { + if (vector.length < 2) + throw new Error('Input has not been templated.'); - // Add some signature slots for us to use if - // there was for some reason not enough. - while (vector.length - 1 < n) - vector.pushInt(0); + if (vector.get(0).length !== 0) + throw new Error('Input has not been templated.'); - // Grab the redeem script's keys to figure - // out where our key should go. - const keys = []; - for (const op of prev.code) { - if (op.data) - keys.push(op.data); - } + // Too many signature slots. Abort. + if (vector.length - 1 > n) + throw new Error('Input has not been templated.'); - // Find the key index so we can place - // the signature in the same index. - let keyIndex = -1; - - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - if (key.equals(ring.publicKey)) { - keyIndex = i; - break; - } - } - - // Our public key is not in the prev_out - // script. We tried to sign a transaction - // that is not redeemable by us. - if (keyIndex === -1) - return null; - - // Offset key index by one to turn it into - // "sig index". Accounts for OP_0 byte at - // the start. - keyIndex += 1; - - // Add our signature to the correct slot - // and increment the total number of - // signatures. - if (keyIndex < vector.length && total < m) { - if (vector.get(keyIndex).length === 0) { - vector.set(keyIndex, sig); - total += 1; - } - } - - // All signatures added. Finalize. - if (total >= m) { - // Remove empty slots left over. - for (let i = vector.length - 1; i >= 1; i--) { + // Count the number of current signatures. + let total = 0; + for (let i = 1; i < vector.length; i++) { const item = vector.get(i); - if (item.length === 0) - vector.remove(i); + if (item.length > 0) + total += 1; } - // Remove signatures which are not required. - // This should never happen. - while (total > m) { - vector.pop(); - total -= 1; + // Signatures are already finalized. + if (total === m && vector.length - 1 === m) + return vector; + + // Add some signature slots for us to use if + // there was for some reason not enough. + while (vector.length - 1 < n) + vector.pushInt(0); + + // Grab the redeem script's keys to figure + // out where our key should go. + const keys = []; + for (const op of prev.code) { + if (op.data) + keys.push(op.data); } - // Sanity checks. - assert(total === m); - assert(vector.length - 1 === m); + // Find the key index so we can place + // the signature in the same index. + let keyIndex = -1; + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key.equals(ring.publicKey)) { + keyIndex = i; + break; + } + } + + // Our public key is not in the prev_out + // script. We tried to sign a transaction + // that is not redeemable by us. + if (keyIndex === -1) + return null; + + // Offset key index by one to turn it into + // "sig index". Accounts for OP_0 byte at + // the start. + keyIndex += 1; + + // Add our signature to the correct slot + // and increment the total number of + // signatures. + if (keyIndex < vector.length && total < m) { + if (vector.get(keyIndex).length === 0) { + vector.set(keyIndex, sig); + total += 1; + } + } + + // All signatures added. Finalize. + if (total >= m) { + // Remove empty slots left over. + for (let i = vector.length - 1; i >= 1; i--) { + const item = vector.get(i); + if (item.length === 0) + vector.remove(i); + } + + // Remove signatures which are not required. + // This should never happen. + while (total > m) { + vector.pop(); + total -= 1; + } + + // Sanity checks. + assert(total === m); + assert(vector.length - 1 === m); + } + + return vector; } - return vector; + return null; } - return null; -}; + /** + * Test whether the transaction is fully-signed. + * @returns {Boolean} + */ -/** - * Test whether the transaction is fully-signed. - * @returns {Boolean} - */ + isSigned() { + for (let i = 0; i < this.inputs.length; i++) { + const {prevout} = this.inputs[i]; + const coin = this.view.getOutput(prevout); -MTX.prototype.isSigned = function isSigned() { - for (let i = 0; i < this.inputs.length; i++) { - const {prevout} = this.inputs[i]; - const coin = this.view.getOutput(prevout); + if (!coin) + return false; - if (!coin) - return false; - - if (!this.isInputSigned(i, coin)) - return false; - } - - return true; -}; - -/** - * Test whether an input is fully-signed. - * @param {Number} index - * @param {Coin|Output} coin - * @returns {Boolean} - */ - -MTX.prototype.isInputSigned = function isInputSigned(index, coin) { - const input = this.inputs[index]; - - assert(input, 'Input does not exist.'); - assert(coin, 'No coin passed.'); - - let prev = coin.script; - let vector = input.script; - let redeem = false; - - // Grab redeem script if possible. - if (prev.isScripthash()) { - prev = input.script.getRedeem(); - if (!prev) - return false; - redeem = true; - } - - // If the output script is a witness program, - // we have to switch the vector to the witness - // and potentially alter the length. - if (prev.isWitnessScripthash()) { - prev = input.witness.getRedeem(); - if (!prev) - return false; - vector = input.witness; - redeem = true; - } else { - const wpkh = prev.getWitnessPubkeyhash(); - if (wpkh) { - prev = Script.fromPubkeyhash(wpkh); - vector = input.witness; - redeem = false; - } - } - - const stack = vector.toStack(); - - if (redeem) - stack.pop(); - - return this.isVectorSigned(prev, stack); -}; - -/** - * Test whether a vector is fully-signed. - * @param {Script} prev - * @param {Stack} vector - * @returns {Boolean} - */ - -MTX.prototype.isVectorSigned = function isVectorSigned(prev, vector) { - if (prev.isPubkey()) { - if (vector.length !== 1) - return false; - - if (vector.get(0).length === 0) - return false; - - return true; - } - - if (prev.isPubkeyhash()) { - if (vector.length !== 2) - return false; - - if (vector.get(0).length === 0) - return false; - - if (vector.get(1).length === 0) - return false; - - return true; - } - - const [m] = prev.getMultisig(); - - if (m !== -1) { - // Ensure we have the correct number - // of required signatures. - if (vector.length - 1 !== m) - return false; - - // Ensure all members are signatures. - for (let i = 1; i < vector.length; i++) { - const item = vector.get(i); - if (item.length === 0) + if (!this.isInputSigned(i, coin)) return false; } return true; } - return false; -}; + /** + * Test whether an input is fully-signed. + * @param {Number} index + * @param {Coin|Output} coin + * @returns {Boolean} + */ -/** - * Build input scripts (or witnesses). - * @param {KeyRing} ring - Address used to sign. The address - * must be able to redeem the coin. - * @returns {Number} Number of inputs templated. - */ + isInputSigned(index, coin) { + const input = this.inputs[index]; -MTX.prototype.template = function template(ring) { - if (Array.isArray(ring)) { - let total = 0; - for (const key of ring) - total += this.template(key); - return total; - } + assert(input, 'Input does not exist.'); + assert(coin, 'No coin passed.'); - let total = 0; + let prev = coin.script; + let vector = input.script; + let redeem = false; - for (let i = 0; i < this.inputs.length; i++) { - const {prevout} = this.inputs[i]; - const coin = this.view.getOutput(prevout); - - if (!coin) - continue; - - if (!ring.ownOutput(coin)) - continue; - - // Build script for input - if (!this.scriptInput(i, coin, ring)) - continue; - - total += 1; - } - - return total; -}; - -/** - * Built input scripts (or witnesses) and sign the inputs. - * @param {KeyRing} ring - Address used to sign. The address - * must be able to redeem the coin. - * @param {SighashType} type - * @returns {Number} Number of inputs signed. - */ - -MTX.prototype.sign = function sign(ring, type) { - if (Array.isArray(ring)) { - let total = 0; - for (const key of ring) - total += this.sign(key, type); - return total; - } - - assert(ring.privateKey, 'No private key available.'); - - let total = 0; - - for (let i = 0; i < this.inputs.length; i++) { - const {prevout} = this.inputs[i]; - const coin = this.view.getOutput(prevout); - - if (!coin) - continue; - - if (!ring.ownOutput(coin)) - continue; - - // Build script for input - if (!this.scriptInput(i, coin, ring)) - continue; - - // Sign input - if (!this.signInput(i, coin, ring, type)) - continue; - - total += 1; - } - - return total; -}; - -/** - * Sign the transaction inputs on the worker pool - * (if workers are enabled). - * @param {KeyRing} ring - * @param {SighashType?} type - * @param {WorkerPool?} pool - * @returns {Promise} - */ - -MTX.prototype.signAsync = async function signAsync(ring, type, pool) { - if (!pool) - return this.sign(ring, type); - - return await pool.sign(this, ring, type); -}; - -/** - * Estimate maximum possible size. - * @param {Function?} estimate - Input script size estimator. - * @returns {Number} - */ - -MTX.prototype.estimateSize = async function estimateSize(estimate) { - const scale = consensus.WITNESS_SCALE_FACTOR; - - let total = 0; - - // Calculate the size, minus the input scripts. - total += 4; - total += encoding.sizeVarint(this.inputs.length); - total += this.inputs.length * 40; - - total += encoding.sizeVarint(this.outputs.length); - - for (const output of this.outputs) - total += output.getSize(); - - total += 4; - - // Add size for signatures and public keys - for (const {prevout} of this.inputs) { - const coin = this.view.getOutput(prevout); - - // We're out of luck here. - // Just assume it's a p2pkh. - if (!coin) { - total += 110; - continue; + // Grab redeem script if possible. + if (prev.isScripthash()) { + prev = input.script.getRedeem(); + if (!prev) + return false; + redeem = true; } - // Previous output script. - const prev = coin.script; - - // P2PK - if (prev.isPubkey()) { - // varint script size - total += 1; - // OP_PUSHDATA0 [signature] - total += 1 + 73; - continue; - } - - // P2PKH - if (prev.isPubkeyhash()) { - // varint script size - total += 1; - // OP_PUSHDATA0 [signature] - total += 1 + 73; - // OP_PUSHDATA0 [key] - total += 1 + 33; - continue; - } - - const [m] = prev.getMultisig(); - if (m !== -1) { - let size = 0; - // Bare Multisig - // OP_0 - size += 1; - // OP_PUSHDATA0 [signature] ... - size += (1 + 73) * m; - // varint len - size += encoding.sizeVarint(size); - total += size; - continue; - } - - // P2WPKH - if (prev.isWitnessPubkeyhash()) { - let size = 0; - // varint-items-len - size += 1; - // varint-len [signature] - size += 1 + 73; - // varint-len [key] - size += 1 + 33; - // vsize - size = (size + scale - 1) / scale | 0; - total += size; - continue; - } - - // Call out to the custom estimator. - if (estimate) { - const size = await estimate(prev); - if (size !== -1) { - total += size; - continue; + // If the output script is a witness program, + // we have to switch the vector to the witness + // and potentially alter the length. + if (prev.isWitnessScripthash()) { + prev = input.witness.getRedeem(); + if (!prev) + return false; + vector = input.witness; + redeem = true; + } else { + const wpkh = prev.getWitnessPubkeyhash(); + if (wpkh) { + prev = Script.fromPubkeyhash(wpkh); + vector = input.witness; + redeem = false; } } - // P2SH - if (prev.isScripthash()) { - // varint size + const stack = vector.toStack(); + + if (redeem) + stack.pop(); + + return this.isVectorSigned(prev, stack); + } + + /** + * Test whether a vector is fully-signed. + * @param {Script} prev + * @param {Stack} vector + * @returns {Boolean} + */ + + isVectorSigned(prev, vector) { + if (prev.isPubkey()) { + if (vector.length !== 1) + return false; + + if (vector.get(0).length === 0) + return false; + + return true; + } + + if (prev.isPubkeyhash()) { + if (vector.length !== 2) + return false; + + if (vector.get(0).length === 0) + return false; + + if (vector.get(1).length === 0) + return false; + + return true; + } + + const [m] = prev.getMultisig(); + + if (m !== -1) { + // Ensure we have the correct number + // of required signatures. + if (vector.length - 1 !== m) + return false; + + // Ensure all members are signatures. + for (let i = 1; i < vector.length; i++) { + const item = vector.get(i); + if (item.length === 0) + return false; + } + + return true; + } + + return false; + } + + /** + * Build input scripts (or witnesses). + * @param {KeyRing} ring - Address used to sign. The address + * must be able to redeem the coin. + * @returns {Number} Number of inputs templated. + */ + + template(ring) { + if (Array.isArray(ring)) { + let total = 0; + for (const key of ring) + total += this.template(key); + return total; + } + + let total = 0; + + for (let i = 0; i < this.inputs.length; i++) { + const {prevout} = this.inputs[i]; + const coin = this.view.getOutput(prevout); + + if (!coin) + continue; + + if (!ring.ownOutput(coin)) + continue; + + // Build script for input + if (!this.scriptInput(i, coin, ring)) + continue; + total += 1; - // 2-of-3 multisig input - total += 149; - continue; } - // P2WSH - if (prev.isWitnessScripthash()) { - let size = 0; - // varint-items-len - size += 1; - // 2-of-3 multisig input - size += 149; - // vsize - size = (size + scale - 1) / scale | 0; - total += size; - continue; + return total; + } + + /** + * Built input scripts (or witnesses) and sign the inputs. + * @param {KeyRing} ring - Address used to sign. The address + * must be able to redeem the coin. + * @param {SighashType} type + * @returns {Number} Number of inputs signed. + */ + + sign(ring, type) { + if (Array.isArray(ring)) { + let total = 0; + for (const key of ring) + total += this.sign(key, type); + return total; } - // Unknown. - total += 110; + assert(ring.privateKey, 'No private key available.'); + + let total = 0; + + for (let i = 0; i < this.inputs.length; i++) { + const {prevout} = this.inputs[i]; + const coin = this.view.getOutput(prevout); + + if (!coin) + continue; + + if (!ring.ownOutput(coin)) + continue; + + // Build script for input + if (!this.scriptInput(i, coin, ring)) + continue; + + // Sign input + if (!this.signInput(i, coin, ring, type)) + continue; + + total += 1; + } + + return total; } - return total; -}; + /** + * Sign the transaction inputs on the worker pool + * (if workers are enabled). + * @param {KeyRing} ring + * @param {SighashType?} type + * @param {WorkerPool?} pool + * @returns {Promise} + */ -/** - * Select necessary coins based on total output value. - * @param {Coin[]} coins - * @param {Object?} options - * @returns {CoinSelection} - * @throws on not enough funds available. - */ + async signAsync(ring, type, pool) { + if (!pool) + return this.sign(ring, type); -MTX.prototype.selectCoins = function selectCoins(coins, options) { - const selector = new CoinSelector(this, options); - return selector.select(coins); -}; - -/** - * Attempt to subtract a fee from a single output. - * @param {Number} index - * @param {Amount} fee - */ - -MTX.prototype.subtractIndex = function subtractIndex(index, fee) { - assert(typeof index === 'number'); - assert(typeof fee === 'number'); - - const output = this.outputs[index]; - - if (!output) - throw new Error('Subtraction index does not exist.'); - - if (output.value < fee + output.getDustThreshold()) - throw new Error('Could not subtract fee.'); - - output.value -= fee; -}; - -/** - * Attempt to subtract a fee from all outputs evenly. - * @param {Amount} fee - */ - -MTX.prototype.subtractFee = function subtractFee(fee) { - assert(typeof fee === 'number'); - - let outputs = 0; - - for (const output of this.outputs) { - // Ignore nulldatas and - // other OP_RETURN scripts. - if (output.script.isUnspendable()) - continue; - outputs += 1; + return await pool.sign(this, ring, type); } - if (outputs === 0) - throw new Error('Could not subtract fee.'); + /** + * Estimate maximum possible size. + * @param {Function?} estimate - Input script size estimator. + * @returns {Number} + */ - const left = fee % outputs; - const share = (fee - left) / outputs; + async estimateSize(estimate) { + const scale = consensus.WITNESS_SCALE_FACTOR; - // First pass, remove even shares. - for (const output of this.outputs) { - if (output.script.isUnspendable()) - continue; + let total = 0; - if (output.value < share + output.getDustThreshold()) + // Calculate the size, minus the input scripts. + total += 4; + total += encoding.sizeVarint(this.inputs.length); + total += this.inputs.length * 40; + + total += encoding.sizeVarint(this.outputs.length); + + for (const output of this.outputs) + total += output.getSize(); + + total += 4; + + // Add size for signatures and public keys + for (const {prevout} of this.inputs) { + const coin = this.view.getOutput(prevout); + + // We're out of luck here. + // Just assume it's a p2pkh. + if (!coin) { + total += 110; + continue; + } + + // Previous output script. + const prev = coin.script; + + // P2PK + if (prev.isPubkey()) { + // varint script size + total += 1; + // OP_PUSHDATA0 [signature] + total += 1 + 73; + continue; + } + + // P2PKH + if (prev.isPubkeyhash()) { + // varint script size + total += 1; + // OP_PUSHDATA0 [signature] + total += 1 + 73; + // OP_PUSHDATA0 [key] + total += 1 + 33; + continue; + } + + const [m] = prev.getMultisig(); + if (m !== -1) { + let size = 0; + // Bare Multisig + // OP_0 + size += 1; + // OP_PUSHDATA0 [signature] ... + size += (1 + 73) * m; + // varint len + size += encoding.sizeVarint(size); + total += size; + continue; + } + + // P2WPKH + if (prev.isWitnessPubkeyhash()) { + let size = 0; + // varint-items-len + size += 1; + // varint-len [signature] + size += 1 + 73; + // varint-len [key] + size += 1 + 33; + // vsize + size = (size + scale - 1) / scale | 0; + total += size; + continue; + } + + // Call out to the custom estimator. + if (estimate) { + const size = await estimate(prev); + if (size !== -1) { + total += size; + continue; + } + } + + // P2SH + if (prev.isScripthash()) { + // varint size + total += 1; + // 2-of-3 multisig input + total += 149; + continue; + } + + // P2WSH + if (prev.isWitnessScripthash()) { + let size = 0; + // varint-items-len + size += 1; + // 2-of-3 multisig input + size += 149; + // vsize + size = (size + scale - 1) / scale | 0; + total += size; + continue; + } + + // Unknown. + total += 110; + } + + return total; + } + + /** + * Select necessary coins based on total output value. + * @param {Coin[]} coins + * @param {Object?} options + * @returns {CoinSelection} + * @throws on not enough funds available. + */ + + selectCoins(coins, options) { + const selector = new CoinSelector(this, options); + return selector.select(coins); + } + + /** + * Attempt to subtract a fee from a single output. + * @param {Number} index + * @param {Amount} fee + */ + + subtractIndex(index, fee) { + assert(typeof index === 'number'); + assert(typeof fee === 'number'); + + const output = this.outputs[index]; + + if (!output) + throw new Error('Subtraction index does not exist.'); + + if (output.value < fee + output.getDustThreshold()) throw new Error('Could not subtract fee.'); - output.value -= share; + output.value -= fee; } - // Second pass, remove the remainder - // for the one unlucky output. - for (const output of this.outputs) { - if (output.script.isUnspendable()) - continue; + /** + * Attempt to subtract a fee from all outputs evenly. + * @param {Amount} fee + */ - if (output.value >= left + output.getDustThreshold()) { - output.value -= left; - return; + subtractFee(fee) { + assert(typeof fee === 'number'); + + let outputs = 0; + + for (const output of this.outputs) { + // Ignore nulldatas and + // other OP_RETURN scripts. + if (output.script.isUnspendable()) + continue; + outputs += 1; + } + + if (outputs === 0) + throw new Error('Could not subtract fee.'); + + const left = fee % outputs; + const share = (fee - left) / outputs; + + // First pass, remove even shares. + for (const output of this.outputs) { + if (output.script.isUnspendable()) + continue; + + if (output.value < share + output.getDustThreshold()) + throw new Error('Could not subtract fee.'); + + output.value -= share; + } + + // Second pass, remove the remainder + // for the one unlucky output. + for (const output of this.outputs) { + if (output.script.isUnspendable()) + continue; + + if (output.value >= left + output.getDustThreshold()) { + output.value -= left; + return; + } + } + + throw new Error('Could not subtract fee.'); + } + + /** + * Select coins and fill the inputs. + * @param {Coin[]} coins + * @param {Object} options - See {@link MTX#selectCoins} options. + * @returns {CoinSelector} + */ + + async fund(coins, options) { + assert(options, 'Options are required.'); + assert(options.changeAddress, 'Change address is required.'); + assert(this.inputs.length === 0, 'TX is already funded.'); + + // Select necessary coins. + const select = await this.selectCoins(coins, options); + + // Add coins to transaction. + for (const coin of select.chosen) + this.addCoin(coin); + + // Attempt to subtract fee. + if (select.subtractFee) { + const index = select.subtractIndex; + if (index !== -1) + this.subtractIndex(index, select.fee); + else + this.subtractFee(select.fee); + } + + // Add a change output. + const output = new Output(); + output.value = select.change; + output.script.fromAddress(select.changeAddress); + + if (output.isDust(policy.MIN_RELAY)) { + // Do nothing. Change is added to fee. + this.changeIndex = -1; + assert.strictEqual(this.getFee(), select.fee + select.change); + } else { + this.outputs.push(output); + this.changeIndex = this.outputs.length - 1; + assert.strictEqual(this.getFee(), select.fee); + } + + return select; + } + + /** + * Sort inputs and outputs according to BIP69. + * @see https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki + */ + + sortMembers() { + let changeOutput = null; + + if (this.changeIndex !== -1) { + changeOutput = this.outputs[this.changeIndex]; + assert(changeOutput); + } + + this.inputs.sort(sortInputs); + this.outputs.sort(sortOutputs); + + if (this.changeIndex !== -1) { + this.changeIndex = this.outputs.indexOf(changeOutput); + assert(this.changeIndex !== -1); } } - throw new Error('Could not subtract fee.'); -}; + /** + * Avoid fee sniping. + * @param {Number} - Current chain height. + * @see bitcoin/src/wallet/wallet.cpp + */ -/** - * Select coins and fill the inputs. - * @param {Coin[]} coins - * @param {Object} options - See {@link MTX#selectCoins} options. - * @returns {CoinSelector} - */ + avoidFeeSniping(height) { + assert(typeof height === 'number', 'Must pass in height.'); -MTX.prototype.fund = async function fund(coins, options) { - assert(options, 'Options are required.'); - assert(options.changeAddress, 'Change address is required.'); - assert(this.inputs.length === 0, 'TX is already funded.'); + if ((Math.random() * 10 | 0) === 0) { + height -= Math.random() * 100 | 0; - // Select necessary coins. - const select = await this.selectCoins(coins, options); + if (height < 0) + height = 0; + } - // Add coins to transaction. - for (const coin of select.chosen) - this.addCoin(coin); - - // Attempt to subtract fee. - if (select.subtractFee) { - const index = select.subtractIndex; - if (index !== -1) - this.subtractIndex(index, select.fee); - else - this.subtractFee(select.fee); + this.setLocktime(height); } - // Add a change output. - const output = new Output(); - output.value = select.change; - output.script.fromAddress(select.changeAddress); + /** + * Set locktime and sequences appropriately. + * @param {Number} locktime + */ - if (output.isDust(policy.MIN_RELAY)) { - // Do nothing. Change is added to fee. - this.changeIndex = -1; - assert.strictEqual(this.getFee(), select.fee + select.change); - } else { - this.outputs.push(output); - this.changeIndex = this.outputs.length - 1; - assert.strictEqual(this.getFee(), select.fee); + setLocktime(locktime) { + assert((locktime >>> 0) === locktime, 'Locktime must be a uint32.'); + assert(this.inputs.length > 0, 'Cannot set sequence with no inputs.'); + + for (const input of this.inputs) { + if (input.sequence === 0xffffffff) + input.sequence = 0xfffffffe; + } + + this.locktime = locktime; } - return select; -}; + /** + * Set sequence locktime. + * @param {Number} index - Input index. + * @param {Number} locktime + * @param {Boolean?} seconds + */ -/** - * Sort inputs and outputs according to BIP69. - * @see https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki - */ + setSequence(index, locktime, seconds) { + const input = this.inputs[index]; -MTX.prototype.sortMembers = function sortMembers() { - let changeOutput = null; + assert(input, 'Input does not exist.'); + assert((locktime >>> 0) === locktime, 'Locktime must be a uint32.'); - if (this.changeIndex !== -1) { - changeOutput = this.outputs[this.changeIndex]; - assert(changeOutput); + this.version = 2; + + if (seconds) { + locktime >>>= consensus.SEQUENCE_GRANULARITY; + locktime &= consensus.SEQUENCE_MASK; + locktime |= consensus.SEQUENCE_TYPE_FLAG; + } else { + locktime &= consensus.SEQUENCE_MASK; + } + + input.sequence = locktime; } - this.inputs.sort(sortInputs); - this.outputs.sort(sortOutputs); + /** + * Inspect the transaction. + * @returns {Object} + */ - if (this.changeIndex !== -1) { - this.changeIndex = this.outputs.indexOf(changeOutput); - assert(this.changeIndex !== -1); - } -}; - -/** - * Avoid fee sniping. - * @param {Number} - Current chain height. - * @see bitcoin/src/wallet/wallet.cpp - */ - -MTX.prototype.avoidFeeSniping = function avoidFeeSniping(height) { - assert(typeof height === 'number', 'Must pass in height.'); - - if ((Math.random() * 10 | 0) === 0) { - height -= Math.random() * 100 | 0; - - if (height < 0) - height = 0; + inspect() { + return this.format(); } - this.setLocktime(height); -}; + /** + * Inspect the transaction. + * @returns {Object} + */ -/** - * Set locktime and sequences appropriately. - * @param {Number} locktime - */ - -MTX.prototype.setLocktime = function setLocktime(locktime) { - assert((locktime >>> 0) === locktime, 'Locktime must be a uint32.'); - assert(this.inputs.length > 0, 'Cannot set sequence with no inputs.'); - - for (const input of this.inputs) { - if (input.sequence === 0xffffffff) - input.sequence = 0xfffffffe; + format() { + return super.format(this.view); } - this.locktime = locktime; -}; + /** + * Convert transaction to JSON. + * @returns {Object} + */ -/** - * Set sequence locktime. - * @param {Number} index - Input index. - * @param {Number} locktime - * @param {Boolean?} seconds - */ - -MTX.prototype.setSequence = function setSequence(index, locktime, seconds) { - const input = this.inputs[index]; - - assert(input, 'Input does not exist.'); - assert((locktime >>> 0) === locktime, 'Locktime must be a uint32.'); - - this.version = 2; - - if (seconds) { - locktime >>>= consensus.SEQUENCE_GRANULARITY; - locktime &= consensus.SEQUENCE_MASK; - locktime |= consensus.SEQUENCE_TYPE_FLAG; - } else { - locktime &= consensus.SEQUENCE_MASK; + toJSON() { + return super.toJSON(null, this.view); } - input.sequence = locktime; -}; + /** + * Convert transaction to JSON. + * @param {Network} network + * @returns {Object} + */ -/** - * Inspect the transaction. - * @returns {Object} - */ + getJSON(network) { + return super.getJSON(network, this.view); + } -MTX.prototype.inspect = function inspect() { - return this.format(); -}; + /** + * Instantiate a transaction from a + * jsonified transaction object. + * @param {Object} json - The jsonified transaction object. + * @returns {MTX} + */ -/** - * Inspect the transaction. - * @returns {Object} - */ + static fromJSON(json) { + return new this().fromJSON(json); + } -MTX.prototype.format = function format() { - return TX.prototype.format.call(this, this.view); -}; + /** + * Instantiate a transaction from a buffer reader. + * @param {BufferReader} br + * @returns {MTX} + */ -/** - * Convert transaction to JSON. - * @returns {Object} - */ + static fromReader(br) { + return new this().fromReader(br); + } -MTX.prototype.toJSON = function toJSON() { - return TX.prototype.getJSON.call(this, null, this.view); -}; + /** + * Instantiate a transaction from a serialized Buffer. + * @param {Buffer} data + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {MTX} + */ -/** - * Convert transaction to JSON. - * @param {Network} network - * @returns {Object} - */ + static fromRaw(data, enc) { + if (typeof data === 'string') + data = Buffer.from(data, enc); + return new this().fromRaw(data); + } -MTX.prototype.getJSON = function getJSON(network) { - return TX.prototype.getJSON.call(this, network, this.view); -}; + /** + * Convert the MTX to a TX. + * @returns {TX} + */ -/** - * Instantiate a transaction from a - * jsonified transaction object. - * @param {Object} json - The jsonified transaction object. - * @returns {MTX} - */ + toTX() { + return new TX().inject(this); + } -MTX.fromJSON = function fromJSON(json) { - return new MTX().fromJSON(json); -}; + /** + * Convert the MTX to a TX. + * @returns {Array} [tx, view] + */ -/** - * Instantiate a transaction from a buffer reader. - * @param {BufferReader} br - * @returns {MTX} - */ + commit() { + return [this.toTX(), this.view]; + } -MTX.fromReader = function fromReader(br) { - return new MTX().fromReader(br); -}; + /** + * Instantiate MTX from TX. + * @param {TX} tx + * @returns {MTX} + */ -/** - * Instantiate a transaction from a serialized Buffer. - * @param {Buffer} data - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {MTX} - */ + static fromTX(tx) { + return new this().inject(tx); + } -MTX.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - return new MTX().fromRaw(data); -}; + /** + * Test whether an object is an MTX. + * @param {Object} obj + * @returns {Boolean} + */ -/** - * Convert the MTX to a TX. - * @returns {TX} - */ - -MTX.prototype.toTX = function toTX() { - return new TX().inject(this); -}; - -/** - * Convert the MTX to a TX. - * @returns {Array} [tx, view] - */ - -MTX.prototype.commit = function commit() { - return [this.toTX(), this.view]; -}; - -/** - * Instantiate MTX from TX. - * @param {TX} tx - * @returns {MTX} - */ - -MTX.fromTX = function fromTX(tx) { - return new MTX().inject(tx); -}; - -/** - * Test whether an object is an MTX. - * @param {Object} obj - * @returns {Boolean} - */ - -MTX.isMTX = function isMTX(obj) { - return obj instanceof MTX; -}; + static isMTX(obj) { + return obj instanceof MTX; + } +} /** * Coin Selector * @alias module:primitives.CoinSelector - * @constructor - * @param {TX} tx - * @param {Object?} options */ -function CoinSelector(tx, options) { - if (!(this instanceof CoinSelector)) - return new CoinSelector(tx, options); +class CoinSelector { + /** + * Create a coin selector. + * @constructor + * @param {TX} tx + * @param {Object?} options + */ - this.tx = tx.clone(); - this.coins = []; - this.outputValue = 0; - this.index = 0; - this.chosen = []; - this.change = 0; - this.fee = CoinSelector.MIN_FEE; + constructor(tx, options) { + this.tx = tx.clone(); + this.coins = []; + this.outputValue = 0; + this.index = 0; + this.chosen = []; + this.change = 0; + this.fee = CoinSelector.MIN_FEE; - this.selection = 'value'; - this.subtractFee = false; - this.subtractIndex = -1; - this.height = -1; - this.depth = -1; - this.hardFee = -1; - this.rate = CoinSelector.FEE_RATE; - this.maxFee = -1; - this.round = false; - this.changeAddress = null; + this.selection = 'value'; + this.subtractFee = false; + this.subtractIndex = -1; + this.height = -1; + this.depth = -1; + this.hardFee = -1; + this.rate = CoinSelector.FEE_RATE; + this.maxFee = -1; + this.round = false; + this.changeAddress = null; - // Needed for size estimation. - this.estimate = null; + // Needed for size estimation. + this.estimate = null; - if (options) - this.fromOptions(options); + if (options) + this.fromOptions(options); + } + + /** + * Initialize selector options. + * @param {Object} options + * @private + */ + + fromOptions(options) { + if (options.selection) { + assert(typeof options.selection === 'string'); + this.selection = options.selection; + } + + if (options.subtractFee != null) { + if (typeof options.subtractFee === 'number') { + assert(Number.isSafeInteger(options.subtractFee)); + assert(options.subtractFee >= -1); + this.subtractIndex = options.subtractFee; + this.subtractFee = this.subtractIndex !== -1; + } else { + assert(typeof options.subtractFee === 'boolean'); + this.subtractFee = options.subtractFee; + } + } + + if (options.subtractIndex != null) { + assert(Number.isSafeInteger(options.subtractIndex)); + assert(options.subtractIndex >= -1); + this.subtractIndex = options.subtractIndex; + this.subtractFee = this.subtractIndex !== -1; + } + + if (options.height != null) { + assert(Number.isSafeInteger(options.height)); + assert(options.height >= -1); + this.height = options.height; + } + + if (options.confirmations != null) { + assert(Number.isSafeInteger(options.confirmations)); + assert(options.confirmations >= -1); + this.depth = options.confirmations; + } + + if (options.depth != null) { + assert(Number.isSafeInteger(options.depth)); + assert(options.depth >= -1); + this.depth = options.depth; + } + + if (options.hardFee != null) { + assert(Number.isSafeInteger(options.hardFee)); + assert(options.hardFee >= -1); + this.hardFee = options.hardFee; + } + + if (options.rate != null) { + assert(Number.isSafeInteger(options.rate)); + assert(options.rate >= 0); + this.rate = options.rate; + } + + if (options.maxFee != null) { + assert(Number.isSafeInteger(options.maxFee)); + assert(options.maxFee >= -1); + this.maxFee = options.maxFee; + } + + if (options.round != null) { + assert(typeof options.round === 'boolean'); + this.round = options.round; + } + + if (options.changeAddress) { + const addr = options.changeAddress; + if (typeof addr === 'string') { + this.changeAddress = Address.fromString(addr); + } else { + assert(addr instanceof Address); + this.changeAddress = addr; + } + } + + if (options.estimate) { + assert(typeof options.estimate === 'function'); + this.estimate = options.estimate; + } + + return this; + } + + /** + * Initialize the selector with coins to select from. + * @param {Coin[]} coins + */ + + init(coins) { + this.coins = coins.slice(); + this.outputValue = this.tx.getOutputValue(); + this.index = 0; + this.chosen = []; + this.change = 0; + this.fee = CoinSelector.MIN_FEE; + this.tx.inputs.length = 0; + + switch (this.selection) { + case 'all': + case 'random': + this.coins.sort(sortRandom); + break; + case 'age': + this.coins.sort(sortAge); + break; + case 'value': + this.coins.sort(sortValue); + break; + default: + throw new FundingError(`Bad selection type: ${this.selection}.`); + } + } + + /** + * Calculate total value required. + * @returns {Amount} + */ + + total() { + if (this.subtractFee) + return this.outputValue; + return this.outputValue + this.fee; + } + + /** + * Test whether the selector has + * completely funded the transaction. + * @returns {Boolean} + */ + + isFull() { + return this.tx.getInputValue() >= this.total(); + } + + /** + * Test whether a coin is spendable + * with regards to the options. + * @param {Coin} coin + * @returns {Boolean} + */ + + isSpendable(coin) { + if (this.height === -1) + return true; + + if (coin.coinbase) { + if (coin.height === -1) + return false; + + if (this.height + 1 < coin.height + consensus.COINBASE_MATURITY) + return false; + + return true; + } + + if (this.depth === -1) + return true; + + const depth = coin.getDepth(this.height); + + if (depth < this.depth) + return false; + + return true; + } + + /** + * Get the current fee based on a size. + * @param {Number} size + * @returns {Amount} + */ + + getFee(size) { + // This is mostly here for testing. + // i.e. A fee rounded to the nearest + // kb is easier to predict ahead of time. + if (this.round) { + const fee = policy.getRoundFee(size, this.rate); + return Math.min(fee, CoinSelector.MAX_FEE); + } + + const fee = policy.getMinFee(size, this.rate); + return Math.min(fee, CoinSelector.MAX_FEE); + } + + /** + * Fund the transaction with more + * coins if the `output value + fee` + * total was updated. + */ + + fund() { + while (this.index < this.coins.length) { + const coin = this.coins[this.index++]; + + if (!this.isSpendable(coin)) + continue; + + this.tx.addCoin(coin); + this.chosen.push(coin); + + if (this.selection === 'all') + continue; + + if (this.isFull()) + break; + } + } + + /** + * Initiate selection from `coins`. + * @param {Coin[]} coins + * @returns {CoinSelector} + */ + + async select(coins) { + this.init(coins); + + if (this.hardFee !== -1) { + this.selectHard(); + } else { + // This is potentially asynchronous: + // it may invoke the size estimator + // required for redeem scripts (we + // may be calling out to a wallet + // or something similar). + await this.selectEstimate(); + } + + if (!this.isFull()) { + // Still failing to get enough funds. + throw new FundingError( + 'Not enough funds.', + this.tx.getInputValue(), + this.total()); + } + + // How much money is left after filling outputs. + this.change = this.tx.getInputValue() - this.total(); + + return this; + } + + /** + * Initialize selection based on size estimate. + */ + + async selectEstimate() { + // Set minimum fee and do + // an initial round of funding. + this.fee = CoinSelector.MIN_FEE; + this.fund(); + + // Add dummy output for change. + const change = new Output(); + + if (this.changeAddress) { + change.script.fromAddress(this.changeAddress); + } else { + // In case we don't have a change address, + // we use a fake p2pkh output to gauge size. + change.script.fromPubkeyhash(encoding.ZERO_HASH160); + } + + this.tx.outputs.push(change); + + // Keep recalculating the fee and funding + // until we reach some sort of equilibrium. + do { + const size = await this.tx.estimateSize(this.estimate); + + this.fee = this.getFee(size); + + if (this.maxFee > 0 && this.fee > this.maxFee) + throw new FundingError('Fee is too high.'); + + // Failed to get enough funds, add more coins. + if (!this.isFull()) + this.fund(); + } while (!this.isFull() && this.index < this.coins.length); + } + + /** + * Initiate selection based on a hard fee. + */ + + selectHard() { + this.fee = Math.min(this.hardFee, CoinSelector.MAX_FEE); + this.fund(); + } } /** @@ -1605,337 +1900,44 @@ CoinSelector.MIN_FEE = 10000; CoinSelector.MAX_FEE = consensus.COIN / 10; /** - * Initialize selector options. - * @param {Object} options - * @private - */ - -CoinSelector.prototype.fromOptions = function fromOptions(options) { - if (options.selection) { - assert(typeof options.selection === 'string'); - this.selection = options.selection; - } - - if (options.subtractFee != null) { - if (typeof options.subtractFee === 'number') { - assert(Number.isSafeInteger(options.subtractFee)); - assert(options.subtractFee >= -1); - this.subtractIndex = options.subtractFee; - this.subtractFee = this.subtractIndex !== -1; - } else { - assert(typeof options.subtractFee === 'boolean'); - this.subtractFee = options.subtractFee; - } - } - - if (options.subtractIndex != null) { - assert(Number.isSafeInteger(options.subtractIndex)); - assert(options.subtractIndex >= -1); - this.subtractIndex = options.subtractIndex; - this.subtractFee = this.subtractIndex !== -1; - } - - if (options.height != null) { - assert(Number.isSafeInteger(options.height)); - assert(options.height >= -1); - this.height = options.height; - } - - if (options.confirmations != null) { - assert(Number.isSafeInteger(options.confirmations)); - assert(options.confirmations >= -1); - this.depth = options.confirmations; - } - - if (options.depth != null) { - assert(Number.isSafeInteger(options.depth)); - assert(options.depth >= -1); - this.depth = options.depth; - } - - if (options.hardFee != null) { - assert(Number.isSafeInteger(options.hardFee)); - assert(options.hardFee >= -1); - this.hardFee = options.hardFee; - } - - if (options.rate != null) { - assert(Number.isSafeInteger(options.rate)); - assert(options.rate >= 0); - this.rate = options.rate; - } - - if (options.maxFee != null) { - assert(Number.isSafeInteger(options.maxFee)); - assert(options.maxFee >= -1); - this.maxFee = options.maxFee; - } - - if (options.round != null) { - assert(typeof options.round === 'boolean'); - this.round = options.round; - } - - if (options.changeAddress) { - const addr = options.changeAddress; - if (typeof addr === 'string') { - this.changeAddress = Address.fromString(addr); - } else { - assert(addr instanceof Address); - this.changeAddress = addr; - } - } - - if (options.estimate) { - assert(typeof options.estimate === 'function'); - this.estimate = options.estimate; - } - - return this; -}; - -/** - * Initialize the selector with coins to select from. - * @param {Coin[]} coins - */ - -CoinSelector.prototype.init = function init(coins) { - this.coins = coins.slice(); - this.outputValue = this.tx.getOutputValue(); - this.index = 0; - this.chosen = []; - this.change = 0; - this.fee = CoinSelector.MIN_FEE; - this.tx.inputs.length = 0; - - switch (this.selection) { - case 'all': - case 'random': - this.coins.sort(sortRandom); - break; - case 'age': - this.coins.sort(sortAge); - break; - case 'value': - this.coins.sort(sortValue); - break; - default: - throw new FundingError(`Bad selection type: ${this.selection}.`); - } -}; - -/** - * Calculate total value required. - * @returns {Amount} - */ - -CoinSelector.prototype.total = function total() { - if (this.subtractFee) - return this.outputValue; - return this.outputValue + this.fee; -}; - -/** - * Test whether the selector has - * completely funded the transaction. - * @returns {Boolean} - */ - -CoinSelector.prototype.isFull = function isFull() { - return this.tx.getInputValue() >= this.total(); -}; - -/** - * Test whether a coin is spendable - * with regards to the options. - * @param {Coin} coin - * @returns {Boolean} - */ - -CoinSelector.prototype.isSpendable = function isSpendable(coin) { - if (this.height === -1) - return true; - - if (coin.coinbase) { - if (coin.height === -1) - return false; - - if (this.height + 1 < coin.height + consensus.COINBASE_MATURITY) - return false; - - return true; - } - - if (this.depth === -1) - return true; - - const depth = coin.getDepth(this.height); - - if (depth < this.depth) - return false; - - return true; -}; - -/** - * Get the current fee based on a size. - * @param {Number} size - * @returns {Amount} - */ - -CoinSelector.prototype.getFee = function getFee(size) { - // This is mostly here for testing. - // i.e. A fee rounded to the nearest - // kb is easier to predict ahead of time. - if (this.round) { - const fee = policy.getRoundFee(size, this.rate); - return Math.min(fee, CoinSelector.MAX_FEE); - } - - const fee = policy.getMinFee(size, this.rate); - return Math.min(fee, CoinSelector.MAX_FEE); -}; - -/** - * Fund the transaction with more - * coins if the `output value + fee` - * total was updated. - */ - -CoinSelector.prototype.fund = function fund() { - while (this.index < this.coins.length) { - const coin = this.coins[this.index++]; - - if (!this.isSpendable(coin)) - continue; - - this.tx.addCoin(coin); - this.chosen.push(coin); - - if (this.selection === 'all') - continue; - - if (this.isFull()) - break; - } -}; - -/** - * Initiate selection from `coins`. - * @param {Coin[]} coins - * @returns {CoinSelector} - */ - -CoinSelector.prototype.select = async function select(coins) { - this.init(coins); - - if (this.hardFee !== -1) { - this.selectHard(); - } else { - // This is potentially asynchronous: - // it may invoke the size estimator - // required for redeem scripts (we - // may be calling out to a wallet - // or something similar). - await this.selectEstimate(); - } - - if (!this.isFull()) { - // Still failing to get enough funds. - throw new FundingError( - 'Not enough funds.', - this.tx.getInputValue(), - this.total()); - } - - // How much money is left after filling outputs. - this.change = this.tx.getInputValue() - this.total(); - - return this; -}; - -/** - * Initialize selection based on size estimate. - */ - -CoinSelector.prototype.selectEstimate = async function selectEstimate() { - // Set minimum fee and do - // an initial round of funding. - this.fee = CoinSelector.MIN_FEE; - this.fund(); - - // Add dummy output for change. - const change = new Output(); - - if (this.changeAddress) { - change.script.fromAddress(this.changeAddress); - } else { - // In case we don't have a change address, - // we use a fake p2pkh output to gauge size. - change.script.fromPubkeyhash(encoding.ZERO_HASH160); - } - - this.tx.outputs.push(change); - - // Keep recalculating the fee and funding - // until we reach some sort of equilibrium. - do { - const size = await this.tx.estimateSize(this.estimate); - - this.fee = this.getFee(size); - - if (this.maxFee > 0 && this.fee > this.maxFee) - throw new FundingError('Fee is too high.'); - - // Failed to get enough funds, add more coins. - if (!this.isFull()) - this.fund(); - } while (!this.isFull() && this.index < this.coins.length); -}; - -/** - * Initiate selection based on a hard fee. - */ - -CoinSelector.prototype.selectHard = function selectHard() { - this.fee = Math.min(this.hardFee, CoinSelector.MAX_FEE); - this.fund(); -}; - -/** + * Funding Error * An error thrown from the coin selector. - * @constructor * @ignore * @extends Error - * @param {String} msg - * @param {Amount} available - * @param {Amount} required * @property {String} message - Error message. * @property {Amount} availableFunds * @property {Amount} requiredFunds */ -function FundingError(msg, available, required) { - Error.call(this); +class FundingError extends Error { + /** + * Create a funding error. + * @constructor + * @param {String} msg + * @param {Amount} available + * @param {Amount} required + */ - this.type = 'FundingError'; - this.message = msg; - this.availableFunds = -1; - this.requiredFunds = -1; + constructor(msg, available, required) { + super(); - if (available != null) { - this.message += ` (available=${Amount.btc(available)},`; - this.message += ` required=${Amount.btc(required)})`; - this.availableFunds = available; - this.requiredFunds = required; + this.type = 'FundingError'; + this.message = msg; + this.availableFunds = -1; + this.requiredFunds = -1; + + if (available != null) { + this.message += ` (available=${Amount.btc(available)},`; + this.message += ` required=${Amount.btc(required)})`; + this.availableFunds = available; + this.requiredFunds = required; + } + + if (Error.captureStackTrace) + Error.captureStackTrace(this, FundingError); } - - if (Error.captureStackTrace) - Error.captureStackTrace(this, FundingError); } -Object.setPrototypeOf(FundingError.prototype, Error.prototype); - /* * Helpers */ diff --git a/lib/primitives/netaddress.js b/lib/primitives/netaddress.js index 29e8cc07..fdf73a09 100644 --- a/lib/primitives/netaddress.js +++ b/lib/primitives/netaddress.js @@ -15,33 +15,455 @@ const StaticWriter = require('bufio/lib/staticwriter'); const BufferReader = require('bufio/lib/reader'); /** + * Net Address * Represents a network address. * @alias module:primitives.NetAddress - * @constructor - * @param {Object} options - * @param {Number?} options.time - Timestamp. - * @param {Number?} options.services - Service bits. - * @param {String?} options.host - IP address (IPv6 or IPv4). - * @param {Number?} options.port - Port. * @property {Host} host * @property {Number} port * @property {Number} services * @property {Number} time */ -function NetAddress(options) { - if (!(this instanceof NetAddress)) - return new NetAddress(options); +class NetAddress { + /** + * Create a network address. + * @constructor + * @param {Object} options + * @param {Number?} options.time - Timestamp. + * @param {Number?} options.services - Service bits. + * @param {String?} options.host - IP address (IPv6 or IPv4). + * @param {Number?} options.port - Port. + */ - this.host = '0.0.0.0'; - this.port = 0; - this.services = 0; - this.time = 0; - this.hostname = '0.0.0.0:0'; - this.raw = IP.ZERO_IP; + constructor(options) { + this.host = '0.0.0.0'; + this.port = 0; + this.services = 0; + this.time = 0; + this.hostname = '0.0.0.0:0'; + this.raw = IP.ZERO_IP; - if (options) - this.fromOptions(options); + if (options) + this.fromOptions(options); + } + + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ + + fromOptions(options) { + assert(typeof options.host === 'string'); + assert(typeof options.port === 'number'); + + this.raw = IP.toBuffer(options.host); + this.host = IP.toString(this.raw); + this.port = options.port; + + if (options.services) { + assert(typeof options.services === 'number'); + this.services = options.services; + } + + if (options.time) { + assert(typeof options.time === 'number'); + this.time = options.time; + } + + this.hostname = IP.toHostname(this.host, this.port); + + return this; + } + + /** + * Instantiate network address from options. + * @param {Object} options + * @returns {NetAddress} + */ + + static fromOptions(options) { + return new this().fromOptions(options); + } + + /** + * Test whether required services are available. + * @param {Number} services + * @returns {Boolean} + */ + + hasServices(services) { + return (this.services & services) === services; + } + + /** + * Test whether the address is IPv4. + * @returns {Boolean} + */ + + static isIPv4() { + return IP.isIPv4(this.raw); + } + + /** + * Test whether the address is IPv6. + * @returns {Boolean} + */ + + static isIPv6() { + return IP.isIPv6(this.raw); + } + + /** + * Test whether the host is null. + * @returns {Boolean} + */ + + isNull() { + return IP.isNull(this.raw); + } + + /** + * Test whether the host is a local address. + * @returns {Boolean} + */ + + isLocal() { + return IP.isLocal(this.raw); + } + + /** + * Test whether the host is valid. + * @returns {Boolean} + */ + + isValid() { + return IP.isValid(this.raw); + } + + /** + * Test whether the host is routable. + * @returns {Boolean} + */ + + isRoutable() { + return IP.isRoutable(this.raw); + } + + /** + * Test whether the host is an onion address. + * @returns {Boolean} + */ + + isOnion() { + return IP.isOnion(this.raw); + } + + /** + * Compare against another network address. + * @returns {Boolean} + */ + + equal(addr) { + return this.compare(addr) === 0; + } + + /** + * Compare against another network address. + * @returns {Number} + */ + + compare(addr) { + const cmp = this.raw.compare(addr.raw); + + if (cmp !== 0) + return cmp; + + return this.port - addr.port; + } + + /** + * Get reachable score to destination. + * @param {NetAddress} dest + * @returns {Number} + */ + + getReachability(dest) { + return IP.getReachability(this.raw, dest.raw); + } + + /** + * Set null host. + */ + + setNull() { + this.raw = IP.ZERO_IP; + this.host = '0.0.0.0'; + this.hostname = IP.toHostname(this.host, this.port); + } + + /** + * Set host. + * @param {String} host + */ + + setHost(host) { + this.raw = IP.toBuffer(host); + this.host = IP.toString(this.raw); + this.hostname = IP.toHostname(this.host, this.port); + } + + /** + * Set port. + * @param {Number} port + */ + + setPort(port) { + assert(port >= 0 && port <= 0xffff); + this.port = port; + this.hostname = IP.toHostname(this.host, port); + } + + /** + * Inject properties from host, port, and network. + * @private + * @param {String} host + * @param {Number} port + * @param {(Network|NetworkType)?} network + */ + + fromHost(host, port, network) { + network = Network.get(network); + + assert(port >= 0 && port <= 0xffff); + + this.raw = IP.toBuffer(host); + this.host = IP.toString(this.raw); + this.port = port; + this.services = NetAddress.DEFAULT_SERVICES; + this.time = network.now(); + + this.hostname = IP.toHostname(this.host, this.port); + + return this; + } + + /** + * Instantiate a network address + * from a host and port. + * @param {String} host + * @param {Number} port + * @param {(Network|NetworkType)?} network + * @returns {NetAddress} + */ + + static fromHost(host, port, network) { + return new this().fromHost(host, port, network); + } + + /** + * Inject properties from hostname and network. + * @private + * @param {String} hostname + * @param {(Network|NetworkType)?} network + */ + + fromHostname(hostname, network) { + network = Network.get(network); + + const addr = IP.fromHostname(hostname, network.port); + + return this.fromHost(addr.host, addr.port, network); + } + + /** + * Instantiate a network address + * from a hostname (i.e. 127.0.0.1:8333). + * @param {String} hostname + * @param {(Network|NetworkType)?} network + * @returns {NetAddress} + */ + + static fromHostname(hostname, network) { + return new this().fromHostname(hostname, network); + } + + /** + * Inject properties from socket. + * @private + * @param {net.Socket} socket + */ + + fromSocket(socket, network) { + const host = socket.remoteAddress; + const port = socket.remotePort; + assert(typeof host === 'string'); + assert(typeof port === 'number'); + return this.fromHost(IP.normalize(host), port, network); + } + + /** + * Instantiate a network address + * from a socket. + * @param {net.Socket} socket + * @returns {NetAddress} + */ + + static fromSocket(hostname, network) { + return new this().fromSocket(hostname, network); + } + + /** + * Inject properties from buffer reader. + * @private + * @param {BufferReader} br + * @param {Boolean?} full - Include timestamp. + */ + + fromReader(br, full) { + this.time = full ? br.readU32() : 0; + this.services = br.readU32(); + + // Note: hi service bits + // are currently unused. + br.readU32(); + + this.raw = br.readBytes(16); + this.host = IP.toString(this.raw); + this.port = br.readU16BE(); + this.hostname = IP.toHostname(this.host, this.port); + + return this; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @param {Boolean?} full - Include timestamp. + */ + + fromRaw(data, full) { + return this.fromReader(new BufferReader(data), full); + } + + /** + * Insantiate a network address from buffer reader. + * @param {BufferReader} br + * @param {Boolean?} full - Include timestamp. + * @returns {NetAddress} + */ + + static fromReader(br, full) { + return new this().fromReader(br, full); + } + + /** + * Insantiate a network address from serialized data. + * @param {Buffer} data + * @param {Boolean?} full - Include timestamp. + * @returns {NetAddress} + */ + + static fromRaw(data, full) { + return new this().fromRaw(data, full); + } + + /** + * Write network address to a buffer writer. + * @param {BufferWriter} bw + * @param {Boolean?} full - Include timestamp. + * @returns {Buffer} + */ + + toWriter(bw, full) { + if (full) + bw.writeU32(this.time); + + bw.writeU32(this.services); + bw.writeU32(0); + bw.writeBytes(this.raw); + bw.writeU16BE(this.port); + + return bw; + } + + /** + * Calculate serialization size of address. + * @returns {Number} + */ + + getSize(full) { + return 26 + (full ? 4 : 0); + } + + /** + * Serialize network address. + * @param {Boolean?} full - Include timestamp. + * @returns {Buffer} + */ + + toRaw(full) { + const size = this.getSize(full); + return this.toWriter(new StaticWriter(size), full).render(); + } + + /** + * Convert net address to json-friendly object. + * @returns {Object} + */ + + toJSON() { + return { + host: this.host, + port: this.port, + services: this.services, + time: this.time + }; + } + + /** + * Inject properties from json object. + * @private + * @param {Object} json + * @returns {NetAddress} + */ + + fromJSON(json) { + assert((json.port & 0xffff) === json.port); + assert((json.services >>> 0) === json.services); + assert((json.time >>> 0) === json.time); + this.raw = IP.toBuffer(json.host); + this.host = json.host; + this.port = json.port; + this.services = json.services; + this.time = json.time; + this.hostname = IP.toHostname(this.host, this.port); + return this; + } + + /** + * Instantiate net address from json object. + * @param {Object} json + * @returns {NetAddress} + */ + + static fromJSON(json) { + return new this().fromJSON(json); + } + + /** + * Inspect the network address. + * @returns {Object} + */ + + inspect() { + return ''; + } } /** @@ -56,424 +478,6 @@ NetAddress.DEFAULT_SERVICES = 0 | common.services.WITNESS | common.services.BLOOM; -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -NetAddress.prototype.fromOptions = function fromOptions(options) { - assert(typeof options.host === 'string'); - assert(typeof options.port === 'number'); - - this.raw = IP.toBuffer(options.host); - this.host = IP.toString(this.raw); - this.port = options.port; - - if (options.services) { - assert(typeof options.services === 'number'); - this.services = options.services; - } - - if (options.time) { - assert(typeof options.time === 'number'); - this.time = options.time; - } - - this.hostname = IP.toHostname(this.host, this.port); - - return this; -}; - -/** - * Instantiate network address from options. - * @param {Object} options - * @returns {NetAddress} - */ - -NetAddress.fromOptions = function fromOptions(options) { - return new NetAddress().fromOptions(options); -}; - -/** - * Test whether required services are available. - * @param {Number} services - * @returns {Boolean} - */ - -NetAddress.prototype.hasServices = function hasServices(services) { - return (this.services & services) === services; -}; - -/** - * Test whether the address is IPv4. - * @returns {Boolean} - */ - -NetAddress.isIPv4 = function isIPv4() { - return IP.isIPv4(this.raw); -}; - -/** - * Test whether the address is IPv6. - * @returns {Boolean} - */ - -NetAddress.isIPv6 = function isIPv6() { - return IP.isIPv6(this.raw); -}; - -/** - * Test whether the host is null. - * @returns {Boolean} - */ - -NetAddress.prototype.isNull = function isNull() { - return IP.isNull(this.raw); -}; - -/** - * Test whether the host is a local address. - * @returns {Boolean} - */ - -NetAddress.prototype.isLocal = function isLocal() { - return IP.isLocal(this.raw); -}; - -/** - * Test whether the host is valid. - * @returns {Boolean} - */ - -NetAddress.prototype.isValid = function isValid() { - return IP.isValid(this.raw); -}; - -/** - * Test whether the host is routable. - * @returns {Boolean} - */ - -NetAddress.prototype.isRoutable = function isRoutable() { - return IP.isRoutable(this.raw); -}; - -/** - * Test whether the host is an onion address. - * @returns {Boolean} - */ - -NetAddress.prototype.isOnion = function isOnion() { - return IP.isOnion(this.raw); -}; - -/** - * Compare against another network address. - * @returns {Boolean} - */ - -NetAddress.prototype.equal = function equal(addr) { - return this.compare(addr) === 0; -}; - -/** - * Compare against another network address. - * @returns {Number} - */ - -NetAddress.prototype.compare = function compare(addr) { - const cmp = this.raw.compare(addr.raw); - - if (cmp !== 0) - return cmp; - - return this.port - addr.port; -}; - -/** - * Get reachable score to destination. - * @param {NetAddress} dest - * @returns {Number} - */ - -NetAddress.prototype.getReachability = function getReachability(dest) { - return IP.getReachability(this.raw, dest.raw); -}; - -/** - * Set null host. - */ - -NetAddress.prototype.setNull = function setNull() { - this.raw = IP.ZERO_IP; - this.host = '0.0.0.0'; - this.hostname = IP.toHostname(this.host, this.port); -}; - -/** - * Set host. - * @param {String} host - */ - -NetAddress.prototype.setHost = function setHost(host) { - this.raw = IP.toBuffer(host); - this.host = IP.toString(this.raw); - this.hostname = IP.toHostname(this.host, this.port); -}; - -/** - * Set port. - * @param {Number} port - */ - -NetAddress.prototype.setPort = function setPort(port) { - assert(port >= 0 && port <= 0xffff); - this.port = port; - this.hostname = IP.toHostname(this.host, port); -}; - -/** - * Inject properties from host, port, and network. - * @private - * @param {String} host - * @param {Number} port - * @param {(Network|NetworkType)?} network - */ - -NetAddress.prototype.fromHost = function fromHost(host, port, network) { - network = Network.get(network); - - assert(port >= 0 && port <= 0xffff); - - this.raw = IP.toBuffer(host); - this.host = IP.toString(this.raw); - this.port = port; - this.services = NetAddress.DEFAULT_SERVICES; - this.time = network.now(); - - this.hostname = IP.toHostname(this.host, this.port); - - return this; -}; - -/** - * Instantiate a network address - * from a host and port. - * @param {String} host - * @param {Number} port - * @param {(Network|NetworkType)?} network - * @returns {NetAddress} - */ - -NetAddress.fromHost = function fromHost(host, port, network) { - return new NetAddress().fromHost(host, port, network); -}; - -/** - * Inject properties from hostname and network. - * @private - * @param {String} hostname - * @param {(Network|NetworkType)?} network - */ - -NetAddress.prototype.fromHostname = function fromHostname(hostname, network) { - network = Network.get(network); - - const addr = IP.fromHostname(hostname, network.port); - - return this.fromHost(addr.host, addr.port, network); -}; - -/** - * Instantiate a network address - * from a hostname (i.e. 127.0.0.1:8333). - * @param {String} hostname - * @param {(Network|NetworkType)?} network - * @returns {NetAddress} - */ - -NetAddress.fromHostname = function fromHostname(hostname, network) { - return new NetAddress().fromHostname(hostname, network); -}; - -/** - * Inject properties from socket. - * @private - * @param {net.Socket} socket - */ - -NetAddress.prototype.fromSocket = function fromSocket(socket, network) { - const host = socket.remoteAddress; - const port = socket.remotePort; - assert(typeof host === 'string'); - assert(typeof port === 'number'); - return this.fromHost(IP.normalize(host), port, network); -}; - -/** - * Instantiate a network address - * from a socket. - * @param {net.Socket} socket - * @returns {NetAddress} - */ - -NetAddress.fromSocket = function fromSocket(hostname, network) { - return new NetAddress().fromSocket(hostname, network); -}; - -/** - * Inject properties from buffer reader. - * @private - * @param {BufferReader} br - * @param {Boolean?} full - Include timestamp. - */ - -NetAddress.prototype.fromReader = function fromReader(br, full) { - this.time = full ? br.readU32() : 0; - this.services = br.readU32(); - - // Note: hi service bits - // are currently unused. - br.readU32(); - - this.raw = br.readBytes(16); - this.host = IP.toString(this.raw); - this.port = br.readU16BE(); - this.hostname = IP.toHostname(this.host, this.port); - - return this; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - * @param {Boolean?} full - Include timestamp. - */ - -NetAddress.prototype.fromRaw = function fromRaw(data, full) { - return this.fromReader(new BufferReader(data), full); -}; - -/** - * Insantiate a network address from buffer reader. - * @param {BufferReader} br - * @param {Boolean?} full - Include timestamp. - * @returns {NetAddress} - */ - -NetAddress.fromReader = function fromReader(br, full) { - return new NetAddress().fromReader(br, full); -}; - -/** - * Insantiate a network address from serialized data. - * @param {Buffer} data - * @param {Boolean?} full - Include timestamp. - * @returns {NetAddress} - */ - -NetAddress.fromRaw = function fromRaw(data, full) { - return new NetAddress().fromRaw(data, full); -}; - -/** - * Write network address to a buffer writer. - * @param {BufferWriter} bw - * @param {Boolean?} full - Include timestamp. - * @returns {Buffer} - */ - -NetAddress.prototype.toWriter = function toWriter(bw, full) { - if (full) - bw.writeU32(this.time); - - bw.writeU32(this.services); - bw.writeU32(0); - bw.writeBytes(this.raw); - bw.writeU16BE(this.port); - - return bw; -}; - -/** - * Calculate serialization size of address. - * @returns {Number} - */ - -NetAddress.prototype.getSize = function getSize(full) { - return 26 + (full ? 4 : 0); -}; - -/** - * Serialize network address. - * @param {Boolean?} full - Include timestamp. - * @returns {Buffer} - */ - -NetAddress.prototype.toRaw = function toRaw(full) { - const size = this.getSize(full); - return this.toWriter(new StaticWriter(size), full).render(); -}; - -/** - * Convert net address to json-friendly object. - * @returns {Object} - */ - -NetAddress.prototype.toJSON = function toJSON() { - return { - host: this.host, - port: this.port, - services: this.services, - time: this.time - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - * @returns {NetAddress} - */ - -NetAddress.prototype.fromJSON = function fromJSON(json) { - assert((json.port & 0xffff) === json.port); - assert((json.services >>> 0) === json.services); - assert((json.time >>> 0) === json.time); - this.raw = IP.toBuffer(json.host); - this.host = json.host; - this.port = json.port; - this.services = json.services; - this.time = json.time; - this.hostname = IP.toHostname(this.host, this.port); - return this; -}; - -/** - * Instantiate net address from json object. - * @param {Object} json - * @returns {NetAddress} - */ - -NetAddress.fromJSON = function fromJSON(json) { - return new NetAddress().fromJSON(json); -}; - -/** - * Inspect the network address. - * @returns {Object} - */ - -NetAddress.prototype.inspect = function inspect() { - return ''; -}; - /* * Expose */ diff --git a/lib/primitives/outpoint.js b/lib/primitives/outpoint.js index b695bb7a..4c9f756b 100644 --- a/lib/primitives/outpoint.js +++ b/lib/primitives/outpoint.js @@ -12,330 +12,334 @@ const BufferReader = require('bufio/lib/reader'); const encoding = require('bufio/lib/encoding'); /** + * Outpoint * Represents a COutPoint. * @alias module:primitives.Outpoint - * @constructor - * @param {Hash?} hash - * @param {Number?} index * @property {Hash} hash * @property {Number} index */ -function Outpoint(hash, index) { - if (!(this instanceof Outpoint)) - return new Outpoint(hash, index); +class Outpoint { + /** + * Create an outpoint. + * @constructor + * @param {Hash?} hash + * @param {Number?} index + */ - this.hash = encoding.NULL_HASH; - this.index = 0xffffffff; + constructor(hash, index) { + this.hash = encoding.NULL_HASH; + this.index = 0xffffffff; - if (hash != null) { - assert(typeof hash === 'string', 'Hash must be a string.'); - assert((index >>> 0) === index, 'Index must be a uint32.'); - this.hash = hash; + if (hash != null) { + assert(typeof hash === 'string', 'Hash must be a string.'); + assert((index >>> 0) === index, 'Index must be a uint32.'); + this.hash = hash; + this.index = index; + } + } + + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ + + fromOptions(options) { + assert(options, 'Outpoint data is required.'); + assert(typeof options.hash === 'string', 'Hash must be a string.'); + assert((options.index >>> 0) === options.index, 'Index must be a uint32.'); + this.hash = options.hash; + this.index = options.index; + return this; + } + + /** + * Instantate outpoint from options object. + * @param {Object} options + * @returns {Outpoint} + */ + + static fromOptions(options) { + return new this().fromOptions(options); + } + + /** + * Clone the outpoint. + * @returns {Outpoint} + */ + + clone() { + const outpoint = new this.constructor(); + outpoint.hash = this.value; + outpoint.index = this.index; + return outpoint; + } + + /** + * Test equality against another outpoint. + * @param {Outpoint} prevout + * @returns {Boolean} + */ + + equals(prevout) { + assert(Outpoint.isOutpoint(prevout)); + return this.hash === prevout.hash + && this.index === prevout.index; + } + + /** + * Compare against another outpoint (BIP69). + * @param {Outpoint} prevout + * @returns {Number} + */ + + compare(prevout) { + assert(Outpoint.isOutpoint(prevout)); + + const cmp = strcmp(this.txid(), prevout.txid()); + + if (cmp !== 0) + return cmp; + + return this.index - prevout.index; + } + + /** + * Test whether the outpoint is null (hash of zeroes + * with max-u32 index). Used to detect coinbases. + * @returns {Boolean} + */ + + isNull() { + return this.index === 0xffffffff && this.hash === encoding.NULL_HASH; + } + + /** + * Get little-endian hash. + * @returns {Hash} + */ + + rhash() { + return encoding.revHex(this.hash); + } + + /** + * Get little-endian hash. + * @returns {Hash} + */ + + txid() { + return this.rhash(); + } + + /** + * Serialize outpoint to a key + * suitable for a hash table. + * @returns {String} + */ + + toKey() { + return Outpoint.toKey(this.hash, this.index); + } + + /** + * Inject properties from hash table key. + * @private + * @param {String} key + * @returns {Outpoint} + */ + + fromKey(key) { + assert(key.length > 64); + this.hash = key.slice(0, 64); + this.index = parseInt(key.slice(64), 10); + return this; + } + + /** + * Instantiate outpoint from hash table key. + * @param {String} key + * @returns {Outpoint} + */ + + static fromKey(key) { + return new this().fromKey(key); + } + + /** + * Write outpoint to a buffer writer. + * @param {BufferWriter} bw + */ + + toWriter(bw) { + bw.writeHash(this.hash); + bw.writeU32(this.index); + return bw; + } + + /** + * Calculate size of outpoint. + * @returns {Number} + */ + + getSize() { + return 36; + } + + /** + * Serialize outpoint. + * @returns {Buffer} + */ + + toRaw() { + return this.toWriter(new StaticWriter(36)).render(); + } + + /** + * Inject properties from buffer reader. + * @private + * @param {BufferReader} br + */ + + fromReader(br) { + this.hash = br.readHash('hex'); + this.index = br.readU32(); + return this; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + return this.fromReader(new BufferReader(data)); + } + + /** + * Instantiate outpoint from a buffer reader. + * @param {BufferReader} br + * @returns {Outpoint} + */ + + static fromReader(br) { + return new this().fromReader(br); + } + + /** + * Instantiate outpoint from serialized data. + * @param {Buffer} data + * @returns {Outpoint} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } + + /** + * Inject properties from json object. + * @private + * @params {Object} json + */ + + fromJSON(json) { + assert(json, 'Outpoint data is required.'); + assert(typeof json.hash === 'string', 'Hash must be a string.'); + assert((json.index >>> 0) === json.index, 'Index must be a uint32.'); + this.hash = encoding.revHex(json.hash); + this.index = json.index; + return this; + } + + /** + * Convert the outpoint to an object suitable + * for JSON serialization. Note that the hash + * will be reversed to abide by bitcoind's legacy + * of little-endian uint256s. + * @returns {Object} + */ + + toJSON() { + return { + hash: encoding.revHex(this.hash), + index: this.index + }; + } + + /** + * Instantiate outpoint from json object. + * @param {Object} json + * @returns {Outpoint} + */ + + static fromJSON(json) { + return new this().fromJSON(json); + } + + /** + * Inject properties from tx. + * @private + * @param {TX} tx + * @param {Number} index + */ + + fromTX(tx, index) { + assert(tx); + assert(typeof index === 'number'); + assert(index >= 0); + this.hash = tx.hash('hex'); this.index = index; + return this; + } + + /** + * Instantiate outpoint from tx. + * @param {TX} tx + * @param {Number} index + * @returns {Outpoint} + */ + + static fromTX(tx, index) { + return new this().fromTX(tx, index); + } + + /** + * Serialize outpoint to a key + * suitable for a hash table. + * @param {Hash} hash + * @param {Number} index + * @returns {String} + */ + + static toKey(hash, index) { + assert(typeof hash === 'string'); + assert(hash.length === 64); + assert(index >= 0); + return hash + index; + } + + /** + * Convert the outpoint to a user-friendly string. + * @returns {String} + */ + + inspect() { + return ``; + } + + /** + * Test an object to see if it is an outpoint. + * @param {Object} obj + * @returns {Boolean} + */ + + static isOutpoint(obj) { + return obj instanceof Outpoint; } } -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -Outpoint.prototype.fromOptions = function fromOptions(options) { - assert(options, 'Outpoint data is required.'); - assert(typeof options.hash === 'string', 'Hash must be a string.'); - assert((options.index >>> 0) === options.index, 'Index must be a uint32.'); - this.hash = options.hash; - this.index = options.index; - return this; -}; - -/** - * Instantate outpoint from options object. - * @param {Object} options - * @returns {Outpoint} - */ - -Outpoint.fromOptions = function fromOptions(options) { - return new Outpoint().fromOptions(options); -}; - -/** - * Clone the outpoint. - * @returns {Outpoint} - */ - -Outpoint.prototype.clone = function clone() { - const outpoint = new Outpoint(); - outpoint.hash = this.value; - outpoint.index = this.index; - return outpoint; -}; - -/** - * Test equality against another outpoint. - * @param {Outpoint} prevout - * @returns {Boolean} - */ - -Outpoint.prototype.equals = function equals(prevout) { - assert(Outpoint.isOutpoint(prevout)); - return this.hash === prevout.hash - && this.index === prevout.index; -}; - -/** - * Compare against another outpoint (BIP69). - * @param {Outpoint} prevout - * @returns {Number} - */ - -Outpoint.prototype.compare = function compare(prevout) { - assert(Outpoint.isOutpoint(prevout)); - - const cmp = strcmp(this.txid(), prevout.txid()); - - if (cmp !== 0) - return cmp; - - return this.index - prevout.index; -}; - -/** - * Test whether the outpoint is null (hash of zeroes - * with max-u32 index). Used to detect coinbases. - * @returns {Boolean} - */ - -Outpoint.prototype.isNull = function isNull() { - return this.index === 0xffffffff && this.hash === encoding.NULL_HASH; -}; - -/** - * Get little-endian hash. - * @returns {Hash} - */ - -Outpoint.prototype.rhash = function rhash() { - return encoding.revHex(this.hash); -}; - -/** - * Get little-endian hash. - * @returns {Hash} - */ - -Outpoint.prototype.txid = function txid() { - return this.rhash(); -}; - -/** - * Serialize outpoint to a key - * suitable for a hash table. - * @returns {String} - */ - -Outpoint.prototype.toKey = function toKey() { - return Outpoint.toKey(this.hash, this.index); -}; - -/** - * Inject properties from hash table key. - * @private - * @param {String} key - * @returns {Outpoint} - */ - -Outpoint.prototype.fromKey = function fromKey(key) { - assert(key.length > 64); - this.hash = key.slice(0, 64); - this.index = parseInt(key.slice(64), 10); - return this; -}; - -/** - * Instantiate outpoint from hash table key. - * @param {String} key - * @returns {Outpoint} - */ - -Outpoint.fromKey = function fromKey(key) { - return new Outpoint().fromKey(key); -}; - -/** - * Write outpoint to a buffer writer. - * @param {BufferWriter} bw - */ - -Outpoint.prototype.toWriter = function toWriter(bw) { - bw.writeHash(this.hash); - bw.writeU32(this.index); - return bw; -}; - -/** - * Calculate size of outpoint. - * @returns {Number} - */ - -Outpoint.prototype.getSize = function getSize() { - return 36; -}; - -/** - * Serialize outpoint. - * @returns {Buffer} - */ - -Outpoint.prototype.toRaw = function toRaw() { - return this.toWriter(new StaticWriter(36)).render(); -}; - -/** - * Inject properties from buffer reader. - * @private - * @param {BufferReader} br - */ - -Outpoint.prototype.fromReader = function fromReader(br) { - this.hash = br.readHash('hex'); - this.index = br.readU32(); - return this; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -Outpoint.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(new BufferReader(data)); -}; - -/** - * Instantiate outpoint from a buffer reader. - * @param {BufferReader} br - * @returns {Outpoint} - */ - -Outpoint.fromReader = function fromReader(br) { - return new Outpoint().fromReader(br); -}; - -/** - * Instantiate outpoint from serialized data. - * @param {Buffer} data - * @returns {Outpoint} - */ - -Outpoint.fromRaw = function fromRaw(data) { - return new Outpoint().fromRaw(data); -}; - -/** - * Inject properties from json object. - * @private - * @params {Object} json - */ - -Outpoint.prototype.fromJSON = function fromJSON(json) { - assert(json, 'Outpoint data is required.'); - assert(typeof json.hash === 'string', 'Hash must be a string.'); - assert((json.index >>> 0) === json.index, 'Index must be a uint32.'); - this.hash = encoding.revHex(json.hash); - this.index = json.index; - return this; -}; - -/** - * Convert the outpoint to an object suitable - * for JSON serialization. Note that the hash - * will be reversed to abide by bitcoind's legacy - * of little-endian uint256s. - * @returns {Object} - */ - -Outpoint.prototype.toJSON = function toJSON() { - return { - hash: encoding.revHex(this.hash), - index: this.index - }; -}; - -/** - * Instantiate outpoint from json object. - * @param {Object} json - * @returns {Outpoint} - */ - -Outpoint.fromJSON = function fromJSON(json) { - return new Outpoint().fromJSON(json); -}; - -/** - * Inject properties from tx. - * @private - * @param {TX} tx - * @param {Number} index - */ - -Outpoint.prototype.fromTX = function fromTX(tx, index) { - assert(tx); - assert(typeof index === 'number'); - assert(index >= 0); - this.hash = tx.hash('hex'); - this.index = index; - return this; -}; - -/** - * Instantiate outpoint from tx. - * @param {TX} tx - * @param {Number} index - * @returns {Outpoint} - */ - -Outpoint.fromTX = function fromTX(tx, index) { - return new Outpoint().fromTX(tx, index); -}; - -/** - * Serialize outpoint to a key - * suitable for a hash table. - * @param {Hash} hash - * @param {Number} index - * @returns {String} - */ - -Outpoint.toKey = function toKey(hash, index) { - assert(typeof hash === 'string'); - assert(hash.length === 64); - assert(index >= 0); - return hash + index; -}; - -/** - * Convert the outpoint to a user-friendly string. - * @returns {String} - */ - -Outpoint.prototype.inspect = function inspect() { - return ``; -}; - -/** - * Test an object to see if it is an outpoint. - * @param {Object} obj - * @returns {Boolean} - */ - -Outpoint.isOutpoint = function isOutpoint(obj) { - return obj instanceof Outpoint; -}; - /* * Helpers */ diff --git a/lib/primitives/output.js b/lib/primitives/output.js index 62f36d55..02b4d15d 100644 --- a/lib/primitives/output.js +++ b/lib/primitives/output.js @@ -20,357 +20,360 @@ const policy = require('../protocol/policy'); /** * Represents a transaction output. * @alias module:primitives.Output - * @constructor - * @param {NakedOutput} options - * @property {Amount} value - Value in satoshis. + * @property {Amount} value * @property {Script} script */ -function Output(options) { - if (!(this instanceof Output)) - return new Output(options); +class Output { + /** + * Create an output. + * @constructor + * @param {Object?} options + */ - this.value = 0; - this.script = new Script(); + constructor(options) { + this.value = 0; + this.script = new Script(); - if (options) - this.fromOptions(options); -} + if (options) + this.fromOptions(options); + } -/** - * Inject properties from options object. - * @private - * @param {NakedOutput} options - */ + /** + * Inject properties from options object. + * @private + * @param {NakedOutput} options + */ -Output.prototype.fromOptions = function fromOptions(options) { - assert(options, 'Output data is required.'); + fromOptions(options) { + assert(options, 'Output data is required.'); - if (options.value) { - assert(Number.isSafeInteger(options.value) && options.value >= 0, + if (options.value) { + assert(Number.isSafeInteger(options.value) && options.value >= 0, + 'Value must be a uint64.'); + this.value = options.value; + } + + if (options.script) + this.script.fromOptions(options.script); + + if (options.address) + this.script.fromAddress(options.address); + + return this; + } + + /** + * Instantiate output from options object. + * @param {NakedOutput} options + * @returns {Output} + */ + + static fromOptions(options) { + return new this().fromOptions(options); + } + + /** + * Inject properties from script/value pair. + * @private + * @param {Script|Address} script + * @param {Amount} value + * @returns {Output} + */ + + fromScript(script, value) { + if (typeof script === 'string') + script = Address.fromString(script); + + if (script instanceof Address) + script = Script.fromAddress(script); + + assert(script instanceof Script, 'Script must be a Script.'); + assert(Number.isSafeInteger(value) && value >= 0, 'Value must be a uint64.'); + + this.script = script; + this.value = value; + + return this; + } + + /** + * Instantiate output from script/value pair. + * @param {Script|Address} script + * @param {Amount} value + * @returns {Output} + */ + + static fromScript(script, value) { + return new this().fromScript(script, value); + } + + /** + * Clone the output. + * @returns {Output} + */ + + clone() { + const output = new this.constructor(); + output.value = this.value; + output.script.inject(this.script); + return output; + } + + /** + * Test equality against another output. + * @param {Output} output + * @returns {Boolean} + */ + + equals(output) { + assert(Output.isOutput(output)); + return this.value === output.value + && this.script.equals(output.script); + } + + /** + * Compare against another output (BIP69). + * @param {Output} output + * @returns {Number} + */ + + compare(output) { + assert(Output.isOutput(output)); + + const cmp = this.value - output.value; + + if (cmp !== 0) + return cmp; + + return this.script.compare(output.script); + } + + /** + * Get the script type as a string. + * @returns {ScriptType} type + */ + + getType() { + return Script.typesByVal[this.script.getType()].toLowerCase(); + } + + /** + * Get the address. + * @returns {Address} address + */ + + getAddress() { + return this.script.getAddress(); + } + + /** + * Get the address hash. + * @param {String?} enc + * @returns {Hash} hash + */ + + getHash(enc) { + const addr = this.getAddress(); + + if (!addr) + return null; + + return addr.getHash(enc); + } + + /** + * Convert the input to a more user-friendly object. + * @returns {Object} + */ + + inspect() { + return { + type: this.getType(), + value: Amount.btc(this.value), + script: this.script, + address: this.getAddress() + }; + } + + /** + * Convert the output to an object suitable + * for JSON serialization. + * @returns {Object} + */ + + toJSON() { + return this.getJSON(); + } + + /** + * Convert the output to an object suitable + * for JSON serialization. + * @param {Network} network + * @returns {Object} + */ + + getJSON(network) { + let addr = this.getAddress(); + + network = Network.get(network); + + if (addr) + addr = addr.toString(network); + + return { + value: this.value, + script: this.script.toJSON(), + address: addr + }; + } + + /** + * Calculate the dust threshold for this + * output, based on serialize size and rate. + * @param {Rate?} rate + * @returns {Amount} + */ + + getDustThreshold(rate) { + const scale = consensus.WITNESS_SCALE_FACTOR; + + if (this.script.isUnspendable()) + return 0; + + let size = this.getSize(); + + if (this.script.isProgram()) { + // 75% segwit discount applied to script size. + size += 32 + 4 + 1 + (107 / scale | 0) + 4; + } else { + size += 32 + 4 + 1 + 107 + 4; + } + + return 3 * policy.getMinFee(size, rate); + } + + /** + * Calculate size of serialized output. + * @returns {Number} + */ + + getSize() { + return 8 + this.script.getVarSize(); + } + + /** + * Test whether the output should be considered dust. + * @param {Rate?} rate + * @returns {Boolean} + */ + + isDust(rate) { + return this.value < this.getDustThreshold(rate); + } + + /** + * Inject properties from a JSON object. + * @private + * @param {Object} json + */ + + fromJSON(json) { + assert(json, 'Output data is required.'); + assert(Number.isSafeInteger(json.value) && json.value >= 0, 'Value must be a uint64.'); - this.value = options.value; + this.value = json.value; + this.script.fromJSON(json.script); + return this; } - if (options.script) - this.script.fromOptions(options.script); + /** + * Instantiate an Output from a jsonified output object. + * @param {Object} json - The jsonified output object. + * @returns {Output} + */ - if (options.address) - this.script.fromAddress(options.address); - - return this; -}; - -/** - * Instantiate output from options object. - * @param {NakedOutput} options - * @returns {Output} - */ - -Output.fromOptions = function fromOptions(options) { - return new Output().fromOptions(options); -}; - -/** - * Inject properties from script/value pair. - * @private - * @param {Script|Address} script - * @param {Amount} value - * @returns {Output} - */ - -Output.prototype.fromScript = function fromScript(script, value) { - if (typeof script === 'string') - script = Address.fromString(script); - - if (script instanceof Address) - script = Script.fromAddress(script); - - assert(script instanceof Script, 'Script must be a Script.'); - assert(Number.isSafeInteger(value) && value >= 0, 'Value must be a uint64.'); - - this.script = script; - this.value = value; - - return this; -}; - -/** - * Instantiate output from script/value pair. - * @param {Script|Address} script - * @param {Amount} value - * @returns {Output} - */ - -Output.fromScript = function fromScript(script, value) { - return new Output().fromScript(script, value); -}; - -/** - * Clone the output. - * @returns {Output} - */ - -Output.prototype.clone = function clone() { - const output = new Output(); - output.value = this.value; - output.script.inject(this.script); - return output; -}; - -/** - * Test equality against another output. - * @param {Output} output - * @returns {Boolean} - */ - -Output.prototype.equals = function equals(output) { - assert(Output.isOutput(output)); - return this.value === output.value - && this.script.equals(output.script); -}; - -/** - * Compare against another output (BIP69). - * @param {Output} output - * @returns {Number} - */ - -Output.prototype.compare = function compare(output) { - assert(Output.isOutput(output)); - - const cmp = this.value - output.value; - - if (cmp !== 0) - return cmp; - - return this.script.compare(output.script); -}; - -/** - * Get the script type as a string. - * @returns {ScriptType} type - */ - -Output.prototype.getType = function getType() { - return Script.typesByVal[this.script.getType()].toLowerCase(); -}; - -/** - * Get the address. - * @returns {Address} address - */ - -Output.prototype.getAddress = function getAddress() { - return this.script.getAddress(); -}; - -/** - * Get the address hash. - * @param {String?} enc - * @returns {Hash} hash - */ - -Output.prototype.getHash = function getHash(enc) { - const addr = this.getAddress(); - - if (!addr) - return null; - - return addr.getHash(enc); -}; - -/** - * Convert the input to a more user-friendly object. - * @returns {Object} - */ - -Output.prototype.inspect = function inspect() { - return { - type: this.getType(), - value: Amount.btc(this.value), - script: this.script, - address: this.getAddress() - }; -}; - -/** - * Convert the output to an object suitable - * for JSON serialization. - * @returns {Object} - */ - -Output.prototype.toJSON = function toJSON() { - return this.getJSON(); -}; - -/** - * Convert the output to an object suitable - * for JSON serialization. - * @param {Network} network - * @returns {Object} - */ - -Output.prototype.getJSON = function getJSON(network) { - let addr = this.getAddress(); - - network = Network.get(network); - - if (addr) - addr = addr.toString(network); - - return { - value: this.value, - script: this.script.toJSON(), - address: addr - }; -}; - -/** - * Calculate the dust threshold for this - * output, based on serialize size and rate. - * @param {Rate?} rate - * @returns {Amount} - */ - -Output.prototype.getDustThreshold = function getDustThreshold(rate) { - const scale = consensus.WITNESS_SCALE_FACTOR; - - if (this.script.isUnspendable()) - return 0; - - let size = this.getSize(); - - if (this.script.isProgram()) { - // 75% segwit discount applied to script size. - size += 32 + 4 + 1 + (107 / scale | 0) + 4; - } else { - size += 32 + 4 + 1 + 107 + 4; + static fromJSON(json) { + return new this().fromJSON(json); } - return 3 * policy.getMinFee(size, rate); -}; + /** + * Write the output to a buffer writer. + * @param {BufferWriter} bw + */ -/** - * Calculate size of serialized output. - * @returns {Number} - */ + toWriter(bw) { + bw.writeI64(this.value); + bw.writeVarBytes(this.script.toRaw()); + return bw; + } -Output.prototype.getSize = function getSize() { - return 8 + this.script.getVarSize(); -}; + /** + * Serialize the output. + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {Buffer|String} + */ -/** - * Test whether the output should be considered dust. - * @param {Rate?} rate - * @returns {Boolean} - */ + toRaw() { + const size = this.getSize(); + return this.toWriter(new StaticWriter(size)).render(); + } -Output.prototype.isDust = function isDust(rate) { - return this.value < this.getDustThreshold(rate); -}; + /** + * Inject properties from buffer reader. + * @private + * @param {BufferReader} br + */ -/** - * Inject properties from a JSON object. - * @private - * @param {Object} json - */ + fromReader(br) { + this.value = br.readI64(); + this.script.fromRaw(br.readVarBytes()); + return this; + } -Output.prototype.fromJSON = function fromJSON(json) { - assert(json, 'Output data is required.'); - assert(Number.isSafeInteger(json.value) && json.value >= 0, - 'Value must be a uint64.'); - this.value = json.value; - this.script.fromJSON(json.script); - return this; -}; + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ -/** - * Instantiate an Output from a jsonified output object. - * @param {Object} json - The jsonified output object. - * @returns {Output} - */ + fromRaw(data) { + return this.fromReader(new BufferReader(data)); + } -Output.fromJSON = function fromJSON(json) { - return new Output().fromJSON(json); -}; + /** + * Instantiate an output from a buffer reader. + * @param {BufferReader} br + * @returns {Output} + */ -/** - * Write the output to a buffer writer. - * @param {BufferWriter} bw - */ + static fromReader(br) { + return new this().fromReader(br); + } -Output.prototype.toWriter = function toWriter(bw) { - bw.writeI64(this.value); - bw.writeVarBytes(this.script.toRaw()); - return bw; -}; + /** + * Instantiate an output from a serialized Buffer. + * @param {Buffer} data + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {Output} + */ -/** - * Serialize the output. - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {Buffer|String} - */ + static fromRaw(data, enc) { + if (typeof data === 'string') + data = Buffer.from(data, enc); + return new this().fromRaw(data); + } -Output.prototype.toRaw = function toRaw() { - const size = this.getSize(); - return this.toWriter(new StaticWriter(size)).render(); -}; + /** + * Test an object to see if it is an Output. + * @param {Object} obj + * @returns {Boolean} + */ -/** - * Inject properties from buffer reader. - * @private - * @param {BufferReader} br - */ - -Output.prototype.fromReader = function fromReader(br) { - this.value = br.readI64(); - this.script.fromRaw(br.readVarBytes()); - return this; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -Output.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(new BufferReader(data)); -}; - -/** - * Instantiate an output from a buffer reader. - * @param {BufferReader} br - * @returns {Output} - */ - -Output.fromReader = function fromReader(br) { - return new Output().fromReader(br); -}; - -/** - * Instantiate an output from a serialized Buffer. - * @param {Buffer} data - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {Output} - */ - -Output.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - return new Output().fromRaw(data); -}; - -/** - * Test an object to see if it is an Output. - * @param {Object} obj - * @returns {Boolean} - */ - -Output.isOutput = function isOutput(obj) { - return obj instanceof Output; -}; + static isOutput(obj) { + return obj instanceof Output; + } +} /* * Expose diff --git a/lib/primitives/tx.js b/lib/primitives/tx.js index cda6de41..856058a3 100644 --- a/lib/primitives/tx.js +++ b/lib/primitives/tx.js @@ -28,1630 +28,1589 @@ const ScriptError = require('../script/scripterror'); const hashType = Script.hashType; /** + * TX * A static transaction object. * @alias module:primitives.TX - * @constructor - * @param {Object} options - Transaction fields. - * @property {Number} version - Transaction version. Note that Bcoin reads - * versions as unsigned even though they are signed at the protocol level. - * This value will never be negative. - * @property {Number} flag - Flag field for segregated witness. - * Always non-zero (1 if not present). + * @property {Number} version * @property {Input[]} inputs * @property {Output[]} outputs - * @property {Number} locktime - nLockTime + * @property {Number} locktime */ -function TX(options) { - if (!(this instanceof TX)) - return new TX(options); +class TX { + /** + * Create a transaction. + * @constructor + * @param {Object?} options + */ - this.version = 1; - this.inputs = []; - this.outputs = []; - this.locktime = 0; + constructor(options) { + this.version = 1; + this.inputs = []; + this.outputs = []; + this.locktime = 0; - this.mutable = false; + this.mutable = false; - this._hash = null; - this._hhash = null; - this._whash = null; + this._hash = null; + this._hhash = null; + this._whash = null; - this._raw = null; - this._size = -1; - this._witness = -1; - this._sigops = -1; + this._raw = null; + this._size = -1; + this._witness = -1; + this._sigops = -1; - this._hashPrevouts = null; - this._hashSequence = null; - this._hashOutputs = null; + this._hashPrevouts = null; + this._hashSequence = null; + this._hashOutputs = null; - if (options) - this.fromOptions(options); -} - -/** - * Inject properties from options object. - * @private - * @param {NakedTX} options - */ - -TX.prototype.fromOptions = function fromOptions(options) { - assert(options, 'TX data is required.'); - - if (options.version != null) { - assert((options.version >>> 0) === options.version, - 'Version must be a uint32.'); - this.version = options.version; + if (options) + this.fromOptions(options); } - if (options.inputs) { - assert(Array.isArray(options.inputs), 'Inputs must be an array.'); - for (const input of options.inputs) - this.inputs.push(new Input(input)); - } + /** + * Inject properties from options object. + * @private + * @param {NakedTX} options + */ - if (options.outputs) { - assert(Array.isArray(options.outputs), 'Outputs must be an array.'); - for (const output of options.outputs) - this.outputs.push(new Output(output)); - } + fromOptions(options) { + assert(options, 'TX data is required.'); - if (options.locktime != null) { - assert((options.locktime >>> 0) === options.locktime, - 'Locktime must be a uint32.'); - this.locktime = options.locktime; - } - - return this; -}; - -/** - * Instantiate TX from options object. - * @param {NakedTX} options - * @returns {TX} - */ - -TX.fromOptions = function fromOptions(options) { - return new TX().fromOptions(options); -}; - -/** - * Clone the transaction. - * @returns {TX} - */ - -TX.prototype.clone = function clone() { - return new TX().inject(this); -}; - -/** - * Inject properties from tx. - * Used for cloning. - * @private - * @param {TX} tx - * @returns {TX} - */ - -TX.prototype.inject = function inject(tx) { - this.version = tx.version; - - for (const input of tx.inputs) - this.inputs.push(input.clone()); - - for (const output of tx.outputs) - this.outputs.push(output.clone()); - - this.locktime = tx.locktime; - - return this; -}; - -/** - * Clear any cached values. - */ - -TX.prototype.refresh = function refresh() { - this._hash = null; - this._hhash = null; - this._whash = null; - - this._raw = null; - this._size = -1; - this._witness = -1; - this._sigops = -1; - - this._hashPrevouts = null; - this._hashSequence = null; - this._hashOutputs = null; -}; - -/** - * Hash the transaction with the non-witness serialization. - * @param {String?} enc - Can be `'hex'` or `null`. - * @returns {Hash|Buffer} hash - */ - -TX.prototype.hash = function hash(enc) { - let h = this._hash; - - if (!h) { - h = hash256.digest(this.toNormal()); - if (!this.mutable) - this._hash = h; - } - - if (enc === 'hex') { - let hex = this._hhash; - if (!hex) { - hex = h.toString('hex'); - if (!this.mutable) - this._hhash = hex; + if (options.version != null) { + assert((options.version >>> 0) === options.version, + 'Version must be a uint32.'); + this.version = options.version; } - h = hex; + + if (options.inputs) { + assert(Array.isArray(options.inputs), 'Inputs must be an array.'); + for (const input of options.inputs) + this.inputs.push(new Input(input)); + } + + if (options.outputs) { + assert(Array.isArray(options.outputs), 'Outputs must be an array.'); + for (const output of options.outputs) + this.outputs.push(new Output(output)); + } + + if (options.locktime != null) { + assert((options.locktime >>> 0) === options.locktime, + 'Locktime must be a uint32.'); + this.locktime = options.locktime; + } + + return this; } - return h; -}; + /** + * Instantiate TX from options object. + * @param {NakedTX} options + * @returns {TX} + */ -/** - * Hash the transaction with the witness - * serialization, return the wtxid (normal - * hash if no witness is present, all zeroes - * if coinbase). - * @param {String?} enc - Can be `'hex'` or `null`. - * @returns {Hash|Buffer} hash - */ - -TX.prototype.witnessHash = function witnessHash(enc) { - if (!this.hasWitness()) - return this.hash(enc); - - let hash = this._whash; - - if (!hash) { - hash = hash256.digest(this.toRaw()); - if (!this.mutable) - this._whash = hash; + static fromOptions(options) { + return new this().fromOptions(options); } - return enc === 'hex' ? hash.toString('hex') : hash; -}; + /** + * Clone the transaction. + * @returns {TX} + */ -/** - * Serialize the transaction. Note - * that this is cached. This will use - * the witness serialization if a - * witness is present. - * @returns {Buffer} Serialized transaction. - */ + clone() { + return new this.constructor().inject(this); + } -TX.prototype.toRaw = function toRaw() { - return this.frame().data; -}; + /** + * Inject properties from tx. + * Used for cloning. + * @private + * @param {TX} tx + * @returns {TX} + */ -/** - * Serialize the transaction without the - * witness vector, regardless of whether it - * is a witness transaction or not. - * @returns {Buffer} Serialized transaction. - */ + inject(tx) { + this.version = tx.version; -TX.prototype.toNormal = function toNormal() { - if (this.hasWitness()) - return this.frameNormal().data; - return this.toRaw(); -}; + for (const input of tx.inputs) + this.inputs.push(input.clone()); -/** - * Write the transaction to a buffer writer. - * @param {BufferWriter} bw - */ + for (const output of tx.outputs) + this.outputs.push(output.clone()); -TX.prototype.toWriter = function toWriter(bw) { - if (this.mutable) { + this.locktime = tx.locktime; + + return this; + } + + /** + * Clear any cached values. + */ + + refresh() { + this._hash = null; + this._hhash = null; + this._whash = null; + + this._raw = null; + this._size = -1; + this._witness = -1; + this._sigops = -1; + + this._hashPrevouts = null; + this._hashSequence = null; + this._hashOutputs = null; + } + + /** + * Hash the transaction with the non-witness serialization. + * @param {String?} enc - Can be `'hex'` or `null`. + * @returns {Hash|Buffer} hash + */ + + hash(enc) { + let h = this._hash; + + if (!h) { + h = hash256.digest(this.toNormal()); + if (!this.mutable) + this._hash = h; + } + + if (enc === 'hex') { + let hex = this._hhash; + if (!hex) { + hex = h.toString('hex'); + if (!this.mutable) + this._hhash = hex; + } + h = hex; + } + + return h; + } + + /** + * Hash the transaction with the witness + * serialization, return the wtxid (normal + * hash if no witness is present, all zeroes + * if coinbase). + * @param {String?} enc - Can be `'hex'` or `null`. + * @returns {Hash|Buffer} hash + */ + + witnessHash(enc) { + if (!this.hasWitness()) + return this.hash(enc); + + let hash = this._whash; + + if (!hash) { + hash = hash256.digest(this.toRaw()); + if (!this.mutable) + this._whash = hash; + } + + return enc === 'hex' ? hash.toString('hex') : hash; + } + + /** + * Serialize the transaction. Note + * that this is cached. This will use + * the witness serialization if a + * witness is present. + * @returns {Buffer} Serialized transaction. + */ + + toRaw() { + return this.frame().data; + } + + /** + * Serialize the transaction without the + * witness vector, regardless of whether it + * is a witness transaction or not. + * @returns {Buffer} Serialized transaction. + */ + + toNormal() { if (this.hasWitness()) - return this.writeWitness(bw); - return this.writeNormal(bw); + return this.frameNormal().data; + return this.toRaw(); } - bw.writeBytes(this.toRaw()); + /** + * Write the transaction to a buffer writer. + * @param {BufferWriter} bw + */ - return bw; -}; + toWriter(bw) { + if (this.mutable) { + if (this.hasWitness()) + return this.writeWitness(bw); + return this.writeNormal(bw); + } -/** - * Write the transaction to a buffer writer. - * Uses non-witness serialization. - * @param {BufferWriter} bw - */ + bw.writeBytes(this.toRaw()); -TX.prototype.toNormalWriter = function toNormalWriter(bw) { - if (this.hasWitness()) { - this.writeNormal(bw); return bw; } - return this.toWriter(bw); -}; -/** - * Serialize the transaction. Note - * that this is cached. This will use - * the witness serialization if a - * witness is present. - * @private - * @returns {RawTX} - */ + /** + * Write the transaction to a buffer writer. + * Uses non-witness serialization. + * @param {BufferWriter} bw + */ -TX.prototype.frame = function frame() { - if (this.mutable) { - assert(!this._raw); - if (this.hasWitness()) - return this.frameWitness(); - return this.frameNormal(); + toNormalWriter(bw) { + if (this.hasWitness()) { + this.writeNormal(bw); + return bw; + } + return this.toWriter(bw); } - if (this._raw) { - assert(this._size >= 0); - assert(this._witness >= 0); - const raw = new RawTX(this._size, this._witness); - raw.data = this._raw; + /** + * Serialize the transaction. Note + * that this is cached. This will use + * the witness serialization if a + * witness is present. + * @private + * @returns {RawTX} + */ + + frame() { + if (this.mutable) { + assert(!this._raw); + if (this.hasWitness()) + return this.frameWitness(); + return this.frameNormal(); + } + + if (this._raw) { + assert(this._size >= 0); + assert(this._witness >= 0); + const raw = new RawTX(this._size, this._witness); + raw.data = this._raw; + return raw; + } + + let raw; + if (this.hasWitness()) + raw = this.frameWitness(); + else + raw = this.frameNormal(); + + this._raw = raw.data; + this._size = raw.size; + this._witness = raw.witness; + return raw; } - let raw; - if (this.hasWitness()) - raw = this.frameWitness(); - else - raw = this.frameNormal(); + /** + * Calculate total size and size of the witness bytes. + * @returns {Object} Contains `size` and `witness`. + */ - this._raw = raw.data; - this._size = raw.size; - this._witness = raw.witness; - - return raw; -}; - -/** - * Calculate total size and size of the witness bytes. - * @returns {Object} Contains `size` and `witness`. - */ - -TX.prototype.getSizes = function getSizes() { - if (this.mutable) { - if (this.hasWitness()) - return this.getWitnessSizes(); - return this.getNormalSizes(); - } - return this.frame(); -}; - -/** - * Calculate the virtual size of the transaction. - * Note that this is cached. - * @returns {Number} vsize - */ - -TX.prototype.getVirtualSize = function getVirtualSize() { - const scale = consensus.WITNESS_SCALE_FACTOR; - return (this.getWeight() + scale - 1) / scale | 0; -}; - -/** - * Calculate the virtual size of the transaction - * (weighted against bytes per sigop cost). - * @param {Number} sigops - Sigops cost. - * @returns {Number} vsize - */ - -TX.prototype.getSigopsSize = function getSigopsSize(sigops) { - const scale = consensus.WITNESS_SCALE_FACTOR; - const bytes = policy.BYTES_PER_SIGOP; - const weight = Math.max(this.getWeight(), sigops * bytes); - return (weight + scale - 1) / scale | 0; -}; - -/** - * Calculate the weight of the transaction. - * Note that this is cached. - * @returns {Number} weight - */ - -TX.prototype.getWeight = function getWeight() { - const raw = this.getSizes(); - const base = raw.size - raw.witness; - return base * (consensus.WITNESS_SCALE_FACTOR - 1) + raw.size; -}; - -/** - * Calculate the real size of the transaction - * with the witness included. - * @returns {Number} size - */ - -TX.prototype.getSize = function getSize() { - return this.getSizes().size; -}; - -/** - * Calculate the size of the transaction - * without the witness. - * with the witness included. - * @returns {Number} size - */ - -TX.prototype.getBaseSize = function getBaseSize() { - const raw = this.getSizes(); - return raw.size - raw.witness; -}; - -/** - * Test whether the transaction has a non-empty witness. - * @returns {Boolean} - */ - -TX.prototype.hasWitness = function hasWitness() { - if (this._witness !== -1) - return this._witness !== 0; - - for (const input of this.inputs) { - if (input.witness.items.length > 0) - return true; + getSizes() { + if (this.mutable) { + if (this.hasWitness()) + return this.getWitnessSizes(); + return this.getNormalSizes(); + } + return this.frame(); } - return false; -}; + /** + * Calculate the virtual size of the transaction. + * Note that this is cached. + * @returns {Number} vsize + */ -/** - * Get the signature hash of the transaction for signing verifying. - * @param {Number} index - Index of input being signed/verified. - * @param {Script} prev - Previous output script or redeem script - * (in the case of witnesspubkeyhash, this should be the generated - * p2pkh script). - * @param {Amount} value - Previous output value. - * @param {SighashType} type - Sighash type. - * @param {Number} version - Sighash version (0=legacy, 1=segwit). - * @returns {Buffer} Signature hash. - */ - -TX.prototype.signatureHash = function signatureHash(index, prev, value, type, version) { - assert(index >= 0 && index < this.inputs.length); - assert(prev instanceof Script); - assert(typeof value === 'number'); - assert(typeof type === 'number'); - - // Traditional sighashing - if (version === 0) - return this.signatureHashV0(index, prev, type); - - // Segwit sighashing - if (version === 1) - return this.signatureHashV1(index, prev, value, type); - - throw new Error('Unknown sighash version.'); -}; - -/** - * Legacy sighashing -- O(n^2). - * @private - * @param {Number} index - * @param {Script} prev - * @param {SighashType} type - * @returns {Buffer} - */ - -TX.prototype.signatureHashV0 = function signatureHashV0(index, prev, type) { - if ((type & 0x1f) === hashType.SINGLE) { - // Bitcoind used to return 1 as an error code: - // it ended up being treated like a hash. - if (index >= this.outputs.length) - return Buffer.from(encoding.ONE_HASH); + getVirtualSize() { + const scale = consensus.WITNESS_SCALE_FACTOR; + return (this.getWeight() + scale - 1) / scale | 0; } - // Remove all code separators. - prev = prev.removeSeparators(); + /** + * Calculate the virtual size of the transaction + * (weighted against bytes per sigop cost). + * @param {Number} sigops - Sigops cost. + * @returns {Number} vsize + */ - // Calculate buffer size. - const size = this.hashSize(index, prev, type); - const bw = StaticWriter.pool(size); + getSigopsSize(sigops) { + const scale = consensus.WITNESS_SCALE_FACTOR; + const bytes = policy.BYTES_PER_SIGOP; + const weight = Math.max(this.getWeight(), sigops * bytes); + return (weight + scale - 1) / scale | 0; + } - bw.writeU32(this.version); + /** + * Calculate the weight of the transaction. + * Note that this is cached. + * @returns {Number} weight + */ - // Serialize inputs. - if (type & hashType.ANYONECANPAY) { - // Serialize only the current - // input if ANYONECANPAY. - const input = this.inputs[index]; + getWeight() { + const raw = this.getSizes(); + const base = raw.size - raw.witness; + return base * (consensus.WITNESS_SCALE_FACTOR - 1) + raw.size; + } - // Count. - bw.writeVarint(1); + /** + * Calculate the real size of the transaction + * with the witness included. + * @returns {Number} size + */ - // Outpoint. - input.prevout.toWriter(bw); + getSize() { + return this.getSizes().size; + } - // Replace script with previous - // output script if current index. - bw.writeVarBytes(prev.toRaw()); - bw.writeU32(input.sequence); - } else { - bw.writeVarint(this.inputs.length); - for (let i = 0; i < this.inputs.length; i++) { - const input = this.inputs[i]; + /** + * Calculate the size of the transaction + * without the witness. + * with the witness included. + * @returns {Number} size + */ + + getBaseSize() { + const raw = this.getSizes(); + return raw.size - raw.witness; + } + + /** + * Test whether the transaction has a non-empty witness. + * @returns {Boolean} + */ + + hasWitness() { + if (this._witness !== -1) + return this._witness !== 0; + + for (const input of this.inputs) { + if (input.witness.items.length > 0) + return true; + } + + return false; + } + + /** + * Get the signature hash of the transaction for signing verifying. + * @param {Number} index - Index of input being signed/verified. + * @param {Script} prev - Previous output script or redeem script + * (in the case of witnesspubkeyhash, this should be the generated + * p2pkh script). + * @param {Amount} value - Previous output value. + * @param {SighashType} type - Sighash type. + * @param {Number} version - Sighash version (0=legacy, 1=segwit). + * @returns {Buffer} Signature hash. + */ + + signatureHash(index, prev, value, type, version) { + assert(index >= 0 && index < this.inputs.length); + assert(prev instanceof Script); + assert(typeof value === 'number'); + assert(typeof type === 'number'); + + // Traditional sighashing + if (version === 0) + return this.signatureHashV0(index, prev, type); + + // Segwit sighashing + if (version === 1) + return this.signatureHashV1(index, prev, value, type); + + throw new Error('Unknown sighash version.'); + } + + /** + * Legacy sighashing -- O(n^2). + * @private + * @param {Number} index + * @param {Script} prev + * @param {SighashType} type + * @returns {Buffer} + */ + + signatureHashV0(index, prev, type) { + if ((type & 0x1f) === hashType.SINGLE) { + // Bitcoind used to return 1 as an error code: + // it ended up being treated like a hash. + if (index >= this.outputs.length) + return Buffer.from(encoding.ONE_HASH); + } + + // Remove all code separators. + prev = prev.removeSeparators(); + + // Calculate buffer size. + const size = this.hashSize(index, prev, type); + const bw = StaticWriter.pool(size); + + bw.writeU32(this.version); + + // Serialize inputs. + if (type & hashType.ANYONECANPAY) { + // Serialize only the current + // input if ANYONECANPAY. + const input = this.inputs[index]; + + // Count. + bw.writeVarint(1); // Outpoint. input.prevout.toWriter(bw); // Replace script with previous // output script if current index. - if (i === index) { - bw.writeVarBytes(prev.toRaw()); - bw.writeU32(input.sequence); - continue; - } - - // Script is null. - bw.writeVarint(0); - - // Sequences are 0 if NONE or SINGLE. - switch (type & 0x1f) { - case hashType.NONE: - case hashType.SINGLE: - bw.writeU32(0); - break; - default: - bw.writeU32(input.sequence); - break; - } - } - } - - // Serialize outputs. - switch (type & 0x1f) { - case hashType.NONE: { - // No outputs if NONE. - bw.writeVarint(0); - break; - } - case hashType.SINGLE: { - const output = this.outputs[index]; - - // Drop all outputs after the - // current input index if SINGLE. - bw.writeVarint(index + 1); - - for (let i = 0; i < index; i++) { - // Null all outputs not at - // current input index. - bw.writeI64(-1); - bw.writeVarint(0); - } - - // Regular serialization - // at current input index. - output.toWriter(bw); - - break; - } - default: { - // Regular output serialization if ALL. - bw.writeVarint(this.outputs.length); - for (const output of this.outputs) - output.toWriter(bw); - break; - } - } - - bw.writeU32(this.locktime); - - // Append the hash type. - bw.writeU32(type); - - return hash256.digest(bw.render()); -}; - -/** - * Calculate sighash size. - * @private - * @param {Number} index - * @param {Script} prev - * @param {Number} type - * @returns {Number} - */ - -TX.prototype.hashSize = function hashSize(index, prev, type) { - let size = 0; - - size += 4; - - if (type & hashType.ANYONECANPAY) { - size += 1; - size += 36; - size += prev.getVarSize(); - size += 4; - } else { - size += encoding.sizeVarint(this.inputs.length); - size += 41 * (this.inputs.length - 1); - size += 36; - size += prev.getVarSize(); - size += 4; - } - - switch (type & 0x1f) { - case hashType.NONE: - size += 1; - break; - case hashType.SINGLE: - size += encoding.sizeVarint(index + 1); - size += 9 * index; - size += this.outputs[index].getSize(); - break; - default: - size += encoding.sizeVarint(this.outputs.length); - for (const output of this.outputs) - size += output.getSize(); - break; - } - - size += 8; - - return size; -}; - -/** - * Witness sighashing -- O(n). - * @private - * @param {Number} index - * @param {Script} prev - * @param {Amount} value - * @param {SighashType} type - * @returns {Buffer} - */ - -TX.prototype.signatureHashV1 = function signatureHashV1(index, prev, value, type) { - const input = this.inputs[index]; - let prevouts = encoding.ZERO_HASH; - let sequences = encoding.ZERO_HASH; - let outputs = encoding.ZERO_HASH; - - if (!(type & hashType.ANYONECANPAY)) { - if (this._hashPrevouts) { - prevouts = this._hashPrevouts; + bw.writeVarBytes(prev.toRaw()); + bw.writeU32(input.sequence); } else { - const bw = StaticWriter.pool(this.inputs.length * 36); + bw.writeVarint(this.inputs.length); + for (let i = 0; i < this.inputs.length; i++) { + const input = this.inputs[i]; - for (const input of this.inputs) + // Outpoint. input.prevout.toWriter(bw); - prevouts = hash256.digest(bw.render()); + // Replace script with previous + // output script if current index. + if (i === index) { + bw.writeVarBytes(prev.toRaw()); + bw.writeU32(input.sequence); + continue; + } - if (!this.mutable) - this._hashPrevouts = prevouts; + // Script is null. + bw.writeVarint(0); + + // Sequences are 0 if NONE or SINGLE. + switch (type & 0x1f) { + case hashType.NONE: + case hashType.SINGLE: + bw.writeU32(0); + break; + default: + bw.writeU32(input.sequence); + break; + } + } } - } - if (!(type & hashType.ANYONECANPAY) - && (type & 0x1f) !== hashType.SINGLE - && (type & 0x1f) !== hashType.NONE) { - if (this._hashSequence) { - sequences = this._hashSequence; - } else { - const bw = StaticWriter.pool(this.inputs.length * 4); + // Serialize outputs. + switch (type & 0x1f) { + case hashType.NONE: { + // No outputs if NONE. + bw.writeVarint(0); + break; + } + case hashType.SINGLE: { + const output = this.outputs[index]; - for (const input of this.inputs) - bw.writeU32(input.sequence); + // Drop all outputs after the + // current input index if SINGLE. + bw.writeVarint(index + 1); - sequences = hash256.digest(bw.render()); + for (let i = 0; i < index; i++) { + // Null all outputs not at + // current input index. + bw.writeI64(-1); + bw.writeVarint(0); + } - if (!this.mutable) - this._hashSequence = sequences; - } - } - - if ((type & 0x1f) !== hashType.SINGLE - && (type & 0x1f) !== hashType.NONE) { - if (this._hashOutputs) { - outputs = this._hashOutputs; - } else { - let size = 0; - - for (const output of this.outputs) - size += output.getSize(); - - const bw = StaticWriter.pool(size); - - for (const output of this.outputs) + // Regular serialization + // at current input index. output.toWriter(bw); - outputs = hash256.digest(bw.render()); - - if (!this.mutable) - this._hashOutputs = outputs; + break; + } + default: { + // Regular output serialization if ALL. + bw.writeVarint(this.outputs.length); + for (const output of this.outputs) + output.toWriter(bw); + break; + } } - } else if ((type & 0x1f) === hashType.SINGLE) { - if (index < this.outputs.length) { - const output = this.outputs[index]; - outputs = hash256.digest(output.toRaw()); + + bw.writeU32(this.locktime); + + // Append the hash type. + bw.writeU32(type); + + return hash256.digest(bw.render()); + } + + /** + * Calculate sighash size. + * @private + * @param {Number} index + * @param {Script} prev + * @param {Number} type + * @returns {Number} + */ + + hashSize(index, prev, type) { + let size = 0; + + size += 4; + + if (type & hashType.ANYONECANPAY) { + size += 1; + size += 36; + size += prev.getVarSize(); + size += 4; + } else { + size += encoding.sizeVarint(this.inputs.length); + size += 41 * (this.inputs.length - 1); + size += 36; + size += prev.getVarSize(); + size += 4; + } + + switch (type & 0x1f) { + case hashType.NONE: + size += 1; + break; + case hashType.SINGLE: + size += encoding.sizeVarint(index + 1); + size += 9 * index; + size += this.outputs[index].getSize(); + break; + default: + size += encoding.sizeVarint(this.outputs.length); + for (const output of this.outputs) + size += output.getSize(); + break; + } + + size += 8; + + return size; + } + + /** + * Witness sighashing -- O(n). + * @private + * @param {Number} index + * @param {Script} prev + * @param {Amount} value + * @param {SighashType} type + * @returns {Buffer} + */ + + signatureHashV1(index, prev, value, type) { + const input = this.inputs[index]; + let prevouts = encoding.ZERO_HASH; + let sequences = encoding.ZERO_HASH; + let outputs = encoding.ZERO_HASH; + + if (!(type & hashType.ANYONECANPAY)) { + if (this._hashPrevouts) { + prevouts = this._hashPrevouts; + } else { + const bw = StaticWriter.pool(this.inputs.length * 36); + + for (const input of this.inputs) + input.prevout.toWriter(bw); + + prevouts = hash256.digest(bw.render()); + + if (!this.mutable) + this._hashPrevouts = prevouts; + } + } + + if (!(type & hashType.ANYONECANPAY) + && (type & 0x1f) !== hashType.SINGLE + && (type & 0x1f) !== hashType.NONE) { + if (this._hashSequence) { + sequences = this._hashSequence; + } else { + const bw = StaticWriter.pool(this.inputs.length * 4); + + for (const input of this.inputs) + bw.writeU32(input.sequence); + + sequences = hash256.digest(bw.render()); + + if (!this.mutable) + this._hashSequence = sequences; + } + } + + if ((type & 0x1f) !== hashType.SINGLE + && (type & 0x1f) !== hashType.NONE) { + if (this._hashOutputs) { + outputs = this._hashOutputs; + } else { + let size = 0; + + for (const output of this.outputs) + size += output.getSize(); + + const bw = StaticWriter.pool(size); + + for (const output of this.outputs) + output.toWriter(bw); + + outputs = hash256.digest(bw.render()); + + if (!this.mutable) + this._hashOutputs = outputs; + } + } else if ((type & 0x1f) === hashType.SINGLE) { + if (index < this.outputs.length) { + const output = this.outputs[index]; + outputs = hash256.digest(output.toRaw()); + } + } + + const size = 156 + prev.getVarSize(); + const bw = StaticWriter.pool(size); + + bw.writeU32(this.version); + bw.writeBytes(prevouts); + bw.writeBytes(sequences); + bw.writeHash(input.prevout.hash); + bw.writeU32(input.prevout.index); + bw.writeVarBytes(prev.toRaw()); + bw.writeI64(value); + bw.writeU32(input.sequence); + bw.writeBytes(outputs); + bw.writeU32(this.locktime); + bw.writeU32(type); + + return hash256.digest(bw.render()); + } + + /** + * Verify signature. + * @param {Number} index + * @param {Script} prev + * @param {Amount} value + * @param {Buffer} sig + * @param {Buffer} key + * @param {Number} version + * @returns {Boolean} + */ + + checksig(index, prev, value, sig, key, version) { + if (sig.length === 0) + return false; + + const type = sig[sig.length - 1]; + const hash = this.signatureHash(index, prev, value, type, version); + + return secp256k1.verify(hash, sig.slice(0, -1), key); + } + + /** + * Create a signature suitable for inserting into scriptSigs/witnesses. + * @param {Number} index - Index of input being signed. + * @param {Script} prev - Previous output script or redeem script + * (in the case of witnesspubkeyhash, this should be the generated + * p2pkh script). + * @param {Amount} value - Previous output value. + * @param {Buffer} key + * @param {SighashType} type + * @param {Number} version - Sighash version (0=legacy, 1=segwit). + * @returns {Buffer} Signature in DER format. + */ + + signature(index, prev, value, key, type, version) { + if (type == null) + type = hashType.ALL; + + if (version == null) + version = 0; + + const hash = this.signatureHash(index, prev, value, type, version); + const sig = secp256k1.sign(hash, key); + const bw = new StaticWriter(sig.length + 1); + + bw.writeBytes(sig); + bw.writeU8(type); + + return bw.render(); + } + + /** + * Verify all transaction inputs. + * @param {CoinView} view + * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] + * @throws {ScriptError} on invalid inputs + */ + + check(view, flags) { + if (this.inputs.length === 0) + throw new ScriptError('UNKNOWN_ERROR', 'No inputs.'); + + if (this.isCoinbase()) + return; + + for (let i = 0; i < this.inputs.length; i++) { + const {prevout} = this.inputs[i]; + const coin = view.getOutput(prevout); + + if (!coin) + throw new ScriptError('UNKNOWN_ERROR', 'No coin available.'); + + this.checkInput(i, coin, flags); } } - const size = 156 + prev.getVarSize(); - const bw = StaticWriter.pool(size); + /** + * Verify a transaction input. + * @param {Number} index - Index of output being + * verified. + * @param {Coin|Output} coin - Previous output. + * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] + * @throws {ScriptError} on invalid input + */ - bw.writeU32(this.version); - bw.writeBytes(prevouts); - bw.writeBytes(sequences); - bw.writeHash(input.prevout.hash); - bw.writeU32(input.prevout.index); - bw.writeVarBytes(prev.toRaw()); - bw.writeI64(value); - bw.writeU32(input.sequence); - bw.writeBytes(outputs); - bw.writeU32(this.locktime); - bw.writeU32(type); + checkInput(index, coin, flags) { + const input = this.inputs[index]; - return hash256.digest(bw.render()); -}; + assert(input, 'Input does not exist.'); + assert(coin, 'No coin passed.'); -/** - * Verify signature. - * @param {Number} index - * @param {Script} prev - * @param {Amount} value - * @param {Buffer} sig - * @param {Buffer} key - * @param {Number} version - * @returns {Boolean} - */ + Script.verify( + input.script, + input.witness, + coin.script, + this, + index, + coin.value, + flags + ); + } + + /** + * Verify the transaction inputs on the worker pool + * (if workers are enabled). + * @param {CoinView} view + * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] + * @param {WorkerPool?} pool + * @returns {Promise} + */ + + async checkAsync(view, flags, pool) { + if (this.inputs.length === 0) + throw new ScriptError('UNKNOWN_ERROR', 'No inputs.'); + + if (this.isCoinbase()) + return; + + if (!pool) { + this.check(view, flags); + return; + } + + await pool.check(this, view, flags); + } + + /** + * Verify a transaction input asynchronously. + * @param {Number} index - Index of output being + * verified. + * @param {Coin|Output} coin - Previous output. + * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] + * @param {WorkerPool?} pool + * @returns {Promise} + */ + + async checkInputAsync(index, coin, flags, pool) { + const input = this.inputs[index]; + + assert(input, 'Input does not exist.'); + assert(coin, 'No coin passed.'); + + if (!pool) { + this.checkInput(index, coin, flags); + return; + } + + await pool.checkInput(this, index, coin, flags); + } + + /** + * Verify all transaction inputs. + * @param {CoinView} view + * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] + * @returns {Boolean} Whether the inputs are valid. + */ + + verify(view, flags) { + try { + this.check(view, flags); + } catch (e) { + if (e.type === 'ScriptError') + return false; + throw e; + } + return true; + } + + /** + * Verify a transaction input. + * @param {Number} index - Index of output being + * verified. + * @param {Coin|Output} coin - Previous output. + * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] + * @returns {Boolean} Whether the input is valid. + */ + + verifyInput(index, coin, flags) { + try { + this.checkInput(index, coin, flags); + } catch (e) { + if (e.type === 'ScriptError') + return false; + throw e; + } + return true; + } + + /** + * Verify the transaction inputs on the worker pool + * (if workers are enabled). + * @param {CoinView} view + * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] + * @param {WorkerPool?} pool + * @returns {Promise} + */ + + async verifyAsync(view, flags, pool) { + try { + await this.checkAsync(view, flags, pool); + } catch (e) { + if (e.type === 'ScriptError') + return false; + throw e; + } + return true; + } + + /** + * Verify a transaction input asynchronously. + * @param {Number} index - Index of output being + * verified. + * @param {Coin|Output} coin - Previous output. + * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] + * @param {WorkerPool?} pool + * @returns {Promise} + */ + + async verifyInputAsync(index, coin, flags, pool) { + try { + await this.checkInput(index, coin, flags, pool); + } catch (e) { + if (e.type === 'ScriptError') + return false; + throw e; + } + return true; + } + + /** + * Test whether the transaction is a coinbase + * by examining the inputs. + * @returns {Boolean} + */ + + isCoinbase() { + return this.inputs.length === 1 && this.inputs[0].prevout.isNull(); + } + + /** + * Test whether the transaction is replaceable. + * @returns {Boolean} + */ + + isRBF() { + // Core doesn't do this, but it should: + if (this.version === 2) + return false; + + for (const input of this.inputs) { + if (input.isRBF()) + return true; + } -TX.prototype.checksig = function checksig(index, prev, value, sig, key, version) { - if (sig.length === 0) return false; - - const type = sig[sig.length - 1]; - const hash = this.signatureHash(index, prev, value, type, version); - - return secp256k1.verify(hash, sig.slice(0, -1), key); -}; - -/** - * Create a signature suitable for inserting into scriptSigs/witnesses. - * @param {Number} index - Index of input being signed. - * @param {Script} prev - Previous output script or redeem script - * (in the case of witnesspubkeyhash, this should be the generated - * p2pkh script). - * @param {Amount} value - Previous output value. - * @param {Buffer} key - * @param {SighashType} type - * @param {Number} version - Sighash version (0=legacy, 1=segwit). - * @returns {Buffer} Signature in DER format. - */ - -TX.prototype.signature = function signature(index, prev, value, key, type, version) { - if (type == null) - type = hashType.ALL; - - if (version == null) - version = 0; - - const hash = this.signatureHash(index, prev, value, type, version); - const sig = secp256k1.sign(hash, key); - const bw = new StaticWriter(sig.length + 1); - - bw.writeBytes(sig); - bw.writeU8(type); - - return bw.render(); -}; - -/** - * Verify all transaction inputs. - * @param {CoinView} view - * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] - * @throws {ScriptError} on invalid inputs - */ - -TX.prototype.check = function check(view, flags) { - if (this.inputs.length === 0) - throw new ScriptError('UNKNOWN_ERROR', 'No inputs.'); - - if (this.isCoinbase()) - return; - - for (let i = 0; i < this.inputs.length; i++) { - const {prevout} = this.inputs[i]; - const coin = view.getOutput(prevout); - - if (!coin) - throw new ScriptError('UNKNOWN_ERROR', 'No coin available.'); - - this.checkInput(i, coin, flags); - } -}; - -/** - * Verify a transaction input. - * @param {Number} index - Index of output being - * verified. - * @param {Coin|Output} coin - Previous output. - * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] - * @throws {ScriptError} on invalid input - */ - -TX.prototype.checkInput = function checkInput(index, coin, flags) { - const input = this.inputs[index]; - - assert(input, 'Input does not exist.'); - assert(coin, 'No coin passed.'); - - Script.verify( - input.script, - input.witness, - coin.script, - this, - index, - coin.value, - flags - ); -}; - -/** - * Verify the transaction inputs on the worker pool - * (if workers are enabled). - * @param {CoinView} view - * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] - * @param {WorkerPool?} pool - * @returns {Promise} - */ - -TX.prototype.checkAsync = async function checkAsync(view, flags, pool) { - if (this.inputs.length === 0) - throw new ScriptError('UNKNOWN_ERROR', 'No inputs.'); - - if (this.isCoinbase()) - return; - - if (!pool) { - this.check(view, flags); - return; } - await pool.check(this, view, flags); -}; + /** + * Calculate the fee for the transaction. + * @param {CoinView} view + * @returns {Amount} fee (zero if not all coins are available). + */ -/** - * Verify a transaction input asynchronously. - * @param {Number} index - Index of output being - * verified. - * @param {Coin|Output} coin - Previous output. - * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] - * @param {WorkerPool?} pool - * @returns {Promise} - */ - -TX.prototype.checkInputAsync = async function checkInputAsync(index, coin, flags, pool) { - const input = this.inputs[index]; - - assert(input, 'Input does not exist.'); - assert(coin, 'No coin passed.'); - - if (!pool) { - this.checkInput(index, coin, flags); - return; - } - - await pool.checkInput(this, index, coin, flags); -}; - -/** - * Verify all transaction inputs. - * @param {CoinView} view - * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] - * @returns {Boolean} Whether the inputs are valid. - */ - -TX.prototype.verify = function verify(view, flags) { - try { - this.check(view, flags); - } catch (e) { - if (e.type === 'ScriptError') - return false; - throw e; - } - return true; -}; - -/** - * Verify a transaction input. - * @param {Number} index - Index of output being - * verified. - * @param {Coin|Output} coin - Previous output. - * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] - * @returns {Boolean} Whether the input is valid. - */ - -TX.prototype.verifyInput = function verifyInput(index, coin, flags) { - try { - this.checkInput(index, coin, flags); - } catch (e) { - if (e.type === 'ScriptError') - return false; - throw e; - } - return true; -}; - -/** - * Verify the transaction inputs on the worker pool - * (if workers are enabled). - * @param {CoinView} view - * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] - * @param {WorkerPool?} pool - * @returns {Promise} - */ - -TX.prototype.verifyAsync = async function verifyAsync(view, flags, pool) { - try { - await this.checkAsync(view, flags, pool); - } catch (e) { - if (e.type === 'ScriptError') - return false; - throw e; - } - return true; -}; - -/** - * Verify a transaction input asynchronously. - * @param {Number} index - Index of output being - * verified. - * @param {Coin|Output} coin - Previous output. - * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] - * @param {WorkerPool?} pool - * @returns {Promise} - */ - -TX.prototype.verifyInputAsync = async function verifyInputAsync(index, coin, flags, pool) { - try { - await this.checkInput(index, coin, flags, pool); - } catch (e) { - if (e.type === 'ScriptError') - return false; - throw e; - } - return true; -}; - -/** - * Test whether the transaction is a coinbase - * by examining the inputs. - * @returns {Boolean} - */ - -TX.prototype.isCoinbase = function isCoinbase() { - return this.inputs.length === 1 && this.inputs[0].prevout.isNull(); -}; - -/** - * Test whether the transaction is replaceable. - * @returns {Boolean} - */ - -TX.prototype.isRBF = function isRBF() { - // Core doesn't do this, but it should: - if (this.version === 2) - return false; - - for (const input of this.inputs) { - if (input.isRBF()) - return true; - } - - return false; -}; - -/** - * Calculate the fee for the transaction. - * @param {CoinView} view - * @returns {Amount} fee (zero if not all coins are available). - */ - -TX.prototype.getFee = function getFee(view) { - if (!this.hasCoins(view)) - return 0; - - return this.getInputValue(view) - this.getOutputValue(); -}; - -/** - * Calculate the total input value. - * @param {CoinView} view - * @returns {Amount} value - */ - -TX.prototype.getInputValue = function getInputValue(view) { - let total = 0; - - for (const {prevout} of this.inputs) { - const coin = view.getOutput(prevout); - - if (!coin) + getFee(view) { + if (!this.hasCoins(view)) return 0; - total += coin.value; + return this.getInputValue(view) - this.getOutputValue(); } - return total; -}; + /** + * Calculate the total input value. + * @param {CoinView} view + * @returns {Amount} value + */ -/** - * Calculate the total output value. - * @returns {Amount} value - */ + getInputValue(view) { + let total = 0; -TX.prototype.getOutputValue = function getOutputValue() { - let total = 0; + for (const {prevout} of this.inputs) { + const coin = view.getOutput(prevout); - for (const output of this.outputs) - total += output.value; + if (!coin) + return 0; - return total; -}; - -/** - * Get all input addresses. - * @private - * @param {CoinView} view - * @returns {Array} [addrs, table] - */ - -TX.prototype._getInputAddresses = function _getInputAddresses(view) { - const table = Object.create(null); - const addrs = []; - - if (this.isCoinbase()) - return [addrs, table]; - - for (const input of this.inputs) { - const coin = view ? view.getOutputFor(input) : null; - const addr = input.getAddress(coin); - - if (!addr) - continue; - - const hash = addr.getHash('hex'); - - if (!table[hash]) { - table[hash] = true; - addrs.push(addr); + total += coin.value; } + + return total; } - return [addrs, table]; -}; + /** + * Calculate the total output value. + * @returns {Amount} value + */ -/** - * Get all output addresses. - * @private - * @returns {Array} [addrs, table] - */ + getOutputValue() { + let total = 0; -TX.prototype._getOutputAddresses = function _getOutputAddresses() { - const table = Object.create(null); - const addrs = []; + for (const output of this.outputs) + total += output.value; - for (const output of this.outputs) { - const addr = output.getAddress(); - - if (!addr) - continue; - - const hash = addr.getHash('hex'); - - if (!table[hash]) { - table[hash] = true; - addrs.push(addr); - } + return total; } - return [addrs, table]; -}; + /** + * Get all input addresses. + * @private + * @param {CoinView} view + * @returns {Array} [addrs, table] + */ -/** - * Get all addresses. - * @private - * @param {CoinView} view - * @returns {Array} [addrs, table] - */ + _getInputAddresses(view) { + const table = Object.create(null); + const addrs = []; -TX.prototype._getAddresses = function _getAddresses(view) { - const [addrs, table] = this._getInputAddresses(view); - const output = this.getOutputAddresses(); + if (this.isCoinbase()) + return [addrs, table]; - for (const addr of output) { - const hash = addr.getHash('hex'); - - if (!table[hash]) { - table[hash] = true; - addrs.push(addr); - } - } - - return [addrs, table]; -}; - -/** - * Get all input addresses. - * @param {CoinView|null} view - * @returns {Address[]} addresses - */ - -TX.prototype.getInputAddresses = function getInputAddresses(view) { - const [addrs] = this._getInputAddresses(view); - return addrs; -}; - -/** - * Get all output addresses. - * @returns {Address[]} addresses - */ - -TX.prototype.getOutputAddresses = function getOutputAddresses() { - const [addrs] = this._getOutputAddresses(); - return addrs; -}; - -/** - * Get all addresses. - * @param {CoinView|null} view - * @returns {Address[]} addresses - */ - -TX.prototype.getAddresses = function getAddresses(view) { - const [addrs] = this._getAddresses(view); - return addrs; -}; - -/** - * Get all input address hashes. - * @param {CoinView|null} view - * @returns {Hash[]} hashes - */ - -TX.prototype.getInputHashes = function getInputHashes(view, enc) { - if (enc === 'hex') { - const [, table] = this._getInputAddresses(view); - return Object.keys(table); - } - - const addrs = this.getInputAddresses(view); - const hashes = []; - - for (const addr of addrs) - hashes.push(addr.getHash()); - - return hashes; -}; - -/** - * Get all output address hashes. - * @returns {Hash[]} hashes - */ - -TX.prototype.getOutputHashes = function getOutputHashes(enc) { - if (enc === 'hex') { - const [, table] = this._getOutputAddresses(); - return Object.keys(table); - } - - const addrs = this.getOutputAddresses(); - const hashes = []; - - for (const addr of addrs) - hashes.push(addr.getHash()); - - return hashes; -}; - -/** - * Get all address hashes. - * @param {CoinView|null} view - * @returns {Hash[]} hashes - */ - -TX.prototype.getHashes = function getHashes(view, enc) { - if (enc === 'hex') { - const [, table] = this._getAddresses(view); - return Object.keys(table); - } - - const addrs = this.getAddresses(view); - const hashes = []; - - for (const addr of addrs) - hashes.push(addr.getHash()); - - return hashes; -}; - -/** - * Test whether the transaction has - * all coins available. - * @param {CoinView} view - * @returns {Boolean} - */ - -TX.prototype.hasCoins = function hasCoins(view) { - if (this.inputs.length === 0) - return false; - - for (const {prevout} of this.inputs) { - if (!view.hasEntry(prevout)) - return false; - } - - return true; -}; - -/** - * Check finality of transaction by examining - * nLocktime and nSequence values. - * @example - * tx.isFinal(chain.height + 1, network.now()); - * @param {Number} height - Height at which to test. This - * is usually the chain height, or the chain height + 1 - * when the transaction entered the mempool. - * @param {Number} time - Time at which to test. This is - * usually the chain tip's parent's median time, or the - * time at which the transaction entered the mempool. If - * MEDIAN_TIME_PAST is enabled this will be the median - * time of the chain tip's previous entry's median time. - * @returns {Boolean} - */ - -TX.prototype.isFinal = function isFinal(height, time) { - const THRESHOLD = consensus.LOCKTIME_THRESHOLD; - - if (this.locktime === 0) - return true; - - if (this.locktime < (this.locktime < THRESHOLD ? height : time)) - return true; - - for (const input of this.inputs) { - if (input.sequence !== 0xffffffff) - return false; - } - - return true; -}; - -/** - * Verify the absolute locktime of a transaction. - * Called by OP_CHECKLOCKTIMEVERIFY. - * @param {Number} index - Index of input being verified. - * @param {Number} predicate - Locktime to verify against. - * @returns {Boolean} - */ - -TX.prototype.verifyLocktime = function verifyLocktime(index, predicate) { - const THRESHOLD = consensus.LOCKTIME_THRESHOLD; - const input = this.inputs[index]; - - assert(input, 'Input does not exist.'); - assert(predicate >= 0, 'Locktime must be non-negative.'); - - // Locktimes must be of the same type (blocks or seconds). - if ((this.locktime < THRESHOLD) !== (predicate < THRESHOLD)) - return false; - - if (predicate > this.locktime) - return false; - - if (input.sequence === 0xffffffff) - return false; - - return true; -}; - -/** - * Verify the relative locktime of an input. - * Called by OP_CHECKSEQUENCEVERIFY. - * @param {Number} index - Index of input being verified. - * @param {Number} predicate - Relative locktime to verify against. - * @returns {Boolean} - */ - -TX.prototype.verifySequence = function verifySequence(index, predicate) { - const DISABLE_FLAG = consensus.SEQUENCE_DISABLE_FLAG; - const TYPE_FLAG = consensus.SEQUENCE_TYPE_FLAG; - const MASK = consensus.SEQUENCE_MASK; - const input = this.inputs[index]; - - assert(input, 'Input does not exist.'); - assert(predicate >= 0, 'Locktime must be non-negative.'); - - // For future softfork capability. - if (predicate & DISABLE_FLAG) - return true; - - // Version must be >=2. - if (this.version < 2) - return false; - - // Cannot use the disable flag without - // the predicate also having the disable - // flag (for future softfork capability). - if (input.sequence & DISABLE_FLAG) - return false; - - // Locktimes must be of the same type (blocks or seconds). - if ((input.sequence & TYPE_FLAG) !== (predicate & TYPE_FLAG)) - return false; - - if ((predicate & MASK) > (input.sequence & MASK)) - return false; - - return true; -}; - -/** - * Calculate legacy (inaccurate) sigop count. - * @returns {Number} sigop count - */ - -TX.prototype.getLegacySigops = function getLegacySigops() { - if (this._sigops !== -1) - return this._sigops; - - let total = 0; - - for (const input of this.inputs) - total += input.script.getSigops(false); - - for (const output of this.outputs) - total += output.script.getSigops(false); - - if (!this.mutable) - this._sigops = total; - - return total; -}; - -/** - * Calculate accurate sigop count, taking into account redeem scripts. - * @param {CoinView} view - * @returns {Number} sigop count - */ - -TX.prototype.getScripthashSigops = function getScripthashSigops(view) { - if (this.isCoinbase()) - return 0; - - let total = 0; - - for (const input of this.inputs) { - const coin = view.getOutputFor(input); - - if (!coin) - continue; - - if (!coin.script.isScripthash()) - continue; - - total += coin.script.getScripthashSigops(input.script); - } - - return total; -}; - -/** - * Calculate accurate sigop count, taking into account redeem scripts. - * @param {CoinView} view - * @returns {Number} sigop count - */ - -TX.prototype.getWitnessSigops = function getWitnessSigops(view) { - if (this.isCoinbase()) - return 0; - - let total = 0; - - for (const input of this.inputs) { - const coin = view.getOutputFor(input); - - if (!coin) - continue; - - total += coin.script.getWitnessSigops(input.script, input.witness); - } - - return total; -}; - -/** - * Calculate sigops cost, taking into account witness programs. - * @param {CoinView} view - * @param {VerifyFlags?} flags - * @returns {Number} sigop weight - */ - -TX.prototype.getSigopsCost = function getSigopsCost(view, flags) { - if (flags == null) - flags = Script.flags.STANDARD_VERIFY_FLAGS; - - const scale = consensus.WITNESS_SCALE_FACTOR; - - let cost = this.getLegacySigops() * scale; - - if (flags & Script.flags.VERIFY_P2SH) - cost += this.getScripthashSigops(view) * scale; - - if (flags & Script.flags.VERIFY_WITNESS) - cost += this.getWitnessSigops(view); - - return cost; -}; - -/** - * Calculate virtual sigop count. - * @param {CoinView} view - * @param {VerifyFlags?} flags - * @returns {Number} sigop count - */ - -TX.prototype.getSigops = function getSigops(view, flags) { - const scale = consensus.WITNESS_SCALE_FACTOR; - return (this.getSigopsCost(view, flags) + scale - 1) / scale | 0; -}; - -/** - * Non-contextual sanity checks for the transaction. - * Will mostly verify coin and output values. - * @see CheckTransaction() - * @returns {Array} [result, reason, score] - */ - -TX.prototype.isSane = function isSane() { - const [valid] = this.checkSanity(); - return valid; -}; - -/** - * Non-contextual sanity checks for the transaction. - * Will mostly verify coin and output values. - * @see CheckTransaction() - * @returns {Array} [valid, reason, score] - */ - -TX.prototype.checkSanity = function checkSanity() { - if (this.inputs.length === 0) - return [false, 'bad-txns-vin-empty', 100]; - - if (this.outputs.length === 0) - return [false, 'bad-txns-vout-empty', 100]; - - if (this.getBaseSize() > consensus.MAX_BLOCK_SIZE) - return [false, 'bad-txns-oversize', 100]; - - let total = 0; - - for (const output of this.outputs) { - if (output.value < 0) - return [false, 'bad-txns-vout-negative', 100]; - - if (output.value > consensus.MAX_MONEY) - return [false, 'bad-txns-vout-toolarge', 100]; - - total += output.value; - - if (total < 0 || total > consensus.MAX_MONEY) - return [false, 'bad-txns-txouttotal-toolarge', 100]; - } - - const prevout = new Set(); - - for (const input of this.inputs) { - const key = input.prevout.toKey(); - - if (prevout.has(key)) - return [false, 'bad-txns-inputs-duplicate', 100]; - - prevout.add(key); - } - - if (this.isCoinbase()) { - const size = this.inputs[0].script.getSize(); - if (size < 2 || size > 100) - return [false, 'bad-cb-length', 100]; - } else { for (const input of this.inputs) { - if (input.prevout.isNull()) - return [false, 'bad-txns-prevout-null', 10]; - } - } + const coin = view ? view.getOutputFor(input) : null; + const addr = input.getAddress(coin); - return [true, 'valid', 0]; -}; + if (!addr) + continue; -/** - * Non-contextual checks to determine whether the - * transaction has all standard output script - * types and standard input script size with only - * pushdatas in the code. - * Will mostly verify coin and output values. - * @see IsStandardTx() - * @returns {Array} [valid, reason, score] - */ + const hash = addr.getHash('hex'); -TX.prototype.isStandard = function isStandard() { - const [valid] = this.checkStandard(); - return valid; -}; - -/** - * Non-contextual checks to determine whether the - * transaction has all standard output script - * types and standard input script size with only - * pushdatas in the code. - * Will mostly verify coin and output values. - * @see IsStandardTx() - * @returns {Array} [valid, reason, score] - */ - -TX.prototype.checkStandard = function checkStandard() { - if (this.version < 1 || this.version > policy.MAX_TX_VERSION) - return [false, 'version', 0]; - - if (this.getWeight() >= policy.MAX_TX_WEIGHT) - return [false, 'tx-size', 0]; - - for (const input of this.inputs) { - if (input.script.getSize() > 1650) - return [false, 'scriptsig-size', 0]; - - if (!input.script.isPushOnly()) - return [false, 'scriptsig-not-pushonly', 0]; - } - - let nulldata = 0; - - for (const output of this.outputs) { - if (!output.script.isStandard()) - return [false, 'scriptpubkey', 0]; - - if (output.script.isNulldata()) { - nulldata++; - continue; + if (!table[hash]) { + table[hash] = true; + addrs.push(addr); + } } - if (output.script.isMultisig() && !policy.BARE_MULTISIG) - return [false, 'bare-multisig', 0]; - - if (output.isDust(policy.MIN_RELAY)) - return [false, 'dust', 0]; + return [addrs, table]; } - if (nulldata > 1) - return [false, 'multi-op-return', 0]; + /** + * Get all output addresses. + * @private + * @returns {Array} [addrs, table] + */ - return [true, 'valid', 0]; -}; + _getOutputAddresses() { + const table = Object.create(null); + const addrs = []; -/** - * Perform contextual checks to verify coin and input - * script standardness (including the redeem script). - * @see AreInputsStandard() - * @param {CoinView} view - * @param {VerifyFlags?} flags - * @returns {Boolean} - */ + for (const output of this.outputs) { + const addr = output.getAddress(); + + if (!addr) + continue; + + const hash = addr.getHash('hex'); + + if (!table[hash]) { + table[hash] = true; + addrs.push(addr); + } + } + + return [addrs, table]; + } + + /** + * Get all addresses. + * @private + * @param {CoinView} view + * @returns {Array} [addrs, table] + */ + + _getAddresses(view) { + const [addrs, table] = this._getInputAddresses(view); + const output = this.getOutputAddresses(); + + for (const addr of output) { + const hash = addr.getHash('hex'); + + if (!table[hash]) { + table[hash] = true; + addrs.push(addr); + } + } + + return [addrs, table]; + } + + /** + * Get all input addresses. + * @param {CoinView|null} view + * @returns {Address[]} addresses + */ + + getInputAddresses(view) { + const [addrs] = this._getInputAddresses(view); + return addrs; + } + + /** + * Get all output addresses. + * @returns {Address[]} addresses + */ + + getOutputAddresses() { + const [addrs] = this._getOutputAddresses(); + return addrs; + } + + /** + * Get all addresses. + * @param {CoinView|null} view + * @returns {Address[]} addresses + */ + + getAddresses(view) { + const [addrs] = this._getAddresses(view); + return addrs; + } + + /** + * Get all input address hashes. + * @param {CoinView|null} view + * @returns {Hash[]} hashes + */ + + getInputHashes(view, enc) { + if (enc === 'hex') { + const [, table] = this._getInputAddresses(view); + return Object.keys(table); + } + + const addrs = this.getInputAddresses(view); + const hashes = []; + + for (const addr of addrs) + hashes.push(addr.getHash()); + + return hashes; + } + + /** + * Get all output address hashes. + * @returns {Hash[]} hashes + */ + + getOutputHashes(enc) { + if (enc === 'hex') { + const [, table] = this._getOutputAddresses(); + return Object.keys(table); + } + + const addrs = this.getOutputAddresses(); + const hashes = []; + + for (const addr of addrs) + hashes.push(addr.getHash()); + + return hashes; + } + + /** + * Get all address hashes. + * @param {CoinView|null} view + * @returns {Hash[]} hashes + */ + + getHashes(view, enc) { + if (enc === 'hex') { + const [, table] = this._getAddresses(view); + return Object.keys(table); + } + + const addrs = this.getAddresses(view); + const hashes = []; + + for (const addr of addrs) + hashes.push(addr.getHash()); + + return hashes; + } + + /** + * Test whether the transaction has + * all coins available. + * @param {CoinView} view + * @returns {Boolean} + */ + + hasCoins(view) { + if (this.inputs.length === 0) + return false; + + for (const {prevout} of this.inputs) { + if (!view.hasEntry(prevout)) + return false; + } -TX.prototype.hasStandardInputs = function hasStandardInputs(view) { - if (this.isCoinbase()) return true; - - for (const input of this.inputs) { - const coin = view.getOutputFor(input); - - if (!coin) - return false; - - if (coin.script.isPubkeyhash()) - continue; - - if (coin.script.isScripthash()) { - const redeem = input.script.getRedeem(); - - if (!redeem) - return false; - - if (redeem.getSigops(true) > policy.MAX_P2SH_SIGOPS) - return false; - - continue; - } - - if (coin.script.isUnknown()) - return false; } - return true; -}; + /** + * Check finality of transaction by examining + * nLocktime and nSequence values. + * @example + * tx.isFinal(chain.height + 1, network.now()); + * @param {Number} height - Height at which to test. This + * is usually the chain height, or the chain height + 1 + * when the transaction entered the mempool. + * @param {Number} time - Time at which to test. This is + * usually the chain tip's parent's median time, or the + * time at which the transaction entered the mempool. If + * MEDIAN_TIME_PAST is enabled this will be the median + * time of the chain tip's previous entry's median time. + * @returns {Boolean} + */ -/** - * Perform contextual checks to verify coin and witness standardness. - * @see IsBadWitness() - * @param {CoinView} view - * @returns {Boolean} - */ + isFinal(height, time) { + const THRESHOLD = consensus.LOCKTIME_THRESHOLD; -TX.prototype.hasStandardWitness = function hasStandardWitness(view) { - if (this.isCoinbase()) - return true; + if (this.locktime === 0) + return true; - for (const input of this.inputs) { - const witness = input.witness; - const coin = view.getOutputFor(input); + if (this.locktime < (this.locktime < THRESHOLD ? height : time)) + return true; - if (!coin) - continue; - - if (witness.items.length === 0) - continue; - - let prev = coin.script; - - if (prev.isScripthash()) { - prev = input.script.getRedeem(); - if (!prev) + for (const input of this.inputs) { + if (input.sequence !== 0xffffffff) return false; } - if (!prev.isProgram()) + return true; + } + + /** + * Verify the absolute locktime of a transaction. + * Called by OP_CHECKLOCKTIMEVERIFY. + * @param {Number} index - Index of input being verified. + * @param {Number} predicate - Locktime to verify against. + * @returns {Boolean} + */ + + verifyLocktime(index, predicate) { + const THRESHOLD = consensus.LOCKTIME_THRESHOLD; + const input = this.inputs[index]; + + assert(input, 'Input does not exist.'); + assert(predicate >= 0, 'Locktime must be non-negative.'); + + // Locktimes must be of the same type (blocks or seconds). + if ((this.locktime < THRESHOLD) !== (predicate < THRESHOLD)) return false; - if (prev.isWitnessPubkeyhash()) { - if (witness.items.length !== 2) - return false; + if (predicate > this.locktime) + return false; - if (witness.items[0].length > 73) - return false; + if (input.sequence === 0xffffffff) + return false; - if (witness.items[1].length > 65) - return false; + return true; + } - continue; + /** + * Verify the relative locktime of an input. + * Called by OP_CHECKSEQUENCEVERIFY. + * @param {Number} index - Index of input being verified. + * @param {Number} predicate - Relative locktime to verify against. + * @returns {Boolean} + */ + + verifySequence(index, predicate) { + const DISABLE_FLAG = consensus.SEQUENCE_DISABLE_FLAG; + const TYPE_FLAG = consensus.SEQUENCE_TYPE_FLAG; + const MASK = consensus.SEQUENCE_MASK; + const input = this.inputs[index]; + + assert(input, 'Input does not exist.'); + assert(predicate >= 0, 'Locktime must be non-negative.'); + + // For future softfork capability. + if (predicate & DISABLE_FLAG) + return true; + + // Version must be >=2. + if (this.version < 2) + return false; + + // Cannot use the disable flag without + // the predicate also having the disable + // flag (for future softfork capability). + if (input.sequence & DISABLE_FLAG) + return false; + + // Locktimes must be of the same type (blocks or seconds). + if ((input.sequence & TYPE_FLAG) !== (predicate & TYPE_FLAG)) + return false; + + if ((predicate & MASK) > (input.sequence & MASK)) + return false; + + return true; + } + + /** + * Calculate legacy (inaccurate) sigop count. + * @returns {Number} sigop count + */ + + getLegacySigops() { + if (this._sigops !== -1) + return this._sigops; + + let total = 0; + + for (const input of this.inputs) + total += input.script.getSigops(false); + + for (const output of this.outputs) + total += output.script.getSigops(false); + + if (!this.mutable) + this._sigops = total; + + return total; + } + + /** + * Calculate accurate sigop count, taking into account redeem scripts. + * @param {CoinView} view + * @returns {Number} sigop count + */ + + getScripthashSigops(view) { + if (this.isCoinbase()) + return 0; + + let total = 0; + + for (const input of this.inputs) { + const coin = view.getOutputFor(input); + + if (!coin) + continue; + + if (!coin.script.isScripthash()) + continue; + + total += coin.script.getScripthashSigops(input.script); } - if (prev.isWitnessScripthash()) { - if (witness.items.length - 1 > policy.MAX_P2WSH_STACK) - return false; + return total; + } - for (let i = 0; i < witness.items.length - 1; i++) { - const item = witness.items[i]; - if (item.length > policy.MAX_P2WSH_PUSH) - return false; + /** + * Calculate accurate sigop count, taking into account redeem scripts. + * @param {CoinView} view + * @returns {Number} sigop count + */ + + getWitnessSigops(view) { + if (this.isCoinbase()) + return 0; + + let total = 0; + + for (const input of this.inputs) { + const coin = view.getOutputFor(input); + + if (!coin) + continue; + + total += coin.script.getWitnessSigops(input.script, input.witness); + } + + return total; + } + + /** + * Calculate sigops cost, taking into account witness programs. + * @param {CoinView} view + * @param {VerifyFlags?} flags + * @returns {Number} sigop weight + */ + + getSigopsCost(view, flags) { + if (flags == null) + flags = Script.flags.STANDARD_VERIFY_FLAGS; + + const scale = consensus.WITNESS_SCALE_FACTOR; + + let cost = this.getLegacySigops() * scale; + + if (flags & Script.flags.VERIFY_P2SH) + cost += this.getScripthashSigops(view) * scale; + + if (flags & Script.flags.VERIFY_WITNESS) + cost += this.getWitnessSigops(view); + + return cost; + } + + /** + * Calculate virtual sigop count. + * @param {CoinView} view + * @param {VerifyFlags?} flags + * @returns {Number} sigop count + */ + + getSigops(view, flags) { + const scale = consensus.WITNESS_SCALE_FACTOR; + return (this.getSigopsCost(view, flags) + scale - 1) / scale | 0; + } + + /** + * Non-contextual sanity checks for the transaction. + * Will mostly verify coin and output values. + * @see CheckTransaction() + * @returns {Array} [result, reason, score] + */ + + isSane() { + const [valid] = this.checkSanity(); + return valid; + } + + /** + * Non-contextual sanity checks for the transaction. + * Will mostly verify coin and output values. + * @see CheckTransaction() + * @returns {Array} [valid, reason, score] + */ + + checkSanity() { + if (this.inputs.length === 0) + return [false, 'bad-txns-vin-empty', 100]; + + if (this.outputs.length === 0) + return [false, 'bad-txns-vout-empty', 100]; + + if (this.getBaseSize() > consensus.MAX_BLOCK_SIZE) + return [false, 'bad-txns-oversize', 100]; + + let total = 0; + + for (const output of this.outputs) { + if (output.value < 0) + return [false, 'bad-txns-vout-negative', 100]; + + if (output.value > consensus.MAX_MONEY) + return [false, 'bad-txns-vout-toolarge', 100]; + + total += output.value; + + if (total < 0 || total > consensus.MAX_MONEY) + return [false, 'bad-txns-txouttotal-toolarge', 100]; + } + + const prevout = new Set(); + + for (const input of this.inputs) { + const key = input.prevout.toKey(); + + if (prevout.has(key)) + return [false, 'bad-txns-inputs-duplicate', 100]; + + prevout.add(key); + } + + if (this.isCoinbase()) { + const size = this.inputs[0].script.getSize(); + if (size < 2 || size > 100) + return [false, 'bad-cb-length', 100]; + } else { + for (const input of this.inputs) { + if (input.prevout.isNull()) + return [false, 'bad-txns-prevout-null', 10]; + } + } + + return [true, 'valid', 0]; + } + + /** + * Non-contextual checks to determine whether the + * transaction has all standard output script + * types and standard input script size with only + * pushdatas in the code. + * Will mostly verify coin and output values. + * @see IsStandardTx() + * @returns {Array} [valid, reason, score] + */ + + isStandard() { + const [valid] = this.checkStandard(); + return valid; + } + + /** + * Non-contextual checks to determine whether the + * transaction has all standard output script + * types and standard input script size with only + * pushdatas in the code. + * Will mostly verify coin and output values. + * @see IsStandardTx() + * @returns {Array} [valid, reason, score] + */ + + checkStandard() { + if (this.version < 1 || this.version > policy.MAX_TX_VERSION) + return [false, 'version', 0]; + + if (this.getWeight() >= policy.MAX_TX_WEIGHT) + return [false, 'tx-size', 0]; + + for (const input of this.inputs) { + if (input.script.getSize() > 1650) + return [false, 'scriptsig-size', 0]; + + if (!input.script.isPushOnly()) + return [false, 'scriptsig-not-pushonly', 0]; + } + + let nulldata = 0; + + for (const output of this.outputs) { + if (!output.script.isStandard()) + return [false, 'scriptpubkey', 0]; + + if (output.script.isNulldata()) { + nulldata++; + continue; } - const raw = witness.items[witness.items.length - 1]; + if (output.script.isMultisig() && !policy.BARE_MULTISIG) + return [false, 'bare-multisig', 0]; - if (raw.length > policy.MAX_P2WSH_SIZE) + if (output.isDust(policy.MIN_RELAY)) + return [false, 'dust', 0]; + } + + if (nulldata > 1) + return [false, 'multi-op-return', 0]; + + return [true, 'valid', 0]; + } + + /** + * Perform contextual checks to verify coin and input + * script standardness (including the redeem script). + * @see AreInputsStandard() + * @param {CoinView} view + * @param {VerifyFlags?} flags + * @returns {Boolean} + */ + + hasStandardInputs(view) { + if (this.isCoinbase()) + return true; + + for (const input of this.inputs) { + const coin = view.getOutputFor(input); + + if (!coin) return false; - const redeem = Script.fromRaw(raw); + if (coin.script.isPubkeyhash()) + continue; - if (redeem.isPubkey()) { - if (witness.items.length - 1 !== 1) + if (coin.script.isScripthash()) { + const redeem = input.script.getRedeem(); + + if (!redeem) return false; - if (witness.items[0].length > 73) + if (redeem.getSigops(true) > policy.MAX_P2SH_SIGOPS) return false; continue; } - if (redeem.isPubkeyhash()) { - if (input.witness.items.length - 1 !== 2) + if (coin.script.isUnknown()) + return false; + } + + return true; + } + + /** + * Perform contextual checks to verify coin and witness standardness. + * @see IsBadWitness() + * @param {CoinView} view + * @returns {Boolean} + */ + + hasStandardWitness(view) { + if (this.isCoinbase()) + return true; + + for (const input of this.inputs) { + const witness = input.witness; + const coin = view.getOutputFor(input); + + if (!coin) + continue; + + if (witness.items.length === 0) + continue; + + let prev = coin.script; + + if (prev.isScripthash()) { + prev = input.script.getRedeem(); + if (!prev) + return false; + } + + if (!prev.isProgram()) + return false; + + if (prev.isWitnessPubkeyhash()) { + if (witness.items.length !== 2) return false; if (witness.items[0].length > 73) @@ -1663,868 +1622,909 @@ TX.prototype.hasStandardWitness = function hasStandardWitness(view) { continue; } - const [m] = redeem.getMultisig(); - - if (m !== -1) { - if (witness.items.length - 1 !== m + 1) + if (prev.isWitnessScripthash()) { + if (witness.items.length - 1 > policy.MAX_P2WSH_STACK) return false; - if (witness.items[0].length !== 0) - return false; - - for (let i = 1; i < witness.items.length - 1; i++) { + for (let i = 0; i < witness.items.length - 1; i++) { const item = witness.items[i]; - if (item.length > 73) + if (item.length > policy.MAX_P2WSH_PUSH) return false; } - } - continue; - } + const raw = witness.items[witness.items.length - 1]; - if (witness.items.length > policy.MAX_P2WSH_STACK) - return false; + if (raw.length > policy.MAX_P2WSH_SIZE) + return false; - for (const item of witness.items) { - if (item.length > policy.MAX_P2WSH_PUSH) - return false; - } - } + const redeem = Script.fromRaw(raw); - return true; -}; + if (redeem.isPubkey()) { + if (witness.items.length - 1 !== 1) + return false; -/** - * Perform contextual checks to verify input, output, - * and fee values, as well as coinbase spend maturity - * (coinbases can only be spent 100 blocks or more - * after they're created). Note that this function is - * consensus critical. - * @param {CoinView} view - * @param {Number} height - Height at which the - * transaction is being spent. In the mempool this is - * the chain height plus one at the time it entered the pool. - * @returns {Boolean} - */ + if (witness.items[0].length > 73) + return false; -TX.prototype.verifyInputs = function verifyInputs(view, height) { - const [fee] = this.checkInputs(view, height); - return fee !== -1; -}; - -/** - * Perform contextual checks to verify input, output, - * and fee values, as well as coinbase spend maturity - * (coinbases can only be spent 100 blocks or more - * after they're created). Note that this function is - * consensus critical. - * @param {CoinView} view - * @param {Number} height - Height at which the - * transaction is being spent. In the mempool this is - * the chain height plus one at the time it entered the pool. - * @returns {Array} [fee, reason, score] - */ - -TX.prototype.checkInputs = function checkInputs(view, height) { - assert(typeof height === 'number'); - - let total = 0; - - for (const {prevout} of this.inputs) { - const entry = view.getEntry(prevout); - - if (!entry) - return [-1, 'bad-txns-inputs-missingorspent', 0]; - - if (entry.coinbase) { - if (height - entry.height < consensus.COINBASE_MATURITY) - return [-1, 'bad-txns-premature-spend-of-coinbase', 0]; - } - - const coin = view.getOutput(prevout); - assert(coin); - - if (coin.value < 0 || coin.value > consensus.MAX_MONEY) - return [-1, 'bad-txns-inputvalues-outofrange', 100]; - - total += coin.value; - - if (total < 0 || total > consensus.MAX_MONEY) - return [-1, 'bad-txns-inputvalues-outofrange', 100]; - } - - // Overflows already checked in `isSane()`. - const value = this.getOutputValue(); - - if (total < value) - return [-1, 'bad-txns-in-belowout', 100]; - - const fee = total - value; - - if (fee < 0) - return [-1, 'bad-txns-fee-negative', 100]; - - if (fee > consensus.MAX_MONEY) - return [-1, 'bad-txns-fee-outofrange', 100]; - - return [fee, 'valid', 0]; -}; - -/** - * Calculate the modified size of the transaction. This - * is used in the mempool for calculating priority. - * @param {Number?} size - The size to modify. If not present, - * virtual size will be used. - * @returns {Number} Modified size. - */ - -TX.prototype.getModifiedSize = function getModifiedSize(size) { - if (size == null) - size = this.getVirtualSize(); - - for (const input of this.inputs) { - const offset = 41 + Math.min(110, input.script.getSize()); - if (size > offset) - size -= offset; - } - - return size; -}; - -/** - * Calculate the transaction priority. - * @param {CoinView} view - * @param {Number} height - * @param {Number?} size - Size to calculate priority - * based on. If not present, virtual size will be used. - * @returns {Number} - */ - -TX.prototype.getPriority = function getPriority(view, height, size) { - assert(typeof height === 'number', 'Must pass in height.'); - - if (this.isCoinbase()) - return 0; - - if (size == null) - size = this.getVirtualSize(); - - let sum = 0; - - for (const {prevout} of this.inputs) { - const coin = view.getOutput(prevout); - - if (!coin) - continue; - - const coinHeight = view.getHeight(prevout); - - if (coinHeight === -1) - continue; - - if (coinHeight <= height) { - const age = height - coinHeight; - sum += coin.value * age; - } - } - - return Math.floor(sum / size); -}; - -/** - * Calculate the transaction's on-chain value. - * @param {CoinView} view - * @returns {Number} - */ - -TX.prototype.getChainValue = function getChainValue(view) { - if (this.isCoinbase()) - return 0; - - let value = 0; - - for (const {prevout} of this.inputs) { - const coin = view.getOutput(prevout); - - if (!coin) - continue; - - const height = view.getHeight(prevout); - - if (height === -1) - continue; - - value += coin.value; - } - - return value; -}; - -/** - * Determine whether the transaction is above the - * free threshold in priority. A transaction which - * passed this test is most likely relayable - * without a fee. - * @param {CoinView} view - * @param {Number?} height - If not present, tx - * height or network height will be used. - * @param {Number?} size - If not present, modified - * size will be calculated and used. - * @returns {Boolean} - */ - -TX.prototype.isFree = function isFree(view, height, size) { - const priority = this.getPriority(view, height, size); - return priority > policy.FREE_THRESHOLD; -}; - -/** - * Calculate minimum fee in order for the transaction - * to be relayable (not the constant min relay fee). - * @param {Number?} size - If not present, max size - * estimation will be calculated and used. - * @param {Rate?} rate - Rate of satoshi per kB. - * @returns {Amount} fee - */ - -TX.prototype.getMinFee = function getMinFee(size, rate) { - if (size == null) - size = this.getVirtualSize(); - - return policy.getMinFee(size, rate); -}; - -/** - * Calculate the minimum fee in order for the transaction - * to be relayable, but _round to the nearest kilobyte - * when taking into account size. - * @param {Number?} size - If not present, max size - * estimation will be calculated and used. - * @param {Rate?} rate - Rate of satoshi per kB. - * @returns {Amount} fee - */ - -TX.prototype.getRoundFee = function getRoundFee(size, rate) { - if (size == null) - size = this.getVirtualSize(); - - return policy.getRoundFee(size, rate); -}; - -/** - * Calculate the transaction's rate based on size - * and fees. Size will be calculated if not present. - * @param {CoinView} view - * @param {Number?} size - * @returns {Rate} - */ - -TX.prototype.getRate = function getRate(view, size) { - const fee = this.getFee(view); - - if (fee < 0) - return 0; - - if (size == null) - size = this.getVirtualSize(); - - return policy.getRate(size, fee); -}; - -/** - * Get all unique outpoint hashes. - * @returns {Hash[]} Outpoint hashes. - */ - -TX.prototype.getPrevout = function getPrevout() { - if (this.isCoinbase()) - return []; - - const prevout = Object.create(null); - - for (const input of this.inputs) - prevout[input.prevout.hash] = true; - - return Object.keys(prevout); -}; - -/** - * Test a transaction against a bloom filter using - * the BIP37 matching algorithm. Note that this may - * update the filter depending on what the `update` - * value is. - * @see "Filter matching algorithm": - * @see https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki - * @param {BloomFilter} filter - * @returns {Boolean} True if the transaction matched. - */ - -TX.prototype.isWatched = function isWatched(filter) { - let found = false; - - // 1. Test the tx hash - if (filter.test(this.hash())) - found = true; - - // 2. Test data elements in output scripts - // (may need to update filter on match) - for (let i = 0; i < this.outputs.length; i++) { - const output = this.outputs[i]; - // Test the output script - if (output.script.test(filter)) { - if (filter.update === BloomFilter.flags.ALL) { - const prevout = Outpoint.fromTX(this, i); - filter.add(prevout.toRaw()); - } else if (filter.update === BloomFilter.flags.PUBKEY_ONLY) { - if (output.script.isPubkey() || output.script.isMultisig()) { - const prevout = Outpoint.fromTX(this, i); - filter.add(prevout.toRaw()); + continue; } + + if (redeem.isPubkeyhash()) { + if (input.witness.items.length - 1 !== 2) + return false; + + if (witness.items[0].length > 73) + return false; + + if (witness.items[1].length > 65) + return false; + + continue; + } + + const [m] = redeem.getMultisig(); + + if (m !== -1) { + if (witness.items.length - 1 !== m + 1) + return false; + + if (witness.items[0].length !== 0) + return false; + + for (let i = 1; i < witness.items.length - 1; i++) { + const item = witness.items[i]; + if (item.length > 73) + return false; + } + } + + continue; + } + + if (witness.items.length > policy.MAX_P2WSH_STACK) + return false; + + for (const item of witness.items) { + if (item.length > policy.MAX_P2WSH_PUSH) + return false; } - found = true; } + + return true; } - if (found) - return found; + /** + * Perform contextual checks to verify input, output, + * and fee values, as well as coinbase spend maturity + * (coinbases can only be spent 100 blocks or more + * after they're created). Note that this function is + * consensus critical. + * @param {CoinView} view + * @param {Number} height - Height at which the + * transaction is being spent. In the mempool this is + * the chain height plus one at the time it entered the pool. + * @returns {Boolean} + */ - // 3. Test prev_out structure - // 4. Test data elements in input scripts - for (const input of this.inputs) { - const prevout = input.prevout; - - // Test the COutPoint structure - if (filter.test(prevout.toRaw())) - return true; - - // Test the input script - if (input.script.test(filter)) - return true; + verifyInputs(view, height) { + const [fee] = this.checkInputs(view, height); + return fee !== -1; } - // 5. No match - return false; -}; + /** + * Perform contextual checks to verify input, output, + * and fee values, as well as coinbase spend maturity + * (coinbases can only be spent 100 blocks or more + * after they're created). Note that this function is + * consensus critical. + * @param {CoinView} view + * @param {Number} height - Height at which the + * transaction is being spent. In the mempool this is + * the chain height plus one at the time it entered the pool. + * @returns {Array} [fee, reason, score] + */ -/** - * Get little-endian tx hash. - * @returns {Hash} - */ + checkInputs(view, height) { + assert(typeof height === 'number'); -TX.prototype.rhash = function rhash() { - return encoding.revHex(this.hash('hex')); -}; + let total = 0; -/** - * Get little-endian wtx hash. - * @returns {Hash} - */ + for (const {prevout} of this.inputs) { + const entry = view.getEntry(prevout); -TX.prototype.rwhash = function rwhash() { - return encoding.revHex(this.witnessHash('hex')); -}; + if (!entry) + return [-1, 'bad-txns-inputs-missingorspent', 0]; -/** - * Get little-endian tx hash. - * @returns {Hash} - */ + if (entry.coinbase) { + if (height - entry.height < consensus.COINBASE_MATURITY) + return [-1, 'bad-txns-premature-spend-of-coinbase', 0]; + } -TX.prototype.txid = function txid() { - return this.rhash(); -}; + const coin = view.getOutput(prevout); + assert(coin); -/** - * Get little-endian wtx hash. - * @returns {Hash} - */ + if (coin.value < 0 || coin.value > consensus.MAX_MONEY) + return [-1, 'bad-txns-inputvalues-outofrange', 100]; -TX.prototype.wtxid = function wtxid() { - return this.rwhash(); -}; + total += coin.value; -/** - * Convert the tx to an inv item. - * @returns {InvItem} - */ + if (total < 0 || total > consensus.MAX_MONEY) + return [-1, 'bad-txns-inputvalues-outofrange', 100]; + } -TX.prototype.toInv = function toInv() { - return new InvItem(InvItem.types.TX, this.hash('hex')); -}; + // Overflows already checked in `isSane()`. + const value = this.getOutputValue(); -/** - * Inspect the transaction and return a more - * user-friendly representation of the data. - * @returns {Object} - */ + if (total < value) + return [-1, 'bad-txns-in-belowout', 100]; -TX.prototype.inspect = function inspect() { - return this.format(); -}; + const fee = total - value; -/** - * Inspect the transaction and return a more - * user-friendly representation of the data. - * @param {CoinView} view - * @param {ChainEntry} entry - * @param {Number} index - * @returns {Object} - */ + if (fee < 0) + return [-1, 'bad-txns-fee-negative', 100]; -TX.prototype.format = function format(view, entry, index) { - let rate = 0; - let fee = 0; - let height = -1; - let block = null; - let time = 0; - let date = null; + if (fee > consensus.MAX_MONEY) + return [-1, 'bad-txns-fee-outofrange', 100]; - if (view) { - fee = this.getFee(view); - rate = this.getRate(view); - - // Rate can exceed 53 bits in testing. - if (!Number.isSafeInteger(rate)) - rate = 0; + return [fee, 'valid', 0]; } - if (entry) { - height = entry.height; - block = encoding.revHex(entry.hash); - time = entry.time; - date = util.date(time); - } + /** + * Calculate the modified size of the transaction. This + * is used in the mempool for calculating priority. + * @param {Number?} size - The size to modify. If not present, + * virtual size will be used. + * @returns {Number} Modified size. + */ - if (index == null) - index = -1; - - return { - hash: this.txid(), - witnessHash: this.wtxid(), - size: this.getSize(), - virtualSize: this.getVirtualSize(), - value: Amount.btc(this.getOutputValue()), - fee: Amount.btc(fee), - rate: Amount.btc(rate), - minFee: Amount.btc(this.getMinFee()), - height: height, - block: block, - time: time, - date: date, - index: index, - version: this.version, - inputs: this.inputs.map((input) => { - const coin = view ? view.getOutputFor(input) : null; - return input.format(coin); - }), - outputs: this.outputs, - locktime: this.locktime - }; -}; - -/** - * Convert the transaction to an object suitable - * for JSON serialization. - * @returns {Object} - */ - -TX.prototype.toJSON = function toJSON() { - return this.getJSON(); -}; - -/** - * Convert the transaction to an object suitable - * for JSON serialization. Note that the hashes - * will be reversed to abide by bitcoind's legacy - * of little-endian uint256s. - * @param {Network} network - * @param {CoinView} view - * @param {ChainEntry} entry - * @param {Number} index - * @returns {Object} - */ - -TX.prototype.getJSON = function getJSON(network, view, entry, index) { - let rate, fee, height, block, time, date; - - if (view) { - fee = this.getFee(view); - rate = this.getRate(view); - - // Rate can exceed 53 bits in testing. - if (!Number.isSafeInteger(rate)) - rate = 0; - } - - if (entry) { - height = entry.height; - block = encoding.revHex(entry.hash); - time = entry.time; - date = util.date(time); - } - - network = Network.get(network); - - return { - hash: this.txid(), - witnessHash: this.wtxid(), - fee: fee, - rate: rate, - mtime: util.now(), - height: height, - block: block, - time: time, - date: date, - index: index, - version: this.version, - inputs: this.inputs.map((input) => { - const coin = view ? view.getCoinFor(input) : null; - return input.getJSON(network, coin); - }), - outputs: this.outputs.map((output) => { - return output.getJSON(network); - }), - locktime: this.locktime, - hex: this.toRaw().toString('hex') - }; -}; - -/** - * Inject properties from a json object. - * @private - * @param {Object} json - */ - -TX.prototype.fromJSON = function fromJSON(json) { - assert(json, 'TX data is required.'); - assert((json.version >>> 0) === json.version, 'Version must be a uint32.'); - assert(Array.isArray(json.inputs), 'Inputs must be an array.'); - assert(Array.isArray(json.outputs), 'Outputs must be an array.'); - assert((json.locktime >>> 0) === json.locktime, 'Locktime must be a uint32.'); - - this.version = json.version; - - for (const input of json.inputs) - this.inputs.push(Input.fromJSON(input)); - - for (const output of json.outputs) - this.outputs.push(Output.fromJSON(output)); - - this.locktime = json.locktime; - - return this; -}; - -/** - * Instantiate a transaction from a - * jsonified transaction object. - * @param {Object} json - The jsonified transaction object. - * @returns {TX} - */ - -TX.fromJSON = function fromJSON(json) { - return new TX().fromJSON(json); -}; - -/** - * Instantiate a transaction from a serialized Buffer. - * @param {Buffer} data - * @param {String?} enc - Encoding, can be `'hex'` or null. - * @returns {TX} - */ - -TX.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - return new TX().fromRaw(data); -}; - -/** - * Instantiate a transaction from a buffer reader. - * @param {BufferReader} br - * @returns {TX} - */ - -TX.fromReader = function fromReader(br) { - return new TX().fromReader(br); -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -TX.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(new BufferReader(data)); -}; - -/** - * Inject properties from buffer reader. - * @private - * @param {BufferReader} br - */ - -TX.prototype.fromReader = function fromReader(br) { - if (hasWitnessBytes(br)) - return this.fromWitnessReader(br); - - br.start(); - - this.version = br.readU32(); - - const inCount = br.readVarint(); - - for (let i = 0; i < inCount; i++) - this.inputs.push(Input.fromReader(br)); - - const outCount = br.readVarint(); - - for (let i = 0; i < outCount; i++) - this.outputs.push(Output.fromReader(br)); - - this.locktime = br.readU32(); - - if (!this.mutable) { - this._raw = br.endData(); - this._size = this._raw.length; - this._witness = 0; - } else { - br.end(); - } - - return this; -}; - -/** - * Inject properties from serialized - * buffer reader (witness serialization). - * @private - * @param {BufferReader} br - */ - -TX.prototype.fromWitnessReader = function fromWitnessReader(br) { - br.start(); - - this.version = br.readU32(); - - assert(br.readU8() === 0, 'Non-zero marker.'); - - let flags = br.readU8(); - - assert(flags !== 0, 'Flags byte is zero.'); - - const inCount = br.readVarint(); - - for (let i = 0; i < inCount; i++) - this.inputs.push(Input.fromReader(br)); - - const outCount = br.readVarint(); - - for (let i = 0; i < outCount; i++) - this.outputs.push(Output.fromReader(br)); - - let witness = 0; - let hasWitness = false; - - if (flags & 1) { - flags ^= 1; - - witness = br.offset; + getModifiedSize(size) { + if (size == null) + size = this.getVirtualSize(); for (const input of this.inputs) { - input.witness.fromReader(br); - if (input.witness.items.length > 0) - hasWitness = true; + const offset = 41 + Math.min(110, input.script.getSize()); + if (size > offset) + size -= offset; } - witness = (br.offset - witness) + 2; + return size; } - if (flags !== 0) - throw new Error('Unknown witness flag.'); + /** + * Calculate the transaction priority. + * @param {CoinView} view + * @param {Number} height + * @param {Number?} size - Size to calculate priority + * based on. If not present, virtual size will be used. + * @returns {Number} + */ - // We'll never be able to reserialize - // this to get the regular txid, and - // there's no way it's valid anyway. - if (this.inputs.length === 0 && this.outputs.length !== 0) - throw new Error('Zero input witness tx.'); + getPriority(view, height, size) { + assert(typeof height === 'number', 'Must pass in height.'); - this.locktime = br.readU32(); + if (this.isCoinbase()) + return 0; - if (!this.mutable && hasWitness) { - this._raw = br.endData(); - this._size = this._raw.length; - this._witness = witness; - } else { - br.end(); + if (size == null) + size = this.getVirtualSize(); + + let sum = 0; + + for (const {prevout} of this.inputs) { + const coin = view.getOutput(prevout); + + if (!coin) + continue; + + const coinHeight = view.getHeight(prevout); + + if (coinHeight === -1) + continue; + + if (coinHeight <= height) { + const age = height - coinHeight; + sum += coin.value * age; + } + } + + return Math.floor(sum / size); } - return this; -}; + /** + * Calculate the transaction's on-chain value. + * @param {CoinView} view + * @returns {Number} + */ -/** - * Serialize transaction without witness. - * @private - * @returns {RawTX} - */ + getChainValue(view) { + if (this.isCoinbase()) + return 0; -TX.prototype.frameNormal = function frameNormal() { - const raw = this.getNormalSizes(); - const bw = new StaticWriter(raw.size); - this.writeNormal(bw); - raw.data = bw.render(); - return raw; -}; + let value = 0; -/** - * Serialize transaction with witness. Calculates the witness - * size as it is framing (exposed on return value as `witness`). - * @private - * @returns {RawTX} - */ + for (const {prevout} of this.inputs) { + const coin = view.getOutput(prevout); -TX.prototype.frameWitness = function frameWitness() { - const raw = this.getWitnessSizes(); - const bw = new StaticWriter(raw.size); - this.writeWitness(bw); - raw.data = bw.render(); - return raw; -}; + if (!coin) + continue; -/** - * Serialize transaction without witness. - * @private - * @param {BufferWriter} bw - * @returns {RawTX} - */ + const height = view.getHeight(prevout); -TX.prototype.writeNormal = function writeNormal(bw) { - if (this.inputs.length === 0 && this.outputs.length !== 0) - throw new Error('Cannot serialize zero-input tx.'); + if (height === -1) + continue; - bw.writeU32(this.version); + value += coin.value; + } - bw.writeVarint(this.inputs.length); - - for (const input of this.inputs) - input.toWriter(bw); - - bw.writeVarint(this.outputs.length); - - for (const output of this.outputs) - output.toWriter(bw); - - bw.writeU32(this.locktime); - - return bw; -}; - -/** - * Serialize transaction with witness. Calculates the witness - * size as it is framing (exposed on return value as `witness`). - * @private - * @param {BufferWriter} bw - * @returns {RawTX} - */ - -TX.prototype.writeWitness = function writeWitness(bw) { - if (this.inputs.length === 0 && this.outputs.length !== 0) - throw new Error('Cannot serialize zero-input tx.'); - - bw.writeU32(this.version); - bw.writeU8(0); - bw.writeU8(1); - - bw.writeVarint(this.inputs.length); - - for (const input of this.inputs) - input.toWriter(bw); - - bw.writeVarint(this.outputs.length); - - for (const output of this.outputs) - output.toWriter(bw); - - const start = bw.offset; - - for (const input of this.inputs) - input.witness.toWriter(bw); - - const witness = bw.offset - start; - - bw.writeU32(this.locktime); - - if (witness === this.inputs.length) - throw new Error('Cannot serialize empty-witness tx.'); - - return bw; -}; - -/** - * Calculate the real size of the transaction - * without the witness vector. - * @returns {RawTX} - */ - -TX.prototype.getNormalSizes = function getNormalSizes() { - let base = 0; - - base += 4; - - base += encoding.sizeVarint(this.inputs.length); - - for (const input of this.inputs) - base += input.getSize(); - - base += encoding.sizeVarint(this.outputs.length); - - for (const output of this.outputs) - base += output.getSize(); - - base += 4; - - return new RawTX(base, 0); -}; - -/** - * Calculate the real size of the transaction - * with the witness included. - * @returns {RawTX} - */ - -TX.prototype.getWitnessSizes = function getWitnessSizes() { - let base = 0; - let witness = 0; - - base += 4; - witness += 2; - - base += encoding.sizeVarint(this.inputs.length); - - for (const input of this.inputs) { - base += input.getSize(); - witness += input.witness.getVarSize(); + return value; } - base += encoding.sizeVarint(this.outputs.length); + /** + * Determine whether the transaction is above the + * free threshold in priority. A transaction which + * passed this test is most likely relayable + * without a fee. + * @param {CoinView} view + * @param {Number?} height - If not present, tx + * height or network height will be used. + * @param {Number?} size - If not present, modified + * size will be calculated and used. + * @returns {Boolean} + */ - for (const output of this.outputs) - base += output.getSize(); + isFree(view, height, size) { + const priority = this.getPriority(view, height, size); + return priority > policy.FREE_THRESHOLD; + } - base += 4; + /** + * Calculate minimum fee in order for the transaction + * to be relayable (not the constant min relay fee). + * @param {Number?} size - If not present, max size + * estimation will be calculated and used. + * @param {Rate?} rate - Rate of satoshi per kB. + * @returns {Amount} fee + */ - return new RawTX(base + witness, witness); -}; + getMinFee(size, rate) { + if (size == null) + size = this.getVirtualSize(); -/** - * Test whether an object is a TX. - * @param {Object} obj - * @returns {Boolean} - */ + return policy.getMinFee(size, rate); + } -TX.isTX = function isTX(obj) { - return obj instanceof TX; -}; + /** + * Calculate the minimum fee in order for the transaction + * to be relayable, but _round to the nearest kilobyte + * when taking into account size. + * @param {Number?} size - If not present, max size + * estimation will be calculated and used. + * @param {Rate?} rate - Rate of satoshi per kB. + * @returns {Amount} fee + */ + + getRoundFee(size, rate) { + if (size == null) + size = this.getVirtualSize(); + + return policy.getRoundFee(size, rate); + } + + /** + * Calculate the transaction's rate based on size + * and fees. Size will be calculated if not present. + * @param {CoinView} view + * @param {Number?} size + * @returns {Rate} + */ + + getRate(view, size) { + const fee = this.getFee(view); + + if (fee < 0) + return 0; + + if (size == null) + size = this.getVirtualSize(); + + return policy.getRate(size, fee); + } + + /** + * Get all unique outpoint hashes. + * @returns {Hash[]} Outpoint hashes. + */ + + getPrevout() { + if (this.isCoinbase()) + return []; + + const prevout = Object.create(null); + + for (const input of this.inputs) + prevout[input.prevout.hash] = true; + + return Object.keys(prevout); + } + + /** + * Test a transaction against a bloom filter using + * the BIP37 matching algorithm. Note that this may + * update the filter depending on what the `update` + * value is. + * @see "Filter matching algorithm": + * @see https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki + * @param {BloomFilter} filter + * @returns {Boolean} True if the transaction matched. + */ + + isWatched(filter) { + let found = false; + + // 1. Test the tx hash + if (filter.test(this.hash())) + found = true; + + // 2. Test data elements in output scripts + // (may need to update filter on match) + for (let i = 0; i < this.outputs.length; i++) { + const output = this.outputs[i]; + // Test the output script + if (output.script.test(filter)) { + if (filter.update === BloomFilter.flags.ALL) { + const prevout = Outpoint.fromTX(this, i); + filter.add(prevout.toRaw()); + } else if (filter.update === BloomFilter.flags.PUBKEY_ONLY) { + if (output.script.isPubkey() || output.script.isMultisig()) { + const prevout = Outpoint.fromTX(this, i); + filter.add(prevout.toRaw()); + } + } + found = true; + } + } + + if (found) + return found; + + // 3. Test prev_out structure + // 4. Test data elements in input scripts + for (const input of this.inputs) { + const prevout = input.prevout; + + // Test the COutPoint structure + if (filter.test(prevout.toRaw())) + return true; + + // Test the input script + if (input.script.test(filter)) + return true; + } + + // 5. No match + return false; + } + + /** + * Get little-endian tx hash. + * @returns {Hash} + */ + + rhash() { + return encoding.revHex(this.hash('hex')); + } + + /** + * Get little-endian wtx hash. + * @returns {Hash} + */ + + rwhash() { + return encoding.revHex(this.witnessHash('hex')); + } + + /** + * Get little-endian tx hash. + * @returns {Hash} + */ + + txid() { + return this.rhash(); + } + + /** + * Get little-endian wtx hash. + * @returns {Hash} + */ + + wtxid() { + return this.rwhash(); + } + + /** + * Convert the tx to an inv item. + * @returns {InvItem} + */ + + toInv() { + return new InvItem(InvItem.types.TX, this.hash('hex')); + } + + /** + * Inspect the transaction and return a more + * user-friendly representation of the data. + * @returns {Object} + */ + + inspect() { + return this.format(); + } + + /** + * Inspect the transaction and return a more + * user-friendly representation of the data. + * @param {CoinView} view + * @param {ChainEntry} entry + * @param {Number} index + * @returns {Object} + */ + + format(view, entry, index) { + let rate = 0; + let fee = 0; + let height = -1; + let block = null; + let time = 0; + let date = null; + + if (view) { + fee = this.getFee(view); + rate = this.getRate(view); + + // Rate can exceed 53 bits in testing. + if (!Number.isSafeInteger(rate)) + rate = 0; + } + + if (entry) { + height = entry.height; + block = encoding.revHex(entry.hash); + time = entry.time; + date = util.date(time); + } + + if (index == null) + index = -1; + + return { + hash: this.txid(), + witnessHash: this.wtxid(), + size: this.getSize(), + virtualSize: this.getVirtualSize(), + value: Amount.btc(this.getOutputValue()), + fee: Amount.btc(fee), + rate: Amount.btc(rate), + minFee: Amount.btc(this.getMinFee()), + height: height, + block: block, + time: time, + date: date, + index: index, + version: this.version, + inputs: this.inputs.map((input) => { + const coin = view ? view.getOutputFor(input) : null; + return input.format(coin); + }), + outputs: this.outputs, + locktime: this.locktime + }; + } + + /** + * Convert the transaction to an object suitable + * for JSON serialization. + * @returns {Object} + */ + + toJSON() { + return this.getJSON(); + } + + /** + * Convert the transaction to an object suitable + * for JSON serialization. Note that the hashes + * will be reversed to abide by bitcoind's legacy + * of little-endian uint256s. + * @param {Network} network + * @param {CoinView} view + * @param {ChainEntry} entry + * @param {Number} index + * @returns {Object} + */ + + getJSON(network, view, entry, index) { + let rate, fee, height, block, time, date; + + if (view) { + fee = this.getFee(view); + rate = this.getRate(view); + + // Rate can exceed 53 bits in testing. + if (!Number.isSafeInteger(rate)) + rate = 0; + } + + if (entry) { + height = entry.height; + block = encoding.revHex(entry.hash); + time = entry.time; + date = util.date(time); + } + + network = Network.get(network); + + return { + hash: this.txid(), + witnessHash: this.wtxid(), + fee: fee, + rate: rate, + mtime: util.now(), + height: height, + block: block, + time: time, + date: date, + index: index, + version: this.version, + inputs: this.inputs.map((input) => { + const coin = view ? view.getCoinFor(input) : null; + return input.getJSON(network, coin); + }), + outputs: this.outputs.map((output) => { + return output.getJSON(network); + }), + locktime: this.locktime, + hex: this.toRaw().toString('hex') + }; + } + + /** + * Inject properties from a json object. + * @private + * @param {Object} json + */ + + fromJSON(json) { + assert(json, 'TX data is required.'); + assert((json.version >>> 0) === json.version, 'Version must be a uint32.'); + assert(Array.isArray(json.inputs), 'Inputs must be an array.'); + assert(Array.isArray(json.outputs), 'Outputs must be an array.'); + assert((json.locktime >>> 0) === json.locktime, 'Locktime must be a uint32.'); + + this.version = json.version; + + for (const input of json.inputs) + this.inputs.push(Input.fromJSON(input)); + + for (const output of json.outputs) + this.outputs.push(Output.fromJSON(output)); + + this.locktime = json.locktime; + + return this; + } + + /** + * Instantiate a transaction from a + * jsonified transaction object. + * @param {Object} json - The jsonified transaction object. + * @returns {TX} + */ + + static fromJSON(json) { + return new this().fromJSON(json); + } + + /** + * Instantiate a transaction from a serialized Buffer. + * @param {Buffer} data + * @param {String?} enc - Encoding, can be `'hex'` or null. + * @returns {TX} + */ + + static fromRaw(data, enc) { + if (typeof data === 'string') + data = Buffer.from(data, enc); + return new this().fromRaw(data); + } + + /** + * Instantiate a transaction from a buffer reader. + * @param {BufferReader} br + * @returns {TX} + */ + + static fromReader(br) { + return new this().fromReader(br); + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + return this.fromReader(new BufferReader(data)); + } + + /** + * Inject properties from buffer reader. + * @private + * @param {BufferReader} br + */ + + fromReader(br) { + if (hasWitnessBytes(br)) + return this.fromWitnessReader(br); + + br.start(); + + this.version = br.readU32(); + + const inCount = br.readVarint(); + + for (let i = 0; i < inCount; i++) + this.inputs.push(Input.fromReader(br)); + + const outCount = br.readVarint(); + + for (let i = 0; i < outCount; i++) + this.outputs.push(Output.fromReader(br)); + + this.locktime = br.readU32(); + + if (!this.mutable) { + this._raw = br.endData(); + this._size = this._raw.length; + this._witness = 0; + } else { + br.end(); + } + + return this; + } + + /** + * Inject properties from serialized + * buffer reader (witness serialization). + * @private + * @param {BufferReader} br + */ + + fromWitnessReader(br) { + br.start(); + + this.version = br.readU32(); + + assert(br.readU8() === 0, 'Non-zero marker.'); + + let flags = br.readU8(); + + assert(flags !== 0, 'Flags byte is zero.'); + + const inCount = br.readVarint(); + + for (let i = 0; i < inCount; i++) + this.inputs.push(Input.fromReader(br)); + + const outCount = br.readVarint(); + + for (let i = 0; i < outCount; i++) + this.outputs.push(Output.fromReader(br)); + + let witness = 0; + let hasWitness = false; + + if (flags & 1) { + flags ^= 1; + + witness = br.offset; + + for (const input of this.inputs) { + input.witness.fromReader(br); + if (input.witness.items.length > 0) + hasWitness = true; + } + + witness = (br.offset - witness) + 2; + } + + if (flags !== 0) + throw new Error('Unknown witness flag.'); + + // We'll never be able to reserialize + // this to get the regular txid, and + // there's no way it's valid anyway. + if (this.inputs.length === 0 && this.outputs.length !== 0) + throw new Error('Zero input witness tx.'); + + this.locktime = br.readU32(); + + if (!this.mutable && hasWitness) { + this._raw = br.endData(); + this._size = this._raw.length; + this._witness = witness; + } else { + br.end(); + } + + return this; + } + + /** + * Serialize transaction without witness. + * @private + * @returns {RawTX} + */ + + frameNormal() { + const raw = this.getNormalSizes(); + const bw = new StaticWriter(raw.size); + this.writeNormal(bw); + raw.data = bw.render(); + return raw; + } + + /** + * Serialize transaction with witness. Calculates the witness + * size as it is framing (exposed on return value as `witness`). + * @private + * @returns {RawTX} + */ + + frameWitness() { + const raw = this.getWitnessSizes(); + const bw = new StaticWriter(raw.size); + this.writeWitness(bw); + raw.data = bw.render(); + return raw; + } + + /** + * Serialize transaction without witness. + * @private + * @param {BufferWriter} bw + * @returns {RawTX} + */ + + writeNormal(bw) { + if (this.inputs.length === 0 && this.outputs.length !== 0) + throw new Error('Cannot serialize zero-input tx.'); + + bw.writeU32(this.version); + + bw.writeVarint(this.inputs.length); + + for (const input of this.inputs) + input.toWriter(bw); + + bw.writeVarint(this.outputs.length); + + for (const output of this.outputs) + output.toWriter(bw); + + bw.writeU32(this.locktime); + + return bw; + } + + /** + * Serialize transaction with witness. Calculates the witness + * size as it is framing (exposed on return value as `witness`). + * @private + * @param {BufferWriter} bw + * @returns {RawTX} + */ + + writeWitness(bw) { + if (this.inputs.length === 0 && this.outputs.length !== 0) + throw new Error('Cannot serialize zero-input tx.'); + + bw.writeU32(this.version); + bw.writeU8(0); + bw.writeU8(1); + + bw.writeVarint(this.inputs.length); + + for (const input of this.inputs) + input.toWriter(bw); + + bw.writeVarint(this.outputs.length); + + for (const output of this.outputs) + output.toWriter(bw); + + const start = bw.offset; + + for (const input of this.inputs) + input.witness.toWriter(bw); + + const witness = bw.offset - start; + + bw.writeU32(this.locktime); + + if (witness === this.inputs.length) + throw new Error('Cannot serialize empty-witness tx.'); + + return bw; + } + + /** + * Calculate the real size of the transaction + * without the witness vector. + * @returns {RawTX} + */ + + getNormalSizes() { + let base = 0; + + base += 4; + + base += encoding.sizeVarint(this.inputs.length); + + for (const input of this.inputs) + base += input.getSize(); + + base += encoding.sizeVarint(this.outputs.length); + + for (const output of this.outputs) + base += output.getSize(); + + base += 4; + + return new RawTX(base, 0); + } + + /** + * Calculate the real size of the transaction + * with the witness included. + * @returns {RawTX} + */ + + getWitnessSizes() { + let base = 0; + let witness = 0; + + base += 4; + witness += 2; + + base += encoding.sizeVarint(this.inputs.length); + + for (const input of this.inputs) { + base += input.getSize(); + witness += input.witness.getVarSize(); + } + + base += encoding.sizeVarint(this.outputs.length); + + for (const output of this.outputs) + base += output.getSize(); + + base += 4; + + return new RawTX(base + witness, witness); + } + + /** + * Test whether an object is a TX. + * @param {Object} obj + * @returns {Boolean} + */ + + static isTX(obj) { + return obj instanceof TX; + } +} /* * Helpers @@ -2538,10 +2538,12 @@ function hasWitnessBytes(br) { && br.data[br.offset + 5] !== 0; } -function RawTX(size, witness) { - this.data = null; - this.size = size; - this.witness = witness; +class RawTX { + constructor(size, witness) { + this.data = null; + this.size = size; + this.witness = witness; + } } /* diff --git a/lib/primitives/txmeta.js b/lib/primitives/txmeta.js index b06d9e89..b3d61ece 100644 --- a/lib/primitives/txmeta.js +++ b/lib/primitives/txmeta.js @@ -14,293 +14,297 @@ const BufferReader = require('bufio/lib/reader'); const encoding = require('bufio/lib/encoding'); /** + * TXMeta * An extended transaction object. * @alias module:primitives.TXMeta - * @constructor - * @param {Object} options */ -function TXMeta(options) { - if (!(this instanceof TXMeta)) - return new TXMeta(options); +class TXMeta { + /** + * Create an extended transaction. + * @constructor + * @param {Object?} options + */ - this.tx = new TX(); - this.mtime = util.now(); - this.height = -1; - this.block = null; - this.time = 0; - this.index = -1; + constructor(options) { + this.tx = new TX(); + this.mtime = util.now(); + this.height = -1; + this.block = null; + this.time = 0; + this.index = -1; - if (options) - this.fromOptions(options); + if (options) + this.fromOptions(options); + } + + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ + + fromOptions(options) { + if (options.tx) { + assert(options.tx instanceof TX); + this.tx = options.tx; + } + + if (options.mtime != null) { + assert((options.mtime >>> 0) === options.mtime); + this.mtime = options.mtime; + } + + if (options.height != null) { + assert(Number.isSafeInteger(options.height)); + this.height = options.height; + } + + if (options.block !== undefined) { + assert(options.block === null || typeof options.block === 'string'); + this.block = options.block; + } + + if (options.time != null) { + assert((options.time >>> 0) === options.time); + this.time = options.time; + } + + if (options.index != null) { + assert(Number.isSafeInteger(options.index)); + this.index = options.index; + } + + return this; + } + + /** + * Instantiate TXMeta from options. + * @param {Object} options + * @returns {TXMeta} + */ + + static fromOptions(options) { + return new this().fromOptions(options); + } + + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ + + fromTX(tx, entry, index) { + this.tx = tx; + if (entry) { + this.height = entry.height; + this.block = entry.hash; + this.time = entry.time; + this.index = index; + } + return this; + } + + /** + * Instantiate TXMeta from options. + * @param {Object} options + * @returns {TXMeta} + */ + + static fromTX(tx, entry, index) { + return new this().fromTX(tx, entry, index); + } + + /** + * Inspect the transaction. + * @returns {Object} + */ + + inspect() { + return this.format(); + } + + /** + * Inspect the transaction. + * @returns {Object} + */ + + format(view) { + const data = this.tx.format(view, null, this.index); + data.mtime = this.mtime; + data.height = this.height; + data.block = this.block ? encoding.revHex(this.block) : null; + data.time = this.time; + return data; + } + + /** + * Convert transaction to JSON. + * @returns {Object} + */ + + toJSON() { + return this.getJSON(); + } + + /** + * Convert the transaction to an object suitable + * for JSON serialization. + * @param {Network} network + * @param {CoinView} view + * @returns {Object} + */ + + getJSON(network, view, chainHeight) { + const json = this.tx.getJSON(network, view, null, this.index); + json.mtime = this.mtime; + json.height = this.height; + json.block = this.block ? encoding.revHex(this.block) : null; + json.time = this.time; + json.confirmations = 0; + + if (chainHeight != null) + json.confirmations = chainHeight - this.height + 1; + + return json; + } + + /** + * Inject properties from a json object. + * @private + * @param {Object} json + */ + + fromJSON(json) { + this.tx.fromJSON(json); + + assert((json.mtime >>> 0) === json.mtime); + assert(Number.isSafeInteger(json.height)); + assert(!json.block || typeof json.block === 'string'); + assert((json.time >>> 0) === json.time); + assert(Number.isSafeInteger(json.index)); + + this.mtime = json.mtime; + this.height = json.height; + this.block = encoding.revHex(json.block); + this.index = json.index; + + return this; + } + + /** + * Instantiate a transaction from a + * jsonified transaction object. + * @param {Object} json - The jsonified transaction object. + * @returns {TX} + */ + + static fromJSON(json) { + return new this().fromJSON(JSON); + } + + /** + * Calculate serialization size. + * @returns {Number} + */ + + getSize() { + let size = 0; + + size += this.tx.getSize(); + size += 4; + + if (this.block) { + size += 1; + size += 32; + size += 4 * 3; + } else { + size += 1; + } + + return size; + } + + /** + * Serialize a transaction to "extended format". + * This is the serialization format bcoin uses internally + * to store transactions in the database. The extended + * serialization includes the height, block hash, index, + * timestamp, and pending-since time. + * @returns {Buffer} + */ + + toRaw() { + const size = this.getSize(); + const bw = new StaticWriter(size); + + this.tx.toWriter(bw); + + bw.writeU32(this.mtime); + + if (this.block) { + bw.writeU8(1); + bw.writeHash(this.block); + bw.writeU32(this.height); + bw.writeU32(this.time); + bw.writeU32(this.index); + } else { + bw.writeU8(0); + } + + return bw.render(); + } + + /** + * Inject properties from "extended" serialization format. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + const br = new BufferReader(data); + + this.tx.fromReader(br); + + this.mtime = br.readU32(); + + if (br.readU8() === 1) { + this.block = br.readHash('hex'); + this.height = br.readU32(); + this.time = br.readU32(); + this.index = br.readU32(); + if (this.index === 0x7fffffff) + this.index = -1; + } + + return this; + } + + /** + * Instantiate a transaction from a Buffer + * in "extended" serialization format. + * @param {Buffer} data + * @param {String?} enc - One of `"hex"` or `null`. + * @returns {TX} + */ + + static fromRaw(data, enc) { + if (typeof data === 'string') + data = Buffer.from(data, enc); + return new this().fromRaw(data); + } + + /** + * Test whether an object is an TXMeta. + * @param {Object} obj + * @returns {Boolean} + */ + + static isTXMeta(obj) { + return obj instanceof TXMeta; + } } -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -TXMeta.prototype.fromOptions = function fromOptions(options) { - if (options.tx) { - assert(options.tx instanceof TX); - this.tx = options.tx; - } - - if (options.mtime != null) { - assert((options.mtime >>> 0) === options.mtime); - this.mtime = options.mtime; - } - - if (options.height != null) { - assert(Number.isSafeInteger(options.height)); - this.height = options.height; - } - - if (options.block !== undefined) { - assert(options.block === null || typeof options.block === 'string'); - this.block = options.block; - } - - if (options.time != null) { - assert((options.time >>> 0) === options.time); - this.time = options.time; - } - - if (options.index != null) { - assert(Number.isSafeInteger(options.index)); - this.index = options.index; - } - - return this; -}; - -/** - * Instantiate TXMeta from options. - * @param {Object} options - * @returns {TXMeta} - */ - -TXMeta.fromOptions = function fromOptions(options) { - return new TXMeta().fromOptions(options); -}; - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -TXMeta.prototype.fromTX = function fromTX(tx, entry, index) { - this.tx = tx; - if (entry) { - this.height = entry.height; - this.block = entry.hash; - this.time = entry.time; - this.index = index; - } - return this; -}; - -/** - * Instantiate TXMeta from options. - * @param {Object} options - * @returns {TXMeta} - */ - -TXMeta.fromTX = function fromTX(tx, entry, index) { - return new TXMeta().fromTX(tx, entry, index); -}; - -/** - * Inspect the transaction. - * @returns {Object} - */ - -TXMeta.prototype.inspect = function inspect() { - return this.format(); -}; - -/** - * Inspect the transaction. - * @returns {Object} - */ - -TXMeta.prototype.format = function format(view) { - const data = this.tx.format(view, null, this.index); - data.mtime = this.mtime; - data.height = this.height; - data.block = this.block ? encoding.revHex(this.block) : null; - data.time = this.time; - return data; -}; - -/** - * Convert transaction to JSON. - * @returns {Object} - */ - -TXMeta.prototype.toJSON = function toJSON() { - return this.getJSON(); -}; - -/** - * Convert the transaction to an object suitable - * for JSON serialization. - * @param {Network} network - * @param {CoinView} view - * @returns {Object} - */ - -TXMeta.prototype.getJSON = function getJSON(network, view, chainHeight) { - const json = this.tx.getJSON(network, view, null, this.index); - json.mtime = this.mtime; - json.height = this.height; - json.block = this.block ? encoding.revHex(this.block) : null; - json.time = this.time; - json.confirmations = 0; - - if (chainHeight != null) - json.confirmations = chainHeight - this.height + 1; - - return json; -}; - -/** - * Inject properties from a json object. - * @private - * @param {Object} json - */ - -TXMeta.prototype.fromJSON = function fromJSON(json) { - this.tx.fromJSON(json); - - assert((json.mtime >>> 0) === json.mtime); - assert(Number.isSafeInteger(json.height)); - assert(!json.block || typeof json.block === 'string'); - assert((json.time >>> 0) === json.time); - assert(Number.isSafeInteger(json.index)); - - this.mtime = json.mtime; - this.height = json.height; - this.block = encoding.revHex(json.block); - this.index = json.index; - - return this; -}; - -/** - * Instantiate a transaction from a - * jsonified transaction object. - * @param {Object} json - The jsonified transaction object. - * @returns {TX} - */ - -TXMeta.fromJSON = function fromJSON(json) { - return new TXMeta().fromJSON(JSON); -}; - -/** - * Calculate serialization size. - * @returns {Number} - */ - -TXMeta.prototype.getSize = function getSize() { - let size = 0; - - size += this.tx.getSize(); - size += 4; - - if (this.block) { - size += 1; - size += 32; - size += 4 * 3; - } else { - size += 1; - } - - return size; -}; - -/** - * Serialize a transaction to "extended format". - * This is the serialization format bcoin uses internally - * to store transactions in the database. The extended - * serialization includes the height, block hash, index, - * timestamp, and pending-since time. - * @returns {Buffer} - */ - -TXMeta.prototype.toRaw = function toRaw() { - const size = this.getSize(); - const bw = new StaticWriter(size); - - this.tx.toWriter(bw); - - bw.writeU32(this.mtime); - - if (this.block) { - bw.writeU8(1); - bw.writeHash(this.block); - bw.writeU32(this.height); - bw.writeU32(this.time); - bw.writeU32(this.index); - } else { - bw.writeU8(0); - } - - return bw.render(); -}; - -/** - * Inject properties from "extended" serialization format. - * @private - * @param {Buffer} data - */ - -TXMeta.prototype.fromRaw = function fromRaw(data) { - const br = new BufferReader(data); - - this.tx.fromReader(br); - - this.mtime = br.readU32(); - - if (br.readU8() === 1) { - this.block = br.readHash('hex'); - this.height = br.readU32(); - this.time = br.readU32(); - this.index = br.readU32(); - if (this.index === 0x7fffffff) - this.index = -1; - } - - return this; -}; - -/** - * Instantiate a transaction from a Buffer - * in "extended" serialization format. - * @param {Buffer} data - * @param {String?} enc - One of `"hex"` or `null`. - * @returns {TX} - */ - -TXMeta.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - return new TXMeta().fromRaw(data); -}; - -/** - * Test whether an object is an TXMeta. - * @param {Object} obj - * @returns {Boolean} - */ - -TXMeta.isTXMeta = function isTXMeta(obj) { - return obj instanceof TXMeta; -}; - /* * Expose */ diff --git a/lib/wallet/walletkey.js b/lib/wallet/walletkey.js index 3c562b5e..fe0ac9cc 100644 --- a/lib/wallet/walletkey.js +++ b/lib/wallet/walletkey.js @@ -12,273 +12,276 @@ const KeyRing = require('../primitives/keyring'); const Path = require('./path'); /** + * Wallet Key * Represents a key ring which amounts to an address. * @alias module:wallet.WalletKey - * @constructor - * @param {Object} options + * @extends KeyRing */ -function WalletKey(options) { - if (!(this instanceof WalletKey)) - return new WalletKey(options); +class WalletKey extends KeyRing { + /** + * Create a wallet key. + * @constructor + * @param {Object?} options + */ - KeyRing.call(this, options); + constructor(options) { + super(options); - this.keyType = Path.types.HD; + this.keyType = Path.types.HD; - this.name = null; - this.account = -1; - this.branch = -1; - this.index = -1; -} - -Object.setPrototypeOf(WalletKey.prototype, KeyRing.prototype); - -/** - * Instantiate key ring from options. - * @param {Object} options - * @returns {WalletKey} - */ - -WalletKey.fromOptions = function fromOptions(options) { - return new WalletKey().fromOptions(options); -}; - -/** - * Instantiate wallet key from a private key. - * @param {Buffer} key - * @param {Boolean?} compressed - * @returns {WalletKey} - */ - -WalletKey.fromPrivate = function fromPrivate(key, compressed) { - return new WalletKey().fromPrivate(key, compressed); -}; - -/** - * Generate a wallet key. - * @param {Boolean?} compressed - * @returns {WalletKey} - */ - -WalletKey.generate = function generate(compressed) { - return new WalletKey().generate(compressed); -}; - -/** - * Instantiate wallet key from a public key. - * @param {Buffer} publicKey - * @returns {WalletKey} - */ - -WalletKey.fromPublic = function fromPublic(key) { - return new WalletKey().fromPublic(key); -}; - -/** - * Instantiate wallet key from a public key. - * @param {Buffer} publicKey - * @returns {WalletKey} - */ - -WalletKey.fromKey = function fromKey(key, compressed) { - return new WalletKey().fromKey(key, compressed); -}; - -/** - * Instantiate wallet key from script. - * @param {Buffer} key - * @param {Script} script - * @returns {WalletKey} - */ - -WalletKey.fromScript = function fromScript(key, script, compressed) { - return new WalletKey().fromScript(key, script, compressed); -}; - -/** - * Instantiate a wallet key from a serialized CBitcoinSecret. - * @param {Base58String} secret - * @param {Network?} network - * @returns {WalletKey} - */ - -WalletKey.fromSecret = function fromSecret(data, network) { - return new WalletKey().fromSecret(data, network); -}; - -/** - * Convert an WalletKey to a more json-friendly object. - * @returns {Object} - */ - -WalletKey.prototype.toJSON = function toJSON(network) { - return { - name: this.name, - account: this.account, - branch: this.branch, - index: this.index, - witness: this.witness, - nested: this.nested, - publicKey: this.publicKey.toString('hex'), - script: this.script ? this.script.toRaw().toString('hex') : null, - program: this.witness ? this.getProgram().toRaw().toString('hex') : null, - type: Address.typesByVal[this.getType()].toLowerCase(), - address: this.getAddress('string', network) - }; -}; - -/** - * Instantiate an WalletKey from a jsonified transaction object. - * @param {Object} json - The jsonified transaction object. - * @returns {WalletKey} - */ - -WalletKey.fromJSON = function fromJSON(json) { - return new WalletKey().fromJSON(json); -}; - -/** - * Instantiate a wallet key from serialized data. - * @param {Buffer} data - * @returns {WalletKey} - */ - -WalletKey.fromRaw = function fromRaw(data) { - return new WalletKey().fromRaw(data); -}; - -/** - * Inject properties from hd key. - * @private - * @param {Account} account - * @param {HDPrivateKey|HDPublicKey} key - * @param {Number} branch - * @param {Number} index - * @returns {WalletKey} - */ - -WalletKey.prototype.fromHD = function fromHD(account, key, branch, index) { - this.keyType = Path.types.HD; - this.name = account.name; - this.account = account.accountIndex; - this.branch = branch; - this.index = index; - this.witness = account.witness; - this.nested = branch === 2; - - if (key.privateKey) - return this.fromPrivate(key.privateKey); - - return this.fromPublic(key.publicKey); -}; - -/** - * Instantiate a wallet key from hd key. - * @param {Account} account - * @param {HDPrivateKey|HDPublicKey} key - * @param {Number} branch - * @param {Number} index - * @returns {WalletKey} - */ - -WalletKey.fromHD = function fromHD(account, key, branch, index) { - return new WalletKey().fromHD(account, key, branch, index); -}; - -/** - * Inject properties from imported data. - * @private - * @param {Account} account - * @param {Buffer} data - * @returns {WalletKey} - */ - -WalletKey.prototype.fromImport = function fromImport(account, data) { - this.keyType = Path.types.KEY; - this.name = account.name; - this.account = account.accountIndex; - this.witness = account.witness; - return this.fromRaw(data); -}; - -/** - * Instantiate a wallet key from imported data. - * @param {Account} account - * @param {Buffer} data - * @returns {WalletKey} - */ - -WalletKey.fromImport = function fromImport(account, data) { - return new WalletKey().fromImport(account, data); -}; - -/** - * Inject properties from key. - * @private - * @param {Account} account - * @param {KeyRing} ring - * @returns {WalletKey} - */ - -WalletKey.prototype.fromRing = function fromRing(account, ring) { - this.keyType = Path.types.KEY; - this.name = account.name; - this.account = account.accountIndex; - this.witness = account.witness; - return this.fromOptions(ring); -}; - -/** - * Instantiate a wallet key from regular key. - * @param {Account} account - * @param {KeyRing} ring - * @returns {WalletKey} - */ - -WalletKey.fromRing = function fromRing(account, ring) { - return new WalletKey().fromRing(account, ring); -}; - -/** - * Convert wallet key to a path. - * @returns {Path} - */ - -WalletKey.prototype.toPath = function toPath() { - const path = new Path(); - - path.name = this.name; - path.account = this.account; - - switch (this.keyType) { - case Path.types.HD: - path.branch = this.branch; - path.index = this.index; - break; - case Path.types.KEY: - path.data = this.toRaw(); - break; + this.name = null; + this.account = -1; + this.branch = -1; + this.index = -1; } - path.keyType = this.keyType; + /** + * Instantiate key ring from options. + * @param {Object} options + * @returns {WalletKey} + */ - path.version = this.getVersion(); - path.type = this.getType(); - path.hash = this.getHash('hex'); + static fromOptions(options) { + return new this().fromOptions(options); + } - return path; -}; + /** + * Instantiate wallet key from a private key. + * @param {Buffer} key + * @param {Boolean?} compressed + * @returns {WalletKey} + */ -/** - * Test whether an object is a WalletKey. - * @param {Object} obj - * @returns {Boolean} - */ + static fromPrivate(key, compressed) { + return new this().fromPrivate(key, compressed); + } -WalletKey.isWalletKey = function isWalletKey(obj) { - return obj instanceof WalletKey; -}; + /** + * Generate a wallet key. + * @param {Boolean?} compressed + * @returns {WalletKey} + */ + + static generate(compressed) { + return new this().generate(compressed); + } + + /** + * Instantiate wallet key from a public key. + * @param {Buffer} publicKey + * @returns {WalletKey} + */ + + static fromPublic(key) { + return new this().fromPublic(key); + } + + /** + * Instantiate wallet key from a public key. + * @param {Buffer} publicKey + * @returns {WalletKey} + */ + + static fromKey(key, compressed) { + return new this().fromKey(key, compressed); + } + + /** + * Instantiate wallet key from script. + * @param {Buffer} key + * @param {Script} script + * @returns {WalletKey} + */ + + static fromScript(key, script, compressed) { + return new this().fromScript(key, script, compressed); + } + + /** + * Instantiate a wallet key from a serialized CBitcoinSecret. + * @param {Base58String} secret + * @param {Network?} network + * @returns {WalletKey} + */ + + static fromSecret(data, network) { + return new this().fromSecret(data, network); + } + + /** + * Convert an WalletKey to a more json-friendly object. + * @returns {Object} + */ + + toJSON(network) { + return { + name: this.name, + account: this.account, + branch: this.branch, + index: this.index, + witness: this.witness, + nested: this.nested, + publicKey: this.publicKey.toString('hex'), + script: this.script ? this.script.toRaw().toString('hex') : null, + program: this.witness ? this.getProgram().toRaw().toString('hex') : null, + type: Address.typesByVal[this.getType()].toLowerCase(), + address: this.getAddress('string', network) + }; + } + + /** + * Instantiate an WalletKey from a jsonified transaction object. + * @param {Object} json - The jsonified transaction object. + * @returns {WalletKey} + */ + + static fromJSON(json) { + return new this().fromJSON(json); + } + + /** + * Instantiate a wallet key from serialized data. + * @param {Buffer} data + * @returns {WalletKey} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } + + /** + * Inject properties from hd key. + * @private + * @param {Account} account + * @param {HDPrivateKey|HDPublicKey} key + * @param {Number} branch + * @param {Number} index + * @returns {WalletKey} + */ + + fromHD(account, key, branch, index) { + this.keyType = Path.types.HD; + this.name = account.name; + this.account = account.accountIndex; + this.branch = branch; + this.index = index; + this.witness = account.witness; + this.nested = branch === 2; + + if (key.privateKey) + return this.fromPrivate(key.privateKey); + + return this.fromPublic(key.publicKey); + } + + /** + * Instantiate a wallet key from hd key. + * @param {Account} account + * @param {HDPrivateKey|HDPublicKey} key + * @param {Number} branch + * @param {Number} index + * @returns {WalletKey} + */ + + static fromHD(account, key, branch, index) { + return new this().fromHD(account, key, branch, index); + } + + /** + * Inject properties from imported data. + * @private + * @param {Account} account + * @param {Buffer} data + * @returns {WalletKey} + */ + + fromImport(account, data) { + this.keyType = Path.types.KEY; + this.name = account.name; + this.account = account.accountIndex; + this.witness = account.witness; + return this.fromRaw(data); + } + + /** + * Instantiate a wallet key from imported data. + * @param {Account} account + * @param {Buffer} data + * @returns {WalletKey} + */ + + static fromImport(account, data) { + return new this().fromImport(account, data); + } + + /** + * Inject properties from key. + * @private + * @param {Account} account + * @param {KeyRing} ring + * @returns {WalletKey} + */ + + fromRing(account, ring) { + this.keyType = Path.types.KEY; + this.name = account.name; + this.account = account.accountIndex; + this.witness = account.witness; + return this.fromOptions(ring); + } + + /** + * Instantiate a wallet key from regular key. + * @param {Account} account + * @param {KeyRing} ring + * @returns {WalletKey} + */ + + static fromRing(account, ring) { + return new this().fromRing(account, ring); + } + + /** + * Convert wallet key to a path. + * @returns {Path} + */ + + toPath() { + const path = new Path(); + + path.name = this.name; + path.account = this.account; + + switch (this.keyType) { + case Path.types.HD: + path.branch = this.branch; + path.index = this.index; + break; + case Path.types.KEY: + path.data = this.toRaw(); + break; + } + + path.keyType = this.keyType; + + path.version = this.getVersion(); + path.type = this.getType(); + path.hash = this.getHash('hex'); + + return path; + } + + /** + * Test whether an object is a WalletKey. + * @param {Object} obj + * @returns {Boolean} + */ + + static isWalletKey(obj) { + return obj instanceof WalletKey; + } +} /* * Expose diff --git a/test/util/memwallet.js b/test/util/memwallet.js index aa95bac4..b69b1320 100644 --- a/test/util/memwallet.js +++ b/test/util/memwallet.js @@ -168,7 +168,7 @@ MemWallet.prototype.getUndo = function getUndo(key) { }; MemWallet.prototype.addCoin = function addCoin(coin) { - const op = Outpoint(coin.hash, coin.index); + const op = new Outpoint(coin.hash, coin.index); const key = op.toKey(); this.filter.add(op.toRaw()); @@ -301,7 +301,7 @@ MemWallet.prototype.removeTX = function removeTX(tx, height) { return false; for (let i = 0; i < tx.outputs.length; i++) { - const op = Outpoint(hash, i).toKey(); + const op = new Outpoint(hash, i).toKey(); const coin = this.getCoin(op); if (!coin)