diff --git a/src/core.test.ts b/src/core.test.ts index 8822a46..c2059d7 100644 --- a/src/core.test.ts +++ b/src/core.test.ts @@ -55,12 +55,12 @@ describe('core properties', () => { describe('core error conditions', () => { it('zero leaves', () => { - assert.throws(() => makeMerkleTree([]), /^Error: Expected non-zero number of leaves$/); + assert.throws(() => makeMerkleTree([]), /^InvalidArgumentError: Expected non-zero number of leaves$/); }); it('multiproof duplicate index', () => { const tree = makeMerkleTree(new Array(2).fill(zero)); - assert.throws(() => getMultiProof(tree, [1, 1]), /^Error: Cannot prove duplicated index$/); + assert.throws(() => getMultiProof(tree, [1, 1]), /^InvalidArgumentError: Cannot prove duplicated index$/); }); it('tree validity', () => { @@ -68,7 +68,7 @@ describe('core error conditions', () => { assert(!isValidMerkleTree([zero, zero]), 'even number of nodes'); assert(!isValidMerkleTree([zero, zero, zero]), 'inner node not hash of children'); - assert.throws(() => renderMerkleTree([]), /^Error: Expected non-zero number of nodes$/); + assert.throws(() => renderMerkleTree([]), /^InvalidArgumentError: Expected non-zero number of nodes$/); }); it('multiproof invariants', () => { @@ -81,6 +81,6 @@ describe('core error conditions', () => { proofFlags: [true, true, false], }; - assert.throws(() => processMultiProof(badMultiProof), /^Error: Broken invariant$/); + assert.throws(() => processMultiProof(badMultiProof), /^InvariantError$/); }); }); diff --git a/src/core.ts b/src/core.ts index 64e4672..18d76a9 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,6 +1,6 @@ import { keccak256 } from '@ethersproject/keccak256'; import { BytesLike, HexString, toHex, toBytes, concat, compare } from './bytes'; -import { throwError } from './utils/throw-error'; +import { invariant, throwError, validateArgument } from './utils/errors'; const hashPair = (a: BytesLike, b: BytesLike): HexString => keccak256(concat([a, b].sort(compare))); @@ -21,9 +21,7 @@ const checkValidMerkleNode = (node: BytesLike) => export function makeMerkleTree(leaves: BytesLike[]): HexString[] { leaves.forEach(checkValidMerkleNode); - if (leaves.length === 0) { - throwError('Expected non-zero number of leaves'); - } + validateArgument(leaves.length !== 0, 'Expected non-zero number of leaves'); const tree = new Array(2 * leaves.length - 1); @@ -65,9 +63,7 @@ export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof< indices.forEach(i => checkLeafNode(tree, i)); indices.sort((a, b) => b - a); - if (indices.slice(1).some((i, p) => i === indices[p])) { - throwError('Cannot prove duplicated index'); - } + validateArgument(!indices.slice(1).some((i, p) => i === indices[p]), 'Cannot prove duplicated index'); const stack = indices.concat(); // copy const proof = []; @@ -103,13 +99,14 @@ export function processMultiProof(multiproof: MultiProof): HexString multiproof.leaves.forEach(checkValidMerkleNode); multiproof.proof.forEach(checkValidMerkleNode); - if (multiproof.proof.length < multiproof.proofFlags.filter(b => !b).length) { - throwError('Invalid multiproof format'); - } - - if (multiproof.leaves.length + multiproof.proof.length !== multiproof.proofFlags.length + 1) { - throwError('Provided leaves and multiproof are not compatible'); - } + validateArgument( + multiproof.proof.length >= multiproof.proofFlags.filter(b => !b).length, + 'Invalid multiproof format', + ); + validateArgument( + multiproof.leaves.length + multiproof.proof.length === multiproof.proofFlags.length + 1, + 'Provided leaves and multiproof are not compatible', + ); const stack = multiproof.leaves.concat(); // copy const proof = multiproof.proof.concat(); // copy @@ -117,15 +114,11 @@ export function processMultiProof(multiproof: MultiProof): HexString for (const flag of multiproof.proofFlags) { const a = stack.shift(); const b = flag ? stack.shift() : proof.shift(); - if (a === undefined || b === undefined) { - throwError('Broken invariant'); - } + invariant(a !== undefined && b !== undefined); stack.push(hashPair(a, b)); } - if (stack.length + proof.length !== 1) { - throwError('Broken invariant'); - } + invariant(stack.length + proof.length === 1); return toHex(stack.pop() ?? proof.shift()!); } @@ -152,9 +145,7 @@ export function isValidMerkleTree(tree: BytesLike[]): boolean { } export function renderMerkleTree(tree: BytesLike[]): HexString { - if (tree.length === 0) { - throwError('Expected non-zero number of nodes'); - } + validateArgument(tree.length !== 0, 'Expected non-zero number of nodes'); const stack: [number, number[]][] = [[0, []]]; diff --git a/src/merkletree.ts b/src/merkletree.ts index 229edd9..0f26cf8 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -12,8 +12,7 @@ import { } from './core'; import { MerkleTreeOptions, defaultOptions } from './options'; -import { checkBounds } from './utils/check-bounds'; -import { throwError } from './utils/throw-error'; +import { invariant } from './utils/errors'; export type MerkleTreeData = { format: string; @@ -94,13 +93,13 @@ export abstract class MerkleTreeImpl implements MerkleTree { for (let i = 0; i < this.values.length; i++) { this.validateValue(i); } - if (!isValidMerkleTree(this.tree)) { - throwError('Merkle tree is invalid'); - } + invariant(isValidMerkleTree(this.tree), 'Merkle tree is invalid'); } leafLookup(leaf: T): number { - return this.hashLookup[toHex(this.leafHash(leaf))] ?? throwError('Leaf is not in tree'); + const lookup = this.hashLookup[toHex(this.leafHash(leaf))]; + invariant(typeof lookup !== 'undefined', 'Leaf is not in tree'); + return lookup; } getProof(leaf: number | T): HexString[] { @@ -113,9 +112,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { const proof = getProof(this.tree, treeIndex); // sanity check proof - if (!this._verify(this.tree[treeIndex]!, proof)) { - throwError('Unable to prove value'); - } + invariant(this._verify(this.tree[treeIndex]!, proof), 'Unable to prove value'); // return proof in hex format return proof; @@ -131,9 +128,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { const proof = getMultiProof(this.tree, indices); // sanity check proof - if (!this._verifyMultiProof(proof)) { - throwError('Unable to prove values'); - } + invariant(this._verifyMultiProof(proof), 'Unable to prove values'); // return multiproof in hex format return { @@ -156,13 +151,11 @@ export abstract class MerkleTreeImpl implements MerkleTree { } protected validateValue(valueIndex: number): HexString { - checkBounds(this.values, valueIndex); + invariant(valueIndex >= 0 && valueIndex < this.values.length, 'Index out of bounds'); const { value: leaf, treeIndex } = this.values[valueIndex]!; - checkBounds(this.tree, treeIndex); + invariant(valueIndex >= 0 && valueIndex < this.values.length, 'Index out of bounds'); const hashedLeaf = this.leafHash(leaf); - if (hashedLeaf !== this.tree[treeIndex]!) { - throwError('Merkle tree does not contain the expected value'); - } + invariant(hashedLeaf === this.tree[treeIndex], 'Merkle tree does not contain the expected value'); return hashedLeaf; } diff --git a/src/simple.test.ts b/src/simple.test.ts index 1cb62c5..4a3e431 100644 --- a/src/simple.test.ts +++ b/src/simple.test.ts @@ -96,14 +96,14 @@ describe('simple merkle tree', () => { }); it('reject out of bounds value index', () => { - assert.throws(() => tree.getProof(leaves.length), /^Error: Index out of bounds$/); + assert.throws(() => tree.getProof(leaves.length), /^InvariantError: Index out of bounds$/); }); it('reject invalid leaf size', () => { const invalidLeaf = [zero + '00']; // 33 bytes (all zero) assert.throws( () => SimpleMerkleTree.of(invalidLeaf, opts), - `Error: ${invalidLeaf} is not a valid 32 bytes object (pos: 0)`, + `InvalidArgumentError: ${invalidLeaf} is not a valid 32 bytes object (pos: 0)`, ); }); }); @@ -113,12 +113,12 @@ describe('simple merkle tree', () => { it('reject unrecognized tree dump', () => { assert.throws( () => SimpleMerkleTree.load({ format: 'nonstandard' } as any), - /^Error: Unknown format 'nonstandard'$/, + /^InvalidArgumentError: Unknown format 'nonstandard'$/, ); assert.throws( () => SimpleMerkleTree.load({ format: 'standard-v1' } as any), - /^Error: Unknown format 'standard-v1'$/, + /^InvalidArgumentError: Unknown format 'standard-v1'$/, ); }); @@ -133,14 +133,14 @@ describe('simple merkle tree', () => { }, ], }); - assert.throws(() => loadedTree1.getProof(0), /^Error: Merkle tree does not contain the expected value$/); + assert.throws(() => loadedTree1.getProof(0), /^InvariantError: Merkle tree does not contain the expected value$/); const loadedTree2 = SimpleMerkleTree.load({ format: 'simple-v1', tree: [zero, zero, zero], values: [{ value: zero, treeIndex: 2 }], }); - assert.throws(() => loadedTree2.getProof(0), /^Error: Unable to prove value$/); + assert.throws(() => loadedTree2.getProof(0), /^InvariantError: Unable to prove value$/); }); }); }); diff --git a/src/simple.ts b/src/simple.ts index d08c92d..b228fe5 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -3,7 +3,7 @@ import { BytesLike, HexString, toHex } from './bytes'; import { MultiProof, processProof, processMultiProof } from './core'; import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; import { MerkleTreeOptions } from './options'; -import { throwError } from './utils/throw-error'; +import { validateArgument } from './utils/errors'; export type StandardMerkleTreeData = MerkleTreeData & { format: 'simple-v1'; @@ -20,9 +20,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { } static load(data: StandardMerkleTreeData): SimpleMerkleTree { - if (data.format !== 'simple-v1') { - throwError(`Unknown format '${data.format}'`); - } + validateArgument(data.format === 'simple-v1', `Unknown format '${data.format}'`); return new SimpleMerkleTree(data.tree, data.values, formatLeaf); } diff --git a/src/standard.test.ts b/src/standard.test.ts index 505ed65..b1409a0 100644 --- a/src/standard.test.ts +++ b/src/standard.test.ts @@ -31,7 +31,7 @@ describe('standard merkle tree', () => { tree: [zero], values: [{ value: ['0'], treeIndex: 0 }], } as StandardMerkleTreeData<[string]>), - /^Error: Expected leaf encoding$/, + /^InvalidArgumentError: Expected leaf encoding$/, ); }); @@ -125,7 +125,7 @@ describe('standard merkle tree', () => { }); it('reject out of bounds value index', () => { - assert.throws(() => tree.getProof(leaves.length), /^Error: Index out of bounds$/); + assert.throws(() => tree.getProof(leaves.length), /^InvariantError: Index out of bounds$/); }); }); } @@ -138,7 +138,7 @@ describe('standard merkle tree', () => { format: 'nonstandard', leafEncoding: ['string'], } as any), - /^Error: Unknown format 'nonstandard'$/, + /^InvalidArgumentError: Unknown format 'nonstandard'$/, ); assert.throws( @@ -147,7 +147,7 @@ describe('standard merkle tree', () => { format: 'simple-v1', leafEncoding: ['string'], } as any), - /^Error: Unknown format 'simple-v1'$/, + /^InvalidArgumentError: Unknown format 'simple-v1'$/, ); }); @@ -158,7 +158,7 @@ describe('standard merkle tree', () => { values: [{ value: ['0'], treeIndex: 0 }], leafEncoding: ['uint256'], }); - assert.throws(() => loadedTree1.getProof(0), /^Error: Merkle tree does not contain the expected value$/); + assert.throws(() => loadedTree1.getProof(0), /^InvariantError: Merkle tree does not contain the expected value$/); const loadedTree2 = StandardMerkleTree.load({ format: 'standard-v1', @@ -166,7 +166,7 @@ describe('standard merkle tree', () => { values: [{ value: ['0'], treeIndex: 2 }], leafEncoding: ['uint256'], }); - assert.throws(() => loadedTree2.getProof(0), /^Error: Unable to prove value$/); + assert.throws(() => loadedTree2.getProof(0), /^InvariantError: Unable to prove value$/); }); }); }); diff --git a/src/standard.ts b/src/standard.ts index c9b42d1..30c33cc 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -4,7 +4,7 @@ import { BytesLike, HexString, toHex } from './bytes'; import { MultiProof, processProof, processMultiProof } from './core'; import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; import { MerkleTreeOptions } from './options'; -import { throwError } from './utils/throw-error'; +import { validateArgument } from './utils/errors'; export type StandardMerkleTreeData = MerkleTreeData & { format: 'standard-v1'; @@ -36,12 +36,8 @@ export class StandardMerkleTree extends MerkleTreeImpl { } static load(data: StandardMerkleTreeData): StandardMerkleTree { - if (data.format !== 'standard-v1') { - throwError(`Unknown format '${data.format}'`); - } - if (data.leafEncoding === undefined) { - throwError('Expected leaf encoding'); - } + validateArgument(data.format === 'standard-v1', `Unknown format '${data.format}'`); + validateArgument(data.leafEncoding !== undefined, 'Expected leaf encoding'); return new StandardMerkleTree(data.tree, data.values, data.leafEncoding); } diff --git a/src/utils/check-bounds.ts b/src/utils/check-bounds.ts deleted file mode 100644 index 5834b51..0000000 --- a/src/utils/check-bounds.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { throwError } from './throw-error'; - -export function checkBounds(array: unknown[], index: number) { - if (index < 0 || index >= array.length) { - throwError('Index out of bounds'); - } -} diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..ee76786 --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,29 @@ +export function throwError(message?: string): never { + throw new Error(message); +} + +export class InvariantError extends Error { + constructor(message?: string) { + super(message); + this.name = 'InvariantError'; + } +} + +export class InvalidArgumentError extends Error { + constructor(message?: string) { + super(message); + this.name = 'InvalidArgumentError'; + } +} + +export function validateArgument(condition: unknown, message?: string): asserts condition { + if (!condition) { + throw new InvalidArgumentError(message); + } +} + +export function invariant(condition: unknown, message?: string): asserts condition { + if (!condition) { + throw new InvariantError(message); + } +} diff --git a/src/utils/throw-error.ts b/src/utils/throw-error.ts deleted file mode 100644 index 503ccf6..0000000 --- a/src/utils/throw-error.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function throwError(message?: string): never { - throw new Error(message); -}