merkletreejs/index.ts

439 lines
11 KiB
TypeScript
Raw Normal View History

import * as reverse from 'buffer-reverse'
import * as CryptoJS from 'crypto-js'
import * as treeify from 'treeify'
2017-07-22 15:31:30 +08:00
2019-06-07 15:45:17 +08:00
interface Options {
2019-06-07 16:00:36 +08:00
/** If set to `true`, an odd node will be duplicated and combined to make a pair to generate the layer hash. */
2019-06-07 15:45:17 +08:00
duplicateOdd: boolean
2019-06-07 16:00:36 +08:00
/** If set to `true`, the leaves will hashed using the set hashing algorithms. */
2019-06-07 15:45:17 +08:00
hashLeaves: boolean
2019-06-07 16:00:36 +08:00
/** 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. */
2019-06-07 15:45:17 +08:00
isBitcoinTree: boolean
2019-06-08 06:15:16 +08:00
/** If set to `true`, the leaves will be sorted. */
sortLeaves: boolean
/** If set to `true`, the hashing pairs will be sorted. */
sortPairs: boolean
2019-06-07 15:45:17 +08:00
}
2019-06-07 16:00:36 +08:00
2017-07-22 15:31:30 +08:00
/**
* Class reprensenting a Merkle Tree
* @namespace MerkleTree
*/
export class MerkleTree {
2019-06-07 15:45:17 +08:00
duplicateOdd: boolean
hashAlgo: (value:any) => any
2019-06-07 15:29:42 +08:00
hashLeaves: boolean
isBitcoinTree: boolean
2019-06-07 15:45:17 +08:00
leaves: any[]
layers: any[]
2019-06-08 06:15:16 +08:00
sortLeaves: boolean
sortPairs: boolean
2017-07-22 15:31:30 +08:00
/**
* @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.
2017-07-22 16:05:52 +08:00
* @param {Buffer[]} leaves - Array of hashed leaves. Each leaf must be a Buffer.
2017-07-22 15:38:06 +08:00
* @param {Function} hashAlgorithm - Algorithm used for hashing leaves and nodes
2017-07-22 15:31:30 +08:00
* @param {Object} options - Additional options
* @example
2019-06-07 15:45:17 +08:00
*```js
*const MerkleTree = require('merkletreejs')
*const crypto = require('crypto')
2017-07-22 15:31:30 +08:00
*
2019-06-07 15:45:17 +08:00
*function sha256(data) {
* // returns Buffer
* return crypto.createHash('sha256').update(data).digest()
*}
2017-07-22 15:31:30 +08:00
*
2019-06-07 15:45:17 +08:00
*const leaves = ['a', 'b', 'c'].map(x => sha3(x))
2017-07-22 15:31:30 +08:00
*
2019-06-07 15:45:17 +08:00
*const tree = new MerkleTree(leaves, sha256)
*```
2017-07-22 15:31:30 +08:00
*/
2019-06-07 15:45:17 +08:00
constructor(leaves, hashAlgorithm, options:Options={} as any) {
2019-06-07 15:29:42 +08:00
this.isBitcoinTree = !!options.isBitcoinTree
this.hashLeaves = !!options.hashLeaves
2019-06-08 06:15:16 +08:00
this.sortLeaves = !!options.sortLeaves
this.sortPairs = !!options.sortPairs
2019-06-07 15:29:42 +08:00
this.duplicateOdd = !!options.duplicateOdd
2018-10-27 03:42:14 +08:00
this.hashAlgo = bufferifyFn(hashAlgorithm)
2019-06-07 15:29:42 +08:00
if (this.hashLeaves) {
leaves = leaves.map(this.hashAlgo)
}
2018-10-27 03:42:14 +08:00
this.leaves = leaves.map(bufferify)
2019-06-08 06:15:16 +08:00
if (this.sortLeaves) {
this.leaves = this.leaves.sort(Buffer.compare)
}
2018-10-27 04:09:38 +08:00
this.layers = [this.leaves]
2017-07-22 15:31:30 +08:00
this.createHashes(this.leaves)
}
2018-12-10 11:57:31 +08:00
// TODO: documentation
2017-07-22 15:31:30 +08:00
createHashes(nodes) {
2018-10-26 13:04:51 +08:00
while (nodes.length > 1) {
2017-07-22 15:31:30 +08:00
const layerIndex = this.layers.length
2017-07-22 15:31:30 +08:00
this.layers.push([])
2017-07-22 15:31:30 +08:00
2019-06-07 15:29:42 +08:00
for (let i = 0; i < nodes.length; i += 2) {
if (i+1 === nodes.length) {
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)
continue
} else {
if (!this.duplicateOdd) {
this.layers[layerIndex].push(nodes[i])
continue
}
}
}
}
const left = nodes[i]
2019-06-07 15:29:42 +08:00
const right = i + 1 == nodes.length ? left : nodes[i + 1];
let data = null
2019-06-08 06:15:16 +08:00
let combined = null
if (this.isBitcoinTree) {
2019-06-08 06:15:16 +08:00
combined = [reverse(left), reverse(right)]
} else {
2019-06-08 06:15:16 +08:00
combined = [left, right]
}
2019-06-07 15:29:42 +08:00
2019-06-08 06:15:16 +08:00
if (this.sortPairs) {
combined.sort(Buffer.compare)
}
2017-07-22 15:31:30 +08:00
2019-06-08 06:15:16 +08:00
data = Buffer.concat(combined)
let hash = this.hashAlgo(data)
2017-07-22 15:31:30 +08:00
// double hash if bitcoin tree
if (this.isBitcoinTree) {
hash = reverse(this.hashAlgo(hash))
}
this.layers[layerIndex].push(hash)
2017-07-22 15:31:30 +08:00
}
nodes = this.layers[layerIndex]
2017-07-22 15:31:30 +08:00
}
}
/**
* getLeaves
2017-07-22 16:05:52 +08:00
* @desc Returns array of leaves of Merkle Tree.
* @return {Buffer[]}
2017-07-22 15:31:30 +08:00
* @example
2019-06-07 15:45:17 +08:00
*```js
*const leaves = tree.getLeaves()
*```
2017-07-22 15:31:30 +08:00
*/
getLeaves() {
return this.leaves
}
/**
* getLayers
2017-07-22 16:05:52 +08:00
* @desc Returns array of all layers of Merkle Tree, including leaves and root.
* @return {Buffer[]}
2017-07-22 15:31:30 +08:00
* @example
2019-06-07 15:45:17 +08:00
*```js
*const layers = tree.getLayers()
*```
2017-07-22 15:31:30 +08:00
*/
getLayers() {
return this.layers
}
/**
* getRoot
2017-07-22 16:05:52 +08:00
* @desc Returns the Merkle root hash as a Buffer.
* @return {Buffer}
2017-07-22 15:31:30 +08:00
* @example
2019-06-07 15:45:17 +08:00
*```js
*const root = tree.getRoot()
*```
2017-07-22 15:31:30 +08:00
*/
getRoot() {
2018-10-26 13:04:51 +08:00
return this.layers[this.layers.length-1][0] || Buffer.from([])
2017-07-22 15:31:30 +08:00
}
2019-06-08 08:41:48 +08:00
// TODO: documentation
getHexRoot() {
return bufferToHex(this.getRoot())
}
2017-07-22 15:31:30 +08:00
/**
* 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.
2019-06-07 15:45:17 +08:00
*@example
* ```js
*const proof = tree.getProof(leaves[2])
*```
2017-07-22 15:31:30 +08:00
*
* @example
2019-06-07 15:45:17 +08:00
*```js
*const leaves = ['a', 'b', 'a'].map(x => sha3(x))
*const tree = new MerkleTree(leaves, sha3)
*const proof = tree.getProof(leaves[2], 2)
*```
2017-07-22 15:31:30 +08:00
*/
getProof(leaf, index?) {
2018-10-30 04:58:49 +08:00
leaf = bufferify(leaf)
const proof = []
2017-07-22 15:31:30 +08:00
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 []
}
2018-07-11 07:31:09 +08:00
if (this.isBitcoinTree && index === (this.leaves.length - 1)) {
2018-07-11 05:29:30 +08:00
// Proof Generation for Bitcoin Trees
2017-07-22 15:31:30 +08:00
2018-07-11 05:29:30 +08:00
for (let i = 0; i < this.layers.length - 1; i++) {
const layer = this.layers[i]
const isRightNode = index % 2
2018-07-11 07:31:09 +08:00
const pairIndex = (isRightNode ? index - 1: index)
2019-06-08 08:29:26 +08:00
const position = isRightNode ? 'left': 'right'
2018-07-11 05:29:30 +08:00
if (pairIndex < layer.length) {
proof.push({
data: layer[pairIndex]
})
}
// set index to parent index
index = (index / 2)|0
2019-06-07 15:29:42 +08:00
}
2018-07-11 05:29:30 +08:00
2019-06-07 15:29:42 +08:00
return proof
} else {
2018-07-11 05:29:30 +08:00
2019-06-07 15:29:42 +08:00
// Proof Generation for Non-Bitcoin Trees
2018-07-11 05:29:30 +08:00
2019-06-07 15:29:42 +08:00
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)
2018-07-11 05:29:30 +08:00
2019-06-07 15:29:42 +08:00
if (pairIndex < layer.length) {
proof.push({
position: isRightNode ? 'left': 'right',
data: layer[pairIndex]
})
}
2018-07-11 05:29:30 +08:00
2019-06-07 15:29:42 +08:00
// set index to parent index
index = (index / 2)|0
2018-07-11 05:29:30 +08:00
2019-06-07 15:29:42 +08:00
}
2018-07-11 05:29:30 +08:00
2019-06-07 15:29:42 +08:00
return proof
}
}
2017-07-22 15:31:30 +08:00
2019-06-08 08:41:48 +08:00
// TODO: documentation
getHexProof(leaf, index?) {
return this.getProof(leaf, index).map(x => bufferToHex(x.data))
}
2017-07-22 15:31:30 +08:00
/**
* 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
2017-07-22 15:31:30 +08:00
* target node to Merkle root.
* @param {Buffer} targetNode - Target node Buffer
* @param {Buffer} root - Merkle root Buffer
* @return {Boolean}
* @example
2019-06-07 15:45:17 +08:00
*```js
*const root = tree.getRoot()
*const proof = tree.getProof(leaves[2])
*const verified = tree.verify(proof, leaves[2], root)
*```
2017-07-22 15:31:30 +08:00
*/
verify(proof, targetNode, root) {
2018-10-27 04:09:38 +08:00
let hash = bufferify(targetNode)
2018-12-10 11:10:43 +08:00
root = bufferify(root)
2017-07-22 15:31:30 +08:00
if (!Array.isArray(proof) ||
2019-06-07 15:29:42 +08:00
!proof.length ||
!targetNode ||
!root) {
2017-07-22 15:31:30 +08:00
return false
}
for (let i = 0; i < proof.length; i++) {
const node = proof[i]
2019-06-17 08:14:53 +08:00
let data = null
let isLeftNode = null
// NOTE: case for when proof is hex values only
if (typeof node === 'string') {
data = bufferify(node)
isLeftNode = true
} else {
data = node.data
isLeftNode = (node.position === 'left')
}
2017-07-22 15:31:30 +08:00
const buffers = []
if (this.isBitcoinTree) {
buffers.push(reverse(hash))
2019-06-17 08:14:53 +08:00
buffers[isLeftNode ? 'unshift' : 'push'](reverse(data))
2017-07-22 15:31:30 +08:00
hash = this.hashAlgo(Buffer.concat(buffers))
hash = reverse(this.hashAlgo(hash))
2018-07-11 07:31:09 +08:00
2017-07-22 15:31:30 +08:00
} else {
2019-06-08 08:29:26 +08:00
if (this.sortPairs) {
2019-06-17 08:14:53 +08:00
if (Buffer.compare(hash, data) === -1) {
buffers.push(hash, data)
2019-06-08 08:29:26 +08:00
hash = this.hashAlgo(Buffer.concat(buffers));
} else {
2019-06-17 08:14:53 +08:00
buffers.push(data, hash)
2019-06-08 08:29:26 +08:00
hash = this.hashAlgo(Buffer.concat(buffers));
}
} else {
buffers.push(hash);
2019-06-17 08:14:53 +08:00
buffers[isLeftNode ? 'unshift' : 'push'](data);
2019-06-08 08:29:26 +08:00
hash = this.hashAlgo(Buffer.concat(buffers));
}
2017-07-22 15:31:30 +08:00
}
}
return Buffer.compare(hash, root) === 0
}
2018-10-27 04:09:38 +08:00
2018-12-10 11:57:31 +08:00
// TODO: documentation
getLayersAsObject() {
const layers = this.getLayers().map(x => x.map(x => x.toString('hex')))
2018-12-10 11:10:43 +08:00
const objs = []
for (let i = 0; i < layers.length; i++) {
const arr = []
for (let j = 0; j < layers[i].length; j++) {
2018-12-10 11:57:31 +08:00
const obj = { [layers[i][j]]: null }
2018-12-10 11:10:43 +08:00
if (objs.length) {
2018-12-10 11:57:31 +08:00
obj[layers[i][j]] = {}
2018-12-10 11:10:43 +08:00
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)
}
2018-12-10 11:57:31 +08:00
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)
}
2018-12-10 11:10:43 +08:00
2018-12-10 11:57:31 +08:00
// TODO: documentation
static print(tree) {
console.log(tree.toString())
2018-12-10 11:10:43 +08:00
}
2017-07-22 15:31:30 +08:00
}
2019-06-08 08:41:48 +08:00
function bufferToHex(value:Buffer) {
return '0x'+value.toString('hex')
}
2018-10-27 03:42:14 +08:00
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)) {
2019-06-17 07:47:38 +08:00
return Buffer.from(x.replace(/^0x/, ''), 'hex')
2018-10-27 03:42:14 +08:00
} 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
}
2019-06-17 08:14:53 +08:00
if (isHexStr(v)) {
return Buffer.from(v, 'hex')
}
// crypto-js support
return Buffer.from(f(CryptoJS.enc.Hex.parse(x.toString('hex'))).toString(CryptoJS.enc.Hex), 'hex')
}
}
2018-12-10 11:57:31 +08:00
function isHexStr(v) {
2018-10-27 03:42:14 +08:00
return (typeof v === 'string' && /^(0x)?[0-9A-Fa-f]*$/.test(v))
}
export default MerkleTree