MerkleMountainRange rollUp
This commit is contained in:
parent
62b273556b
commit
ffd7c4fa9a
|
@ -2,6 +2,9 @@ import Base from './Base'
|
|||
import SHA256 from 'crypto-js/sha256'
|
||||
|
||||
// @credit: https://github.com/wanseob/solidity-mmr
|
||||
/**
|
||||
* @desc The index of this MMR implementation starts from 1 not 0.
|
||||
*/
|
||||
export class MerkleMountainRange extends Base {
|
||||
root: Buffer = Buffer.alloc(0)
|
||||
size: number = 0
|
||||
|
@ -26,7 +29,11 @@ export class MerkleMountainRange extends Base {
|
|||
}
|
||||
}
|
||||
|
||||
append (data: any) {
|
||||
/**
|
||||
* @desc This only stores the hashed value of the leaf.
|
||||
* If you need to retrieve the detail data later, use a map to store them.
|
||||
*/
|
||||
append (data: Buffer | string) {
|
||||
data = this.bufferify(data)
|
||||
const dataHash = this.hashFn(data)
|
||||
const dataHashHex = this.bufferToHex(dataHash)
|
||||
|
@ -54,6 +61,10 @@ export class MerkleMountainRange extends Base {
|
|||
this.root = this.peakBagging(this.width, peaks)
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc It returns the hash of a leaf node with hash(M | DATA )
|
||||
* M is the index of the node.
|
||||
*/
|
||||
hashLeaf (index: number, dataHash: Buffer | string) {
|
||||
dataHash = this.bufferify(dataHash)
|
||||
if (this.hashLeafFn) {
|
||||
|
@ -62,6 +73,10 @@ export class MerkleMountainRange extends Base {
|
|||
return this.hashFn(Buffer.concat([this.bufferify(index), dataHash]))
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc It returns the hash a parent node with hash(M | Left child | Right child)
|
||||
* M is the index of the node.
|
||||
*/
|
||||
hashBranch (index: number, left: any, right: any): any {
|
||||
if (this.hashBranchFn) {
|
||||
return this.bufferify(this.hashBranchFn(index, left, right))
|
||||
|
@ -86,6 +101,10 @@ export class MerkleMountainRange extends Base {
|
|||
return this.getSize(width - 1) + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc It returns all peaks of the smallest merkle mountain range tree which includes
|
||||
* the given index(size).
|
||||
*/
|
||||
getPeakIndexes (width: number): number[] {
|
||||
const numPeaks = this.numOfPeaks(width)
|
||||
const peakIndexes = []
|
||||
|
@ -103,7 +122,7 @@ export class MerkleMountainRange extends Base {
|
|||
}
|
||||
|
||||
if (count !== peakIndexes.length) {
|
||||
throw new Error('Invalid bit calculation')
|
||||
throw new Error('invalid bit calculation')
|
||||
}
|
||||
|
||||
return peakIndexes
|
||||
|
@ -124,7 +143,11 @@ export class MerkleMountainRange extends Base {
|
|||
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')
|
||||
throw new Error('received invalid number of peaks')
|
||||
}
|
||||
|
||||
if (width === 0 && !peaks.length) {
|
||||
return Buffer.alloc(0)
|
||||
}
|
||||
|
||||
if (this.peakBaggingFn) {
|
||||
|
@ -134,10 +157,16 @@ export class MerkleMountainRange extends Base {
|
|||
return this.hashFn(Buffer.concat([this.bufferify(size), ...peaks.map(this.bufferify)]))
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc It returns the size of the tree.
|
||||
*/
|
||||
getSize (width: number): number {
|
||||
return (width << 1) - this.numOfPeaks(width)
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc It returns the root value of the tree.
|
||||
*/
|
||||
getRoot (): any {
|
||||
return this.root
|
||||
}
|
||||
|
@ -146,10 +175,16 @@ export class MerkleMountainRange extends Base {
|
|||
return this.bufferToHex(this.getRoot())
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev It returns the hash value of a node for the given position. Note that the index starts from 1.
|
||||
*/
|
||||
getNode (index: number): any {
|
||||
return this.hashes[index]
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc It returns the height of the highest peak.
|
||||
*/
|
||||
mountainHeight (size: number): number {
|
||||
let height = 1
|
||||
while (1 << height <= size + height) {
|
||||
|
@ -158,6 +193,9 @@ export class MerkleMountainRange extends Base {
|
|||
return height - 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc It returns the height of the index.
|
||||
*/
|
||||
heightAt (index: number): number {
|
||||
let reducedIndex = index
|
||||
let peakIndex = 0
|
||||
|
@ -174,41 +212,35 @@ export class MerkleMountainRange extends Base {
|
|||
return height - (peakIndex - reducedIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc It returns whether the index is the leaf node or not
|
||||
*/
|
||||
isLeaf (index: number): boolean {
|
||||
return this.heightAt(index) === 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc It returns the children when it is a parent node.
|
||||
*/
|
||||
getChildren (index: number) {
|
||||
const left = index - (1 << (this.heightAt(index) - 1))
|
||||
const right = index - 1
|
||||
if (left === right) {
|
||||
throw new Error('Not a parent')
|
||||
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]
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc It returns a merkle proof for a leaf. Note that the index starts from 1.
|
||||
*/
|
||||
getMerkleProof (index: number) {
|
||||
if (index >= this.size) {
|
||||
throw new Error('Out of range')
|
||||
throw new Error('out of range')
|
||||
}
|
||||
if (!this.isLeaf(index)) {
|
||||
throw new Error('Not a leaf')
|
||||
throw new Error('not a leaf')
|
||||
}
|
||||
|
||||
const root = this.root
|
||||
|
@ -255,16 +287,19 @@ export class MerkleMountainRange extends Base {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc It returns true when the given params verifies that the given value exists in the tree or reverts the transaction.
|
||||
*/
|
||||
verify (root: any, width: number, index: number, value: Buffer | string, peaks: any[], siblings: any[]) {
|
||||
value = this.bufferify(value)
|
||||
const size = this.getSize(width)
|
||||
if (size < index) {
|
||||
throw new Error('Index is out of range')
|
||||
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')
|
||||
throw new Error('invalid root hash from the peaks')
|
||||
}
|
||||
|
||||
// find the mountain where the target index belongs to
|
||||
|
@ -332,19 +367,124 @@ export class MerkleMountainRange extends Base {
|
|||
}
|
||||
|
||||
peaksToPeakMap (width: number, peaks: any[]) {
|
||||
throw new Error('not implemented')
|
||||
const peakMap = {}
|
||||
let bitIndex = 0
|
||||
let peakRef = 0
|
||||
let count = peaks.length
|
||||
for (let height = 1; height <= 32; height++) {
|
||||
// index starts from the right most bit
|
||||
bitIndex = 32 - height
|
||||
peakRef = 1 << (height - 1)
|
||||
if ((width & peakRef) !== 0) {
|
||||
peakMap[bitIndex] = peaks[--count]
|
||||
} else {
|
||||
peakMap[bitIndex] = 0
|
||||
}
|
||||
}
|
||||
|
||||
if (count !== 0) {
|
||||
throw new Error('invalid number of peaks')
|
||||
}
|
||||
|
||||
return peakMap
|
||||
}
|
||||
|
||||
peakMapToPeaks (width: number, peakMap: any) {
|
||||
throw new Error('not implemented')
|
||||
const arrLength = this.numOfPeaks(width)
|
||||
const peaks = new Array(arrLength)
|
||||
let count = 0
|
||||
for (let i = 0; i < 32; i++) {
|
||||
if (peakMap[i] !== 0) {
|
||||
peaks[count++] = peakMap[i]
|
||||
}
|
||||
}
|
||||
|
||||
if (count !== arrLength) {
|
||||
throw new Error('invalid number of peaks')
|
||||
}
|
||||
|
||||
return peaks
|
||||
}
|
||||
|
||||
peakUpdate (width: number, prevPeakMap: any, itemHash: any) {
|
||||
throw new Error('not implemented')
|
||||
const nextPeakMap = {}
|
||||
const newWidth = width + 1
|
||||
let cursorIndex = this.getLeafIndex(newWidth)
|
||||
let cursorNode = this.hashLeaf(cursorIndex, itemHash)
|
||||
let bitIndex = 0
|
||||
let peakRef = 0
|
||||
let prevPeakExist = false
|
||||
let nextPeakExist = false
|
||||
let obtained = false
|
||||
|
||||
for (let height = 1; height <= 32; height++) {
|
||||
// index starts from the right most bit
|
||||
bitIndex = 32 - height
|
||||
if (obtained) {
|
||||
nextPeakMap[bitIndex] = prevPeakMap[bitIndex]
|
||||
} else {
|
||||
peakRef = 1 << (height - 1)
|
||||
prevPeakExist = (width & peakRef) !== 0
|
||||
nextPeakExist = (newWidth & peakRef) !== 0
|
||||
|
||||
// get new cursor node with hashing the peak and the current cursor
|
||||
cursorIndex++
|
||||
if (prevPeakExist) {
|
||||
cursorNode = this.hashBranch(cursorIndex, prevPeakMap[bitIndex], cursorNode)
|
||||
}
|
||||
// if new peak exists for the bit index
|
||||
if (nextPeakExist) {
|
||||
// if prev peak exists for the bit index
|
||||
if (prevPeakExist) {
|
||||
nextPeakMap[bitIndex] = prevPeakMap[bitIndex]
|
||||
} else {
|
||||
nextPeakMap[bitIndex] = cursorNode
|
||||
}
|
||||
obtained = true
|
||||
} else {
|
||||
nextPeakMap[bitIndex] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextPeakMap
|
||||
}
|
||||
|
||||
rollUp (root: any, width: number, peaks: any[], itemHashes: any[]) {
|
||||
throw new Error('not implemented')
|
||||
// check the root equals the peak bagging hash
|
||||
if (!root.equals(this.peakBagging(width, peaks))) {
|
||||
throw new Error('invalid root hash from the peaks')
|
||||
}
|
||||
|
||||
let tmpWidth = width
|
||||
let tmpPeakMap = this.peaksToPeakMap(width, peaks)
|
||||
for (let i = 0; i < itemHashes.length; i++) {
|
||||
tmpPeakMap = this.peakUpdate(tmpWidth, tmpPeakMap, itemHashes[i])
|
||||
tmpWidth++
|
||||
}
|
||||
|
||||
return this.peakBagging(tmpWidth, this.peakMapToPeaks(tmpWidth, tmpPeakMap))
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc It returns the hash value of the node for the index.
|
||||
* If the hash already exists it simply returns the stored value. On the other hand,
|
||||
* it computes hashes recursively downward.
|
||||
* Only appending an item calls this function.
|
||||
*/
|
||||
private _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]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,34 @@ const { keccak256 } = require('ethereumjs-util')
|
|||
const { MerkleMountainRange } = require('../')
|
||||
const { soliditySha3 } = require('web3-utils')
|
||||
|
||||
const hashLeaf = (index, dataHash) => {
|
||||
const hash = soliditySha3({ t: 'uint256', v: index }, { t: 'bytes32', v: '0x' + dataHash.toString('hex') })
|
||||
return hash
|
||||
}
|
||||
|
||||
const peakBagging = (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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
test('merkle mountain range', t => {
|
||||
t.plan(6)
|
||||
|
||||
|
@ -31,39 +59,11 @@ test('merkle mountain range', t => {
|
|||
'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) => {
|
||||
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
|
||||
}
|
||||
|
||||
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(keccak256, leaves, hashLeaf, peakBagging, hashBranch)
|
||||
const root = tree.getHexRoot()
|
||||
|
||||
const proof = tree.getMerkleProof(2)
|
||||
|
||||
t.equal(root, '0x5d6caccebed1b6720659b4a6c022b77d7ed5706ece976aaf44b7cf8e34650eb7')
|
||||
t.equal(proof.root.toString('hex'), '5d6caccebed1b6720659b4a6c022b77d7ed5706ece976aaf44b7cf8e34650eb7')
|
||||
t.equal(proof.width, leaves.length)
|
||||
t.deepEqual(proof.peakBagging.map(x => x.toString('hex')), [
|
||||
|
@ -77,10 +77,50 @@ test('merkle mountain range', t => {
|
|||
'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)
|
||||
})
|
||||
|
||||
test('merkle mountain range - rollUp', t => {
|
||||
t.plan(3)
|
||||
|
||||
const leaves = [
|
||||
'0xf6c5123c17fe0d9c8fc675504ffa8cc8f1613c185d16fc8099ffab31cc0f39b8',
|
||||
'0x6579ce25f7cc6797b1386af5acc4ff8acc9f8054b65dad5fb0c3b0a32c6c4610',
|
||||
'0x6423808868aca9b46c7ccf2d060ff079cb8cf252f87abe767ea3bdce6c933d0c',
|
||||
'0xf0f21a35fc749991e54d26c2a7e6ae3509667bc0d897ac7bcf6a462d1bbe7996',
|
||||
'0x2db0abbe3ef9894b15b7e5e25b538b68ce19812792e3245746cb16c6cfc14345',
|
||||
'0xfcd354a2e527cb6040e15b1f04aa6df4eb5cd7f250ead21809ef16a348beca6b',
|
||||
'0xe747a507a27368d45b0e9df1b6cc2cff173e3f739daff1e8bbdd7159263ba8ad',
|
||||
'0x07d438f3191418363c43a92bcf0555a8b2cf5451d6854932259ede6096c59ed4',
|
||||
'0xd49690a48a2c2a6772092af6d6eec62cc4285a88997cae9fa4835bf0835c2b4d',
|
||||
'0x50b25445c2139a3cec3c593b49273f1f47af0eb7d9d9ed37667ef5673d163bb9',
|
||||
'0x22a6e30c867f4109491e90313ebd0b1438bd8b559cbe85ee9239aa8902a67e74',
|
||||
'0x1b70b853c66181ccf5d149c263c7cf5e1123d6655bcc0f668c9957db1d98e038',
|
||||
'0x527efa47060e8eead056eb3b2b7c20821bf6e246828fdd835f3ccc16946568b1',
|
||||
'0x22a33bbf9177edebe708656bcbe42b7cf077965d2d5fe268dff8878654db06d6',
|
||||
'0x02c4f6983e8616f6b3e600b3c8b768e71755220a04173b8281440c6b8eb91cd9',
|
||||
'0x3ed1f8b63a683529477f8813c0955ae058048b9a4fd9a706454f73f86baf977f',
|
||||
'0x70153ddc9ab0028e922b6b2f9eb319a9a6f1ae37866e7f9771f2866dfbecbc1d',
|
||||
'0x4c26cfe12f5b2e8b2d992d99c9359a261d0bf50450acb10a5060231b5d41c31c',
|
||||
'0x86da6c8cc46b60f9eec99353c42e144525a202880db5c877d6d55ee0568c66e4',
|
||||
'0x1b6a474c3e33a28104dd0e75484414b9d35e93c878823bc3b4d9fb5a5109e9eb'
|
||||
]
|
||||
|
||||
const tree = new MerkleMountainRange(keccak256, [], hashLeaf, peakBagging, hashBranch)
|
||||
const root = tree.getHexRoot()
|
||||
t.equal(root, '0x')
|
||||
|
||||
const hashedLeaves = leaves.map(x => keccak256(Buffer.from(x.replace('0x', ''), 'hex')))
|
||||
const rollupRoot = tree.rollUp(tree.root, tree.width, tree.getPeaks(), hashedLeaves)
|
||||
|
||||
for (const leaf of leaves) {
|
||||
tree.append(leaf)
|
||||
}
|
||||
|
||||
const newRoot = tree.getHexRoot()
|
||||
t.equal(newRoot, '0x5d6caccebed1b6720659b4a6c022b77d7ed5706ece976aaf44b7cf8e34650eb7')
|
||||
t.equal(newRoot, tree.bufferToHex(rollupRoot))
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue