/*! * coins.js - coins object for bcoin * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; var assert = require('assert'); var util = require('../lib/utils/util'); var encoding = require('../lib/utils/encoding'); var Coin = require('../lib/primitives/coin'); var Output = require('../lib/primitives/output'); var BufferReader = require('../lib/utils/reader'); var BufferWriter = require('../lib/utils/writer'); var compressor = require('./compress-old'); var compress = compressor.compress; var decompress = compressor.decompress; /** * Represents the outputs for a single transaction. * @exports Coins * @constructor * @param {TX|Object} tx/options - TX or options object. * @property {Hash} hash - Transaction hash. * @property {Number} version - Transaction version. * @property {Number} height - Transaction height (-1 if unconfirmed). * @property {Boolean} coinbase - Whether the containing * transaction is a coinbase. * @property {Coin[]} outputs - Coins. */ function Coins(options) { if (!(this instanceof Coins)) return new Coins(options); this.version = 1; this.hash = encoding.NULL_HASH; this.height = -1; this.coinbase = true; this.outputs = []; if (options) this.fromOptions(options); } /** * Inject properties from options object. * @private * @param {Object} options */ Coins.prototype.fromOptions = function fromOptions(options) { if (options.version != null) { assert(util.isNumber(options.version)); this.version = options.version; } if (options.hash) { assert(typeof options.hash === 'string'); this.hash = options.hash; } if (options.height != null) { assert(util.isNumber(options.height)); this.height = options.height; } if (options.coinbase != null) { assert(typeof options.coinbase === 'boolean'); this.coinbase = options.coinbase; } if (options.outputs) { assert(Array.isArray(options.outputs)); this.outputs = options.outputs; } return this; }; /** * Instantiate coins from options object. * @param {Object} options * @returns {Coins} */ Coins.fromOptions = function fromOptions(options) { return new Coins().fromOptions(options); }; /** * Add a single coin to the collection. * @param {Coin} coin */ Coins.prototype.add = function add(coin) { if (this.outputs.length === 0) { this.version = coin.version; this.hash = coin.hash; this.height = coin.height; this.coinbase = coin.coinbase; } while (this.outputs.length <= coin.index) this.outputs.push(null); if (coin.script.isUnspendable()) { this.outputs[coin.index] = null; return; } this.outputs[coin.index] = CoinEntry.fromCoin(coin); }; /** * Test whether the collection has a coin. * @param {Number} index * @returns {Boolean} */ Coins.prototype.has = function has(index) { if (index >= this.outputs.length) return false; return this.outputs[index] != null; }; /** * Get a coin. * @param {Number} index * @returns {Coin} */ Coins.prototype.get = function get(index) { var coin; if (index >= this.outputs.length) return; coin = this.outputs[index]; if (!coin) return; return coin.toCoin(this, index); }; /** * Remove a coin and return it. * @param {Number} index * @returns {Coin} */ Coins.prototype.spend = function spend(index) { var coin = this.get(index); if (!coin) return; this.outputs[index] = null; return coin; }; /** * Count up to the last available index. * @returns {Number} */ Coins.prototype.size = function size() { var index = -1; var i, output; for (i = this.outputs.length - 1; i >= 0; i--) { output = this.outputs[i]; if (output) { index = i; break; } } return index + 1; }; /** * Test whether the coins are fully spent. * @returns {Boolean} */ Coins.prototype.isEmpty = function isEmpty() { return this.size() === 0; }; /* * Coins serialization: * version: varint * bits: uint32 (31-bit height | 1-bit coinbase-flag) * spent-field: varint size | bitfield (0=unspent, 1=spent) * outputs (repeated): * compressed-script: * prefix: 0x00 = varint size | raw script * 0x01 = 20 byte pubkey hash * 0x02 = 20 byte script hash * 0x03 = 33 byte compressed key * data: script data, dictated by the prefix * value: varint * * The compression below sacrifices some cpu in exchange * for reduced size, but in some cases the use of varints * actually increases speed (varint versions and values * for example). We do as much compression as possible * without sacrificing too much cpu. Value compression * is intentionally excluded for now as it seems to be * too much of a perf hit. Maybe when v8 optimizes * non-smi arithmetic better we can enable it. */ /** * Serialize the coins object. * @param {TX|Coins} tx * @returns {Buffer} */ Coins.prototype.toRaw = function toRaw() { var bw = new BufferWriter(); var length = this.size(); var len = Math.ceil(length / 8); var i, output, bits, start, bit, oct, data; // Return nothing if we're fully spent. if (length === 0) return; // Varint version: hopefully we // never run into `-1` versions. bw.writeVarint(this.version); // Create the `bits` value: // (height | coinbase-flag). bits = this.height << 1; // Append the coinbase bit. if (this.coinbase) bits |= 1; if (bits < 0) bits += 0x100000000; // Making this a varint would actually // make 99% of coins bigger. Varints // are really only useful up until // 0x10000, but since we're also // storing the coinbase flag on the // lo bit, varints are useless (and // actually harmful) after height // 32767 (0x7fff). bw.writeU32(bits); // Fill the spent field with zeroes to avoid // allocating a buffer. We mark the spents // after rendering the final buffer. bw.writeVarint(len); start = bw.written; bw.fill(0, len); // Write the compressed outputs. for (i = 0; i < length; i++) { output = this.outputs[i]; if (!output) continue; output.toWriter(bw); } // Render the buffer with all // zeroes in the spent field. data = bw.render(); // Mark the spents in the spent field. // This is essentially a NOP for new coins. for (i = 0; i < length; i++) { output = this.outputs[i]; if (output) continue; bit = i % 8; oct = (i - bit) / 8; oct += start; data[oct] |= 1 << (7 - bit); } return data; }; /** * Parse serialized coins. * @param {Buffer} data * @param {Hash} hash * @returns {Object} A "naked" coins object. */ Coins.prototype.fromRaw = function fromRaw(data, hash, index) { var br = new BufferReader(data); var pos = 0; var bits, len, start, bit, oct, spent, coin; this.version = br.readVarint(); bits = br.readU32(); this.height = bits >>> 1; this.hash = hash; this.coinbase = (bits & 1) !== 0; // Mark the start of the spent field and // seek past it to avoid reading a buffer. len = br.readVarint(); start = br.offset; br.seek(len); while (br.left()) { bit = pos % 8; oct = (pos - bit) / 8; oct += start; // Read a single bit out of the spent field. spent = data[oct] >>> (7 - bit); spent &= 1; // Already spent. if (spent) { this.outputs.push(null); pos++; continue; } // Store the offset and size // in the compressed coin object. coin = CoinEntry.fromReader(br); this.outputs.push(coin); pos++; } return this; }; /** * Parse a single serialized coin. * @param {Buffer} data * @param {Hash} hash * @param {Number} index * @returns {Coin} */ Coins.parseCoin = function parseCoin(data, hash, index) { var br = new BufferReader(data); var coin = new Coin(); var pos = 0; var bits, len, start, bit, oct, spent; coin.version = br.readVarint(); bits = br.readU32(); coin.hash = hash; coin.index = index; coin.height = bits >>> 1; coin.hash = hash; coin.coinbase = (bits & 1) !== 0; // Mark the start of the spent field and // seek past it to avoid reading a buffer. len = br.readVarint(); start = br.offset; br.seek(len); while (br.left()) { bit = pos % 8; oct = (pos - bit) / 8; oct += start; // Read a single bit out of the spent field. spent = data[oct] >>> (7 - bit); spent &= 1; // We found our coin. if (pos === index) { if (spent) return; decompress.script(coin.script, br); coin.value = br.readVarint(); return coin; } // Already spent. if (spent) { pos++; continue; } // Skip past the compressed coin. skipCoin(br); pos++; } }; /** * Instantiate coins from a serialized Buffer. * @param {Buffer} data * @param {Hash} hash - Transaction hash. * @returns {Coins} */ Coins.fromRaw = function fromRaw(data, hash) { return new Coins().fromRaw(data, hash); }; /** * Inject properties from tx. * @private * @param {TX} tx */ Coins.prototype.fromTX = function fromTX(tx) { var i, output; this.version = tx.version; this.hash = tx.hash('hex'); this.height = tx.height; this.coinbase = tx.isCoinbase(); for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; if (output.script.isUnspendable()) { this.outputs.push(null); continue; } this.outputs.push(CoinEntry.fromTX(tx, i)); } return this; }; /** * Instantiate a coins object from a transaction. * @param {TX} tx * @returns {Coins} */ Coins.fromTX = function fromTX(tx) { return new Coins().fromTX(tx); }; /** * A compressed coin is an object which defers * parsing of a coin. Say there is a transaction * with 100 outputs. When a block comes in, * there may only be _one_ input in that entire * block which redeems an output from that * transaction. When parsing the Coins, there * is no sense to get _all_ of them into their * abstract form. A compressed coin is just a * pointer to that coin in the Coins buffer, as * well as a size. Parsing is done only if that * coin is being redeemed. * @constructor * @private * @param {Number} offset * @param {Number} size * @param {Buffer} raw */ function CoinEntry() { this.offset = 0; this.size = 0; this.raw = null; this.output = null; } /** * Parse the deferred data and return a Coin. * @param {Coins} coins * @param {Number} index * @returns {Coin} */ CoinEntry.prototype.toCoin = function toCoin(coins, index) { var coin = new Coin(); var br; // Load in all necessary properties // from the parent Coins object. coin.version = coins.version; coin.coinbase = coins.coinbase; coin.height = coins.height; coin.hash = coins.hash; coin.index = index; if (this.output) { coin.script = this.output.script; coin.value = this.output.value; return coin; } br = new BufferReader(this.raw); // Seek to the coin's offset. br.seek(this.offset); decompress.script(coin.script, br); coin.value = br.readVarint(); return coin; }; /** * Slice off the part of the buffer * relevant to this particular coin. */ CoinEntry.prototype.toWriter = function toWriter(bw) { var raw; if (this.output) { compress.script(this.output.script, bw); bw.writeVarint(this.output.value); return; } assert(this.raw); // If we read this coin from the db and // didn't use it, it's still in its // compressed form. Just write it back // as a buffer for speed. raw = this.raw.slice(this.offset, this.offset + this.size); bw.writeBytes(raw); }; /** * Instantiate compressed coin from reader. * @param {BufferReader} br * @returns {CoinEntry} */ CoinEntry.fromReader = function fromReader(br) { var entry = new CoinEntry(); entry.offset = br.offset; entry.size = skipCoin(br); entry.raw = br.data; return entry; }; /** * Instantiate compressed coin from tx. * @param {TX} tx * @param {Number} index * @returns {CoinEntry} */ CoinEntry.fromTX = function fromTX(tx, index) { var entry = new CoinEntry(); entry.output = tx.outputs[index]; return entry; }; /** * Instantiate compressed coin from coin. * @param {Coin} coin * @returns {CoinEntry} */ CoinEntry.fromCoin = function fromCoin(coin) { var entry = new CoinEntry(); entry.output = new Output(); entry.output.script = coin.script; entry.output.value = coin.value; return entry; }; /* * Helpers */ function skipCoin(br) { var start = br.offset; // Skip past the compressed scripts. switch (br.readU8()) { case 0: br.seek(br.readVarint()); break; case 1: case 2: br.seek(20); break; case 3: br.seek(33); break; default: throw new Error('Bad prefix.'); } // Skip past the value. br.skipVarint(); return br.offset - start; } /* * Expose */ module.exports = Coins;