Skip to content

Commit

Permalink
Unify errors
Browse files Browse the repository at this point in the history
  • Loading branch information
ernestognw committed Feb 25, 2024
1 parent 826ffab commit ada28f1
Show file tree
Hide file tree
Showing 10 changed files with 74 additions and 77 deletions.
8 changes: 4 additions & 4 deletions src/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,20 @@ 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', () => {
assert(!isValidMerkleTree([]), 'empty tree');
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', () => {
Expand All @@ -81,6 +81,6 @@ describe('core error conditions', () => {
proofFlags: [true, true, false],
};

assert.throws(() => processMultiProof(badMultiProof), /^Error: Broken invariant$/);
assert.throws(() => processMultiProof(badMultiProof), /^InvariantError$/);
});
});
37 changes: 14 additions & 23 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -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)));

Expand All @@ -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<HexString>(2 * leaves.length - 1);

Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -103,29 +99,26 @@ export function processMultiProof(multiproof: MultiProof<BytesLike>): 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

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()!);
}
Expand All @@ -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, []]];

Expand Down
27 changes: 10 additions & 17 deletions src/merkletree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = {
format: string;
Expand Down Expand Up @@ -94,13 +93,13 @@ export abstract class MerkleTreeImpl<T> implements MerkleTree<T> {
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[] {
Expand All @@ -113,9 +112,7 @@ export abstract class MerkleTreeImpl<T> implements MerkleTree<T> {
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;
Expand All @@ -131,9 +128,7 @@ export abstract class MerkleTreeImpl<T> implements MerkleTree<T> {
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 {
Expand All @@ -156,13 +151,11 @@ export abstract class MerkleTreeImpl<T> implements MerkleTree<T> {
}

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

Expand Down
12 changes: 6 additions & 6 deletions src/simple.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)`,
);
});
});
Expand All @@ -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'$/,
);
});

Expand All @@ -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$/);
});
});
});
6 changes: 2 additions & 4 deletions src/simple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = MerkleTreeData<T> & {
format: 'simple-v1';
Expand All @@ -20,9 +20,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl<BytesLike> {
}

static load(data: StandardMerkleTreeData<BytesLike>): 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);
}

Expand Down
12 changes: 6 additions & 6 deletions src/standard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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$/,
);
});

Expand Down Expand Up @@ -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$/);
});
});
}
Expand All @@ -138,7 +138,7 @@ describe('standard merkle tree', () => {
format: 'nonstandard',
leafEncoding: ['string'],
} as any),
/^Error: Unknown format 'nonstandard'$/,
/^InvalidArgumentError: Unknown format 'nonstandard'$/,
);

assert.throws(
Expand All @@ -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'$/,
);
});

Expand All @@ -158,15 +158,15 @@ 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',
tree: [zero, zero, keccak256(keccak256(zero))],
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$/);
});
});
});
10 changes: 3 additions & 7 deletions src/standard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends any[]> = MerkleTreeData<T> & {
format: 'standard-v1';
Expand Down Expand Up @@ -36,12 +36,8 @@ export class StandardMerkleTree<T extends any[]> extends MerkleTreeImpl<T> {
}

static load<T extends any[]>(data: StandardMerkleTreeData<T>): StandardMerkleTree<T> {
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);
}

Expand Down
7 changes: 0 additions & 7 deletions src/utils/check-bounds.ts

This file was deleted.

29 changes: 29 additions & 0 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export function throwError(message?: string): never {
throw new Error(message);
}

Check warning on line 3 in src/utils/errors.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/errors.ts#L1-L3

Added lines #L1 - L3 were not covered by tests

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);
}
}
3 changes: 0 additions & 3 deletions src/utils/throw-error.ts

This file was deleted.

0 comments on commit ada28f1

Please sign in to comment.