This commit is contained in:
r0qs 2022-10-08 04:05:08 +09:30 committed by GitHub
commit ae3d778df3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 5001 additions and 519 deletions

36
lib/BaseTree.d.ts vendored
View File

@ -1,4 +1,4 @@
import { Element, HashFunction, ProofPath } from './';
import { Element, HashFunction, ProofPath, MultiProofPath } from './';
export declare class BaseTree {
levels: number;
protected _hashFn: HashFunction<Element>;
@ -37,6 +37,40 @@ export declare class BaseTree {
* @returns {{pathElements: Object[], pathIndex: number[]}} An object containing adjacent elements and left-right index
*/
path(index: number): ProofPath;
/**
* Return the indices for the next layer in the multiPath calculation
* @param {number} indices A list of leaf indices
* @returns {number[]} the new list of indices
*/
static nextLayerMultiPathIndices(indices: number[]): number[];
/**
* Get merkle path to a list of leaves
* @param {number} indices A list of leaf indices to generate path for
* @returns {{pathElements: Element[], leafIndices: number[]}} An object containing adjacent elements and leaves indices
*/
multiPath(indices: number[]): MultiProofPath;
/**
* Verifies a merkle proof
* @param {Element} root the root of the merkle tree
* @param {number} levels the number of levels of the tree
* @param {HashFunction<Element>} hashFn hash function
* @param {Element} leaf the leaf to be verified
* @param {Element[]} pathElements adjacent path elements
* @param {number[]} pathIndices left-right indices
* @returns {Boolean} whether the proof is valid for the given root
*/
static verifyProof(root: Element, levels: number, hashFn: HashFunction<Element>, leaf: Element, pathElements: Element[], pathIndices: number[]): boolean;
/**
* Verifies a merkle multiproof
* @param {Element} root the root of the merkle tree
* @param {number} levels the number of levels of the tree
* @param {HashFunction<Element>} hashFn hash function
* @param {Element[]} leaves the list of leaves to be verified
* @param {Element[]} pathElements multiproof path elements
* @param {number[]} leafIndices multiproof leaf indices
* @returns {Boolean} whether the proof is valid for the given root
*/
static verifyMultiProof(root: Element, levels: number, hashFn: HashFunction<Element>, leaves: Element[], pathElements: Element[], leafIndices: number[]): boolean;
protected _buildZeros(): void;
protected _processNodes(nodes: Element[], layerIndex: number): any[];
protected _processUpdate(index: number): void;

View File

@ -117,6 +117,126 @@ class BaseTree {
pathRoot: this.root,
};
}
/**
* Return the indices for the next layer in the multiPath calculation
* @param {number} indices A list of leaf indices
* @returns {number[]} the new list of indices
*/
static nextLayerMultiPathIndices(indices) {
const nextIndices = new Set();
for (let i = 0; i < indices.length; i++) {
nextIndices.add(indices[i] >> 1);
}
return [...nextIndices];
}
/**
* Get merkle path to a list of leaves
* @param {number} indices A list of leaf indices to generate path for
* @returns {{pathElements: Element[], leafIndices: number[]}} An object containing adjacent elements and leaves indices
*/
multiPath(indices) {
let pathElements = [];
let layerIndices = indices;
for (let level = 0; level < this.levels; level++) {
// find whether there is a neighbor idx that is not in layerIndices
const proofElements = layerIndices.reduce((elements, idx) => {
const leafIndex = idx ^ 1;
if (!layerIndices.includes(leafIndex)) {
if (leafIndex < this._layers[level].length) {
elements.push(this._layers[level][leafIndex]);
}
else {
elements.push(this._zeros[level]);
}
}
return elements;
}, []);
pathElements = pathElements.concat(proofElements);
layerIndices = BaseTree.nextLayerMultiPathIndices(layerIndices);
}
return {
pathElements,
leafIndices: indices,
pathRoot: this.root,
};
}
/**
* Verifies a merkle proof
* @param {Element} root the root of the merkle tree
* @param {number} levels the number of levels of the tree
* @param {HashFunction<Element>} hashFn hash function
* @param {Element} leaf the leaf to be verified
* @param {Element[]} pathElements adjacent path elements
* @param {number[]} pathIndices left-right indices
* @returns {Boolean} whether the proof is valid for the given root
*/
static verifyProof(root, levels, hashFn, leaf, pathElements, pathIndices) {
const layerProofs = [];
for (let level = 0; level < levels; level++) {
let elem = level == 0 ? leaf : layerProofs[level - 1];
if (pathIndices[level] == 0) {
layerProofs[level] = hashFn(elem, pathElements[level]);
}
else {
layerProofs[level] = hashFn(pathElements[level], elem);
}
}
return root === layerProofs[levels - 1];
}
/**
* Verifies a merkle multiproof
* @param {Element} root the root of the merkle tree
* @param {number} levels the number of levels of the tree
* @param {HashFunction<Element>} hashFn hash function
* @param {Element[]} leaves the list of leaves to be verified
* @param {Element[]} pathElements multiproof path elements
* @param {number[]} leafIndices multiproof leaf indices
* @returns {Boolean} whether the proof is valid for the given root
*/
static verifyMultiProof(root, levels, hashFn, leaves, pathElements, leafIndices) {
let layerElements = leaves;
let layerIndices = leafIndices;
const proofElements = pathElements;
const layerProofs = [];
for (let level = 0; level < levels; level++) {
for (let i = 0; i < layerIndices.length; i++) {
let layerHash;
const elIndex = layerIndices[i];
const leafIndex = elIndex ^ 1;
if (layerIndices.includes(leafIndex)) {
if (elIndex % 2 === 0) {
layerHash = hashFn(layerElements[0], layerElements[1]);
}
else {
layerHash = hashFn(layerElements[1], layerElements[0]);
}
layerElements.splice(0, 2); // remove 1st and 2nd element
i++; // skip next idx
layerProofs.push(layerHash);
}
else {
if (elIndex % 2 === 0) {
layerHash = hashFn(layerElements[0], proofElements[0]);
}
else {
layerHash = hashFn(proofElements[0], layerElements[0]);
}
layerElements.shift(); // remove 1st element
layerProofs.push(layerHash);
if (proofElements.shift() === undefined) {
break;
}
}
}
layerIndices = BaseTree.nextLayerMultiPathIndices(layerIndices);
layerElements = layerProofs;
if (proofElements.length == 0 && layerElements.length == 2) {
layerProofs[0] = hashFn(layerProofs[0], layerProofs[1]);
break;
}
}
return root === layerProofs[0];
}
_buildZeros() {
this._zeros = [this.zeroElement];
for (let i = 1; i <= this.levels; i++) {

View File

@ -1,4 +1,4 @@
import { Element, HashFunction, MerkleTreeOptions, ProofPath, SerializedTreeState, TreeEdge, TreeSlice } from './';
import { Element, HashFunction, MerkleTreeOptions, ProofPath, MultiProofPath, SerializedTreeState, TreeEdge, TreeSlice } from './';
import { BaseTree } from './BaseTree';
export default class MerkleTree extends BaseTree {
constructor(levels: number, elements?: Element[], { hashFunction, zeroElement, }?: MerkleTreeOptions);
@ -10,6 +10,7 @@ export default class MerkleTree extends BaseTree {
bulkInsert(elements: Element[]): void;
indexOf(element: Element, comparator?: <T>(arg0: T, arg1: T) => boolean): number;
proof(element: Element): ProofPath;
multiProof(elements: Element[]): MultiProofPath;
getTreeEdge(edgeIndex: number): TreeEdge;
/**
* 🪓

View File

@ -59,6 +59,13 @@ class MerkleTree extends BaseTree_1.BaseTree {
const index = this.indexOf(element);
return this.path(index);
}
multiProof(elements) {
const indexes = [];
for (let i = 0; i < elements.length; i++) {
indexes.push(this.indexOf(elements[i]));
}
return this.multiPath(indexes);
}
getTreeEdge(edgeIndex) {
const edgeElement = this._layers[0][edgeIndex];
if (edgeElement === undefined) {

5
lib/index.d.ts vendored
View File

@ -29,6 +29,11 @@ export declare type ProofPath = {
pathPositions: number[];
pathRoot: Element;
};
export declare type MultiProofPath = {
pathElements: Element[];
leafIndices: number[];
pathRoot: Element;
};
export declare type TreeEdge = {
edgeElement: Element;
edgePath: ProofPath;

5083
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,15 +26,15 @@
],
"devDependencies": {
"@types/expect": "^24.3.0",
"@types/mocha": "^9.1.0",
"@types/mocha": "^9.1.1",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"chai": "^4.2.0",
"chai": "^4.3.6",
"eslint": "^8.9.0",
"eslint-config-prettier": "^8.3.0",
"mocha": "^9.2.2",
"mocha": "^10.0.0",
"nyc": "^15.1.0",
"ts-mocha": "^9.0.2",
"typescript": "^4.6.2"
"ts-mocha": "^10.0.0",
"typescript": "^4.7.4"
}
}

View File

@ -1,4 +1,4 @@
import { Element, HashFunction, ProofPath } from './'
import { Element, HashFunction, ProofPath, MultiProofPath } from './'
export class BaseTree {
levels: number
@ -35,7 +35,12 @@ export class BaseTree {
* @param fromIndex The index to start the search at. If the index is greater than or equal to the array's length, -1 is returned
* @returns {number} Index if element is found, otherwise -1
*/
static indexOf(elements: Element[], element: Element, fromIndex?: number, comparator?: <T> (arg0: T, arg1: T) => boolean): number {
static indexOf(
elements: Element[],
element: Element,
fromIndex?: number,
comparator?: <T>(arg0: T, arg1: T) => boolean,
): number {
if (comparator) {
return elements.findIndex((el) => comparator<Element>(element, el))
} else {
@ -84,7 +89,6 @@ export class BaseTree {
this.insert(elements[elements.length - 1])
}
/**
* Change an element in the tree
* @param {number} index Index of element to change
@ -110,7 +114,7 @@ export class BaseTree {
let elIndex = +index
const pathElements: Element[] = []
const pathIndices: number[] = []
const pathPositions: number [] = []
const pathPositions: number[] = []
for (let level = 0; level < this.levels; level++) {
pathIndices[level] = elIndex % 2
const leafIndex = elIndex ^ 1
@ -131,6 +135,141 @@ export class BaseTree {
}
}
/**
* Return the indices for the next layer in the multiPath calculation
* @param {number} indices A list of leaf indices
* @returns {number[]} the new list of indices
*/
static nextLayerMultiPathIndices(indices: number[]): number[] {
const nextIndices: Set<number> = new Set()
for (let i = 0; i < indices.length; i++) {
nextIndices.add(indices[i] >> 1)
}
return [...nextIndices]
}
/**
* Get merkle path to a list of leaves
* @param {number} indices A list of leaf indices to generate path for
* @returns {{pathElements: Element[], leafIndices: number[]}} An object containing adjacent elements and leaves indices
*/
multiPath(indices: number[]): MultiProofPath {
let pathElements: Element[] = []
let layerIndices = indices
for (let level = 0; level < this.levels; level++) {
// find whether there is a neighbor idx that is not in layerIndices
const proofElements = layerIndices.reduce((elements, idx) => {
const leafIndex = idx ^ 1
if (!layerIndices.includes(leafIndex)) {
if (leafIndex < this._layers[level].length) {
elements.push(this._layers[level][leafIndex])
} else {
elements.push(this._zeros[level])
}
}
return elements
}, [])
pathElements = pathElements.concat(proofElements)
layerIndices = BaseTree.nextLayerMultiPathIndices(layerIndices)
}
return {
pathElements,
leafIndices: indices,
pathRoot: this.root,
}
}
/**
* Verifies a merkle proof
* @param {Element} root the root of the merkle tree
* @param {number} levels the number of levels of the tree
* @param {HashFunction<Element>} hashFn hash function
* @param {Element} leaf the leaf to be verified
* @param {Element[]} pathElements adjacent path elements
* @param {number[]} pathIndices left-right indices
* @returns {Boolean} whether the proof is valid for the given root
*/
static verifyProof(
root: Element,
levels: number,
hashFn: HashFunction<Element>,
leaf: Element,
pathElements: Element[],
pathIndices: number[],
): boolean {
const layerProofs: Element[] = []
for (let level = 0; level < levels; level++) {
let elem = level == 0 ? leaf : layerProofs[level - 1]
if (pathIndices[level] == 0) {
layerProofs[level] = hashFn(elem, pathElements[level])
} else {
layerProofs[level] = hashFn(pathElements[level], elem)
}
}
return root === layerProofs[levels - 1]
}
/**
* Verifies a merkle multiproof
* @param {Element} root the root of the merkle tree
* @param {number} levels the number of levels of the tree
* @param {HashFunction<Element>} hashFn hash function
* @param {Element[]} leaves the list of leaves to be verified
* @param {Element[]} pathElements multiproof path elements
* @param {number[]} leafIndices multiproof leaf indices
* @returns {Boolean} whether the proof is valid for the given root
*/
static verifyMultiProof(
root: Element,
levels: number,
hashFn: HashFunction<Element>,
leaves: Element[],
pathElements: Element[],
leafIndices: number[],
): boolean {
let layerElements: Element[] = leaves
let layerIndices: number[] = leafIndices
const proofElements: Element[] = pathElements
const layerProofs: Element[] = []
for (let level = 0; level < levels; level++) {
for (let i = 0; i < layerIndices.length; i++) {
let layerHash: string
const elIndex = layerIndices[i]
const leafIndex = elIndex ^ 1
if (layerIndices.includes(leafIndex)) {
if (elIndex % 2 === 0) {
layerHash = hashFn(layerElements[0], layerElements[1])
} else {
layerHash = hashFn(layerElements[1], layerElements[0])
}
layerElements.splice(0, 2) // remove 1st and 2nd element
i++ // skip next idx
layerProofs.push(layerHash)
} else {
if (elIndex % 2 === 0) {
layerHash = hashFn(layerElements[0], proofElements[0])
} else {
layerHash = hashFn(proofElements[0], layerElements[0])
}
layerElements.shift() // remove 1st element
layerProofs.push(layerHash)
if (proofElements.shift() === undefined) {
break
}
}
}
layerIndices = BaseTree.nextLayerMultiPathIndices(layerIndices)
layerElements = layerProofs
if (proofElements.length == 0 && layerElements.length == 2) {
layerProofs[0] = hashFn(layerProofs[0], layerProofs[1])
break
}
}
return root === layerProofs[0]
}
protected _buildZeros() {
this._zeros = [this.zeroElement]
for (let i = 1; i <= this.levels; i++) {
@ -165,5 +304,4 @@ export class BaseTree {
this._layers[level][index] = this._hashFn(left, right)
}
}
}

View File

@ -1,4 +1,4 @@
import { Element, HashFunction, MerkleTreeOptions, ProofPath, SerializedTreeState, TreeEdge, TreeSlice } from './'
import { Element, HashFunction, MerkleTreeOptions, ProofPath, MultiProofPath, SerializedTreeState, TreeEdge, TreeSlice } from './'
import defaultHash from './simpleHash'
import { BaseTree } from './BaseTree'
@ -29,7 +29,6 @@ export default class MerkleTree extends BaseTree {
}
}
/**
* Insert multiple elements into the tree.
* @param {Array} elements Elements to insert
@ -70,6 +69,14 @@ export default class MerkleTree extends BaseTree {
return this.path(index)
}
multiProof(elements: Element[]): MultiProofPath {
const indexes = []
for (let i = 0; i < elements.length; i++) {
indexes.push(this.indexOf(elements[i]))
}
return this.multiPath(indexes)
}
getTreeEdge(edgeIndex: number): TreeEdge {
const edgeElement = this._layers[0][edgeIndex]
if (edgeElement === undefined) {

View File

@ -34,6 +34,13 @@ export type ProofPath = {
pathPositions: number[],
pathRoot: Element
}
export type MultiProofPath = {
pathElements: Element[],
leafIndices: number[],
pathRoot: Element
}
export type TreeEdge = {
edgeElement: Element;
edgePath: ProofPath;

View File

@ -1,15 +1,15 @@
import { MerkleTree, PartialMerkleTree, TreeEdge } from '../src'
import { assert, should } from 'chai'
import { MerkleTree, ProofPath, MultiProofPath, PartialMerkleTree, TreeEdge } from '../src'
import { assert, should, expect } from 'chai'
import { createHash } from 'crypto'
import { it } from 'mocha'
import { BaseTree } from '../src/BaseTree'
import defaultHash from '../src/simpleHash'
const sha256Hash = (left, right) => createHash('sha256').update(`${left}${right}`).digest('hex')
const ZERO_ELEMENT = '21663839004416932945382355908790599225266501822907911457504978515578255421292'
describe('MerkleTree', () => {
describe('#constructor', () => {
it('should have correct zero root', () => {
const tree = new MerkleTree(10, [])
return should().equal(tree.root, '3060353338620102847451617558650138132480')
@ -207,7 +207,6 @@ describe('MerkleTree', () => {
'4986731814143931240516913804278285467648',
'1918547053077726613961101558405545328640',
'5444383861051812288142814494928935059456',
])
})
@ -253,7 +252,6 @@ describe('MerkleTree', () => {
'4986731814143931240516913804278285467648',
'1918547053077726613961101558405545328640',
'5444383861051812288142814494928935059456',
])
})
})
@ -264,6 +262,83 @@ describe('MerkleTree', () => {
})
})
describe('#verifyProof', () => {
it('should verify a merkle-proof', () => {
const leaves = [...Array(16)].map((_, i) => i + 1)
const tree = new MerkleTree(4, leaves)
const proof: ProofPath = {
pathElements: [
6,
'1177811302158128621769756786551727063040',
'81822854828781486047086122479545722339328',
'3473994785217814971484840545214368055296'
],
pathIndices: [ 0, 0, 1, 0 ],
pathPositions: [ 5, 3, 0, 1 ],
pathRoot: '4813607112316126252402222488335589310464'
}
expect(
BaseTree.verifyProof(
tree.root,
tree.levels,
defaultHash,
5,
proof.pathElements,
proof.pathIndices,
),
).to.be.true
})
})
describe('#multiproof', () => {
it('should return a merkle-multiproof for a range of leaves in a full tree', () => {
const leaves = [...Array(8)].map((_, i) => i + 1)
const tree = new MerkleTree(3, leaves)
assert.deepEqual(tree.multiProof([1, 2, 6]), tree.multiPath([0, 1, 5]))
})
it('should return a merkle-multiproof for a leaf', () => {
const tree = new MerkleTree(6, [1])
assert.deepEqual(tree.multiProof([1]), tree.multiPath([0]))
})
it('should return a merkle-multiproof for a range of leaves', () => {
const tree = new MerkleTree(8, [1, 2, 3, 4])
assert.deepEqual(tree.multiProof([2, 4]), tree.multiPath([1, 3]))
})
})
describe('#verifyMultiProof', () => {
it('should verify a merkle-multiproof for a range of leaves', () => {
const leaves = [...Array(16)].map((_, i) => i + 1)
const tree = new MerkleTree(4, leaves)
const proof: MultiProofPath = {
pathElements: [
10,
13,
'4027992409016347597424110157229339967488',
'1344833971335891992284786489626349010944',
'811683747745882158385481808019325976576',
'770507832213726794990007789733078368256',
],
leafIndices: [2, 3, 8, 13],
pathRoot: '4813607112316126252402222488335589310464',
}
expect(
BaseTree.verifyMultiProof(
tree.root,
tree.levels,
defaultHash,
[3, 4, 9, 14],
proof.pathElements,
proof.leafIndices,
),
).to.be.true
})
})
describe('#getTreeEdge', () => {
it('should return correct treeEdge', () => {
const expectedEdge: TreeEdge = {
@ -291,6 +366,7 @@ describe('MerkleTree', () => {
should().throw(call, 'Element not found')
})
})
describe('#getTreeSlices', () => {
let fullTree: MerkleTree
before(async () => {
@ -344,7 +420,6 @@ describe('MerkleTree', () => {
'4027992409016347597424110157229339967488',
'923221781152860005594997320673730232320',
'752191049236692618445397735417537626112',
],
[
'81822854828781486047086122479545722339328',
@ -422,7 +497,6 @@ describe('MerkleTree', () => {
dst.insert(10)
should().equal(src.root, dst.root)
})
})
})