'use strict'; var _ = require('lodash'); var BlockHeader = require('./blockheader'); var BufferUtil = require('../util/buffer'); var BufferReader = require('../encoding/bufferreader'); var BufferWriter = require('../encoding/bufferwriter'); var Hash = require('../crypto/hash'); var JSUtil = require('../util/js'); var Transaction = require('../transaction'); var $ = require('../util/preconditions'); /** * Instantiate a MerkleBlock from a Buffer, JSON object, or Object with * the properties of the Block * * @param {*} - A Buffer, JSON string, or Object representing a MerkleBlock * @returns {MerkleBlock} * @constructor */ function MerkleBlock(arg) { /* jshint maxstatements: 18 */ if (!(this instanceof MerkleBlock)) { return new MerkleBlock(arg); } var info = {}; if (BufferUtil.isBuffer(arg)) { info = MerkleBlock._fromBufferReader(BufferReader(arg)); } else if (JSUtil.isValidJSON(arg)) { info = MerkleBlock._fromJSON(arg); } else if (_.isObject(arg)) { var header; if(arg.header instanceof BlockHeader) { header = arg.header; } else { header = BlockHeader.fromJSON(JSON.stringify(arg.header)); } info = { /** * @name MerkleBlock#header * @type {BlockHeader} */ header: header, /** * @name MerkleBlock#numTransactions * @type {Number} */ numTransactions: arg.numTransactions, /** * @name MerkleBlock#hashes * @type {String[]} */ hashes: arg.hashes, /** * @name MerkleBlock#flags * @type {Number[]} */ flags: arg.flags }; } else { throw new TypeError('Unrecognized argument for MerkleBlock'); } _.extend(this,info); this._flagBitsUsed = 0; this._hashesUsed = 0; return this; } /** * @param {Buffer} - MerkleBlock data in a Buffer object * @returns {MerkleBlock} - A MerkleBlock object */ MerkleBlock.fromBuffer = function fromBuffer(buf) { return MerkleBlock.fromBufferReader(BufferReader(buf)); }; /** * @param {BufferReader} - MerkleBlock data in a BufferReader object * @returns {MerkleBlock} - A MerkleBlock object */ MerkleBlock.fromBufferReader = function fromBufferReader(br) { return new MerkleBlock(MerkleBlock._fromBufferReader(br)); }; /** * @param {String|Object} - A JSON String or Object * @returns {MerkleBlock} - A MerkleBlock object */ MerkleBlock.fromJSON = function fromJSON(buf) { return new MerkleBlock(MerkleBlock._fromJSON(buf)); }; /** * @returns {Buffer} - A buffer of the block */ MerkleBlock.prototype.toBuffer = function toBuffer() { return this.toBufferWriter().concat(); }; /** * @param {BufferWriter} - An existing instance of BufferWriter * @returns {BufferWriter} - An instance of BufferWriter representation of the MerkleBlock */ MerkleBlock.prototype.toBufferWriter = function toBufferWriter(bw) { if (!bw) { bw = new BufferWriter(); } bw.write(this.header.toBuffer()); bw.writeUInt32LE(this.numTransactions); bw.writeVarintNum(this.hashes.length); for (var i = 0; i < this.hashes.length; i++) { bw.write(new Buffer(this.hashes[i], 'hex')); } bw.writeVarintNum(this.flags.length); for (i = 0; i < this.flags.length; i++) { bw.writeUInt8(this.flags[i]); } return bw; }; /** * @returns {Object} - A plain object with the MerkleBlock properties */ MerkleBlock.prototype.toObject = function toObject() { return { header: this.header.toObject(), numTransactions: this.numTransactions, hashes: this.hashes, flags: this.flags }; }; /** * @returns {String} - A JSON string of a MerkleBlock */ MerkleBlock.prototype.toJSON = function toJSON() { return JSON.stringify(this.toObject()); }; /** * Verify that the MerkleBlock is valid * @returns {Boolean} - True/False whether this MerkleBlock is Valid */ MerkleBlock.prototype.validMerkleTree = function validMerkleTree() { $.checkState(_.isArray(this.flags), 'MerkleBlock flags is not an array'); $.checkState(_.isArray(this.hashes), 'MerkleBlock hashes is not an array'); // Can't have more hashes than numTransactions if(this.hashes.length > this.numTransactions) { return false; } // Can't have more flag bits than num hashes if(this.flags.length * 8 < this.hashes.length) { return false; } var height = this._calcTreeHeight(); var opts = { hashesUsed: 0, flagBitsUsed: 0 }; var root = this._traverseMerkleTree(height, 0, opts); if(opts.hashesUsed !== this.hashes.length) { return false; } return BufferUtil.equals(root, this.header.merkleRoot); }; /** * Traverse a the tree in this MerkleBlock, validating it along the way * Modeled after Bitcoin Core merkleblock.cpp TraverseAndExtract() * @param {Number} - depth - Current height * @param {Number} - pos - Current position in the tree * @param {Object} - opts - Object with values that need to be mutated throughout the traversal * @param {Number} - opts.flagBitsUsed - Number of flag bits used, should start at 0 * @param {Number} - opts.hashesUsed - Number of hashes used, should start at 0 * @param {Array} - opts.txs - Will finish populated by transactions found during traversal * @returns {Buffer|null} - Buffer containing the Merkle Hash for that height * @private */ MerkleBlock.prototype._traverseMerkleTree = function traverseMerkleTree(depth, pos, opts) { /* jshint maxcomplexity: 12*/ /* jshint maxstatements: 20 */ opts = opts || {}; opts.txs = opts.txs || []; opts.flagBitsUsed = opts.flagBitsUsed || 0; opts.hashesUsed = opts.hashesUsed || 0; if(opts.flagBitsUsed > this.flags.length * 8) { return null; } var isParentOfMatch = (this.flags[opts.flagBitsUsed >> 3] >>> (opts.flagBitsUsed++ & 7)) & 1; if(depth === 0 || !isParentOfMatch) { if(opts.hashesUsed >= this.hashes.length) { return null; } var hash = this.hashes[opts.hashesUsed++]; if(depth === 0 && isParentOfMatch) { opts.txs.push(hash); } return new Buffer(hash, 'hex'); } else { var left = this._traverseMerkleTree(depth-1, pos*2, opts); var right = left; if(pos*2+1 < this._calcTreeWidth(depth-1)) { right = this._traverseMerkleTree(depth-1, pos*2+1, opts); } return Hash.sha256sha256(new Buffer.concat([left, right])); } }; /** Calculates the width of a merkle tree at a given height. * Modeled after Bitcoin Core merkleblock.h CalcTreeWidth() * @param {Number} - Height at which we want the tree width * @returns {Number} - Width of the tree at a given height * @private */ MerkleBlock.prototype._calcTreeWidth = function calcTreeWidth(height) { return (this.numTransactions + (1 << height) - 1) >> height; }; /** Calculates the height of the merkle tree in this MerkleBlock * @param {Number} - Height at which we want the tree width * @returns {Number} - Height of the merkle tree in this MerkleBlock * @private */ MerkleBlock.prototype._calcTreeHeight = function calcTreeHeight() { var height = 0; while (this._calcTreeWidth(height) > 1) { height++; } return height; }; /** * @param {Transaction|String} - Transaction or Transaction ID Hash * @returns {Boolean} - return true/false if this MerkleBlock has the TX or not * @private */ MerkleBlock.prototype.hasTransaction = function hasTransaction(tx) { $.checkArgument(!_.isUndefined(tx), 'tx cannot be undefined'); $.checkArgument(tx instanceof Transaction || typeof tx === 'string', 'Invalid tx given, tx must be a "string" or "Transaction"'); var hash = tx; if(tx instanceof Transaction) { // We need to reverse the id hash for the lookup hash = BufferUtil.reverse(new Buffer(tx.id, 'hex')).toString('hex'); } var txs = []; var height = this._calcTreeHeight(); this._traverseMerkleTree(height, 0, { txs: txs }); return txs.indexOf(hash) !== -1; }; /** * @param {Buffer} - MerkleBlock data * @returns {Object} - An Object representing merkleblock data * @private */ MerkleBlock._fromBufferReader = function _fromBufferReader(br) { $.checkState(!br.finished(), 'No merkleblock data received'); var info = {}; info.header = BlockHeader.fromBufferReader(br); info.numTransactions = br.readUInt32LE(); var numHashes = br.readVarintNum(); info.hashes = []; for (var i = 0; i < numHashes; i++) { info.hashes.push(br.read(32).toString('hex')); } var numFlags = br.readVarintNum(); info.flags = []; for (i = 0; i < numFlags; i++) { info.flags.push(br.readUInt8()); } return info; }; /** * @param {String|Object} - A JSON or String Object * @returns {Object} - An Object representing merkleblock data * @private */ MerkleBlock._fromJSON = function _fromJSON(data) { if (JSUtil.isValidJSON(data)) { data = JSON.parse(data); } var info = { header: BlockHeader.fromObject(data.header), numTransactions: data.numTransactions, hashes: data.hashes, flags: data.flags, }; return info; }; module.exports = MerkleBlock;