@dashevo/dashcore-lib
Version:
A pure and powerful JavaScript Dash library.
322 lines (296 loc) • 9.81 kB
JavaScript
const _ = require('lodash');
const BlockHeader = require('./blockheader');
const BufferUtil = require('../util/buffer');
const BufferReader = require('../encoding/bufferreader');
const BufferWriter = require('../encoding/bufferwriter');
const Hash = require('../crypto/hash');
const Transaction = require('../transaction');
const PartialMerkleTree = require('./PartialMerkleTree');
const $ = require('../util/preconditions');
/**
* Instantiate a MerkleBlock from a Buffer, JSON object, or Object with
* the properties of the Block
*
* @param {Buffer|string|{
* header: BlockHeader|Object,
* numTransactions: number,
* hashes: string[],
* flags: number[]
* }} arg 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);
}
let info = {};
if (BufferUtil.isBuffer(arg)) {
info = MerkleBlock._fromBufferReader(BufferReader(arg));
} else if (_.isObject(arg)) {
let header;
if (arg.header instanceof BlockHeader) {
// eslint-disable-next-line prefer-destructuring
header = arg.header;
} else {
header = BlockHeader.fromObject(arg.header);
}
info = {
/**
* @name MerkleBlock#header
* @type {BlockHeader}
*/
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;
}
/**
* Builds merkle block from block header, transaction hashes and filter matches
* @param {BlockHeader|Object} header
* @param {Buffer[]} transactionHashes
* @param {boolean[]} filterMatches
* @return {MerkleBlock}
*/
MerkleBlock.build = function build(header, transactionHashes, filterMatches) {
const partialTree = new PartialMerkleTree({
transactionHashes,
filterMatches,
});
return new MerkleBlock({
header,
numTransactions: partialTree.totalTransactions,
hashes: partialTree.merkleHashes,
flags: partialTree.merkleFlags,
});
};
/**
* @param {Buffer} buf - MerkleBlock data in a Buffer object
* @returns {MerkleBlock} - A MerkleBlock object
*/
MerkleBlock.fromBuffer = function fromBuffer(buf) {
return MerkleBlock.fromBufferReader(BufferReader(buf));
};
/**
* @param {BufferReader} br - MerkleBlock data in a BufferReader object
* @returns {MerkleBlock} - A MerkleBlock object
*/
MerkleBlock.fromBufferReader = function fromBufferReader(br) {
return new MerkleBlock(MerkleBlock._fromBufferReader(br));
};
/**
* @returns {Buffer} - A buffer of the block
*/
MerkleBlock.prototype.toBuffer = function toBuffer() {
return this.toBufferWriter().toBuffer();
};
/**
* @param {BufferWriter} bw - An existing instance of BufferWriter
* @returns {BufferWriter} - An instance of BufferWriter representation of the MerkleBlock
*/
MerkleBlock.prototype.toBufferWriter = function toBufferWriter(bw) {
const bufferWriter = bw || new BufferWriter();
bufferWriter.write(this.header.toBuffer());
bufferWriter.writeUInt32LE(this.numTransactions);
bufferWriter.writeVarintNum(this.hashes.length);
for (let i = 0; i < this.hashes.length; i += 1) {
bufferWriter.write(Buffer.from(this.hashes[i], 'hex'));
}
bufferWriter.writeVarintNum(this.flags.length);
for (let i = 0; i < this.flags.length; i += 1) {
bufferWriter.writeUInt8(this.flags[i]);
}
return bufferWriter;
};
/**
* @function
* @returns {Object} - A plain object with the MerkleBlock properties
*/
MerkleBlock.prototype.toJSON = function toObject() {
return {
header: this.header.toObject(),
numTransactions: this.numTransactions,
hashes: this.hashes,
flags: this.flags,
};
};
/**
* @function
* @returns {Object} - A plain object with the MerkleBlock properties
*/
MerkleBlock.prototype.toObject = MerkleBlock.prototype.toJSON;
/**
* 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;
}
const height = this._calcTreeHeight();
const opts = { hashesUsed: 0, flagBitsUsed: 0 };
const root = this._traverseMerkleTree(height, 0, opts);
if (opts.hashesUsed !== this.hashes.length) {
return false;
}
return BufferUtil.equals(root, this.header.merkleRoot);
};
/**
* Traverse 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 */
const options = opts || {};
options.txs = opts.txs || [];
options.flagBitsUsed = opts.flagBitsUsed || 0;
options.hashesUsed = opts.hashesUsed || 0;
if (options.flagBitsUsed > this.flags.length * 8) {
return null;
}
const isParentOfMatch =
// eslint-disable-next-line no-bitwise
(this.flags[options.flagBitsUsed >> 3] >>>
// eslint-disable-next-line no-plusplus, no-bitwise
(options.flagBitsUsed++ & 7)) &
1;
if (depth === 0 || !isParentOfMatch) {
if (options.hashesUsed >= this.hashes.length) {
return null;
}
// eslint-disable-next-line no-plusplus
const hash = this.hashes[options.hashesUsed++];
if (depth === 0 && isParentOfMatch) {
options.txs.push(hash);
}
return Buffer.from(hash, 'hex');
}
const left = this._traverseMerkleTree(depth - 1, pos * 2, options);
let right = left;
if (pos * 2 + 1 < this._calcTreeWidth(depth - 1)) {
right = this._traverseMerkleTree(depth - 1, pos * 2 + 1, options);
}
return Hash.sha256sha256(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 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) {
// eslint-disable-next-line no-bitwise
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() {
let height = 0;
while (this._calcTreeWidth(height) > 1) {
height += 1;
}
return height;
};
/**
* @return {string[]}
*/
MerkleBlock.prototype.getMatchedTransactionHashes =
function getMatchedTransactionHashes() {
const txs = [];
const height = this._calcTreeHeight();
this._traverseMerkleTree(height, 0, { txs });
return txs;
};
/**
* @param {Transaction|String} tx 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"'
);
let hash = tx;
if (tx instanceof Transaction) {
// We need to reverse the id hash for the lookup
hash = BufferUtil.reverse(Buffer.from(tx.id, 'hex')).toString('hex');
}
const txs = this.getMatchedTransactionHashes();
return txs.indexOf(hash) !== -1;
};
/**
* @param {Buffer} br - MerkleBlock data
* @returns {Object} - An Object representing merkleblock data
* @private
*/
MerkleBlock._fromBufferReader = function _fromBufferReader(br) {
$.checkState(!br.finished(), 'No merkleblock data received');
const info = {};
info.header = BlockHeader.fromBufferReader(br);
info.numTransactions = br.readUInt32LE();
const numHashes = br.readVarintNum();
info.hashes = [];
for (let i = 0; i < numHashes; i += 1) {
info.hashes.push(br.read(32).toString('hex'));
}
const numFlags = br.readVarintNum();
info.flags = [];
for (let i = 0; i < numFlags; i += 1) {
info.flags.push(br.readUInt8());
}
return info;
};
/**
* @param {Object} obj - A plain JavaScript object
* @returns {MerkleBlock} - An instance of MerkleBlock
*/
MerkleBlock.fromObject = function fromObject(obj) {
return new MerkleBlock(obj);
};
module.exports = MerkleBlock;