347 lines
8.7 KiB
JavaScript
347 lines
8.7 KiB
JavaScript
const reverse = require('buffer-reverse')
|
|
const CryptoJS = require('crypto-js')
|
|
const treeify = require('treeify')
|
|
|
|
/**
|
|
* Class reprensenting a Merkle Tree
|
|
* @namespace MerkleTree
|
|
*/
|
|
class MerkleTree {
|
|
/**
|
|
* @desc Constructs a Merkle Tree.
|
|
* All nodes and leaves are stored as Buffers.
|
|
* Lonely leaf nodes are promoted to the next level up without being hashed again.
|
|
* @param {Buffer[]} leaves - Array of hashed leaves. Each leaf must be a Buffer.
|
|
* @param {Function} hashAlgorithm - Algorithm used for hashing leaves and nodes
|
|
* @param {Object} options - Additional options
|
|
* @param {Boolean} options.isBitcoinTree - If set to `true`, constructs the Merkle
|
|
* Tree using the [Bitcoin Merkle Tree implementation](http://www.righto.com/2014/02/bitcoin-mining-hard-way-algorithms.html). Enable it when you need
|
|
* to replicate Bitcoin constructed Merkle Trees. In Bitcoin Merkle Trees, single nodes are combined with themselves, and each output hash is hashed again.
|
|
* @example
|
|
* const MerkleTree = require('merkletreejs')
|
|
* const crypto = require('crypto')
|
|
*
|
|
* function sha256(data) {
|
|
* // returns Buffer
|
|
* return crypto.createHash('sha256').update(data).digest()
|
|
* }
|
|
*
|
|
* const leaves = ['a', 'b', 'c'].map(x => sha3(x))
|
|
*
|
|
* const tree = new MerkleTree(leaves, sha256)
|
|
*/
|
|
constructor(leaves, hashAlgorithm, options={}) {
|
|
this.hashAlgo = bufferifyFn(hashAlgorithm)
|
|
this.leaves = leaves.map(bufferify)
|
|
this.layers = [this.leaves]
|
|
this.isBitcoinTree = !!options.isBitcoinTree
|
|
|
|
this.createHashes(this.leaves)
|
|
}
|
|
|
|
// TODO: documentation
|
|
createHashes(nodes) {
|
|
while (nodes.length > 1) {
|
|
|
|
const layerIndex = this.layers.length
|
|
|
|
this.layers.push([])
|
|
|
|
for (let i = 0; i < nodes.length - 1; i += 2) {
|
|
const left = nodes[i]
|
|
const right = nodes[i+1]
|
|
let data = null
|
|
|
|
if (this.isBitcoinTree) {
|
|
data = Buffer.concat([reverse(left), reverse(right)])
|
|
} else {
|
|
data = Buffer.concat([left, right])
|
|
}
|
|
|
|
let hash = this.hashAlgo(data)
|
|
|
|
// double hash if bitcoin tree
|
|
if (this.isBitcoinTree) {
|
|
hash = reverse(this.hashAlgo(hash))
|
|
}
|
|
|
|
this.layers[layerIndex].push(hash)
|
|
}
|
|
|
|
// is odd number of nodes
|
|
if (nodes.length % 2 === 1) {
|
|
let data = nodes[nodes.length-1]
|
|
let hash = data
|
|
|
|
// is bitcoin tree
|
|
if (this.isBitcoinTree) {
|
|
// Bitcoin method of duplicating the odd ending nodes
|
|
data = Buffer.concat([reverse(data), reverse(data)])
|
|
hash = this.hashAlgo(data)
|
|
hash = reverse(this.hashAlgo(hash))
|
|
}
|
|
|
|
this.layers[layerIndex].push(hash)
|
|
}
|
|
|
|
nodes = this.layers[layerIndex]
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* getLeaves
|
|
* @desc Returns array of leaves of Merkle Tree.
|
|
* @return {Buffer[]}
|
|
* @example
|
|
* const leaves = tree.getLeaves()
|
|
*/
|
|
getLeaves() {
|
|
return this.leaves
|
|
}
|
|
|
|
/**
|
|
* getLayers
|
|
* @desc Returns array of all layers of Merkle Tree, including leaves and root.
|
|
* @return {Buffer[]}
|
|
* @example
|
|
* const layers = tree.getLayers()
|
|
*/
|
|
getLayers() {
|
|
return this.layers
|
|
}
|
|
|
|
/**
|
|
* getRoot
|
|
* @desc Returns the Merkle root hash as a Buffer.
|
|
* @return {Buffer}
|
|
* @example
|
|
* const root = tree.getRoot()
|
|
*/
|
|
getRoot() {
|
|
return this.layers[this.layers.length-1][0] || Buffer.from([])
|
|
}
|
|
|
|
/**
|
|
* getProof
|
|
* @desc Returns the proof for a target leaf.
|
|
* @param {Buffer} leaf - Target leaf
|
|
* @param {Number} [index] - Target leaf index in leaves array.
|
|
* Use if there are leaves containing duplicate data in order to distinguish it.
|
|
* @return {Object[]} - Array of objects containing a position property of type string
|
|
* with values of 'left' or 'right' and a data property of type Buffer.
|
|
* @example
|
|
* const proof = tree.getProof(leaves[2])
|
|
*
|
|
* @example
|
|
* const leaves = ['a', 'b', 'a'].map(x => sha3(x))
|
|
* const tree = new MerkleTree(leaves, sha3)
|
|
* const proof = tree.getProof(leaves[2], 2)
|
|
*/
|
|
getProof(leaf, index) {
|
|
leaf = bufferify(leaf)
|
|
const proof = []
|
|
|
|
if (typeof index !== 'number') {
|
|
index = -1
|
|
|
|
for (let i = 0; i < this.leaves.length; i++) {
|
|
if (Buffer.compare(leaf, this.leaves[i]) === 0) {
|
|
index = i
|
|
}
|
|
}
|
|
}
|
|
|
|
if (index <= -1) {
|
|
return []
|
|
}
|
|
|
|
if (this.isBitcoinTree && index === (this.leaves.length - 1)) {
|
|
|
|
// Proof Generation for Bitcoin Trees
|
|
|
|
for (let i = 0; i < this.layers.length - 1; i++) {
|
|
const layer = this.layers[i]
|
|
const isRightNode = index % 2
|
|
const pairIndex = (isRightNode ? index - 1: index)
|
|
|
|
if (pairIndex < layer.length) {
|
|
proof.push({
|
|
position: isRightNode ? 'left': 'right',
|
|
data: layer[pairIndex]
|
|
})
|
|
}
|
|
|
|
// set index to parent index
|
|
index = (index / 2)|0
|
|
|
|
}
|
|
|
|
return proof
|
|
|
|
} else {
|
|
|
|
// Proof Generation for Non-Bitcoin Trees
|
|
|
|
for (let i = 0; i < this.layers.length; i++) {
|
|
const layer = this.layers[i]
|
|
const isRightNode = index % 2
|
|
const pairIndex = (isRightNode ? index - 1 : index + 1)
|
|
|
|
if (pairIndex < layer.length) {
|
|
proof.push({
|
|
position: isRightNode ? 'left': 'right',
|
|
data: layer[pairIndex]
|
|
})
|
|
}
|
|
|
|
// set index to parent index
|
|
index = (index / 2)|0
|
|
|
|
}
|
|
|
|
return proof
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* verify
|
|
* @desc Returns true if the proof path (array of hashes) can connect the target node
|
|
* to the Merkle root.
|
|
* @param {Object[]} proof - Array of proof objects that should connect
|
|
* target node to Merkle root.
|
|
* @param {Buffer} targetNode - Target node Buffer
|
|
* @param {Buffer} root - Merkle root Buffer
|
|
* @return {Boolean}
|
|
* @example
|
|
* const root = tree.getRoot()
|
|
* const proof = tree.getProof(leaves[2])
|
|
* const verified = tree.verify(proof, leaves[2], root)
|
|
*
|
|
*/
|
|
verify(proof, targetNode, root) {
|
|
let hash = bufferify(targetNode)
|
|
root = bufferify(root)
|
|
|
|
if (!Array.isArray(proof) ||
|
|
!proof.length ||
|
|
!targetNode ||
|
|
!root) {
|
|
return false
|
|
}
|
|
|
|
for (let i = 0; i < proof.length; i++) {
|
|
const node = proof[i]
|
|
const isLeftNode = (node.position === 'left')
|
|
const buffers = []
|
|
|
|
if (this.isBitcoinTree) {
|
|
buffers.push(reverse(hash))
|
|
|
|
buffers[isLeftNode ? 'unshift' : 'push'](reverse(node.data))
|
|
|
|
hash = this.hashAlgo(Buffer.concat(buffers))
|
|
hash = reverse(this.hashAlgo(hash))
|
|
|
|
} else {
|
|
buffers.push(hash)
|
|
|
|
buffers[isLeftNode ? 'unshift' : 'push'](node.data)
|
|
|
|
hash = this.hashAlgo(Buffer.concat(buffers))
|
|
}
|
|
}
|
|
|
|
return Buffer.compare(hash, root) === 0
|
|
}
|
|
|
|
// TODO: documentation
|
|
getLayersAsObject() {
|
|
const layers = this.getLayers().map(x => x.map(x => x.toString('hex')))
|
|
const objs = []
|
|
for (let i = 0; i < layers.length; i++) {
|
|
const arr = []
|
|
for (let j = 0; j < layers[i].length; j++) {
|
|
const obj = { [layers[i][j]]: null }
|
|
if (objs.length) {
|
|
obj[layers[i][j]] = {}
|
|
const a = objs.shift()
|
|
const akey = Object.keys(a)[0]
|
|
obj[layers[i][j]][akey] = a[akey]
|
|
if (objs.length) {
|
|
const b = objs.shift()
|
|
const bkey = Object.keys(b)[0]
|
|
obj[layers[i][j]][bkey] = b[bkey]
|
|
}
|
|
}
|
|
|
|
arr.push(obj)
|
|
}
|
|
|
|
objs.push(...arr)
|
|
}
|
|
|
|
return objs[0]
|
|
}
|
|
|
|
// TODO: documentation
|
|
print() {
|
|
MerkleTree.print(this)
|
|
}
|
|
|
|
// TODO: documentation
|
|
toTreeString() {
|
|
const obj = this.getLayersAsObject()
|
|
return treeify.asTree(obj, true)
|
|
}
|
|
|
|
// TODO: documentation
|
|
toString() {
|
|
return this.toTreeString()
|
|
}
|
|
|
|
// TODO: documentation
|
|
static bufferify(x) {
|
|
return bufferify(x)
|
|
}
|
|
|
|
// TODO: documentation
|
|
static print(tree) {
|
|
console.log(tree.toString())
|
|
}
|
|
}
|
|
|
|
function bufferify(x) {
|
|
if (!Buffer.isBuffer(x)) {
|
|
// crypto-js support
|
|
if (typeof x === 'object' && x.words) {
|
|
return Buffer.from(x.toString(CryptoJS.enc.Hex), 'hex')
|
|
} else if (isHexStr(x)) {
|
|
return Buffer.from(x, 'hex')
|
|
} else if (typeof x === 'string') {
|
|
return Buffer.from(x)
|
|
}
|
|
}
|
|
|
|
return x
|
|
}
|
|
|
|
function bufferifyFn (f) {
|
|
return function (x) {
|
|
const v = f(x)
|
|
if (Buffer.isBuffer(v)) {
|
|
return v
|
|
}
|
|
|
|
// crypto-js support
|
|
return Buffer.from(f(CryptoJS.enc.Hex.parse(x.toString('hex'))).toString(CryptoJS.enc.Hex), 'hex')
|
|
}
|
|
}
|
|
|
|
function isHexStr(v) {
|
|
return (typeof v === 'string' && /^(0x)?[0-9A-Fa-f]*$/.test(v))
|
|
}
|
|
|
|
module.exports = MerkleTree
|