initial typescript edit

This commit is contained in:
Sergei SMART 2022-02-22 18:45:31 +10:00
parent e3c54ea818
commit 4501cae4d6
9 changed files with 5116 additions and 5572 deletions

View File

@ -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
}
}

4
.gitignore vendored
View File

@ -1 +1,5 @@
node_modules
build
lib
yarn-error.log
.idea

5454
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,9 +3,11 @@
"version": "0.6.1",
"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",
"test": "ts-mocha 'test/*.spec.ts'",
"build": "tsc",
"lint": "eslint ."
},
"keywords": [
@ -19,13 +21,18 @@
"src/*"
],
"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.1",
"ts-mocha": "^9.0.2",
"typescript": "^4.5.5"
}
}

View File

@ -1,34 +1,28 @@
import { mimcsponge } from 'circomlib'
// 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
const defaultHash = (left: Element, right: Element): string => mimcsponge.multiHash([BigInt(left), BigInt(right)]).toString()
/**
* @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 } = {}) {
export default class MerkleTree {
levels: number
capacity: number
private _hash: HashFunction
private zeroElement: Element
private _zeros: Element[]
private _layers: Array<Element[]>
constructor(levels: number, elements: Element[] = [], {
hashFunction = defaultHash,
zeroElement = DEFAULT_ZERO,
}: MerkleTreeOptions = {}) {
this.levels = levels
this.capacity = 2 ** levels
if (elements.length > this.capacity) {
throw new Error('Tree is full')
}
this._hash = hashFunction || defaultHash
this._hash = hashFunction
this.zeroElement = zeroElement
this._zeros = []
this._zeros[0] = zeroElement
@ -56,17 +50,16 @@ class MerkleTree {
/**
* Get tree root
* @returns {*}
*/
root() {
return this._layers[this.levels].length > 0 ? this._layers[this.levels][0] : this._zeros[this.levels]
root(): string {
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) {
insert(element: Element) {
if (this._layers[0].length >= this.capacity) {
throw new Error('Tree is full')
}
@ -77,7 +70,7 @@ class MerkleTree {
* Insert multiple elements into the tree.
* @param {Array} elements Elements to insert
*/
bulkInsert(elements) {
bulkInsert(elements: Element[]) {
if (!elements.length) {
return
}
@ -109,7 +102,7 @@ class MerkleTree {
* @param {number} index Index of element to change
* @param element Updated element value
*/
update(index, element) {
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)
}
@ -130,17 +123,18 @@ class MerkleTree {
* @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) {
path(index: Element) {
if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) {
throw new Error('Index out of bounds: ' + index)
}
const pathElements = []
const pathIndices = []
let elIndex = +index
const pathElements: Element[] = []
const pathIndices: number[] = []
for (let level = 0; level < this.levels; level++) {
pathIndices[level] = index % 2
pathIndices[level] = elIndex % 2
pathElements[level] =
(index ^ 1) < this._layers[level].length ? this._layers[level][index ^ 1] : this._zeros[level]
index >>= 1
(elIndex ^ 1) < this._layers[level].length ? this._layers[level][elIndex ^ 1] : this._zeros[level]
elIndex >>= 1
}
return {
pathElements,
@ -154,17 +148,16 @@ class MerkleTree {
* @param comparator A function that checks leaf value equality
* @returns {number} Index if element is found, otherwise -1
*/
indexOf(element, comparator) {
indexOf(element: Element, comparator?: <T, R> (arg0: T, arg1: T) => R): number {
if (comparator) {
return this._layers[0].findIndex((el) => comparator(element, el))
return this._layers[0].findIndex((el) => comparator<Element, number>(element, el))
} else {
return this._layers[0].indexOf(element)
}
}
/**
* Returns a copy of non-zero tree elements
* @returns {Object[]}
* Returns a copy of non-zero tree elements.
*/
elements() {
return this._layers[0].slice()
@ -172,7 +165,6 @@ class MerkleTree {
/**
* Returns a copy of n-th zero elements array
* @returns {Object[]}
*/
zeros() {
return this._zeros.slice()
@ -183,7 +175,7 @@ class MerkleTree {
* 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() {
serialize(): SerializedTreeState {
return {
levels: this.levels,
_zeros: this._zeros,
@ -195,18 +187,27 @@ class MerkleTree {
* 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
static deserialize(data: SerializedTreeState, hashFunction?: HashFunction): MerkleTree {
return new MerkleTree(data.levels, data._layers[0], { hashFunction, zeroElement: data._zeros[0] })
}
}
module.exports = MerkleTree
export type HashFunction = {
(left: string | number, right: string | number): string
}
export type MerkleTreeOptions = {
hashFunction?: HashFunction
zeroElement?: Element
}
export type Element = string | number
export type SerializedTreeState = {
levels: number,
_zeros: Array<Element>,
_layers: Array<Element[]>
}
export type Mimcsponge = {
multiHash: (arr: BigInt[], key?: Element, numOutputs?) => string
}

View File

@ -1,3 +0,0 @@
const { mimcsponge } = require('circomlib')
const { bigInt } = require('snarkjs')
module.exports = (left, right) => mimcsponge.multiHash([bigInt(left), bigInt(right)]).toString()

View File

@ -1,26 +1,28 @@
const MerkleTree = require('../src/merkleTree')
require('chai').should()
import MerkleTree from '../src'
import { assert, should } from 'chai'
describe('MerkleTree', () => {
describe('#constructor', () => {
it('should have correct zero root', () => {
const tree = new MerkleTree(10)
return tree.root().should.equal('14030416097908897320437553787826300082392928432242046897689557706485311282736')
const tree = new MerkleTree(10, [])
return should().equal(tree.root(), '14030416097908897320437553787826300082392928432242046897689557706485311282736')
})
it('should have correct 1 element root', () => {
const tree = new MerkleTree(10, [1])
tree.root().should.equal('8423266420989796135179818298985240707844287090553672312129988553683991994663')
should().equal(tree.root(), '8423266420989796135179818298985240707844287090553672312129988553683991994663')
})
it('should have correct even elements root', () => {
const tree = new MerkleTree(10, [1, 2])
tree.root().should.equal('6632020347849276860492323008882350357301732786233864934344775324188835172576')
should().equal(tree.root(), '6632020347849276860492323008882350357301732786233864934344775324188835172576')
})
it('should have correct odd elements root', () => {
const tree = new MerkleTree(10, [1, 2, 3])
tree.root().should.equal('13605252518346649016266481317890801910232739395710162921320863289825142055129')
should().equal(tree.root(), '13605252518346649016266481317890801910232739395710162921320863289825142055129')
})
it('should be able to create a full tree', () => {
@ -29,7 +31,7 @@ describe('MerkleTree', () => {
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')
should().throw(call, 'Tree is full')
})
})
@ -37,19 +39,19 @@ describe('MerkleTree', () => {
it('should insert into empty tree', () => {
const tree = new MerkleTree(10)
tree.insert(42)
tree.root().should.equal('5305397050004975530787056746976521882221645950652996479084366175595194436378')
should().equal(tree.root(), '5305397050004975530787056746976521882221645950652996479084366175595194436378')
})
it('should insert into odd tree', () => {
const tree = new MerkleTree(10, [1])
tree.insert(42)
tree.root().should.equal('4732716818150428188641303198013632061441036732749853605989871103991103096471')
should().equal(tree.root(), '4732716818150428188641303198013632061441036732749853605989871103991103096471')
})
it('should insert into even tree', () => {
const tree = new MerkleTree(10, [1, 2])
tree.insert(42)
tree.root().should.equal('6204016789747878948181936326719724987136198810274146408545977300318734508764')
should().equal(tree.root(), '6204016789747878948181936326719724987136198810274146408545977300318734508764')
})
it('should insert last element', () => {
@ -60,7 +62,7 @@ describe('MerkleTree', () => {
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')
should().throw(call, 'Tree is full')
})
})
@ -68,7 +70,7 @@ describe('MerkleTree', () => {
it('should work', () => {
const tree = new MerkleTree(10, [1, 2, 3])
tree.bulkInsert([4, 5, 6])
tree.root().should.equal('10132905325673518287563057607527946096399700874345297651940963130460267058606')
should().equal(tree.root(), '10132905325673518287563057607527946096399700874345297651940963130460267058606')
})
it('should give the same result as sequental inserts', () => {
@ -92,7 +94,7 @@ describe('MerkleTree', () => {
for (const item of inserted) {
tree2.insert(item)
}
tree1.root().should.equal(tree2.root())
should().equal(tree1.root(), tree2.root())
}
}
}).timeout(10000)
@ -105,7 +107,7 @@ describe('MerkleTree', () => {
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')
should().throw(call, 'Tree is full')
})
})
@ -113,56 +115,57 @@ describe('MerkleTree', () => {
it('should update first element', () => {
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
tree.update(0, 42)
tree.root().should.equal('153077538697962715163231177553585573790587443799974092612333826693999310199')
should().equal(tree.root(), '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')
should().equal(tree.root(), '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')
should().equal(tree.root(), '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')
should().equal(tree.root(), '11739358667442647096377238675718917508981868161724701476635082606510350785683')
})
it('should update extra element', () => {
const tree = new MerkleTree(10, [1, 2, 3, 4])
tree.update(4, 5)
tree.root().should.equal('6341751103515285836339987888606244815365572869367801108789753151704260302930')
should().equal(tree.root(), '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')
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)
call.should.throw('Insert index out of bounds: 4')
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])
tree.indexOf(3).should.equal(2)
should().equal(tree.indexOf(3), 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)
should().equal(tree.indexOf(42), -1)
})
})
@ -170,8 +173,8 @@ describe('MerkleTree', () => {
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([
assert.deepEqual(path.pathIndices, [0, 1, 0, 0, 0, 0, 0, 0, 0, 0])
assert.deepEqual(path.pathElements, [
4,
'19814528709687996974327303300007262407299502847885145507292406548098437687919',
'21305827034995891902714687670641862055126514524916463201449278400604999416145',
@ -188,8 +191,8 @@ describe('MerkleTree', () => {
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([
assert.deepEqual(path.pathIndices, [1, 1, 0, 0, 0, 0, 0, 0, 0, 0])
assert.deepEqual(path.pathElements, [
3,
'19814528709687996974327303300007262407299502847885145507292406548098437687919',
'21305827034995891902714687670641862055126514524916463201449278400604999416145',
@ -204,17 +207,17 @@ describe('MerkleTree', () => {
})
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')
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')
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])
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([
assert.deepEqual(path.pathIndices, [0, 1, 0, 0, 0, 0, 0, 0, 0, 0])
assert.deepEqual(path.pathElements, [
4,
'19814528709687996974327303300007262407299502847885145507292406548098437687919',
'21305827034995891902714687670641862055126514524916463201449278400604999416145',
@ -231,16 +234,15 @@ describe('MerkleTree', () => {
describe('#serialize', () => {
it('should work', () => {
const src = new MerkleTree(10, [1, 2, 3])
const src = new MerkleTree(10, [1, 2, 3, 4, 5, 6, 7, 8, 9])
const data = src.serialize()
const dst = MerkleTree.deserialize(data)
src.root().should.equal(dst.root())
should().equal(src.root(), dst.root())
src.insert(10)
dst.insert(10)
src.root().should.equal(dst.root())
should().equal(src.root(), dst.root())
})
})
})

26
tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": [
"es2019"
],
"target": "es2018",
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "./lib",
"rootDir": "src",
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": false,
"allowSyntheticDefaultImports": true,
"declaration": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"types",
"**/*.spec.ts"
]
}

4930
yarn.lock Normal file

File diff suppressed because it is too large Load Diff