Add Merkle Mountain Range (MMR) tree class
This commit is contained in:
parent
ffd68dde08
commit
8deaa7fc53
|
@ -65,9 +65,11 @@
|
|||
"node": ">= 7.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bignumber.js": "^9.0.1",
|
||||
"buffer-reverse": "^1.0.1",
|
||||
"crypto-js": "^3.1.9-1",
|
||||
"treeify": "^1.1.0"
|
||||
"treeify": "^1.1.0",
|
||||
"web3-utils": "^1.3.4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"rules": {
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
import CryptoJS from 'crypto-js'
|
||||
|
||||
export class Base {
|
||||
/**
|
||||
* print
|
||||
* @desc Prints out a visual representation of the merkle tree.
|
||||
* @example
|
||||
*```js
|
||||
*tree.print()
|
||||
*```
|
||||
*/
|
||||
print ():void {
|
||||
Base.print(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* bufferIndexOf
|
||||
* @desc Returns the first index of which given buffer is found in array.
|
||||
* @param {Buffer[]} haystack - Array of buffers.
|
||||
* @param {Buffer} needle - Buffer to find.
|
||||
* @return {Number} - Index number
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*const index = tree.bufferIndexOf(haystack, needle)
|
||||
*```
|
||||
*/
|
||||
protected _bufferIndexOf (array: Buffer[], element: Buffer):number {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (element.equals(array[i])) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* bufferify
|
||||
* @desc Returns a buffer type for the given value.
|
||||
* @param {String|Number|Object|Buffer} value
|
||||
* @return {Buffer}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*const buf = MerkleTree.bufferify('0x1234')
|
||||
*```
|
||||
*/
|
||||
static bufferify (value: any):Buffer {
|
||||
if (!Buffer.isBuffer(value)) {
|
||||
// crypto-js support
|
||||
if (typeof value === 'object' && value.words) {
|
||||
return Buffer.from(value.toString(CryptoJS.enc.Hex), 'hex')
|
||||
} else if (Base.isHexString(value)) {
|
||||
return Buffer.from(value.replace(/^0x/, ''), 'hex')
|
||||
} else if (typeof value === 'string') {
|
||||
return Buffer.from(value)
|
||||
} else if (typeof value === 'number') {
|
||||
let s = value.toString()
|
||||
if (s.length % 2) {
|
||||
s = `0${s}`
|
||||
}
|
||||
return Buffer.from(s, 'hex')
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* isHexString
|
||||
* @desc Returns true if value is a hex string.
|
||||
* @param {String} value
|
||||
* @return {Boolean}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*console.log(MerkleTree.isHexString('0x1234'))
|
||||
*```
|
||||
*/
|
||||
static isHexString (v: string):boolean {
|
||||
return (typeof v === 'string' && /^(0x)?[0-9A-Fa-f]*$/.test(v))
|
||||
}
|
||||
|
||||
/**
|
||||
* print
|
||||
* @desc Prints out a visual representation of the given merkle tree.
|
||||
* @param {Object} tree - Merkle tree instance.
|
||||
* @return {String}
|
||||
* @example
|
||||
*```js
|
||||
*MerkleTree.print(tree)
|
||||
*```
|
||||
*/
|
||||
static print (tree: any):void {
|
||||
console.log(tree.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* bufferToHex
|
||||
* @desc Returns a hex string with 0x prefix for given buffer.
|
||||
* @param {Buffer} value
|
||||
* @return {String}
|
||||
* @example
|
||||
*```js
|
||||
*const hexStr = tree.bufferToHex(Buffer.from('A'))
|
||||
*```
|
||||
*/
|
||||
bufferToHex (value: Buffer, withPrefix: boolean = true):string {
|
||||
return Base.bufferToHex(value, withPrefix)
|
||||
}
|
||||
|
||||
/**
|
||||
* bufferToHex
|
||||
* @desc Returns a hex string with 0x prefix for given buffer.
|
||||
* @param {Buffer} value
|
||||
* @return {String}
|
||||
* @example
|
||||
*```js
|
||||
*const hexStr = MerkleTree.bufferToHex(Buffer.from('A'))
|
||||
*```
|
||||
*/
|
||||
static bufferToHex (value: Buffer, withPrefix: boolean = true):string {
|
||||
return `${withPrefix ? '0x' : ''}${(value || Buffer.alloc(0)).toString('hex')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* bufferify
|
||||
* @desc Returns a buffer type for the given value.
|
||||
* @param {String|Number|Object|Buffer} value
|
||||
* @return {Buffer}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*const buf = tree.bufferify('0x1234')
|
||||
*```
|
||||
*/
|
||||
bufferify (value: any):Buffer {
|
||||
return Base.bufferify(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* bufferifyFn
|
||||
* @desc Returns a function that will bufferify the return value.
|
||||
* @param {Function}
|
||||
* @return {Function}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*const fn = tree.bufferifyFn((value) => sha256(value))
|
||||
*```
|
||||
*/
|
||||
protected _bufferifyFn (f: any):any {
|
||||
return function (value: any) {
|
||||
const v = f(value)
|
||||
if (Buffer.isBuffer(v)) {
|
||||
return v
|
||||
}
|
||||
|
||||
if (this._isHexString(v)) {
|
||||
return Buffer.from(v, 'hex')
|
||||
}
|
||||
|
||||
// crypto-js support
|
||||
return Buffer.from(f(CryptoJS.enc.Hex.parse(value.toString('hex'))).toString(CryptoJS.enc.Hex), 'hex')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* isHexString
|
||||
* @desc Returns true if value is a hex string.
|
||||
* @param {String} value
|
||||
* @return {Boolean}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*console.log(MerkleTree.isHexString('0x1234'))
|
||||
*```
|
||||
*/
|
||||
protected _isHexString (value: string):boolean {
|
||||
return Base.isHexString(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* log2
|
||||
* @desc Returns the log2 of number.
|
||||
* @param {Number} value
|
||||
* @return {Number}
|
||||
*/
|
||||
protected _log2 (n: number):number {
|
||||
return n === 1 ? 0 : 1 + this._log2((n / 2) | 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* zip
|
||||
* @desc Returns true if value is a hex string.
|
||||
* @param {String[]|Number[]|Buffer[]} a - first array
|
||||
* @param {String[]|Number[]|Buffer[]} b - second array
|
||||
* @return {String[][]|Number[][]|Buffer[][]}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*const zipped = tree.zip(['a', 'b'],['A', 'B'])
|
||||
*console.log(zipped) // [ [ 'a', 'A' ], [ 'b', 'B' ] ]
|
||||
*```
|
||||
*/
|
||||
protected _zip (a: any[], b: any[]):any[][] {
|
||||
return a.map((e, i) => [e, b[i]])
|
||||
}
|
||||
}
|
||||
|
||||
export default Base
|
|
@ -0,0 +1,348 @@
|
|||
import Base from './Base'
|
||||
import SHA256 from 'crypto-js/sha256'
|
||||
|
||||
// @credit: https://github.com/wanseob/solidity-mmr
|
||||
export class MerkleMountainRange extends Base {
|
||||
root: Buffer = Buffer.alloc(0)
|
||||
size: number = 0
|
||||
width: number = 0
|
||||
hashes: any = {}
|
||||
data: any = {}
|
||||
hashLeafFn: any
|
||||
peakBaggingFn: any
|
||||
hashBranchFn: any
|
||||
private hashAlgo: any
|
||||
|
||||
constructor (hashAlgorithm = SHA256, leaves: any[] = [], hashLeafFn?: any, peakBaggingFn?: any, hashBranchFn?: any) {
|
||||
super()
|
||||
leaves = leaves.map(this.bufferify)
|
||||
this.hashAlgo = this._bufferifyFn(hashAlgorithm)
|
||||
this.hashLeafFn = hashLeafFn
|
||||
this.peakBaggingFn = peakBaggingFn
|
||||
this.hashBranchFn = hashBranchFn
|
||||
|
||||
for (const leaf of leaves) {
|
||||
this.append(leaf)
|
||||
}
|
||||
}
|
||||
|
||||
append (data: any) {
|
||||
data = this.bufferify(data)
|
||||
const dataHash = this.hashAlgo(data)
|
||||
if (this.bufferToHex(this.hashAlgo(this.data[dataHash])) !== this.bufferToHex(dataHash)) {
|
||||
this.data[this.bufferToHex(dataHash)] = data
|
||||
}
|
||||
|
||||
const leaf = this.hashLeaf(this.size + 1, dataHash)
|
||||
this.hashes[this.size + 1] = leaf
|
||||
this.width += 1
|
||||
|
||||
// find peaks for enlarged tree
|
||||
const peakIndexes = this.getPeakIndexes(this.width)
|
||||
|
||||
// the right most peak's value is the new size of the updated tree
|
||||
this.size = this.getSize(this.width)
|
||||
|
||||
// starting from the left-most peak, get all peak hashes
|
||||
const peaks = []
|
||||
for (let i = 0; i < peakIndexes.length; i++) {
|
||||
peaks[i] = this._getOrCreateNode(peakIndexes[i])
|
||||
}
|
||||
|
||||
// update the tree root hash
|
||||
this.root = this.peakBagging(this.width, peaks)
|
||||
}
|
||||
|
||||
hashLeaf (index: number, dataHash: any) {
|
||||
if (this.hashLeafFn) {
|
||||
return this.bufferify(this.hashLeafFn(index, dataHash))
|
||||
}
|
||||
return this.hashAlgo(Buffer.concat([this.bufferify(index), this.bufferify(dataHash)]))
|
||||
}
|
||||
|
||||
hashBranch (index: number, left: any, right: any): any {
|
||||
if (this.hashBranchFn) {
|
||||
return this.bufferify(this.hashBranchFn(index, left, right))
|
||||
}
|
||||
return this.hashAlgo(Buffer.concat([this.bufferify(index), this.bufferify(left), this.bufferify(right)]))
|
||||
}
|
||||
|
||||
getPeaks () {
|
||||
const peakIndexes = this.getPeakIndexes(this.width)
|
||||
const peaks = []
|
||||
for (let i = 0; i < peakIndexes.length; i++) {
|
||||
peaks[i] = this.hashes[peakIndexes[i]]
|
||||
}
|
||||
return peaks
|
||||
}
|
||||
|
||||
getLeafIndex (width: number) {
|
||||
if (width % 2 === 1) {
|
||||
return this.getSize(width)
|
||||
}
|
||||
|
||||
return this.getSize(width - 1) + 1
|
||||
}
|
||||
|
||||
getPeakIndexes (width: number): number[] {
|
||||
const numPeaks = this.numOfPeaks(width)
|
||||
const peakIndexes = []
|
||||
let count = 0
|
||||
let size = 0
|
||||
for (let i = 255; i > 0; i--) {
|
||||
if ((width & (1 << (i - 1))) !== 0) {
|
||||
// peak exists
|
||||
size = size + (1 << i) - 1
|
||||
peakIndexes[count++] = size
|
||||
if (peakIndexes.length >= numPeaks) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count !== peakIndexes.length) {
|
||||
throw new Error('Invalid bit calculation')
|
||||
}
|
||||
|
||||
return peakIndexes
|
||||
}
|
||||
|
||||
numOfPeaks (width: number): number {
|
||||
let bits = width
|
||||
let num = 0
|
||||
while (bits > 0) {
|
||||
if (bits % 2 === 1) {
|
||||
num++
|
||||
}
|
||||
bits = bits >> 1
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
peakBagging (width: number, peaks: any[]): any {
|
||||
const size = this.getSize(width)
|
||||
if (this.numOfPeaks(width) !== peaks.length) {
|
||||
throw new Error('Received invalid number of peaks')
|
||||
}
|
||||
|
||||
if (this.peakBaggingFn) {
|
||||
return this.bufferify(this.peakBaggingFn(size, peaks))
|
||||
}
|
||||
|
||||
return this.hashAlgo(Buffer.concat([this.bufferify(size), ...peaks.map(this.bufferify)]))
|
||||
}
|
||||
|
||||
getSize (width: number): number {
|
||||
return (width << 1) - this.numOfPeaks(width)
|
||||
}
|
||||
|
||||
getRoot (): any {
|
||||
return this.root
|
||||
}
|
||||
|
||||
getHexRoot (): any {
|
||||
return this.bufferToHex(this.getRoot())
|
||||
}
|
||||
|
||||
getNode (index: number): any {
|
||||
return this.hashes[index]
|
||||
}
|
||||
|
||||
mountainHeight (size: number): number {
|
||||
let height = 1
|
||||
while (1 << height <= size + height) {
|
||||
height++
|
||||
}
|
||||
return height - 1
|
||||
}
|
||||
|
||||
heightAt (index: number): number {
|
||||
let reducedIndex = index
|
||||
let peakIndex = 0
|
||||
let height = 0
|
||||
|
||||
// if an index has a left mountain then subtract the mountain
|
||||
while (reducedIndex > peakIndex) {
|
||||
reducedIndex -= (1 << height) - 1
|
||||
height = this.mountainHeight(reducedIndex)
|
||||
peakIndex = (1 << height) - 1
|
||||
}
|
||||
|
||||
// index is on the right slope
|
||||
return height - (peakIndex - reducedIndex)
|
||||
}
|
||||
|
||||
isLeaf (index: number): boolean {
|
||||
return this.heightAt(index) === 1
|
||||
}
|
||||
|
||||
getChildren (index: number) {
|
||||
const left = index - (1 << (this.heightAt(index) - 1))
|
||||
const right = index - 1
|
||||
if (left === right) {
|
||||
throw new Error('Not a parent')
|
||||
}
|
||||
|
||||
return [left, right]
|
||||
}
|
||||
|
||||
_getOrCreateNode (index: number) {
|
||||
if (index > this.size) {
|
||||
throw new Error('Out of range')
|
||||
}
|
||||
|
||||
if (!this.hashes[index]) {
|
||||
const [leftIndex, rightIndex] = this.getChildren(index)
|
||||
const leftHash = this._getOrCreateNode(leftIndex)
|
||||
const rightHash = this._getOrCreateNode(rightIndex)
|
||||
this.hashes[index] = this.hashBranch(index, leftHash, rightHash)
|
||||
}
|
||||
|
||||
return this.hashes[index]
|
||||
}
|
||||
|
||||
getMerkleProof (index: number) {
|
||||
if (index >= this.size) {
|
||||
throw new Error('Out of range')
|
||||
}
|
||||
if (!this.isLeaf(index)) {
|
||||
throw new Error('Not a leaf')
|
||||
}
|
||||
|
||||
const root = this.root
|
||||
const width = this.width
|
||||
|
||||
// find all peaks for bagging
|
||||
const peaks = this.getPeakIndexes(this.width)
|
||||
const peakBagging = []
|
||||
let cursor = 0
|
||||
|
||||
for (let i = 0; i < peaks.length; i++) {
|
||||
// collect the hash of all peaks
|
||||
peakBagging[i] = this.hashes[peaks[i]]
|
||||
|
||||
// find the peak which includes the target index
|
||||
if (peaks[i] >= index && cursor === 0) {
|
||||
cursor = peaks[i]
|
||||
}
|
||||
}
|
||||
|
||||
let left = 0
|
||||
let right = 0
|
||||
|
||||
// get hashes of the siblings in the mountain which the index belgons to.
|
||||
// it moves the cursor from the summit of the mountain down to the target index
|
||||
let height = this.heightAt(cursor)
|
||||
const siblings = []
|
||||
while (cursor !== index) {
|
||||
height--
|
||||
([left, right] = this.getChildren(cursor))
|
||||
|
||||
// move the cursor down to the left size or right size
|
||||
cursor = index <= left ? left : right
|
||||
|
||||
// remaining node is the sibling
|
||||
siblings[height - 1] = this.hashes[index < left ? right : left]
|
||||
}
|
||||
|
||||
return {
|
||||
root,
|
||||
width,
|
||||
peakBagging,
|
||||
siblings
|
||||
}
|
||||
}
|
||||
|
||||
verify (root: any, width: number, index: number, value: any, peaks: any[], siblings: any[]) {
|
||||
const size = this.getSize(width)
|
||||
if (size < index) {
|
||||
throw new Error('Index is out of range')
|
||||
}
|
||||
|
||||
// check the root equals the peak bagging hash
|
||||
if (!root.equals(this.peakBagging(width, peaks))) {
|
||||
throw new Error('Invalid root hash from the peaks')
|
||||
}
|
||||
|
||||
// find the mountain where the target index belongs to
|
||||
let cursor = 0
|
||||
let targetPeak
|
||||
|
||||
const peakIndexes = this.getPeakIndexes(width)
|
||||
for (let i = 0; i < peakIndexes.length; i++) {
|
||||
if (peakIndexes[i] >= index) {
|
||||
targetPeak = peaks[i]
|
||||
cursor = peakIndexes[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetPeak) {
|
||||
throw new Error('target not found')
|
||||
}
|
||||
|
||||
// find the path climbing down
|
||||
let height = siblings.length + 1
|
||||
const path = new Array(height)
|
||||
let left = 0
|
||||
let right = 0
|
||||
|
||||
while (height > 0) {
|
||||
// record the current cursor and climb down
|
||||
path[--height] = cursor
|
||||
if (cursor === index) {
|
||||
// on the leaf node. Stop climbing down
|
||||
break
|
||||
} else {
|
||||
// on the parent node. Go left or right
|
||||
([left, right] = this.getChildren(cursor))
|
||||
cursor = index > left ? right : left
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the summit hash climbing up again
|
||||
let node
|
||||
while (height < path.length) {
|
||||
// move cursor
|
||||
cursor = path[height]
|
||||
if (height === 0) {
|
||||
// cusor is on the leaf
|
||||
node = this.hashLeaf(cursor, this.hashAlgo(value))
|
||||
} else if (cursor - 1 === path[height - 1]) {
|
||||
// cursor is on a parent and a siblings is on the left
|
||||
node = this.hashBranch(cursor, siblings[height - 1], node)
|
||||
} else {
|
||||
// cursor is on a parent and a siblings is on the right
|
||||
node = this.hashBranch(cursor, node, siblings[height - 1])
|
||||
}
|
||||
// climb up
|
||||
height++
|
||||
}
|
||||
|
||||
// computed hash value of the summit should equal to the target peak hash
|
||||
if (!node.equals(targetPeak)) {
|
||||
throw new Error('hashed peak is invalid')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
peaksToPeakMap (width: number, peaks: any[]) {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
peakMapToPeaks (width: number, peakMap: any) {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
peakUpdate (width: number, prevPeakMap: any, itemHash: any) {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
rollUp (root: any, width: number, peaks: any[], itemHashes: any[]) {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
}
|
||||
|
||||
export default MerkleMountainRange
|
|
@ -1,6 +1,6 @@
|
|||
import reverse from 'buffer-reverse'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import SHA256 from 'crypto-js/sha256'
|
||||
import Base from './Base'
|
||||
import treeify from 'treeify'
|
||||
|
||||
export interface Options {
|
||||
|
@ -27,7 +27,7 @@ type TLayer = any
|
|||
* Class reprensenting a Merkle Tree
|
||||
* @namespace MerkleTree
|
||||
*/
|
||||
export class MerkleTree {
|
||||
export class MerkleTree extends Base {
|
||||
private duplicateOdd: boolean
|
||||
private hashAlgo: (value: TValue) => THashAlgo
|
||||
private hashLeaves: boolean
|
||||
|
@ -61,6 +61,7 @@ export class MerkleTree {
|
|||
*```
|
||||
*/
|
||||
constructor (leaves: any[], hashAlgorithm = SHA256, options: Options = {}) {
|
||||
super()
|
||||
this.isBitcoinTree = !!options.isBitcoinTree
|
||||
this.hashLeaves = !!options.hashLeaves
|
||||
this.sortLeaves = !!options.sortLeaves
|
||||
|
@ -861,44 +862,6 @@ export class MerkleTree {
|
|||
return objs[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* print
|
||||
* @desc Prints out a visual representation of the merkle tree.
|
||||
* @example
|
||||
*```js
|
||||
*tree.print()
|
||||
*```
|
||||
*/
|
||||
print ():void {
|
||||
MerkleTree.print(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* toTreeString
|
||||
* @desc Returns a visual representation of the merkle tree as a string.
|
||||
* @return {String}
|
||||
* @example
|
||||
*```js
|
||||
*console.log(tree.toTreeString())
|
||||
*```
|
||||
*/
|
||||
private _toTreeString ():string {
|
||||
const obj = this.getLayersAsObject()
|
||||
return treeify.asTree(obj, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* toString
|
||||
* @desc Returns a visual representation of the merkle tree as a string.
|
||||
* @example
|
||||
*```js
|
||||
*console.log(tree.toString())
|
||||
*```
|
||||
*/
|
||||
toString ():string {
|
||||
return this._toTreeString()
|
||||
}
|
||||
|
||||
/**
|
||||
* getMultiProof
|
||||
* @desc Returns the multiproof for given tree indices.
|
||||
|
@ -941,192 +904,29 @@ export class MerkleTree {
|
|||
}
|
||||
|
||||
/**
|
||||
* bufferIndexOf
|
||||
* @desc Returns the first index of which given buffer is found in array.
|
||||
* @param {Buffer[]} haystack - Array of buffers.
|
||||
* @param {Buffer} needle - Buffer to find.
|
||||
* @return {Number} - Index number
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*const index = tree.bufferIndexOf(haystack, needle)
|
||||
*```
|
||||
*/
|
||||
private _bufferIndexOf (array: Buffer[], element: Buffer):number {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (element.equals(array[i])) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* bufferify
|
||||
* @desc Returns a buffer type for the given value.
|
||||
* @param {String|Number|Object|Buffer} value
|
||||
* @return {Buffer}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*const buf = MerkleTree.bufferify('0x1234')
|
||||
*```
|
||||
*/
|
||||
static bufferify (value: any):Buffer {
|
||||
if (!Buffer.isBuffer(value)) {
|
||||
// crypto-js support
|
||||
if (typeof value === 'object' && value.words) {
|
||||
return Buffer.from(value.toString(CryptoJS.enc.Hex), 'hex')
|
||||
} else if (MerkleTree.isHexString(value)) {
|
||||
return Buffer.from(value.replace(/^0x/, ''), 'hex')
|
||||
} else if (typeof value === 'string') {
|
||||
return Buffer.from(value)
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* isHexString
|
||||
* @desc Returns true if value is a hex string.
|
||||
* @param {String} value
|
||||
* @return {Boolean}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*console.log(MerkleTree.isHexString('0x1234'))
|
||||
*```
|
||||
*/
|
||||
static isHexString (v: string):boolean {
|
||||
return (typeof v === 'string' && /^(0x)?[0-9A-Fa-f]*$/.test(v))
|
||||
}
|
||||
|
||||
/**
|
||||
* print
|
||||
* @desc Prints out a visual representation of the given merkle tree.
|
||||
* @param {Object} tree - Merkle tree instance.
|
||||
* toTreeString
|
||||
* @desc Returns a visual representation of the merkle tree as a string.
|
||||
* @return {String}
|
||||
* @example
|
||||
*```js
|
||||
*MerkleTree.print(tree)
|
||||
*console.log(tree.toTreeString())
|
||||
*```
|
||||
*/
|
||||
static print (tree: any):void {
|
||||
console.log(tree.toString())
|
||||
protected _toTreeString ():string {
|
||||
const obj = this.getLayersAsObject()
|
||||
return treeify.asTree(obj, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* bufferToHex
|
||||
* @desc Returns a hex string with 0x prefix for given buffer.
|
||||
* @param {Buffer} value
|
||||
* @return {String}
|
||||
* toString
|
||||
* @desc Returns a visual representation of the merkle tree as a string.
|
||||
* @example
|
||||
*```js
|
||||
*const hexStr = tree.bufferToHex(Buffer.from('A'))
|
||||
*console.log(tree.toString())
|
||||
*```
|
||||
*/
|
||||
bufferToHex (value: Buffer, withPrefix: boolean = true):string {
|
||||
return MerkleTree.bufferToHex(value, withPrefix)
|
||||
}
|
||||
|
||||
/**
|
||||
* bufferToHex
|
||||
* @desc Returns a hex string with 0x prefix for given buffer.
|
||||
* @param {Buffer} value
|
||||
* @return {String}
|
||||
* @example
|
||||
*```js
|
||||
*const hexStr = MerkleTree.bufferToHex(Buffer.from('A'))
|
||||
*```
|
||||
*/
|
||||
static bufferToHex (value: Buffer, withPrefix: boolean = true):string {
|
||||
return `${withPrefix ? '0x' : ''}${value.toString('hex')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* bufferify
|
||||
* @desc Returns a buffer type for the given value.
|
||||
* @param {String|Number|Object|Buffer} value
|
||||
* @return {Buffer}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*const buf = tree.bufferify('0x1234')
|
||||
*```
|
||||
*/
|
||||
bufferify (value: any):Buffer {
|
||||
return MerkleTree.bufferify(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* bufferifyFn
|
||||
* @desc Returns a function that will bufferify the return value.
|
||||
* @param {Function}
|
||||
* @return {Function}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*const fn = tree.bufferifyFn((value) => sha256(value))
|
||||
*```
|
||||
*/
|
||||
private _bufferifyFn (f: any):any {
|
||||
return function (value: any) {
|
||||
const v = f(value)
|
||||
if (Buffer.isBuffer(v)) {
|
||||
return v
|
||||
}
|
||||
|
||||
if (this._isHexString(v)) {
|
||||
return Buffer.from(v, 'hex')
|
||||
}
|
||||
|
||||
// crypto-js support
|
||||
return Buffer.from(f(CryptoJS.enc.Hex.parse(value.toString('hex'))).toString(CryptoJS.enc.Hex), 'hex')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* isHexString
|
||||
* @desc Returns true if value is a hex string.
|
||||
* @param {String} value
|
||||
* @return {Boolean}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*console.log(MerkleTree.isHexString('0x1234'))
|
||||
*```
|
||||
*/
|
||||
private _isHexString (value: string):boolean {
|
||||
return MerkleTree.isHexString(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* log2
|
||||
* @desc Returns the log2 of number.
|
||||
* @param {Number} value
|
||||
* @return {Number}
|
||||
*/
|
||||
private _log2 (n: number):number {
|
||||
return n === 1 ? 0 : 1 + this._log2((n / 2) | 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* zip
|
||||
* @desc Returns true if value is a hex string.
|
||||
* @param {String[]|Number[]|Buffer[]} a - first array
|
||||
* @param {String[]|Number[]|Buffer[]} b - second array
|
||||
* @return {String[][]|Number[][]|Buffer[][]}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*const zipped = tree.zip(['a', 'b'],['A', 'B'])
|
||||
*console.log(zipped) // [ [ 'a', 'A' ], [ 'b', 'B' ] ]
|
||||
*```
|
||||
*/
|
||||
private _zip (a: any[], b: any[]):any[][] {
|
||||
return a.map((e, i) => [e, b[i]])
|
||||
toString ():string {
|
||||
return this._toTreeString()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import MerkleTree from './MerkleTree'
|
||||
export { MerkleTree }
|
||||
export { MerkleMountainRange } from './MerkleMountainRange'
|
||||
export default MerkleTree
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/* eslint camelcase: 0 */
|
||||
|
||||
const test = require('tape')
|
||||
const { keccak } = require('ethereumjs-util')
|
||||
const { MerkleMountainRange } = require('../')
|
||||
const { soliditySha3 } = require('web3-utils')
|
||||
|
||||
test('merkle mountain range', t => {
|
||||
t.plan(6)
|
||||
|
||||
const leaves = [
|
||||
'0xf6c5123c17fe0d9c8fc675504ffa8cc8f1613c185d16fc8099ffab31cc0f39b8',
|
||||
'0x6579ce25f7cc6797b1386af5acc4ff8acc9f8054b65dad5fb0c3b0a32c6c4610',
|
||||
'0x6423808868aca9b46c7ccf2d060ff079cb8cf252f87abe767ea3bdce6c933d0c',
|
||||
'0xf0f21a35fc749991e54d26c2a7e6ae3509667bc0d897ac7bcf6a462d1bbe7996',
|
||||
'0x2db0abbe3ef9894b15b7e5e25b538b68ce19812792e3245746cb16c6cfc14345',
|
||||
'0xfcd354a2e527cb6040e15b1f04aa6df4eb5cd7f250ead21809ef16a348beca6b',
|
||||
'0xe747a507a27368d45b0e9df1b6cc2cff173e3f739daff1e8bbdd7159263ba8ad',
|
||||
'0x07d438f3191418363c43a92bcf0555a8b2cf5451d6854932259ede6096c59ed4',
|
||||
'0xd49690a48a2c2a6772092af6d6eec62cc4285a88997cae9fa4835bf0835c2b4d',
|
||||
'0x50b25445c2139a3cec3c593b49273f1f47af0eb7d9d9ed37667ef5673d163bb9',
|
||||
'0x22a6e30c867f4109491e90313ebd0b1438bd8b559cbe85ee9239aa8902a67e74',
|
||||
'0x1b70b853c66181ccf5d149c263c7cf5e1123d6655bcc0f668c9957db1d98e038',
|
||||
'0x527efa47060e8eead056eb3b2b7c20821bf6e246828fdd835f3ccc16946568b1',
|
||||
'0x22a33bbf9177edebe708656bcbe42b7cf077965d2d5fe268dff8878654db06d6',
|
||||
'0x02c4f6983e8616f6b3e600b3c8b768e71755220a04173b8281440c6b8eb91cd9',
|
||||
'0x3ed1f8b63a683529477f8813c0955ae058048b9a4fd9a706454f73f86baf977f',
|
||||
'0x70153ddc9ab0028e922b6b2f9eb319a9a6f1ae37866e7f9771f2866dfbecbc1d',
|
||||
'0x4c26cfe12f5b2e8b2d992d99c9359a261d0bf50450acb10a5060231b5d41c31c',
|
||||
'0x86da6c8cc46b60f9eec99353c42e144525a202880db5c877d6d55ee0568c66e4',
|
||||
'0x1b6a474c3e33a28104dd0e75484414b9d35e93c878823bc3b4d9fb5a5109e9eb'
|
||||
]
|
||||
|
||||
const hashLeaf = (index, dataHash) => {
|
||||
const hash = soliditySha3({ t: 'uint256', v: index }, { t: 'bytes32', v: '0x' + dataHash.toString('hex') })
|
||||
return hash
|
||||
}
|
||||
|
||||
const peakBagging = (size, peaks) => {
|
||||
// keccak256(abi.encodePacked(size, keccak256(abi.encodePacked(size, peaks))));
|
||||
const a = [{ t: 'uint256', v: size }, ...peaks.map(x => ({ t: 'bytes32', v: x.toString('hex') }))]
|
||||
const x = soliditySha3(...a)
|
||||
const b = [{ t: 'uint256', v: size }, { t: 'bytes32', v: x.toString('hex') }]
|
||||
const res = soliditySha3(...b)
|
||||
|
||||
return res
|
||||
// return keccak256(abi.encodePacked(size, keccak256(abi.encodePacked(size, peaks))));
|
||||
}
|
||||
|
||||
const hashBranch = (index, left, right) => {
|
||||
const args = []
|
||||
if (index !== undefined) {
|
||||
args.push({ t: 'uint256', v: index })
|
||||
}
|
||||
if (left !== undefined) {
|
||||
args.push({ t: 'bytes32', v: '0x' + left.toString('hex') })
|
||||
}
|
||||
if (right !== undefined) {
|
||||
args.push({ t: 'bytes32', v: '0x' + right.toString('hex') })
|
||||
}
|
||||
const hash = soliditySha3(...args)
|
||||
return hash
|
||||
}
|
||||
|
||||
const tree = new MerkleMountainRange(keccak, leaves, hashLeaf, peakBagging, hashBranch)
|
||||
const root = tree.getHexRoot()
|
||||
|
||||
const proof = tree.getMerkleProof(2)
|
||||
|
||||
t.equal(proof.root.toString('hex'), '5d6caccebed1b6720659b4a6c022b77d7ed5706ece976aaf44b7cf8e34650eb7')
|
||||
t.equal(proof.width, leaves.length)
|
||||
t.deepEqual(proof.peakBagging.map(x => x.toString('hex')), [
|
||||
'4965af1a91b3f41c5440e5fd5b529aae6fbf28c7c04a4fc151be321fabe4a784',
|
||||
'581f3f6a99cd8f276fd07845f619d1a271d763dd4ab049c7f8ac98602da58b7f'
|
||||
])
|
||||
t.deepEqual(proof.siblings.map(x => x.toString('hex')), [
|
||||
'd3185c77a90c0b4f44e9c38468ce303924345c756d19f5ee342a1043f308d606',
|
||||
'fc5341e2e1127e54327c793c2b930ca9ca93cd4db16e234b73d04fdb11fa9df7',
|
||||
'db3261e4e8d9dcfcc6a4973172a13338a013fb455a7a10ca90bfcc49699688b1',
|
||||
'37bc386efc898c6c36b1c2e5a754dafd614398abf146904bcea607af5068605c'
|
||||
])
|
||||
|
||||
t.equal(root, '0x5d6caccebed1b6720659b4a6c022b77d7ed5706ece976aaf44b7cf8e34650eb7')
|
||||
|
||||
const leaf = leaves[1]
|
||||
const index = 2
|
||||
const verified = tree.verify(proof.root, proof.width, index, leaf, proof.peakBagging, proof.siblings)
|
||||
t.equal(verified, true)
|
||||
})
|
Loading…
Reference in New Issue