mirror of
https://github.com/tornadocash/fixed-merkle-tree.git
synced 2024-12-27 15:17:56 +01:00
Merge pull request #3 from tornadocash/typescript
Migration to Typescript, add partial tree implementation
This commit is contained in:
commit
73b4b68c9d
65
.eslintrc
65
.eslintrc
@ -1,26 +1,57 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"test/*.spec.ts",
|
||||
"lib"
|
||||
],
|
||||
"rules": {
|
||||
"indent": ["error", 2],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": [
|
||||
"error",
|
||||
"type"
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"require-await": "error"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true,
|
||||
"mocha": true
|
||||
}
|
||||
}
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1 +1,6 @@
|
||||
node_modules
|
||||
build
|
||||
yarn-error.log
|
||||
.idea
|
||||
.nyc_output
|
||||
.run
|
||||
|
73
README.md
73
README.md
@ -5,27 +5,68 @@ This is a fixed depth merkle tree implementation with sequential inserts
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
const MerkleTree = require('MerkleTree')
|
||||
import { MerkleTree, PartialMerkleTree } from 'fixed-merkle-tree'
|
||||
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.insert(6)
|
||||
tree.update(3, 42)
|
||||
const path = tree.path(tree.indexOf(2))
|
||||
const path = tree.proof(3)
|
||||
console.log(path)
|
||||
|
||||
// output:
|
||||
// output
|
||||
{
|
||||
pathIndex: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
pathElements: [
|
||||
'42',
|
||||
'19814528709687996974327303300007262407299502847885145507292406548098437687919',
|
||||
'11545490348087423460235196042660837039811055736960842865648632633825765931887',
|
||||
'14506027710748750947258687001455876266559341618222612722926156490737302846427',
|
||||
'4766583705360062980279572762279781527342845808161105063909171241304075622345',
|
||||
'16640205414190175414380077665118269450294358858897019640557533278896634808665',
|
||||
'13024477302430254842915163302704885770955784224100349847438808884122720088412',
|
||||
'11345696205391376769769683860277269518617256738724086786512014734609753488820',
|
||||
'17235543131546745471991808272245772046758360534180976603221801364506032471936',
|
||||
'155962837046691114236524362966874066300454611955781275944230309195800494087'
|
||||
]
|
||||
42,
|
||||
'4027992409016347597424110157229339967488',
|
||||
'2008015086710634950773855228781840564224',
|
||||
'938972308169430750202858820582946897920',
|
||||
'3743880566844110745576746962917825445888',
|
||||
'2074434463882483178614385966084599578624',
|
||||
'2808856778596740691845240322870189490176',
|
||||
'4986731814143931240516913804278285467648',
|
||||
'1918547053077726613961101558405545328640',
|
||||
'5444383861051812288142814494928935059456'
|
||||
],
|
||||
pathIndices: [
|
||||
0, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0
|
||||
],
|
||||
pathPositions: [
|
||||
3, 0, 1, 0, 0,
|
||||
0, 0, 0, 0, 0
|
||||
],
|
||||
pathRoot: '3917789723822252567979048877718291611648'
|
||||
}
|
||||
|
||||
const treeEdge = tree.getTreeEdge(2)
|
||||
const partialTree = new PartialMerkleTree(10, treeEdge, tree.elements.slice(treeEdge.edgeIndex))
|
||||
console.log(partialTree.elements)
|
||||
// [<2 empty items >, 3, 42, 5, 6]
|
||||
|
||||
const proofPath = partialTree.proof(3)
|
||||
console.log(proofPath)
|
||||
// output
|
||||
{
|
||||
pathElements: [
|
||||
42,
|
||||
'4027992409016347597424110157229339967488',
|
||||
'2008015086710634950773855228781840564224',
|
||||
'938972308169430750202858820582946897920',
|
||||
'3743880566844110745576746962917825445888',
|
||||
'2074434463882483178614385966084599578624',
|
||||
'2808856778596740691845240322870189490176',
|
||||
'4986731814143931240516913804278285467648',
|
||||
'1918547053077726613961101558405545328640',
|
||||
'5444383861051812288142814494928935059456'
|
||||
],
|
||||
pathIndices: [
|
||||
0, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0
|
||||
],
|
||||
pathPositions: [
|
||||
3, 0, 1, 0, 0,
|
||||
0, 0, 0, 0, 0
|
||||
],
|
||||
pathRoot: '3917789723822252567979048877718291611648'
|
||||
}
|
||||
|
||||
```
|
||||
|
43
lib/BaseTree.d.ts
vendored
Normal file
43
lib/BaseTree.d.ts
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
import { Element, HashFunction, ProofPath } from './';
|
||||
export declare class BaseTree {
|
||||
levels: number;
|
||||
protected _hashFn: HashFunction<Element>;
|
||||
protected zeroElement: Element;
|
||||
protected _zeros: Element[];
|
||||
protected _layers: Array<Element[]>;
|
||||
get capacity(): number;
|
||||
get layers(): Array<Element[]>;
|
||||
get zeros(): Element[];
|
||||
get elements(): Element[];
|
||||
get root(): Element;
|
||||
/**
|
||||
* Find an element in the tree
|
||||
* @param elements elements of tree
|
||||
* @param element An element to find
|
||||
* @param comparator A function that checks leaf value equality
|
||||
* @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;
|
||||
/**
|
||||
* Insert new element into the tree
|
||||
* @param element Element to insert
|
||||
*/
|
||||
insert(element: Element): void;
|
||||
bulkInsert(elements: Element[]): void;
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index: number, element: Element): void;
|
||||
/**
|
||||
* Get merkle path to a leaf
|
||||
* @param {number} index Leaf index to generate path for
|
||||
* @returns {{pathElements: Object[], pathIndex: number[]}} An object containing adjacent elements and left-right index
|
||||
*/
|
||||
path(index: number): ProofPath;
|
||||
protected _buildZeros(): void;
|
||||
protected _processNodes(nodes: Element[], layerIndex: number): any[];
|
||||
protected _processUpdate(index: number): void;
|
||||
}
|
154
lib/BaseTree.js
Normal file
154
lib/BaseTree.js
Normal file
@ -0,0 +1,154 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.BaseTree = void 0;
|
||||
class BaseTree {
|
||||
get capacity() {
|
||||
return 2 ** this.levels;
|
||||
}
|
||||
get layers() {
|
||||
return this._layers.slice();
|
||||
}
|
||||
get zeros() {
|
||||
return this._zeros.slice();
|
||||
}
|
||||
get elements() {
|
||||
return this._layers[0].slice();
|
||||
}
|
||||
get root() {
|
||||
var _a;
|
||||
return (_a = this._layers[this.levels][0]) !== null && _a !== void 0 ? _a : this._zeros[this.levels];
|
||||
}
|
||||
/**
|
||||
* Find an element in the tree
|
||||
* @param elements elements of tree
|
||||
* @param element An element to find
|
||||
* @param comparator A function that checks leaf value equality
|
||||
* @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, fromIndex, comparator) {
|
||||
if (comparator) {
|
||||
return elements.findIndex((el) => comparator(element, el));
|
||||
}
|
||||
else {
|
||||
return elements.indexOf(element, fromIndex);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Insert new element into the tree
|
||||
* @param element Element to insert
|
||||
*/
|
||||
insert(element) {
|
||||
if (this._layers[0].length >= this.capacity) {
|
||||
throw new Error('Tree is full');
|
||||
}
|
||||
this.update(this._layers[0].length, element);
|
||||
}
|
||||
/*
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements) {
|
||||
if (!elements.length) {
|
||||
return;
|
||||
}
|
||||
if (this._layers[0].length + elements.length > this.capacity) {
|
||||
throw new Error('Tree is full');
|
||||
}
|
||||
// First we insert all elements except the last one
|
||||
// updating only full subtree hashes (all layers where inserted element has odd index)
|
||||
// the last element will update the full path to the root making the tree consistent again
|
||||
for (let i = 0; i < elements.length - 1; i++) {
|
||||
this._layers[0].push(elements[i]);
|
||||
let level = 0;
|
||||
let index = this._layers[0].length - 1;
|
||||
while (index % 2 === 1) {
|
||||
level++;
|
||||
index >>= 1;
|
||||
const left = this._layers[level - 1][index * 2];
|
||||
const right = this._layers[level - 1][index * 2 + 1];
|
||||
this._layers[level][index] = this._hashFn(left, right);
|
||||
}
|
||||
}
|
||||
this.insert(elements[elements.length - 1]);
|
||||
}
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index, element) {
|
||||
if (isNaN(Number(index)) || index < 0 || index > this._layers[0].length || index >= this.capacity) {
|
||||
throw new Error('Insert index out of bounds: ' + index);
|
||||
}
|
||||
this._layers[0][index] = element;
|
||||
this._processUpdate(index);
|
||||
}
|
||||
/**
|
||||
* Get merkle path to a leaf
|
||||
* @param {number} index Leaf index to generate path for
|
||||
* @returns {{pathElements: Object[], pathIndex: number[]}} An object containing adjacent elements and left-right index
|
||||
*/
|
||||
path(index) {
|
||||
if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) {
|
||||
throw new Error('Index out of bounds: ' + index);
|
||||
}
|
||||
let elIndex = +index;
|
||||
const pathElements = [];
|
||||
const pathIndices = [];
|
||||
const pathPositions = [];
|
||||
for (let level = 0; level < this.levels; level++) {
|
||||
pathIndices[level] = elIndex % 2;
|
||||
const leafIndex = elIndex ^ 1;
|
||||
if (leafIndex < this._layers[level].length) {
|
||||
pathElements[level] = this._layers[level][leafIndex];
|
||||
pathPositions[level] = leafIndex;
|
||||
}
|
||||
else {
|
||||
pathElements[level] = this._zeros[level];
|
||||
pathPositions[level] = 0;
|
||||
}
|
||||
elIndex >>= 1;
|
||||
}
|
||||
return {
|
||||
pathElements,
|
||||
pathIndices,
|
||||
pathPositions,
|
||||
pathRoot: this.root,
|
||||
};
|
||||
}
|
||||
_buildZeros() {
|
||||
this._zeros = [this.zeroElement];
|
||||
for (let i = 1; i <= this.levels; i++) {
|
||||
this._zeros[i] = this._hashFn(this._zeros[i - 1], this._zeros[i - 1]);
|
||||
}
|
||||
}
|
||||
_processNodes(nodes, layerIndex) {
|
||||
const length = nodes.length;
|
||||
let currentLength = Math.ceil(length / 2);
|
||||
const currentLayer = new Array(currentLength);
|
||||
currentLength--;
|
||||
const starFrom = length - ((length % 2) ^ 1);
|
||||
let j = 0;
|
||||
for (let i = starFrom; i >= 0; i -= 2) {
|
||||
if (nodes[i - 1] === undefined)
|
||||
break;
|
||||
const left = nodes[i - 1];
|
||||
const right = (i === starFrom && length % 2 === 1) ? this._zeros[layerIndex - 1] : nodes[i];
|
||||
currentLayer[currentLength - j] = this._hashFn(left, right);
|
||||
j++;
|
||||
}
|
||||
return currentLayer;
|
||||
}
|
||||
_processUpdate(index) {
|
||||
for (let level = 1; level <= this.levels; level++) {
|
||||
index >>= 1;
|
||||
const left = this._layers[level - 1][index * 2];
|
||||
const right = index * 2 + 1 < this._layers[level - 1].length
|
||||
? this._layers[level - 1][index * 2 + 1]
|
||||
: this._zeros[level - 1];
|
||||
this._layers[level][index] = this._hashFn(left, right);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.BaseTree = BaseTree;
|
32
lib/FixedMerkleTree.d.ts
vendored
Normal file
32
lib/FixedMerkleTree.d.ts
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
import { Element, HashFunction, MerkleTreeOptions, ProofPath, SerializedTreeState, TreeEdge, TreeSlice } from './';
|
||||
import { BaseTree } from './BaseTree';
|
||||
export default class MerkleTree extends BaseTree {
|
||||
constructor(levels: number, elements?: Element[], { hashFunction, zeroElement, }?: MerkleTreeOptions);
|
||||
private _buildHashes;
|
||||
/**
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements: Element[]): void;
|
||||
indexOf(element: Element, comparator?: <T>(arg0: T, arg1: T) => boolean): number;
|
||||
proof(element: Element): ProofPath;
|
||||
getTreeEdge(edgeIndex: number): TreeEdge;
|
||||
/**
|
||||
* 🪓
|
||||
* @param count
|
||||
*/
|
||||
getTreeSlices(count?: number): TreeSlice[];
|
||||
/**
|
||||
* Serialize entire tree state including intermediate layers into a plain object
|
||||
* Deserializing it back will not require to recompute any hashes
|
||||
* Elements are not converted to a plain type, this is responsibility of the caller
|
||||
*/
|
||||
serialize(): SerializedTreeState;
|
||||
/**
|
||||
* Deserialize data into a MerkleTree instance
|
||||
* Make sure to provide the same hashFunction as was used in the source tree,
|
||||
* otherwise the tree state will be invalid
|
||||
*/
|
||||
static deserialize(data: SerializedTreeState, hashFunction?: HashFunction<Element>): MerkleTree;
|
||||
toString(): string;
|
||||
}
|
114
lib/FixedMerkleTree.js
Normal file
114
lib/FixedMerkleTree.js
Normal file
@ -0,0 +1,114 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const simpleHash_1 = __importDefault(require("./simpleHash"));
|
||||
const BaseTree_1 = require("./BaseTree");
|
||||
class MerkleTree extends BaseTree_1.BaseTree {
|
||||
constructor(levels, elements = [], { hashFunction = simpleHash_1.default, zeroElement = 0, } = {}) {
|
||||
super();
|
||||
this.levels = levels;
|
||||
if (elements.length > this.capacity) {
|
||||
throw new Error('Tree is full');
|
||||
}
|
||||
this._hashFn = hashFunction;
|
||||
this.zeroElement = zeroElement;
|
||||
this._layers = [];
|
||||
const leaves = elements.slice();
|
||||
this._layers = [leaves];
|
||||
this._buildZeros();
|
||||
this._buildHashes();
|
||||
}
|
||||
_buildHashes() {
|
||||
for (let layerIndex = 1; layerIndex <= this.levels; layerIndex++) {
|
||||
const nodes = this._layers[layerIndex - 1];
|
||||
this._layers[layerIndex] = this._processNodes(nodes, layerIndex);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements) {
|
||||
if (!elements.length) {
|
||||
return;
|
||||
}
|
||||
if (this._layers[0].length + elements.length > this.capacity) {
|
||||
throw new Error('Tree is full');
|
||||
}
|
||||
// First we insert all elements except the last one
|
||||
// updating only full subtree hashes (all layers where inserted element has odd index)
|
||||
// the last element will update the full path to the root making the tree consistent again
|
||||
for (let i = 0; i < elements.length - 1; i++) {
|
||||
this._layers[0].push(elements[i]);
|
||||
let level = 0;
|
||||
let index = this._layers[0].length - 1;
|
||||
while (index % 2 === 1) {
|
||||
level++;
|
||||
index >>= 1;
|
||||
this._layers[level][index] = this._hashFn(this._layers[level - 1][index * 2], this._layers[level - 1][index * 2 + 1]);
|
||||
}
|
||||
}
|
||||
this.insert(elements[elements.length - 1]);
|
||||
}
|
||||
indexOf(element, comparator) {
|
||||
return BaseTree_1.BaseTree.indexOf(this._layers[0], element, 0, comparator);
|
||||
}
|
||||
proof(element) {
|
||||
const index = this.indexOf(element);
|
||||
return this.path(index);
|
||||
}
|
||||
getTreeEdge(edgeIndex) {
|
||||
const edgeElement = this._layers[0][edgeIndex];
|
||||
if (edgeElement === undefined) {
|
||||
throw new Error('Element not found');
|
||||
}
|
||||
const edgePath = this.path(edgeIndex);
|
||||
return { edgePath, edgeElement, edgeIndex, edgeElementsCount: this._layers[0].length };
|
||||
}
|
||||
/**
|
||||
* 🪓
|
||||
* @param count
|
||||
*/
|
||||
getTreeSlices(count = 4) {
|
||||
const length = this._layers[0].length;
|
||||
let size = Math.ceil(length / count);
|
||||
if (size % 2)
|
||||
size++;
|
||||
const slices = [];
|
||||
for (let i = 0; i < length; i += size) {
|
||||
const edgeLeft = i;
|
||||
const edgeRight = i + size;
|
||||
slices.push({ edge: this.getTreeEdge(edgeLeft), elements: this.elements.slice(edgeLeft, edgeRight) });
|
||||
}
|
||||
return slices;
|
||||
}
|
||||
/**
|
||||
* Serialize entire tree state including intermediate layers into a plain object
|
||||
* Deserializing it back will not require to recompute any hashes
|
||||
* Elements are not converted to a plain type, this is responsibility of the caller
|
||||
*/
|
||||
serialize() {
|
||||
return {
|
||||
levels: this.levels,
|
||||
_zeros: this._zeros,
|
||||
_layers: this._layers,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Deserialize data into a MerkleTree instance
|
||||
* Make sure to provide the same hashFunction as was used in the source tree,
|
||||
* otherwise the tree state will be invalid
|
||||
*/
|
||||
static deserialize(data, hashFunction) {
|
||||
const instance = Object.assign(Object.create(this.prototype), data);
|
||||
instance._hashFn = hashFunction || simpleHash_1.default;
|
||||
instance.zeroElement = instance._zeros[0];
|
||||
return instance;
|
||||
}
|
||||
toString() {
|
||||
return JSON.stringify(this.serialize());
|
||||
}
|
||||
}
|
||||
exports.default = MerkleTree;
|
35
lib/PartialMerkleTree.d.ts
vendored
Normal file
35
lib/PartialMerkleTree.d.ts
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
import { Element, HashFunction, MerkleTreeOptions, ProofPath, SerializedPartialTreeState, TreeEdge } from './';
|
||||
import { BaseTree } from './BaseTree';
|
||||
export declare class PartialMerkleTree extends BaseTree {
|
||||
private _leaves;
|
||||
private _leavesAfterEdge;
|
||||
private _edgeLeaf;
|
||||
private _initialRoot;
|
||||
private _edgeLeafProof;
|
||||
private _proofMap;
|
||||
constructor(levels: number, { edgePath, edgeElement, edgeIndex, edgeElementsCount, }: TreeEdge, leaves: Element[], { hashFunction, zeroElement }?: MerkleTreeOptions);
|
||||
get edgeIndex(): number;
|
||||
get edgeElement(): Element;
|
||||
get edgeLeafProof(): ProofPath;
|
||||
private _createProofMap;
|
||||
private _buildTree;
|
||||
private _buildHashes;
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index: number, element: Element): void;
|
||||
path(index: number): ProofPath;
|
||||
indexOf(element: Element, comparator?: <T>(arg0: T, arg1: T) => boolean): number;
|
||||
proof(element: Element): ProofPath;
|
||||
/**
|
||||
* Shifts edge of tree to left
|
||||
* @param edge new TreeEdge below current edge
|
||||
* @param elements leaves between old and new edge
|
||||
*/
|
||||
shiftEdge(edge: TreeEdge, elements: Element[]): void;
|
||||
serialize(): SerializedPartialTreeState;
|
||||
static deserialize(data: SerializedPartialTreeState, hashFunction?: HashFunction<Element>): PartialMerkleTree;
|
||||
toString(): string;
|
||||
}
|
159
lib/PartialMerkleTree.js
Normal file
159
lib/PartialMerkleTree.js
Normal file
@ -0,0 +1,159 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PartialMerkleTree = void 0;
|
||||
const simpleHash_1 = __importDefault(require("./simpleHash"));
|
||||
const BaseTree_1 = require("./BaseTree");
|
||||
class PartialMerkleTree extends BaseTree_1.BaseTree {
|
||||
constructor(levels, { edgePath, edgeElement, edgeIndex, edgeElementsCount, }, leaves, { hashFunction, zeroElement } = {}) {
|
||||
super();
|
||||
if (edgeIndex + leaves.length !== edgeElementsCount)
|
||||
throw new Error('Invalid number of elements');
|
||||
this._edgeLeafProof = edgePath;
|
||||
this._initialRoot = edgePath.pathRoot;
|
||||
this.zeroElement = zeroElement !== null && zeroElement !== void 0 ? zeroElement : 0;
|
||||
this._edgeLeaf = { data: edgeElement, index: edgeIndex };
|
||||
this._leavesAfterEdge = leaves;
|
||||
this.levels = levels;
|
||||
this._hashFn = hashFunction || simpleHash_1.default;
|
||||
this._createProofMap();
|
||||
this._buildTree();
|
||||
}
|
||||
get edgeIndex() {
|
||||
return this._edgeLeaf.index;
|
||||
}
|
||||
get edgeElement() {
|
||||
return this._edgeLeaf.data;
|
||||
}
|
||||
get edgeLeafProof() {
|
||||
return this._edgeLeafProof;
|
||||
}
|
||||
_createProofMap() {
|
||||
this._proofMap = this.edgeLeafProof.pathPositions.reduce((p, c, i) => {
|
||||
p.set(i, [c, this.edgeLeafProof.pathElements[i]]);
|
||||
return p;
|
||||
}, new Map());
|
||||
this._proofMap.set(this.levels, [0, this.edgeLeafProof.pathRoot]);
|
||||
}
|
||||
_buildTree() {
|
||||
const edgeLeafIndex = this._edgeLeaf.index;
|
||||
this._leaves = Array(edgeLeafIndex).concat(this._leavesAfterEdge);
|
||||
if (this._proofMap.has(0)) {
|
||||
const [proofPos, proofEl] = this._proofMap.get(0);
|
||||
this._leaves[proofPos] = proofEl;
|
||||
}
|
||||
this._layers = [this._leaves];
|
||||
this._buildZeros();
|
||||
this._buildHashes();
|
||||
}
|
||||
_buildHashes() {
|
||||
for (let layerIndex = 1; layerIndex <= this.levels; layerIndex++) {
|
||||
const nodes = this._layers[layerIndex - 1];
|
||||
const currentLayer = this._processNodes(nodes, layerIndex);
|
||||
if (this._proofMap.has(layerIndex)) {
|
||||
const [proofPos, proofEl] = this._proofMap.get(layerIndex);
|
||||
if (!currentLayer[proofPos])
|
||||
currentLayer[proofPos] = proofEl;
|
||||
}
|
||||
this._layers[layerIndex] = currentLayer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index, element) {
|
||||
if (isNaN(Number(index)) || index < 0 || index > this._layers[0].length || index >= this.capacity) {
|
||||
throw new Error('Insert index out of bounds: ' + index);
|
||||
}
|
||||
if (index < this._edgeLeaf.index) {
|
||||
throw new Error(`Index ${index} is below the edge: ${this._edgeLeaf.index}`);
|
||||
}
|
||||
this._layers[0][index] = element;
|
||||
this._processUpdate(index);
|
||||
}
|
||||
path(index) {
|
||||
var _a;
|
||||
if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) {
|
||||
throw new Error('Index out of bounds: ' + index);
|
||||
}
|
||||
if (index < this._edgeLeaf.index) {
|
||||
throw new Error(`Index ${index} is below the edge: ${this._edgeLeaf.index}`);
|
||||
}
|
||||
let elIndex = Number(index);
|
||||
const pathElements = [];
|
||||
const pathIndices = [];
|
||||
const pathPositions = [];
|
||||
for (let level = 0; level < this.levels; level++) {
|
||||
pathIndices[level] = elIndex % 2;
|
||||
const leafIndex = elIndex ^ 1;
|
||||
if (leafIndex < this._layers[level].length) {
|
||||
pathElements[level] = this._layers[level][leafIndex];
|
||||
pathPositions[level] = leafIndex;
|
||||
}
|
||||
else {
|
||||
pathElements[level] = this._zeros[level];
|
||||
pathPositions[level] = 0;
|
||||
}
|
||||
const [proofPos, proofEl] = this._proofMap.get(level);
|
||||
pathElements[level] = (_a = pathElements[level]) !== null && _a !== void 0 ? _a : (proofPos === leafIndex ? proofEl : this._zeros[level]);
|
||||
elIndex >>= 1;
|
||||
}
|
||||
return {
|
||||
pathElements,
|
||||
pathIndices,
|
||||
pathPositions,
|
||||
pathRoot: this.root,
|
||||
};
|
||||
}
|
||||
indexOf(element, comparator) {
|
||||
return BaseTree_1.BaseTree.indexOf(this._layers[0], element, this.edgeIndex, comparator);
|
||||
}
|
||||
proof(element) {
|
||||
const index = this.indexOf(element);
|
||||
return this.path(index);
|
||||
}
|
||||
/**
|
||||
* Shifts edge of tree to left
|
||||
* @param edge new TreeEdge below current edge
|
||||
* @param elements leaves between old and new edge
|
||||
*/
|
||||
shiftEdge(edge, elements) {
|
||||
if (this._edgeLeaf.index <= edge.edgeIndex) {
|
||||
throw new Error(`New edgeIndex should be smaller then ${this._edgeLeaf.index}`);
|
||||
}
|
||||
if (elements.length !== (this._edgeLeaf.index - edge.edgeIndex)) {
|
||||
throw new Error(`Elements length should be ${this._edgeLeaf.index - edge.edgeIndex}`);
|
||||
}
|
||||
this._edgeLeafProof = edge.edgePath;
|
||||
this._edgeLeaf = { index: edge.edgeIndex, data: edge.edgeElement };
|
||||
this._leavesAfterEdge = [...elements, ...this._leavesAfterEdge];
|
||||
this._createProofMap();
|
||||
this._buildTree();
|
||||
}
|
||||
serialize() {
|
||||
return {
|
||||
_edgeLeafProof: this._edgeLeafProof,
|
||||
_edgeLeaf: this._edgeLeaf,
|
||||
_layers: this._layers,
|
||||
_zeros: this._zeros,
|
||||
levels: this.levels,
|
||||
};
|
||||
}
|
||||
static deserialize(data, hashFunction) {
|
||||
const instance = Object.assign(Object.create(this.prototype), data);
|
||||
instance._hashFn = hashFunction || simpleHash_1.default;
|
||||
instance._initialRoot = data._edgeLeafProof.pathRoot;
|
||||
instance.zeroElement = instance._zeros[0];
|
||||
instance._leavesAfterEdge = instance._layers[0].slice(data._edgeLeaf.index);
|
||||
instance._createProofMap();
|
||||
return instance;
|
||||
}
|
||||
toString() {
|
||||
return JSON.stringify(this.serialize());
|
||||
}
|
||||
}
|
||||
exports.PartialMerkleTree = PartialMerkleTree;
|
45
lib/index.d.ts
vendored
Normal file
45
lib/index.d.ts
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
import { default as MerkleTree } from './FixedMerkleTree';
|
||||
export { PartialMerkleTree } from './PartialMerkleTree';
|
||||
export { simpleHash } from './simpleHash';
|
||||
export { MerkleTree };
|
||||
export default MerkleTree;
|
||||
export declare type HashFunction<T> = {
|
||||
(left: T, right: T): string;
|
||||
};
|
||||
export declare type MerkleTreeOptions = {
|
||||
hashFunction?: HashFunction<Element>;
|
||||
zeroElement?: Element;
|
||||
};
|
||||
export declare type Element = string | number;
|
||||
export declare type SerializedTreeState = {
|
||||
levels: number;
|
||||
_zeros: Array<Element>;
|
||||
_layers: Array<Element[]>;
|
||||
};
|
||||
export declare type SerializedPartialTreeState = {
|
||||
levels: number;
|
||||
_layers: Element[][];
|
||||
_zeros: Array<Element>;
|
||||
_edgeLeafProof: ProofPath;
|
||||
_edgeLeaf: LeafWithIndex;
|
||||
};
|
||||
export declare type ProofPath = {
|
||||
pathElements: Element[];
|
||||
pathIndices: number[];
|
||||
pathPositions: number[];
|
||||
pathRoot: Element;
|
||||
};
|
||||
export declare type TreeEdge = {
|
||||
edgeElement: Element;
|
||||
edgePath: ProofPath;
|
||||
edgeIndex: number;
|
||||
edgeElementsCount: number;
|
||||
};
|
||||
export declare type TreeSlice = {
|
||||
edge: TreeEdge;
|
||||
elements: Element[];
|
||||
};
|
||||
export declare type LeafWithIndex = {
|
||||
index: number;
|
||||
data: Element;
|
||||
};
|
13
lib/index.js
Normal file
13
lib/index.js
Normal file
@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MerkleTree = exports.simpleHash = exports.PartialMerkleTree = void 0;
|
||||
const FixedMerkleTree_1 = __importDefault(require("./FixedMerkleTree"));
|
||||
Object.defineProperty(exports, "MerkleTree", { enumerable: true, get: function () { return FixedMerkleTree_1.default; } });
|
||||
var PartialMerkleTree_1 = require("./PartialMerkleTree");
|
||||
Object.defineProperty(exports, "PartialMerkleTree", { enumerable: true, get: function () { return PartialMerkleTree_1.PartialMerkleTree; } });
|
||||
var simpleHash_1 = require("./simpleHash");
|
||||
Object.defineProperty(exports, "simpleHash", { enumerable: true, get: function () { return simpleHash_1.simpleHash; } });
|
||||
exports.default = FixedMerkleTree_1.default;
|
10
lib/simpleHash.d.ts
vendored
Normal file
10
lib/simpleHash.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import { Element } from './';
|
||||
/***
|
||||
* This is insecure hash function, just for example only
|
||||
* @param data
|
||||
* @param seed
|
||||
* @param hashLength
|
||||
*/
|
||||
export declare function simpleHash<T>(data: T[], seed?: number, hashLength?: number): string;
|
||||
declare const _default: (left: Element, right: Element) => string;
|
||||
export default _default;
|
21
lib/simpleHash.js
Normal file
21
lib/simpleHash.js
Normal file
@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.simpleHash = void 0;
|
||||
/***
|
||||
* This is insecure hash function, just for example only
|
||||
* @param data
|
||||
* @param seed
|
||||
* @param hashLength
|
||||
*/
|
||||
function simpleHash(data, seed, hashLength = 40) {
|
||||
const str = data.join('');
|
||||
let i, l, hval = seed !== null && seed !== void 0 ? seed : 0x811c9dcc5;
|
||||
for (i = 0, l = str.length; i < l; i++) {
|
||||
hval ^= str.charCodeAt(i);
|
||||
hval += (hval << 1) + (hval << 4) + (hval << 6) + (hval << 8) + (hval << 24);
|
||||
}
|
||||
const hash = (hval >>> 0).toString(16);
|
||||
return BigInt('0x' + hash.padEnd(hashLength - (hash.length - 1), '0')).toString(10);
|
||||
}
|
||||
exports.simpleHash = simpleHash;
|
||||
exports.default = (left, right) => simpleHash([left, right]);
|
7551
package-lock.json
generated
7551
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@ -1,12 +1,17 @@
|
||||
{
|
||||
"name": "fixed-merkle-tree",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.3",
|
||||
"description": "Fixed depth merkle tree implementation with sequential inserts",
|
||||
"repository": "https://github.com/tornadocash/fixed-merkle-tree.git",
|
||||
"main": "src/merkleTree.js",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "mocha",
|
||||
"lint": "eslint ."
|
||||
"test": "ts-mocha 'test/*.spec.ts' -s 10",
|
||||
"coverage": "nyc npm run test",
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf lib/",
|
||||
"prepare": "npm run clean && npm run build",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"keywords": [
|
||||
"merkle",
|
||||
@ -16,16 +21,20 @@
|
||||
"author": "Roman Semenov <semenov.roma@gmail.com>",
|
||||
"license": "ISC",
|
||||
"files": [
|
||||
"src/*"
|
||||
"src/*",
|
||||
"lib/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"snarkjs": "git+https://github.com/tornadocash/snarkjs.git#869181cfaf7526fe8972073d31655493a04326d5",
|
||||
"circomlib": "git+https://github.com/tornadocash/circomlib.git#5beb6aee94923052faeecea40135d45b6ce6172c"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.1.0",
|
||||
"@types/expect": "^24.3.0",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
||||
"@typescript-eslint/parser": "^5.12.0",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^7.5.0",
|
||||
"mocha": "^8.1.0"
|
||||
"eslint": "^8.9.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"mocha": "^9.2.2",
|
||||
"nyc": "^15.1.0",
|
||||
"ts-mocha": "^9.0.2",
|
||||
"typescript": "^4.6.2"
|
||||
}
|
||||
}
|
||||
|
169
src/BaseTree.ts
Normal file
169
src/BaseTree.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import { Element, HashFunction, ProofPath } from './'
|
||||
|
||||
export class BaseTree {
|
||||
levels: number
|
||||
protected _hashFn: HashFunction<Element>
|
||||
protected zeroElement: Element
|
||||
protected _zeros: Element[]
|
||||
protected _layers: Array<Element[]>
|
||||
|
||||
get capacity() {
|
||||
return 2 ** this.levels
|
||||
}
|
||||
|
||||
get layers(): Array<Element[]> {
|
||||
return this._layers.slice()
|
||||
}
|
||||
|
||||
get zeros(): Element[] {
|
||||
return this._zeros.slice()
|
||||
}
|
||||
|
||||
get elements(): Element[] {
|
||||
return this._layers[0].slice()
|
||||
}
|
||||
|
||||
get root(): Element {
|
||||
return this._layers[this.levels][0] ?? this._zeros[this.levels]
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an element in the tree
|
||||
* @param elements elements of tree
|
||||
* @param element An element to find
|
||||
* @param comparator A function that checks leaf value equality
|
||||
* @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 {
|
||||
if (comparator) {
|
||||
return elements.findIndex((el) => comparator<Element>(element, el))
|
||||
} else {
|
||||
return elements.indexOf(element, fromIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new element into the tree
|
||||
* @param element Element to insert
|
||||
*/
|
||||
insert(element: Element) {
|
||||
if (this._layers[0].length >= this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
this.update(this._layers[0].length, element)
|
||||
}
|
||||
|
||||
/*
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements: Element[]): void {
|
||||
if (!elements.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._layers[0].length + elements.length > this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
// First we insert all elements except the last one
|
||||
// updating only full subtree hashes (all layers where inserted element has odd index)
|
||||
// the last element will update the full path to the root making the tree consistent again
|
||||
for (let i = 0; i < elements.length - 1; i++) {
|
||||
this._layers[0].push(elements[i])
|
||||
let level = 0
|
||||
let index = this._layers[0].length - 1
|
||||
while (index % 2 === 1) {
|
||||
level++
|
||||
index >>= 1
|
||||
const left = this._layers[level - 1][index * 2]
|
||||
const right = this._layers[level - 1][index * 2 + 1]
|
||||
this._layers[level][index] = this._hashFn(left, right)
|
||||
}
|
||||
}
|
||||
this.insert(elements[elements.length - 1])
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index: number, element: Element) {
|
||||
if (isNaN(Number(index)) || index < 0 || index > this._layers[0].length || index >= this.capacity) {
|
||||
throw new Error('Insert index out of bounds: ' + index)
|
||||
}
|
||||
this._layers[0][index] = element
|
||||
this._processUpdate(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get merkle path to a leaf
|
||||
* @param {number} index Leaf index to generate path for
|
||||
* @returns {{pathElements: Object[], pathIndex: number[]}} An object containing adjacent elements and left-right index
|
||||
*/
|
||||
path(index: number): ProofPath {
|
||||
if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) {
|
||||
throw new Error('Index out of bounds: ' + index)
|
||||
}
|
||||
let elIndex = +index
|
||||
const pathElements: Element[] = []
|
||||
const pathIndices: number[] = []
|
||||
const pathPositions: number [] = []
|
||||
for (let level = 0; level < this.levels; level++) {
|
||||
pathIndices[level] = elIndex % 2
|
||||
const leafIndex = elIndex ^ 1
|
||||
if (leafIndex < this._layers[level].length) {
|
||||
pathElements[level] = this._layers[level][leafIndex]
|
||||
pathPositions[level] = leafIndex
|
||||
} else {
|
||||
pathElements[level] = this._zeros[level]
|
||||
pathPositions[level] = 0
|
||||
}
|
||||
elIndex >>= 1
|
||||
}
|
||||
return {
|
||||
pathElements,
|
||||
pathIndices,
|
||||
pathPositions,
|
||||
pathRoot: this.root,
|
||||
}
|
||||
}
|
||||
|
||||
protected _buildZeros() {
|
||||
this._zeros = [this.zeroElement]
|
||||
for (let i = 1; i <= this.levels; i++) {
|
||||
this._zeros[i] = this._hashFn(this._zeros[i - 1], this._zeros[i - 1])
|
||||
}
|
||||
}
|
||||
|
||||
protected _processNodes(nodes: Element[], layerIndex: number) {
|
||||
const length = nodes.length
|
||||
let currentLength = Math.ceil(length / 2)
|
||||
const currentLayer = new Array(currentLength)
|
||||
currentLength--
|
||||
const starFrom = length - ((length % 2) ^ 1)
|
||||
let j = 0
|
||||
for (let i = starFrom; i >= 0; i -= 2) {
|
||||
if (nodes[i - 1] === undefined) break
|
||||
const left = nodes[i - 1]
|
||||
const right = (i === starFrom && length % 2 === 1) ? this._zeros[layerIndex - 1] : nodes[i]
|
||||
currentLayer[currentLength - j] = this._hashFn(left, right)
|
||||
j++
|
||||
}
|
||||
return currentLayer
|
||||
}
|
||||
|
||||
protected _processUpdate(index: number) {
|
||||
for (let level = 1; level <= this.levels; level++) {
|
||||
index >>= 1
|
||||
const left = this._layers[level - 1][index * 2]
|
||||
const right = index * 2 + 1 < this._layers[level - 1].length
|
||||
? this._layers[level - 1][index * 2 + 1]
|
||||
: this._zeros[level - 1]
|
||||
this._layers[level][index] = this._hashFn(left, right)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
128
src/FixedMerkleTree.ts
Normal file
128
src/FixedMerkleTree.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { Element, HashFunction, MerkleTreeOptions, ProofPath, SerializedTreeState, TreeEdge, TreeSlice } from './'
|
||||
import defaultHash from './simpleHash'
|
||||
import { BaseTree } from './BaseTree'
|
||||
|
||||
|
||||
export default class MerkleTree extends BaseTree {
|
||||
constructor(levels: number, elements: Element[] = [], {
|
||||
hashFunction = defaultHash,
|
||||
zeroElement = 0,
|
||||
}: MerkleTreeOptions = {}) {
|
||||
super()
|
||||
this.levels = levels
|
||||
if (elements.length > this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
this._hashFn = hashFunction
|
||||
this.zeroElement = zeroElement
|
||||
this._layers = []
|
||||
const leaves = elements.slice()
|
||||
this._layers = [leaves]
|
||||
this._buildZeros()
|
||||
this._buildHashes()
|
||||
}
|
||||
|
||||
private _buildHashes() {
|
||||
for (let layerIndex = 1; layerIndex <= this.levels; layerIndex++) {
|
||||
const nodes = this._layers[layerIndex - 1]
|
||||
this._layers[layerIndex] = this._processNodes(nodes, layerIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements: Element[]): void {
|
||||
if (!elements.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._layers[0].length + elements.length > this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
// First we insert all elements except the last one
|
||||
// updating only full subtree hashes (all layers where inserted element has odd index)
|
||||
// the last element will update the full path to the root making the tree consistent again
|
||||
for (let i = 0; i < elements.length - 1; i++) {
|
||||
this._layers[0].push(elements[i])
|
||||
let level = 0
|
||||
let index = this._layers[0].length - 1
|
||||
while (index % 2 === 1) {
|
||||
level++
|
||||
index >>= 1
|
||||
this._layers[level][index] = this._hashFn(
|
||||
this._layers[level - 1][index * 2],
|
||||
this._layers[level - 1][index * 2 + 1],
|
||||
)
|
||||
}
|
||||
}
|
||||
this.insert(elements[elements.length - 1])
|
||||
}
|
||||
|
||||
indexOf(element: Element, comparator?: <T> (arg0: T, arg1: T) => boolean): number {
|
||||
return BaseTree.indexOf(this._layers[0], element, 0, comparator)
|
||||
}
|
||||
|
||||
proof(element: Element): ProofPath {
|
||||
const index = this.indexOf(element)
|
||||
return this.path(index)
|
||||
}
|
||||
|
||||
getTreeEdge(edgeIndex: number): TreeEdge {
|
||||
const edgeElement = this._layers[0][edgeIndex]
|
||||
if (edgeElement === undefined) {
|
||||
throw new Error('Element not found')
|
||||
}
|
||||
const edgePath = this.path(edgeIndex)
|
||||
return { edgePath, edgeElement, edgeIndex, edgeElementsCount: this._layers[0].length }
|
||||
}
|
||||
|
||||
/**
|
||||
* 🪓
|
||||
* @param count
|
||||
*/
|
||||
getTreeSlices(count = 4): TreeSlice[] {
|
||||
const length = this._layers[0].length
|
||||
let size = Math.ceil(length / count)
|
||||
if (size % 2) size++
|
||||
const slices: TreeSlice[] = []
|
||||
for (let i = 0; i < length; i += size) {
|
||||
const edgeLeft = i
|
||||
const edgeRight = i + size
|
||||
slices.push({ edge: this.getTreeEdge(edgeLeft), elements: this.elements.slice(edgeLeft, edgeRight) })
|
||||
}
|
||||
return slices
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize entire tree state including intermediate layers into a plain object
|
||||
* Deserializing it back will not require to recompute any hashes
|
||||
* Elements are not converted to a plain type, this is responsibility of the caller
|
||||
*/
|
||||
serialize(): SerializedTreeState {
|
||||
return {
|
||||
levels: this.levels,
|
||||
_zeros: this._zeros,
|
||||
_layers: this._layers,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize data into a MerkleTree instance
|
||||
* Make sure to provide the same hashFunction as was used in the source tree,
|
||||
* otherwise the tree state will be invalid
|
||||
*/
|
||||
static deserialize(data: SerializedTreeState, hashFunction?: HashFunction<Element>): MerkleTree {
|
||||
const instance: MerkleTree = Object.assign(Object.create(this.prototype), data)
|
||||
instance._hashFn = hashFunction || defaultHash
|
||||
instance.zeroElement = instance._zeros[0]
|
||||
return instance
|
||||
}
|
||||
|
||||
toString() {
|
||||
return JSON.stringify(this.serialize())
|
||||
}
|
||||
}
|
||||
|
186
src/PartialMerkleTree.ts
Normal file
186
src/PartialMerkleTree.ts
Normal file
@ -0,0 +1,186 @@
|
||||
import {
|
||||
Element,
|
||||
HashFunction,
|
||||
LeafWithIndex,
|
||||
MerkleTreeOptions,
|
||||
ProofPath,
|
||||
SerializedPartialTreeState,
|
||||
TreeEdge,
|
||||
} from './'
|
||||
import defaultHash from './simpleHash'
|
||||
import { BaseTree } from './BaseTree'
|
||||
|
||||
export class PartialMerkleTree extends BaseTree {
|
||||
private _leaves: Element[]
|
||||
private _leavesAfterEdge: Element[]
|
||||
private _edgeLeaf: LeafWithIndex
|
||||
private _initialRoot: Element
|
||||
private _edgeLeafProof: ProofPath
|
||||
private _proofMap: Map<number, [i: number, el: Element]>
|
||||
|
||||
constructor(levels: number, {
|
||||
edgePath,
|
||||
edgeElement,
|
||||
edgeIndex,
|
||||
edgeElementsCount,
|
||||
}: TreeEdge, leaves: Element[], { hashFunction, zeroElement }: MerkleTreeOptions = {}) {
|
||||
super()
|
||||
if (edgeIndex + leaves.length !== edgeElementsCount) throw new Error('Invalid number of elements')
|
||||
this._edgeLeafProof = edgePath
|
||||
this._initialRoot = edgePath.pathRoot
|
||||
this.zeroElement = zeroElement ?? 0
|
||||
this._edgeLeaf = { data: edgeElement, index: edgeIndex }
|
||||
this._leavesAfterEdge = leaves
|
||||
this.levels = levels
|
||||
this._hashFn = hashFunction || defaultHash
|
||||
this._createProofMap()
|
||||
this._buildTree()
|
||||
}
|
||||
|
||||
get edgeIndex(): number {
|
||||
return this._edgeLeaf.index
|
||||
}
|
||||
|
||||
get edgeElement(): Element {
|
||||
return this._edgeLeaf.data
|
||||
}
|
||||
|
||||
get edgeLeafProof(): ProofPath {
|
||||
return this._edgeLeafProof
|
||||
}
|
||||
|
||||
private _createProofMap() {
|
||||
this._proofMap = this.edgeLeafProof.pathPositions.reduce((p, c, i) => {
|
||||
p.set(i, [c, this.edgeLeafProof.pathElements[i]])
|
||||
return p
|
||||
}, new Map())
|
||||
this._proofMap.set(this.levels, [0, this.edgeLeafProof.pathRoot])
|
||||
}
|
||||
|
||||
private _buildTree(): void {
|
||||
const edgeLeafIndex = this._edgeLeaf.index
|
||||
this._leaves = Array(edgeLeafIndex).concat(this._leavesAfterEdge)
|
||||
if (this._proofMap.has(0)) {
|
||||
const [proofPos, proofEl] = this._proofMap.get(0)
|
||||
this._leaves[proofPos] = proofEl
|
||||
}
|
||||
this._layers = [this._leaves]
|
||||
this._buildZeros()
|
||||
this._buildHashes()
|
||||
}
|
||||
|
||||
private _buildHashes() {
|
||||
for (let layerIndex = 1; layerIndex <= this.levels; layerIndex++) {
|
||||
const nodes = this._layers[layerIndex - 1]
|
||||
const currentLayer = this._processNodes(nodes, layerIndex)
|
||||
if (this._proofMap.has(layerIndex)) {
|
||||
const [proofPos, proofEl] = this._proofMap.get(layerIndex)
|
||||
if (!currentLayer[proofPos]) currentLayer[proofPos] = proofEl
|
||||
}
|
||||
this._layers[layerIndex] = currentLayer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index: number, element: Element) {
|
||||
if (isNaN(Number(index)) || index < 0 || index > this._layers[0].length || index >= this.capacity) {
|
||||
throw new Error('Insert index out of bounds: ' + index)
|
||||
}
|
||||
if (index < this._edgeLeaf.index) {
|
||||
throw new Error(`Index ${index} is below the edge: ${this._edgeLeaf.index}`)
|
||||
}
|
||||
this._layers[0][index] = element
|
||||
this._processUpdate(index)
|
||||
}
|
||||
|
||||
path(index: number): ProofPath {
|
||||
if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) {
|
||||
throw new Error('Index out of bounds: ' + index)
|
||||
}
|
||||
if (index < this._edgeLeaf.index) {
|
||||
throw new Error(`Index ${index} is below the edge: ${this._edgeLeaf.index}`)
|
||||
}
|
||||
let elIndex = Number(index)
|
||||
const pathElements: Element[] = []
|
||||
const pathIndices: number[] = []
|
||||
const pathPositions: number [] = []
|
||||
for (let level = 0; level < this.levels; level++) {
|
||||
pathIndices[level] = elIndex % 2
|
||||
const leafIndex = elIndex ^ 1
|
||||
if (leafIndex < this._layers[level].length) {
|
||||
pathElements[level] = this._layers[level][leafIndex]
|
||||
pathPositions[level] = leafIndex
|
||||
} else {
|
||||
pathElements[level] = this._zeros[level]
|
||||
pathPositions[level] = 0
|
||||
}
|
||||
const [proofPos, proofEl] = this._proofMap.get(level)
|
||||
pathElements[level] = pathElements[level] ?? (proofPos === leafIndex ? proofEl : this._zeros[level])
|
||||
elIndex >>= 1
|
||||
}
|
||||
return {
|
||||
pathElements,
|
||||
pathIndices,
|
||||
pathPositions,
|
||||
pathRoot: this.root,
|
||||
}
|
||||
}
|
||||
|
||||
indexOf(element: Element, comparator?: <T> (arg0: T, arg1: T) => boolean): number {
|
||||
return BaseTree.indexOf(this._layers[0], element, this.edgeIndex, comparator)
|
||||
}
|
||||
|
||||
proof(element: Element): ProofPath {
|
||||
const index = this.indexOf(element)
|
||||
return this.path(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts edge of tree to left
|
||||
* @param edge new TreeEdge below current edge
|
||||
* @param elements leaves between old and new edge
|
||||
*/
|
||||
|
||||
shiftEdge(edge: TreeEdge, elements: Element[]) {
|
||||
if (this._edgeLeaf.index <= edge.edgeIndex) {
|
||||
throw new Error(`New edgeIndex should be smaller then ${this._edgeLeaf.index}`)
|
||||
}
|
||||
if (elements.length !== (this._edgeLeaf.index - edge.edgeIndex)) {
|
||||
throw new Error(`Elements length should be ${this._edgeLeaf.index - edge.edgeIndex}`)
|
||||
}
|
||||
this._edgeLeafProof = edge.edgePath
|
||||
this._edgeLeaf = { index: edge.edgeIndex, data: edge.edgeElement }
|
||||
this._leavesAfterEdge = [...elements, ...this._leavesAfterEdge]
|
||||
this._createProofMap()
|
||||
this._buildTree()
|
||||
}
|
||||
|
||||
serialize(): SerializedPartialTreeState {
|
||||
return {
|
||||
_edgeLeafProof: this._edgeLeafProof,
|
||||
_edgeLeaf: this._edgeLeaf,
|
||||
_layers: this._layers,
|
||||
_zeros: this._zeros,
|
||||
levels: this.levels,
|
||||
}
|
||||
}
|
||||
|
||||
static deserialize(data: SerializedPartialTreeState, hashFunction?: HashFunction<Element>): PartialMerkleTree {
|
||||
const instance: PartialMerkleTree = Object.assign(Object.create(this.prototype), data)
|
||||
instance._hashFn = hashFunction || defaultHash
|
||||
instance._initialRoot = data._edgeLeafProof.pathRoot
|
||||
instance.zeroElement = instance._zeros[0]
|
||||
instance._leavesAfterEdge = instance._layers[0].slice(data._edgeLeaf.index)
|
||||
instance._createProofMap()
|
||||
return instance
|
||||
}
|
||||
|
||||
toString() {
|
||||
return JSON.stringify(this.serialize())
|
||||
}
|
||||
}
|
46
src/index.ts
Normal file
46
src/index.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { default as MerkleTree } from './FixedMerkleTree'
|
||||
export { PartialMerkleTree } from './PartialMerkleTree'
|
||||
export { simpleHash } from './simpleHash'
|
||||
export { MerkleTree }
|
||||
export default MerkleTree
|
||||
export type HashFunction<T> = {
|
||||
(left: T, right: T): string
|
||||
}
|
||||
|
||||
export type MerkleTreeOptions = {
|
||||
hashFunction?: HashFunction<Element>
|
||||
zeroElement?: Element
|
||||
}
|
||||
|
||||
export type Element = string | number
|
||||
|
||||
export type SerializedTreeState = {
|
||||
levels: number,
|
||||
_zeros: Array<Element>,
|
||||
_layers: Array<Element[]>
|
||||
}
|
||||
|
||||
export type SerializedPartialTreeState = {
|
||||
levels: number
|
||||
_layers: Element[][]
|
||||
_zeros: Array<Element>
|
||||
_edgeLeafProof: ProofPath
|
||||
_edgeLeaf: LeafWithIndex
|
||||
}
|
||||
|
||||
export type ProofPath = {
|
||||
pathElements: Element[],
|
||||
pathIndices: number[],
|
||||
pathPositions: number[],
|
||||
pathRoot: Element
|
||||
}
|
||||
export type TreeEdge = {
|
||||
edgeElement: Element;
|
||||
edgePath: ProofPath;
|
||||
edgeIndex: number;
|
||||
edgeElementsCount: number;
|
||||
}
|
||||
|
||||
export type TreeSlice = { edge: TreeEdge, elements: Element[] }
|
||||
export type LeafWithIndex = { index: number, data: Element }
|
||||
|
@ -1,212 +0,0 @@
|
||||
// keccak256("tornado") % BN254_FIELD_SIZE
|
||||
const DEFAULT_ZERO = '21663839004416932945382355908790599225266501822907911457504978515578255421292'
|
||||
const defaultHash = require('./mimc')
|
||||
|
||||
// todo ensure consistent types in tree and inserted elements?
|
||||
// todo make sha3 default hasher (and update tests) to get rid of mimc/snarkjs/circomlib dependency
|
||||
|
||||
/**
|
||||
* @callback hashFunction
|
||||
* @param left Left leaf
|
||||
* @param right Right leaf
|
||||
*/
|
||||
/**
|
||||
* Merkle tree
|
||||
*/
|
||||
class MerkleTree {
|
||||
/**
|
||||
* Constructor
|
||||
* @param {number} levels Number of levels in the tree
|
||||
* @param {Array} [elements] Initial elements
|
||||
* @param {Object} options
|
||||
* @param {hashFunction} [options.hashFunction] Function used to hash 2 leaves
|
||||
* @param [options.zeroElement] Value for non-existent leaves
|
||||
*/
|
||||
constructor(levels, elements = [], { hashFunction, zeroElement = DEFAULT_ZERO } = {}) {
|
||||
this.levels = levels
|
||||
this.capacity = 2 ** levels
|
||||
if (elements.length > this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
this._hash = hashFunction || defaultHash
|
||||
this.zeroElement = zeroElement
|
||||
this._zeros = []
|
||||
this._zeros[0] = zeroElement
|
||||
for (let i = 1; i <= levels; i++) {
|
||||
this._zeros[i] = this._hash(this._zeros[i - 1], this._zeros[i - 1])
|
||||
}
|
||||
this._layers = []
|
||||
this._layers[0] = elements.slice()
|
||||
this._rebuild()
|
||||
}
|
||||
|
||||
_rebuild() {
|
||||
for (let level = 1; level <= this.levels; level++) {
|
||||
this._layers[level] = []
|
||||
for (let i = 0; i < Math.ceil(this._layers[level - 1].length / 2); i++) {
|
||||
this._layers[level][i] = this._hash(
|
||||
this._layers[level - 1][i * 2],
|
||||
i * 2 + 1 < this._layers[level - 1].length
|
||||
? this._layers[level - 1][i * 2 + 1]
|
||||
: this._zeros[level - 1],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tree root
|
||||
* @returns {*}
|
||||
*/
|
||||
root() {
|
||||
return this._layers[this.levels].length > 0 ? this._layers[this.levels][0] : this._zeros[this.levels]
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new element into the tree
|
||||
* @param element Element to insert
|
||||
*/
|
||||
insert(element) {
|
||||
if (this._layers[0].length >= this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
this.update(this._layers[0].length, element)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements) {
|
||||
if (!elements.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._layers[0].length + elements.length > this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
// First we insert all elements except the last one
|
||||
// updating only full subtree hashes (all layers where inserted element has odd index)
|
||||
// the last element will update the full path to the root making the tree consistent again
|
||||
for (let i = 0; i < elements.length - 1; i++) {
|
||||
this._layers[0].push(elements[i])
|
||||
let level = 0
|
||||
let index = this._layers[0].length - 1
|
||||
while (index % 2 === 1) {
|
||||
level++
|
||||
index >>= 1
|
||||
this._layers[level][index] = this._hash(
|
||||
this._layers[level - 1][index * 2],
|
||||
this._layers[level - 1][index * 2 + 1],
|
||||
)
|
||||
}
|
||||
}
|
||||
this.insert(elements[elements.length - 1])
|
||||
}
|
||||
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index, element) {
|
||||
if (isNaN(Number(index)) || index < 0 || index > this._layers[0].length || index >= this.capacity) {
|
||||
throw new Error('Insert index out of bounds: ' + index)
|
||||
}
|
||||
this._layers[0][index] = element
|
||||
for (let level = 1; level <= this.levels; level++) {
|
||||
index >>= 1
|
||||
this._layers[level][index] = this._hash(
|
||||
this._layers[level - 1][index * 2],
|
||||
index * 2 + 1 < this._layers[level - 1].length
|
||||
? this._layers[level - 1][index * 2 + 1]
|
||||
: this._zeros[level - 1],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get merkle path to a leaf
|
||||
* @param {number} index Leaf index to generate path for
|
||||
* @returns {{pathElements: Object[], pathIndex: number[]}} An object containing adjacent elements and left-right index
|
||||
*/
|
||||
path(index) {
|
||||
if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) {
|
||||
throw new Error('Index out of bounds: ' + index)
|
||||
}
|
||||
const pathElements = []
|
||||
const pathIndices = []
|
||||
for (let level = 0; level < this.levels; level++) {
|
||||
pathIndices[level] = index % 2
|
||||
pathElements[level] =
|
||||
(index ^ 1) < this._layers[level].length ? this._layers[level][index ^ 1] : this._zeros[level]
|
||||
index >>= 1
|
||||
}
|
||||
return {
|
||||
pathElements,
|
||||
pathIndices,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an element in the tree
|
||||
* @param element An element to find
|
||||
* @param comparator A function that checks leaf value equality
|
||||
* @returns {number} Index if element is found, otherwise -1
|
||||
*/
|
||||
indexOf(element, comparator) {
|
||||
if (comparator) {
|
||||
return this._layers[0].findIndex((el) => comparator(element, el))
|
||||
} else {
|
||||
return this._layers[0].indexOf(element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of non-zero tree elements
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
elements() {
|
||||
return this._layers[0].slice()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of n-th zero elements array
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
zeros() {
|
||||
return this._zeros.slice()
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize entire tree state including intermediate layers into a plain object
|
||||
* Deserializing it back will not require to recompute any hashes
|
||||
* Elements are not converted to a plain type, this is responsibility of the caller
|
||||
*/
|
||||
serialize() {
|
||||
return {
|
||||
levels: this.levels,
|
||||
_zeros: this._zeros,
|
||||
_layers: this._layers,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize data into a MerkleTree instance
|
||||
* Make sure to provide the same hashFunction as was used in the source tree,
|
||||
* otherwise the tree state will be invalid
|
||||
*
|
||||
* @param data
|
||||
* @param hashFunction
|
||||
* @returns {MerkleTree}
|
||||
*/
|
||||
static deserialize(data, hashFunction) {
|
||||
const instance = Object.assign(Object.create(this.prototype), data)
|
||||
instance._hash = hashFunction || defaultHash
|
||||
instance.capacity = 2 ** instance.levels
|
||||
instance.zeroElement = instance._zeros[0]
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MerkleTree
|
@ -1,3 +0,0 @@
|
||||
const { mimcsponge } = require('circomlib')
|
||||
const { bigInt } = require('snarkjs')
|
||||
module.exports = (left, right) => mimcsponge.multiHash([bigInt(left), bigInt(right)]).toString()
|
22
src/simpleHash.ts
Normal file
22
src/simpleHash.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Element } from './'
|
||||
|
||||
/***
|
||||
* This is insecure hash function, just for example only
|
||||
* @param data
|
||||
* @param seed
|
||||
* @param hashLength
|
||||
*/
|
||||
|
||||
export function simpleHash<T>(data: T[], seed?: number, hashLength = 40): string {
|
||||
const str = data.join('')
|
||||
let i, l,
|
||||
hval = seed ?? 0x811c9dcc5
|
||||
for (i = 0, l = str.length; i < l; i++) {
|
||||
hval ^= str.charCodeAt(i)
|
||||
hval += (hval << 1) + (hval << 4) + (hval << 6) + (hval << 8) + (hval << 24)
|
||||
}
|
||||
const hash = (hval >>> 0).toString(16)
|
||||
return BigInt('0x' + hash.padEnd(hashLength - (hash.length - 1), '0')).toString(10)
|
||||
}
|
||||
|
||||
export default (left: Element, right: Element): string => simpleHash([left, right])
|
428
test/fixedMerkleTree.spec.ts
Normal file
428
test/fixedMerkleTree.spec.ts
Normal file
@ -0,0 +1,428 @@
|
||||
import { MerkleTree, PartialMerkleTree, TreeEdge } from '../src'
|
||||
import { assert, should } from 'chai'
|
||||
import { createHash } from 'crypto'
|
||||
import { it } from 'mocha'
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
it('should have correct 1 element root', () => {
|
||||
const tree = new MerkleTree(10, [1])
|
||||
should().equal(tree.root, '4059654748770657324723044385589999697920')
|
||||
})
|
||||
|
||||
it('should have correct even elements root', () => {
|
||||
const tree = new MerkleTree(10, [1, 2])
|
||||
should().equal(tree.root, '3715471817149864798706576217905179918336')
|
||||
})
|
||||
|
||||
it('should have correct odd elements root', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3])
|
||||
should().equal(tree.root, '5199180210167621115778229238102210117632')
|
||||
})
|
||||
|
||||
it('should be able to create a full tree', () => {
|
||||
new MerkleTree(2, [1, 2, 3, 4])
|
||||
})
|
||||
|
||||
it('should fail to create tree with too many elements', () => {
|
||||
const call = () => new MerkleTree(2, [1, 2, 3, 4, 5])
|
||||
should().throw(call, 'Tree is full')
|
||||
})
|
||||
|
||||
it('should work with optional hash function and zero element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5, 6], { hashFunction: sha256Hash, zeroElement: 'zero' })
|
||||
should().equal(tree.root, 'a377b9fa0ed41add83e56f7e1d0e2ebdb46550b9d8b26b77dece60cb67283f19')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#insert', () => {
|
||||
it('should insert into empty tree', () => {
|
||||
const tree = new MerkleTree(10)
|
||||
tree.insert(42)
|
||||
should().equal(tree.root, '750572848877730275626358141391262973952')
|
||||
})
|
||||
|
||||
it('should insert into odd tree', () => {
|
||||
const tree = new MerkleTree(10, [1])
|
||||
tree.insert(42)
|
||||
should().equal(tree.root, '5008383558940708447763798816817296703488')
|
||||
})
|
||||
|
||||
it('should insert into even tree', () => {
|
||||
const tree = new MerkleTree(10, [1, 2])
|
||||
tree.insert(42)
|
||||
should().equal(tree.root, '5005864318873356880627322373636156817408')
|
||||
})
|
||||
|
||||
it('should insert last element', () => {
|
||||
const tree = new MerkleTree(2, [1, 2, 3])
|
||||
tree.insert(4)
|
||||
})
|
||||
|
||||
it('should fail to insert when tree is full', () => {
|
||||
const tree = new MerkleTree(2, [1, 2, 3, 4])
|
||||
const call = () => tree.insert(5)
|
||||
should().throw(call, 'Tree is full')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#bulkInsert', () => {
|
||||
it('should work', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3])
|
||||
tree.bulkInsert([4, 5, 6])
|
||||
should().equal(tree.root, '4066635800770511602067209448381558554624')
|
||||
})
|
||||
|
||||
it('should give the same result as sequential inserts', () => {
|
||||
const initialArray = [
|
||||
[1],
|
||||
[1, 2],
|
||||
[1, 2, 3],
|
||||
[1, 2, 3, 4],
|
||||
]
|
||||
const insertedArray = [
|
||||
[11],
|
||||
[11, 12],
|
||||
[11, 12, 13],
|
||||
[11, 12, 13, 14],
|
||||
]
|
||||
for (const initial of initialArray) {
|
||||
for (const inserted of insertedArray) {
|
||||
const tree1 = new MerkleTree(10, initial)
|
||||
const tree2 = new MerkleTree(10, initial)
|
||||
tree1.bulkInsert(inserted)
|
||||
for (const item of inserted) {
|
||||
tree2.insert(item)
|
||||
}
|
||||
should().equal(tree1.root, tree2.root)
|
||||
}
|
||||
}
|
||||
}).timeout(10000)
|
||||
|
||||
it('should work with max elements', () => {
|
||||
const tree = new MerkleTree(2, [1, 2])
|
||||
tree.bulkInsert([3, 4])
|
||||
})
|
||||
|
||||
it('should fail to insert too many elements', () => {
|
||||
const tree = new MerkleTree(2, [1, 2])
|
||||
const call = () => tree.bulkInsert([3, 4, 5])
|
||||
should().throw(call, 'Tree is full')
|
||||
})
|
||||
|
||||
it('should bypass empty elements', () => {
|
||||
const elements = [1, 2, 3, 4]
|
||||
const tree = new MerkleTree(2, elements)
|
||||
tree.bulkInsert([])
|
||||
assert.deepEqual(tree.elements, elements, 'No elements inserted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#update', () => {
|
||||
it('should update first element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.update(0, 42)
|
||||
should().equal(tree.root, '3884161948856565981263417078389340635136')
|
||||
})
|
||||
|
||||
it('should update last element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.update(4, 42)
|
||||
should().equal(tree.root, '3564959811529894228734180300843252711424')
|
||||
})
|
||||
|
||||
it('should update odd element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.update(1, 42)
|
||||
should().equal(tree.root, '4576704573778433422699674477203122290688')
|
||||
})
|
||||
|
||||
it('should update even element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.update(2, 42)
|
||||
should().equal(tree.root, '1807994110952186123819489133812038762496')
|
||||
})
|
||||
|
||||
it('should update extra element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4])
|
||||
tree.update(4, 5)
|
||||
should().equal(tree.root, '1099080610107164849381389194938128793600')
|
||||
})
|
||||
|
||||
it('should fail to update incorrect index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
should().throw((() => tree.update(-1, 42)), 'Insert index out of bounds: -1')
|
||||
should().throw((() => tree.update(6, 42)), 'Insert index out of bounds: 6')
|
||||
// @ts-ignore
|
||||
should().throw((() => tree.update('qwe', 42)), 'Insert index out of bounds: qwe')
|
||||
})
|
||||
|
||||
it('should fail to update over capacity', () => {
|
||||
const tree = new MerkleTree(2, [1, 2, 3, 4])
|
||||
const call = () => tree.update(4, 42)
|
||||
should().throw(call, 'Insert index out of bounds: 4')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#indexOf', () => {
|
||||
it('should find index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
should().equal(tree.indexOf(3), 2)
|
||||
})
|
||||
|
||||
it('should work with comparator', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
should().equal(tree.indexOf(4, (arg0, arg1) => arg0 === arg1), 3)
|
||||
})
|
||||
|
||||
it('should return -1 for non existent element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
should().equal(tree.indexOf(42), -1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#path', () => {
|
||||
it('should work for even index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
const path = tree.path(2)
|
||||
assert.deepEqual(path.pathIndices, [0, 1, 0, 0, 0, 0, 0, 0, 0, 0])
|
||||
assert.deepEqual(path.pathElements, [
|
||||
4,
|
||||
'4027992409016347597424110157229339967488',
|
||||
'3591172241203040147397382471352592629760',
|
||||
'938972308169430750202858820582946897920',
|
||||
'3743880566844110745576746962917825445888',
|
||||
'2074434463882483178614385966084599578624',
|
||||
'2808856778596740691845240322870189490176',
|
||||
'4986731814143931240516913804278285467648',
|
||||
'1918547053077726613961101558405545328640',
|
||||
'5444383861051812288142814494928935059456',
|
||||
|
||||
])
|
||||
})
|
||||
|
||||
it('should work for odd index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
const path = tree.path(3)
|
||||
assert.deepEqual(path.pathIndices, [1, 1, 0, 0, 0, 0, 0, 0, 0, 0])
|
||||
assert.deepEqual(path.pathElements, [
|
||||
3,
|
||||
'4027992409016347597424110157229339967488',
|
||||
'3591172241203040147397382471352592629760',
|
||||
'938972308169430750202858820582946897920',
|
||||
'3743880566844110745576746962917825445888',
|
||||
'2074434463882483178614385966084599578624',
|
||||
'2808856778596740691845240322870189490176',
|
||||
'4986731814143931240516913804278285467648',
|
||||
'1918547053077726613961101558405545328640',
|
||||
'5444383861051812288142814494928935059456',
|
||||
])
|
||||
})
|
||||
|
||||
it('should fail on incorrect index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4])
|
||||
should().throw((() => tree.path(-1)), 'Index out of bounds: -1')
|
||||
should().throw((() => tree.path(5)), 'Index out of bounds: 5')
|
||||
// @ts-ignore
|
||||
should().throw((() => tree.path('qwe')), 'Index out of bounds: qwe')
|
||||
})
|
||||
|
||||
it('should work for correct string index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
// @ts-ignore
|
||||
const path = tree.path('2')
|
||||
assert.deepEqual(path.pathIndices, [0, 1, 0, 0, 0, 0, 0, 0, 0, 0])
|
||||
assert.deepEqual(path.pathElements, [
|
||||
4,
|
||||
'4027992409016347597424110157229339967488',
|
||||
'3591172241203040147397382471352592629760',
|
||||
'938972308169430750202858820582946897920',
|
||||
'3743880566844110745576746962917825445888',
|
||||
'2074434463882483178614385966084599578624',
|
||||
'2808856778596740691845240322870189490176',
|
||||
'4986731814143931240516913804278285467648',
|
||||
'1918547053077726613961101558405545328640',
|
||||
'5444383861051812288142814494928935059456',
|
||||
|
||||
])
|
||||
})
|
||||
})
|
||||
describe('#proof', () => {
|
||||
it('should return proof for leaf', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
assert.deepEqual(tree.proof(4), tree.path(3))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getTreeEdge', () => {
|
||||
it('should return correct treeEdge', () => {
|
||||
const expectedEdge: TreeEdge = {
|
||||
edgePath: {
|
||||
pathElements: [
|
||||
5,
|
||||
'1390935134112885103361924701261056180224',
|
||||
'1952916572242076545231119328171167580160',
|
||||
'938972308169430750202858820582946897920',
|
||||
],
|
||||
pathIndices: [0, 0, 1, 0],
|
||||
pathPositions: [5, 0, 0, 0],
|
||||
pathRoot: '3283298202329284319899364273680487022592',
|
||||
},
|
||||
edgeElement: 4,
|
||||
edgeIndex: 4,
|
||||
edgeElementsCount: 6,
|
||||
}
|
||||
const tree = new MerkleTree(4, [0, 1, 2, 3, 4, 5])
|
||||
assert.deepEqual(tree.getTreeEdge(4), expectedEdge)
|
||||
})
|
||||
it('should fail if element not found', () => {
|
||||
const tree = new MerkleTree(4, [0, 1, 2, 3, 4, 5])
|
||||
const call = () => tree.getTreeEdge(6)
|
||||
should().throw(call, 'Element not found')
|
||||
})
|
||||
})
|
||||
describe('#getTreeSlices', () => {
|
||||
let fullTree: MerkleTree
|
||||
before(async () => {
|
||||
const elements = Array.from({ length: 2 ** 10 }, (_, i) => i)
|
||||
fullTree = new MerkleTree(10, elements)
|
||||
return Promise.resolve()
|
||||
})
|
||||
it('should return correct slices count', () => {
|
||||
const count = 4
|
||||
const slicesCount = fullTree.getTreeSlices(4).length
|
||||
should().equal(count, slicesCount)
|
||||
}).timeout(10000)
|
||||
|
||||
it('should be able to create partial tree from last slice', () => {
|
||||
const [, , , lastSlice] = fullTree.getTreeSlices()
|
||||
const partialTree = new PartialMerkleTree(10, lastSlice.edge, lastSlice.elements)
|
||||
assert.deepEqual(fullTree.root, partialTree.root)
|
||||
}).timeout(10000)
|
||||
|
||||
it('should be able to build full tree from slices', () => {
|
||||
const slices = fullTree.getTreeSlices()
|
||||
const lastSlice = slices.pop()
|
||||
const partialTree = new PartialMerkleTree(10, lastSlice.edge, lastSlice.elements)
|
||||
slices.reverse().forEach(({ edge, elements }) => {
|
||||
partialTree.shiftEdge(edge, elements)
|
||||
})
|
||||
assert.deepEqual(fullTree.layers, partialTree.layers)
|
||||
}).timeout(10000)
|
||||
|
||||
it('should return same path', () => {
|
||||
const slices = fullTree.getTreeSlices()
|
||||
const lastSlice = slices.pop()
|
||||
const partialTree = new PartialMerkleTree(10, lastSlice.edge, lastSlice.elements)
|
||||
slices.reverse().forEach(({ edge, elements }) => {
|
||||
partialTree.shiftEdge(edge, elements)
|
||||
})
|
||||
assert.deepEqual(fullTree.path(100), partialTree.path(100))
|
||||
}).timeout(10000)
|
||||
|
||||
it('should throw if invalid number of elements', () => {
|
||||
const [firstSlice] = fullTree.getTreeSlices()
|
||||
const call = () => new PartialMerkleTree(10, firstSlice.edge, firstSlice.elements)
|
||||
should().throw(call, 'Invalid number of elements')
|
||||
}).timeout(10000)
|
||||
})
|
||||
describe('#getters', () => {
|
||||
const elements = [1, 2, 3, 4, 5]
|
||||
const layers = [
|
||||
[1, 2, 3, 4, 5],
|
||||
[
|
||||
'4027992409016347597424110157229339967488',
|
||||
'923221781152860005594997320673730232320',
|
||||
'752191049236692618445397735417537626112',
|
||||
|
||||
],
|
||||
[
|
||||
'81822854828781486047086122479545722339328',
|
||||
'3591172241203040147397382471352592629760',
|
||||
|
||||
],
|
||||
['2729943778107054496417267081388406865920'],
|
||||
['4562739390655416913642128116127918718976'],
|
||||
]
|
||||
|
||||
it('should return same elements in array', () => {
|
||||
const tree = new MerkleTree(10, elements)
|
||||
assert.deepEqual(tree.elements, elements)
|
||||
})
|
||||
it('should return copy of elements array', () => {
|
||||
const tree = new MerkleTree(10, elements)
|
||||
const elements1 = tree.elements
|
||||
tree.insert(6)
|
||||
const elements2 = tree.elements
|
||||
should().not.equal(elements1, elements2)
|
||||
})
|
||||
|
||||
it('should return same layers in array', () => {
|
||||
const tree = new MerkleTree(4, elements)
|
||||
assert.deepEqual(tree.layers, layers)
|
||||
})
|
||||
it('should return copy of elements array', () => {
|
||||
const tree = new MerkleTree(4, elements)
|
||||
const layers1 = tree.layers
|
||||
tree.insert(6)
|
||||
const layers2 = tree.layers
|
||||
should().not.equal(layers1, layers2)
|
||||
})
|
||||
it('should return correct zeros array', () => {
|
||||
const zeros = [
|
||||
0,
|
||||
'1390935134112885103361924701261056180224',
|
||||
'3223901263414086620636498663535535980544',
|
||||
'938972308169430750202858820582946897920',
|
||||
'3743880566844110745576746962917825445888',
|
||||
]
|
||||
const tree = new MerkleTree(4, [])
|
||||
assert.deepEqual(tree.zeros, zeros, 'Not equal')
|
||||
})
|
||||
it('should return copy of zeros array', () => {
|
||||
const tree = new MerkleTree(4, [])
|
||||
const zeros1 = tree.zeros
|
||||
tree.insert(6)
|
||||
const zeros2 = tree.zeros
|
||||
should().not.equal(zeros1, zeros2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#serialize', () => {
|
||||
it('should work', () => {
|
||||
const src = new MerkleTree(10, [1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
const data = src.serialize()
|
||||
const dst = MerkleTree.deserialize(data)
|
||||
should().equal(src.root, dst.root)
|
||||
|
||||
src.insert(10)
|
||||
dst.insert(10)
|
||||
|
||||
should().equal(src.root, dst.root)
|
||||
})
|
||||
})
|
||||
describe('#toString', () => {
|
||||
it('should return correct stringified representation', () => {
|
||||
const src = new MerkleTree(10, [1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
const str = src.toString()
|
||||
const dst = MerkleTree.deserialize(JSON.parse(str))
|
||||
should().equal(src.root, dst.root)
|
||||
|
||||
src.insert(10)
|
||||
dst.insert(10)
|
||||
|
||||
should().equal(src.root, dst.root)
|
||||
|
||||
})
|
||||
})
|
||||
})
|
@ -1,246 +0,0 @@
|
||||
const MerkleTree = require('../src/merkleTree')
|
||||
require('chai').should()
|
||||
|
||||
describe('MerkleTree', () => {
|
||||
describe('#constructor', () => {
|
||||
it('should have correct zero root', () => {
|
||||
const tree = new MerkleTree(10)
|
||||
return tree.root().should.equal('14030416097908897320437553787826300082392928432242046897689557706485311282736')
|
||||
})
|
||||
|
||||
it('should have correct 1 element root', () => {
|
||||
const tree = new MerkleTree(10, [1])
|
||||
tree.root().should.equal('8423266420989796135179818298985240707844287090553672312129988553683991994663')
|
||||
})
|
||||
|
||||
it('should have correct even elements root', () => {
|
||||
const tree = new MerkleTree(10, [1, 2])
|
||||
tree.root().should.equal('6632020347849276860492323008882350357301732786233864934344775324188835172576')
|
||||
})
|
||||
|
||||
it('should have correct odd elements root', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3])
|
||||
tree.root().should.equal('13605252518346649016266481317890801910232739395710162921320863289825142055129')
|
||||
})
|
||||
|
||||
it('should be able to create a full tree', () => {
|
||||
new MerkleTree(2, [1, 2, 3, 4])
|
||||
})
|
||||
|
||||
it('should fail to create tree with too many elements', () => {
|
||||
const call = () => new MerkleTree(2, [1, 2, 3, 4, 5])
|
||||
call.should.throw('Tree is full')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#insert', () => {
|
||||
it('should insert into empty tree', () => {
|
||||
const tree = new MerkleTree(10)
|
||||
tree.insert(42)
|
||||
tree.root().should.equal('5305397050004975530787056746976521882221645950652996479084366175595194436378')
|
||||
})
|
||||
|
||||
it('should insert into odd tree', () => {
|
||||
const tree = new MerkleTree(10, [1])
|
||||
tree.insert(42)
|
||||
tree.root().should.equal('4732716818150428188641303198013632061441036732749853605989871103991103096471')
|
||||
})
|
||||
|
||||
it('should insert into even tree', () => {
|
||||
const tree = new MerkleTree(10, [1, 2])
|
||||
tree.insert(42)
|
||||
tree.root().should.equal('6204016789747878948181936326719724987136198810274146408545977300318734508764')
|
||||
})
|
||||
|
||||
it('should insert last element', () => {
|
||||
const tree = new MerkleTree(2, [1, 2, 3])
|
||||
tree.insert(4)
|
||||
})
|
||||
|
||||
it('should fail to insert when tree is full', () => {
|
||||
const tree = new MerkleTree(2, [1, 2, 3, 4])
|
||||
const call = () => tree.insert(5)
|
||||
call.should.throw('Tree is full')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#bulkInsert', () => {
|
||||
it('should work', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3])
|
||||
tree.bulkInsert([4, 5, 6])
|
||||
tree.root().should.equal('10132905325673518287563057607527946096399700874345297651940963130460267058606')
|
||||
})
|
||||
|
||||
it('should give the same result as sequental inserts', () => {
|
||||
const initialArray = [
|
||||
[1],
|
||||
[1, 2],
|
||||
[1, 2, 3],
|
||||
[1, 2, 3, 4],
|
||||
]
|
||||
const insertedArray = [
|
||||
[11],
|
||||
[11, 12],
|
||||
[11, 12, 13],
|
||||
[11, 12, 13, 14],
|
||||
]
|
||||
for (const initial of initialArray) {
|
||||
for (const inserted of insertedArray) {
|
||||
const tree1 = new MerkleTree(10, initial)
|
||||
const tree2 = new MerkleTree(10, initial)
|
||||
tree1.bulkInsert(inserted)
|
||||
for (const item of inserted) {
|
||||
tree2.insert(item)
|
||||
}
|
||||
tree1.root().should.equal(tree2.root())
|
||||
}
|
||||
}
|
||||
}).timeout(10000)
|
||||
|
||||
it('should work with max elements', () => {
|
||||
const tree = new MerkleTree(2, [1, 2])
|
||||
tree.bulkInsert([3, 4])
|
||||
})
|
||||
|
||||
it('should fail to insert too many elements', () => {
|
||||
const tree = new MerkleTree(2, [1, 2])
|
||||
const call = () => tree.bulkInsert([3, 4, 5])
|
||||
call.should.throw('Tree is full')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#update', () => {
|
||||
it('should update first element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.update(0, 42)
|
||||
tree.root().should.equal('153077538697962715163231177553585573790587443799974092612333826693999310199')
|
||||
})
|
||||
|
||||
it('should update last element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.update(4, 42)
|
||||
tree.root().should.equal('1955192134603843666100093417117434845771298375724087600313714421260719033775')
|
||||
})
|
||||
|
||||
it('should update odd element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.update(1, 42)
|
||||
tree.root().should.equal('6642888742811380760154112624880866754768235565211186414088321870395007150538')
|
||||
})
|
||||
|
||||
it('should update even element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.update(2, 42)
|
||||
tree.root().should.equal('11739358667442647096377238675718917508981868161724701476635082606510350785683')
|
||||
})
|
||||
|
||||
it('should update extra element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4])
|
||||
tree.update(4, 5)
|
||||
tree.root().should.equal('6341751103515285836339987888606244815365572869367801108789753151704260302930')
|
||||
})
|
||||
|
||||
it('should fail to update incorrect index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5]);
|
||||
(() => tree.update(-1, 42)).should.throw('Insert index out of bounds: -1');
|
||||
(() => tree.update(6, 42)).should.throw('Insert index out of bounds: 6');
|
||||
(() => tree.update('qwe', 42)).should.throw('Insert index out of bounds: qwe')
|
||||
})
|
||||
|
||||
it('should fail to update over capacity', () => {
|
||||
const tree = new MerkleTree(2, [1, 2, 3, 4])
|
||||
const call = () => tree.update(4, 42)
|
||||
call.should.throw('Insert index out of bounds: 4')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#indexOf', () => {
|
||||
it('should find index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.indexOf(3).should.equal(2)
|
||||
})
|
||||
|
||||
it('should return -1 for non existent element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.indexOf(42).should.equal(-1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#path', () => {
|
||||
it('should work for even index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
const path = tree.path(2)
|
||||
path.pathIndices.should.be.deep.equal([0, 1, 0, 0, 0, 0, 0, 0, 0, 0])
|
||||
path.pathElements.should.be.deep.equal([
|
||||
4,
|
||||
'19814528709687996974327303300007262407299502847885145507292406548098437687919',
|
||||
'21305827034995891902714687670641862055126514524916463201449278400604999416145',
|
||||
'14506027710748750947258687001455876266559341618222612722926156490737302846427',
|
||||
'4766583705360062980279572762279781527342845808161105063909171241304075622345',
|
||||
'16640205414190175414380077665118269450294358858897019640557533278896634808665',
|
||||
'13024477302430254842915163302704885770955784224100349847438808884122720088412',
|
||||
'11345696205391376769769683860277269518617256738724086786512014734609753488820',
|
||||
'17235543131546745471991808272245772046758360534180976603221801364506032471936',
|
||||
'155962837046691114236524362966874066300454611955781275944230309195800494087',
|
||||
])
|
||||
})
|
||||
|
||||
it('should work for odd index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
const path = tree.path(3)
|
||||
path.pathIndices.should.be.deep.equal([1, 1, 0, 0, 0, 0, 0, 0, 0, 0])
|
||||
path.pathElements.should.be.deep.equal([
|
||||
3,
|
||||
'19814528709687996974327303300007262407299502847885145507292406548098437687919',
|
||||
'21305827034995891902714687670641862055126514524916463201449278400604999416145',
|
||||
'14506027710748750947258687001455876266559341618222612722926156490737302846427',
|
||||
'4766583705360062980279572762279781527342845808161105063909171241304075622345',
|
||||
'16640205414190175414380077665118269450294358858897019640557533278896634808665',
|
||||
'13024477302430254842915163302704885770955784224100349847438808884122720088412',
|
||||
'11345696205391376769769683860277269518617256738724086786512014734609753488820',
|
||||
'17235543131546745471991808272245772046758360534180976603221801364506032471936',
|
||||
'155962837046691114236524362966874066300454611955781275944230309195800494087',
|
||||
])
|
||||
})
|
||||
|
||||
it('should fail on incorrect index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4]);
|
||||
(() => tree.path(-1)).should.throw('Index out of bounds: -1');
|
||||
(() => tree.path(5)).should.throw('Index out of bounds: 5');
|
||||
(() => tree.path('qwe')).should.throw('Index out of bounds: qwe')
|
||||
})
|
||||
|
||||
it('should work for correct string index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
const path = tree.path('2')
|
||||
path.pathIndices.should.be.deep.equal([0, 1, 0, 0, 0, 0, 0, 0, 0, 0])
|
||||
path.pathElements.should.be.deep.equal([
|
||||
4,
|
||||
'19814528709687996974327303300007262407299502847885145507292406548098437687919',
|
||||
'21305827034995891902714687670641862055126514524916463201449278400604999416145',
|
||||
'14506027710748750947258687001455876266559341618222612722926156490737302846427',
|
||||
'4766583705360062980279572762279781527342845808161105063909171241304075622345',
|
||||
'16640205414190175414380077665118269450294358858897019640557533278896634808665',
|
||||
'13024477302430254842915163302704885770955784224100349847438808884122720088412',
|
||||
'11345696205391376769769683860277269518617256738724086786512014734609753488820',
|
||||
'17235543131546745471991808272245772046758360534180976603221801364506032471936',
|
||||
'155962837046691114236524362966874066300454611955781275944230309195800494087',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('#serialize', () => {
|
||||
it('should work', () => {
|
||||
const src = new MerkleTree(10, [1, 2, 3])
|
||||
const data = src.serialize()
|
||||
const dst = MerkleTree.deserialize(data)
|
||||
|
||||
src.root().should.equal(dst.root())
|
||||
|
||||
src.insert(10)
|
||||
dst.insert(10)
|
||||
|
||||
src.root().should.equal(dst.root())
|
||||
})
|
||||
})
|
||||
})
|
297
test/partialMerkleTree.spec.ts
Normal file
297
test/partialMerkleTree.spec.ts
Normal file
@ -0,0 +1,297 @@
|
||||
import { Element, MerkleTree, MerkleTreeOptions, PartialMerkleTree } from '../src'
|
||||
import { it } from 'mocha'
|
||||
import { should } from 'chai'
|
||||
import * as assert from 'assert'
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
const sha256Hash = (left, right) => createHash('sha256').update(`${left}${right}`).digest('hex')
|
||||
|
||||
describe('PartialMerkleTree', () => {
|
||||
const getTestTrees = (levels: number, elements: Element[], edgeIndex: number, treeOptions: MerkleTreeOptions = {}) => {
|
||||
const fullTree = new MerkleTree(levels, elements, treeOptions)
|
||||
const edge = fullTree.getTreeEdge(edgeIndex)
|
||||
const leavesAfterEdge = elements.slice(edge.edgeIndex)
|
||||
const partialTree = new PartialMerkleTree(levels, edge, leavesAfterEdge, treeOptions)
|
||||
return { fullTree, partialTree }
|
||||
}
|
||||
describe('#constructor', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(20, ['0', '1', '2', '3', '4', '5'], 2)
|
||||
it('should initialize merkle tree with same root', () => {
|
||||
should().equal(fullTree.root, partialTree.root)
|
||||
})
|
||||
|
||||
it('should initialize merkle tree with same leaves count', () => {
|
||||
should().equal(fullTree.elements.length, partialTree.elements.length)
|
||||
})
|
||||
|
||||
it('should work with optional hash function and zero element', () => {
|
||||
const { partialTree, fullTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6], 3, {
|
||||
hashFunction: sha256Hash,
|
||||
zeroElement: 'zero',
|
||||
})
|
||||
should().equal(partialTree.root, fullTree.root)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#insert', () => {
|
||||
|
||||
it('should have equal root to full tree after insertion ', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, ['0', '1', '2', '3', '4', '5', '6', '7'], 5)
|
||||
fullTree.insert('9')
|
||||
partialTree.insert('9')
|
||||
should().equal(fullTree.root, partialTree.root)
|
||||
})
|
||||
|
||||
it('should fail to insert when tree is full', () => {
|
||||
const { partialTree } = getTestTrees(3, ['0', '1', '2', '3', '4', '5', '6', '7'], 5)
|
||||
const call = () => partialTree.insert('8')
|
||||
should().throw(call, 'Tree is full')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#bulkInsert', () => {
|
||||
|
||||
it('should work like full tree', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(20, [1, 2, 3, 4, 5], 2)
|
||||
partialTree.bulkInsert([6, 7, 8])
|
||||
fullTree.bulkInsert([6, 7, 8])
|
||||
should().equal(fullTree.root, partialTree.root)
|
||||
})
|
||||
|
||||
it('should give the same result as sequential inserts', () => {
|
||||
const initialArray = [
|
||||
[1],
|
||||
[1, 2],
|
||||
[1, 2, 3],
|
||||
[1, 2, 3, 4],
|
||||
]
|
||||
const insertedArray = [
|
||||
[11],
|
||||
[11, 12],
|
||||
[11, 12, 13],
|
||||
[11, 12, 13, 14],
|
||||
]
|
||||
for (const initial of initialArray) {
|
||||
for (const inserted of insertedArray) {
|
||||
const { partialTree: tree1 } = getTestTrees(10, initial, initial.length - 1)
|
||||
const { partialTree: tree2 } = getTestTrees(10, initial, initial.length - 1)
|
||||
tree1.bulkInsert(inserted)
|
||||
for (const item of inserted) {
|
||||
tree2.insert(item)
|
||||
}
|
||||
should().equal(tree1.root, tree2.root)
|
||||
}
|
||||
}
|
||||
}).timeout(10000)
|
||||
|
||||
it('should fail to insert too many elements', () => {
|
||||
const { partialTree } = getTestTrees(2, [1, 2, 3, 4], 2)
|
||||
const call = () => partialTree.bulkInsert([5, 6, 7])
|
||||
should().throw(call, 'Tree is full')
|
||||
})
|
||||
it('should bypass empty elements', () => {
|
||||
const elements = [1, 2, 3, 4]
|
||||
const { partialTree } = getTestTrees(2, elements, 2)
|
||||
partialTree.bulkInsert([])
|
||||
should().equal(partialTree.elements.length, elements.length, 'No elements inserted')
|
||||
})
|
||||
})
|
||||
describe('#update', () => {
|
||||
it('should update last element', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
partialTree.update(4, 42)
|
||||
fullTree.update(4, 42)
|
||||
should().equal(partialTree.root, fullTree.root)
|
||||
})
|
||||
|
||||
|
||||
it('should update odd element', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6, 7, 8], 2)
|
||||
partialTree.update(4, 42)
|
||||
fullTree.update(4, 42)
|
||||
should().equal(partialTree.root, fullTree.root)
|
||||
})
|
||||
|
||||
it('should update even element', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6, 7, 8], 2)
|
||||
partialTree.update(3, 42)
|
||||
fullTree.update(3, 42)
|
||||
should().equal(partialTree.root, fullTree.root)
|
||||
})
|
||||
|
||||
it('should update extra element', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
partialTree.update(5, 6)
|
||||
fullTree.update(5, 6)
|
||||
should().equal(fullTree.root, partialTree.root)
|
||||
})
|
||||
|
||||
it('should fail to update incorrect index', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 3)
|
||||
should().throw((() => partialTree.update(-1, 42)), 'Insert index out of bounds: -1')
|
||||
should().throw((() => partialTree.update(6, 42)), 'Insert index out of bounds: 6')
|
||||
should().throw((() => partialTree.update(2, 42)), 'Index 2 is below the edge: 3')
|
||||
// @ts-ignore
|
||||
should().throw((() => partialTree.update('qwe', 42)), 'Insert index out of bounds: qwe')
|
||||
})
|
||||
|
||||
it('should fail to update over capacity', () => {
|
||||
const { partialTree } = getTestTrees(2, [1, 2, 3, 4], 1)
|
||||
const call = () => partialTree.update(4, 42)
|
||||
should().throw(call, 'Insert index out of bounds: 4')
|
||||
})
|
||||
})
|
||||
describe('#indexOf', () => {
|
||||
it('should return same result as full tree', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6, 7, 8], 3)
|
||||
should().equal(partialTree.indexOf(5), fullTree.indexOf(5))
|
||||
})
|
||||
|
||||
it('should find index', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
should().equal(partialTree.indexOf(3), 2)
|
||||
})
|
||||
|
||||
it('should work with comparator', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
should().equal(partialTree.indexOf(4, (arg0, arg1) => arg0 === arg1), 3)
|
||||
})
|
||||
|
||||
it('should return -1 for non existent element', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
should().equal(partialTree.indexOf(42), -1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#proof', () => {
|
||||
it('should return proof for known leaf', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
assert.deepEqual(partialTree.proof(4), partialTree.path(3))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getters', () => {
|
||||
it('should return capacity', () => {
|
||||
const levels = 10
|
||||
const capacity = 2 ** levels
|
||||
const { fullTree, partialTree } = getTestTrees(levels, [1, 2, 3, 4, 5], 2)
|
||||
should().equal(fullTree.capacity, capacity)
|
||||
should().equal(partialTree.capacity, capacity)
|
||||
})
|
||||
|
||||
it('should return same elements count as full tree', () => {
|
||||
const levels = 10
|
||||
const capacity = 2 ** levels
|
||||
const elements = Array.from({ length: capacity }, (_, i) => i)
|
||||
const { fullTree, partialTree } = getTestTrees(levels, elements, 200)
|
||||
should().equal(partialTree.elements.length, fullTree.elements.length)
|
||||
})
|
||||
|
||||
it('should return copy of layers', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
const layers = partialTree.layers
|
||||
should().not.equal(layers, partialTree.layers)
|
||||
})
|
||||
|
||||
it('should return copy of zeros', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
const zeros = partialTree.zeros
|
||||
should().not.equal(zeros, partialTree.zeros)
|
||||
})
|
||||
|
||||
it('should return edge leaf', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
should().equal(partialTree.edgeElement, 3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#path', () => {
|
||||
|
||||
it('should return path for known nodes', () => {
|
||||
const levels = 10
|
||||
const capacity = 2 ** levels
|
||||
const elements = Array.from({ length: capacity / 2 }, (_, i) => i)
|
||||
const { fullTree, partialTree } = getTestTrees(levels, elements, 250)
|
||||
assert.deepEqual(fullTree.path(251), partialTree.path(251))
|
||||
}).timeout(1000)
|
||||
|
||||
it('should fail on incorrect index', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6, 7, 8, 9], 4)
|
||||
should().throw((() => partialTree.path(-1)), 'Index out of bounds: -1')
|
||||
should().throw((() => partialTree.path(10)), 'Index out of bounds: 10')
|
||||
// @ts-ignore
|
||||
should().throw((() => partialTree.path('qwe')), 'Index out of bounds: qwe')
|
||||
})
|
||||
|
||||
it('should fail if index is below edge', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6, 7, 8, 9], 4)
|
||||
const call = () => partialTree.path(2)
|
||||
should().throw(call, 'Index 2 is below the edge: 4')
|
||||
})
|
||||
})
|
||||
describe('#shiftEdge', () => {
|
||||
const levels = 20
|
||||
const elements: Element[] = Array.from({ length: 2 ** 18 }, (_, i) => i)
|
||||
const tree = new MerkleTree(levels, elements)
|
||||
it('should work', () => {
|
||||
const edge1 = tree.getTreeEdge(200)
|
||||
const edge2 = tree.getTreeEdge(100)
|
||||
const partialTree = new PartialMerkleTree(levels, edge1, elements.slice(edge1.edgeIndex))
|
||||
partialTree.shiftEdge(edge2, elements.slice(edge2.edgeIndex, partialTree.edgeIndex))
|
||||
tree.insert('1111')
|
||||
partialTree.insert('1111')
|
||||
assert.deepEqual(partialTree.path(150), tree.path(150))
|
||||
})
|
||||
it('should be able to build full tree from slices', () => {
|
||||
const slices = tree.getTreeSlices(6)
|
||||
const lastSlice = slices.pop()
|
||||
const partialTree = new PartialMerkleTree(levels, lastSlice.edge, lastSlice.elements)
|
||||
for (let i = slices.length - 1; i >= 0; i--) {
|
||||
partialTree.shiftEdge(slices[i].edge, slices[i].elements)
|
||||
}
|
||||
partialTree.insert('1')
|
||||
tree.insert('1')
|
||||
assert.deepStrictEqual(partialTree.path(432), tree.path(432))
|
||||
}).timeout(10000)
|
||||
|
||||
it('should fail if new edge index is over current edge', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)
|
||||
const newEdge = fullTree.getTreeEdge(4)
|
||||
const call = () => partialTree.shiftEdge(newEdge, [1, 2])
|
||||
should().throw(call, 'New edgeIndex should be smaller then 4')
|
||||
})
|
||||
it('should fail if elements length are incorrect', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6, 7, 8, 9], 4)
|
||||
const newEdge = fullTree.getTreeEdge(3)
|
||||
const call = () => partialTree.shiftEdge(newEdge, [1, 2])
|
||||
should().throw(call, 'Elements length should be 1')
|
||||
})
|
||||
})
|
||||
describe('#serialize', () => {
|
||||
it('should work', () => {
|
||||
const { partialTree } = getTestTrees(5, [1, 2, 3, 4, 5, 6, 7, 8, 9], 5)
|
||||
const data = partialTree.serialize()
|
||||
const dst = PartialMerkleTree.deserialize(data)
|
||||
should().equal(partialTree.root, dst.root)
|
||||
|
||||
partialTree.insert(10)
|
||||
dst.insert(10)
|
||||
assert.deepStrictEqual(partialTree.path(6), dst.path(6))
|
||||
should().equal(partialTree.root, dst.root)
|
||||
})
|
||||
})
|
||||
describe('#toString', () => {
|
||||
it('should return correct stringified representation', () => {
|
||||
const { partialTree } = getTestTrees(5, [1, 2, 3, 4, 5, 6, 7, 8, 9], 5)
|
||||
const str = partialTree.toString()
|
||||
const dst = PartialMerkleTree.deserialize(JSON.parse(str))
|
||||
assert.deepStrictEqual(partialTree.path(6), dst.path(6))
|
||||
partialTree.insert(10)
|
||||
dst.insert(10)
|
||||
|
||||
assert.deepStrictEqual(partialTree.path(6), dst.path(6))
|
||||
assert.deepStrictEqual(partialTree.root, dst.root)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
18
test/simpleHash.spec.ts
Normal file
18
test/simpleHash.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { it } from 'mocha'
|
||||
import { should } from 'chai'
|
||||
import { simpleHash } from '../src'
|
||||
|
||||
describe('SimpleHash', () => {
|
||||
it('should return correct hash string with default params', () => {
|
||||
const hash = simpleHash([1, 2, 3])
|
||||
return should().equal(hash, '3530513397947785053296897142557895557120')
|
||||
})
|
||||
it('should return correct hash string with length param', () => {
|
||||
const hash = simpleHash([1, 2, 3], null, 77)
|
||||
return should().equal(hash, '1259729275322113643079999203492506359813191573070980317691663537897682854338069790720')
|
||||
})
|
||||
it('should return correct hash string with seed param', () => {
|
||||
const hash = simpleHash(['1', '2', '3'], 123)
|
||||
return should().equal(hash, '1371592418687375416654554138100746944512')
|
||||
})
|
||||
})
|
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es2019"
|
||||
],
|
||||
"target": "es2017",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"types",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user