Skip to content

Commit

Permalink
Merge branch 'master' into feature/custom-node-hash
Browse files Browse the repository at this point in the history
  • Loading branch information
Amxx committed Feb 27, 2024
2 parents bc11903 + 952fd9f commit b471e47
Show file tree
Hide file tree
Showing 11 changed files with 67 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .mocharc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extension": ["ts"],
"spec": "src/**/*.test.ts",
"spec": "{src,test}/**/*.test.ts",
"require": "ts-node/register",
"timeout": 60000
}
19 changes: 11 additions & 8 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BytesLike, HexString, toHex, toBytes } from './bytes';
import { BytesLike, HexString, toHex, toBytes, compare } from './bytes';
import { NodeHash, standardNodeHash } from './hashes';
import { invariant, throwError, validateArgument } from './utils/errors';

Expand Down Expand Up @@ -38,7 +38,7 @@ export function getProof(tree: BytesLike[], index: number): HexString[] {

const proof = [];
while (index > 0) {
proof.push(tree[siblingIndex(index)]!);
proof.push(toHex(tree[siblingIndex(index)]!));
index = parentIndex(index);
}
return proof.map(node => toHex(node));
Expand All @@ -61,7 +61,10 @@ export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof<
indices.forEach(i => checkLeafNode(tree, i));
indices.sort((a, b) => b - a);

validateArgument(!indices.slice(1).some((i, p) => i === indices[p]), 'Cannot prove duplicated index');
validateArgument(
indices.slice(1).every((i, p) => i !== indices[p]),
'Cannot prove duplicated index',
);

const stack = Array.from(indices); // copy
const proof = [];
Expand All @@ -77,18 +80,18 @@ export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof<
stack.shift(); // consume from the stack
} else {
proofFlags.push(false);
proof.push(tree[s]!);
proof.push(toHex(tree[s]!));
}
stack.push(p);
}

if (indices.length === 0) {
proof.push(tree[0]!);
proof.push(toHex(tree[0]!));
}

return {
leaves: indices.map(i => tree[i]!).map(node => toHex(node)),
proof: proof.map(node => toHex(node)),
leaves: indices.map(i => toHex(tree[i]!)),
proof,
proofFlags,
};
}
Expand Down Expand Up @@ -134,7 +137,7 @@ export function isValidMerkleTree(tree: BytesLike[], nodeHash?: NodeHash): boole
if (l < tree.length) {
return false;
}
} else if (node !== (nodeHash ?? standardNodeHash)(tree[l]!, tree[r]!)) {
} else if (compare(node, (nodeHash ?? standardNodeHash)(tree[l]!, tree[r]!))) {
return false;
}
}
Expand Down
28 changes: 18 additions & 10 deletions src/merkletree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ import {
} from './core';

import { MerkleTreeOptions, defaultOptions } from './options';
import { LeafHash, NodeHash, standardNodeHash } from './hashes';
import { LeafHash, NodeHash } from './hashes';
import { validateArgument, invariant } from './utils/errors';

export type MerkleTreeData<T> = {
export interface MerkleTreeData<T> {
format: string;
tree: HexString[];
values: { value: T; treeIndex: number }[];
};
}

export interface MerkleTree<T> {
root: HexString;
render(): string;
dump(): MerkleTreeData<T>;
entries(): Iterable<[number, T]>;
validate(): void;
leafHash(leaf: T): HexString;
Expand All @@ -32,7 +33,6 @@ export interface MerkleTree<T> {
getMultiProof(leaves: (number | T)[]): MultiProof<HexString, T>;
verify(leaf: number | T, proof: HexString[]): boolean;
verifyMultiProof(multiproof: MultiProof<BytesLike, number | T>): boolean;
dump(): MerkleTreeData<T>;
}

export abstract class MerkleTreeImpl<T> implements MerkleTree<T> {
Expand All @@ -44,7 +44,11 @@ export abstract class MerkleTreeImpl<T> implements MerkleTree<T> {
public readonly leafHash: LeafHash<T>,
protected readonly nodeHash?: NodeHash,
) {
this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree.at(treeIndex), valueIndex]));
validateArgument(
values.every(({ value }) => typeof value != 'number'),
'Leaf values cannot be numbers',
);
this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree[treeIndex], valueIndex]));
}

protected static prepare<T>(
Expand Down Expand Up @@ -149,15 +153,19 @@ export abstract class MerkleTreeImpl<T> implements MerkleTree<T> {
}

private _validateValueAt(index: number): void {
validateArgument(this.values.at(index) !== undefined, 'Index out of bounds');
const { value, treeIndex } = this.values[index]!;
invariant(this.tree.at(treeIndex) === this.leafHash(value), 'Merkle tree does not contain the expected value');
const value = this.values[index];
validateArgument(value !== undefined, 'Index out of bounds');
invariant(
this.tree[value.treeIndex] === this.leafHash(value.value),
'Merkle tree does not contain the expected value',
);
}

private _leafHash(leaf: number | T): HexString {
if (typeof leaf === 'number') {
validateArgument(this.values.at(leaf) !== undefined, 'Index out of bounds');
leaf = this.values[leaf]?.value;
const lookup = this.values[leaf];
validateArgument(lookup !== undefined, 'Index out of bounds');
leaf = lookup.value;
}
return this.leafHash(leaf);
}
Expand Down
3 changes: 2 additions & 1 deletion src/simple.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('simple merkle tree', () => {
const tree = SimpleMerkleTree.of(leaves, opts);
const otherTree = SimpleMerkleTree.of(otherLeaves, opts);

it('generates valid single proofs for all leaves', () => {
it('generates a valid tree', () => {
tree.validate();
});

Expand Down Expand Up @@ -111,6 +111,7 @@ describe('simple merkle tree', () => {

it('dump and load', () => {
const recoveredTree = SimpleMerkleTree.load(tree.dump(), opts.nodeHash);

recoveredTree.validate();
assert.deepEqual(tree, recoveredTree);
});
Expand Down
6 changes: 3 additions & 3 deletions src/simple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { MerkleTreeOptions } from './options';
import { NodeHash } from './hashes';
import { validateArgument } from './utils/errors';

export type SimpleMerkleTreeData = MerkleTreeData<BytesLike> & {
export interface SimpleMerkleTreeData extends MerkleTreeData<HexString> {
format: 'simple-v1';
hash?: 'custom';
};
}

export function formatLeaf(value: BytesLike): HexString {
return defaultAbiCoder.encode(['bytes32'], [value]);
Expand Down Expand Up @@ -43,7 +43,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl<BytesLike> {
return {
format: 'simple-v1',
tree: this.tree,
values: this.values,
values: this.values.map(({ value, treeIndex }) => ({ value: toHex(value), treeIndex })),
hash: this.nodeHash ? 'custom' : undefined,
};
}
Expand Down
6 changes: 3 additions & 3 deletions src/standard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class StandardMerkleTree<T extends any[]> extends MerkleTreeImpl<T> {
leafEncoding: string[],
options: MerkleTreeOptions = {},
): StandardMerkleTree<T> {
// use standardNodeHash
// use default nodeHash (standardNodeHash)
const [tree, indexedValues] = MerkleTreeImpl.prepare(values, options, leaf => standardLeafHash(leafEncoding, leaf));
return new StandardMerkleTree(tree, indexedValues, leafEncoding);
}
Expand All @@ -36,7 +36,7 @@ export class StandardMerkleTree<T extends any[]> extends MerkleTreeImpl<T> {
}

static verify<T extends any[]>(root: BytesLike, leafEncoding: string[], leaf: T, proof: BytesLike[]): boolean {
// use standardNodeHash for processProof
// use default nodeHash (standardNodeHash) for processProof
return toHex(root) === processProof(standardLeafHash(leafEncoding, leaf), proof);
}

Expand All @@ -45,7 +45,7 @@ export class StandardMerkleTree<T extends any[]> extends MerkleTreeImpl<T> {
leafEncoding: string[],
multiproof: MultiProof<BytesLike, T>,
): boolean {
// use standardNodeHash for processMultiProof
// use default nodeHash (standardNodeHash) for processMultiProof
return (
toHex(root) ===
processMultiProof({
Expand Down
25 changes: 25 additions & 0 deletions test/dumps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import assert from 'assert/strict';
import fs from 'fs';
import path from 'path';
import { StandardMerkleTree, SimpleMerkleTree } from '../src';

const DUMPS_DIR = 'test/dumps/';

describe('load dumped trees', () => {
for (const file of fs.readdirSync(DUMPS_DIR).map(filename => path.join(DUMPS_DIR, filename))) {
it(file, function () {
const dump = JSON.parse(fs.readFileSync(file, 'utf-8'));

switch (dump.format) {
case 'standard-v1':
StandardMerkleTree.load(dump).validate();
break;
case 'simple-v1':
SimpleMerkleTree.load(dump).validate();
break;
default:
assert.fail(`Unknown format '${dump.format}'`);
}
});
}
});
1 change: 1 addition & 0 deletions test/dumps/simple-v1.sorted.1_0_6.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"format":"simple-v1","tree":["0x1b404f199ea828ec5771fb30139c222d8417a82175fefad5cd42bc3a189bd8d5","0xec554bdfb01d31fa838d0830339b0e6e8a70e0d55a8f172ffa8bebbf8e8d5ba0","0xaf46af0745b433e1d5bed9a04b1fdf4002f67a733c20db2fca5b2af6120d9bcb","0x434d51cfeb80272378f4c3a8fd2824561c2cad9fce556ea600d46f20550976a6","0x7dea550f679f3caab547cbbc5ee1a4c978c8c039b572ba00af1baa6481b88360","0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3","0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483","0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510","0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761","0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb","0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2"],"values":[{"value":"0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb","treeIndex":9},{"value":"0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510","treeIndex":7},{"value":"0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2","treeIndex":10},{"value":"0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3","treeIndex":5},{"value":"0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761","treeIndex":8},{"value":"0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483","treeIndex":6}]}
1 change: 1 addition & 0 deletions test/dumps/simple-v1.unsorted.1_0_6.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"format":"simple-v1","tree":["0x9012f1e18a87790d2e01faace75aaaca38e53df437cdce2c0552464dda4af49c","0x68203f90e9d07dc5859259d7536e87a6ba9d345f2552b5b9de2999ddce9ce1bf","0xf0b49bb4b0d9396e0315755ceafaa280707b32e75e6c9053f5cdf2679dcd5c6a","0xd253a52d4cb00de2895e85f2529e2976e6aaaa5c18106b68ab66813e14415669","0x805b21d846b189efaeb0377d6bb0d201b3872a363e607c25088f025b0c6ae1f8","0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483","0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761","0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3","0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2","0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510","0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb"],"values":[{"value":"0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb","treeIndex":10},{"value":"0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510","treeIndex":9},{"value":"0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2","treeIndex":8},{"value":"0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3","treeIndex":7},{"value":"0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761","treeIndex":6},{"value":"0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483","treeIndex":5}]}
1 change: 1 addition & 0 deletions test/dumps/standard-v1.sorted.1_0_6.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"format":"standard-v1","leafEncoding":["string"],"tree":["0x6deb52b5da8fd108f79fab00341f38d2587896634c646ee52e49f845680a70c8","0x52426e0f1f65ff7e209a13b8c29cffe82e3acaf3dad0a9b9088f3b9a61a929c3","0xfd3cf45654e88d1cc5d663578c82c76f4b5e3826bacaa1216441443504538f51","0x8076923e76cf01a7c048400a2304c9a9c23bbbdac3a98ea3946340fdafbba34f","0x965b92c6cf08303cc4feb7f3e0819c436c2cec17c6f0688a6af139c9a368707c","0xeba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b","0xc62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848","0x9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b","0x9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c","0x9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e","0x19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681"],"values":[{"value":["a"],"treeIndex":8},{"value":["b"],"treeIndex":10},{"value":["c"],"treeIndex":7},{"value":["d"],"treeIndex":5},{"value":["e"],"treeIndex":9},{"value":["f"],"treeIndex":6}]}
1 change: 1 addition & 0 deletions test/dumps/standard-v1.unsorted.1_0_6.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"format":"standard-v1","leafEncoding":["string"],"tree":["0x23be0977360f08bb0bd7f709a7d543d2cd779c79c66d74e0441919871647de2b","0x8f7234e8cfe39c08ca84a3a3e3274f574af26fd15165fe29e09cbab742daccd9","0x7b0c6cd04b82bfc0e250030a5d2690c52585e0cc6a4f3bc7909d7723b0236ece","0x03707d7802a71ca56a8ad8028da98c4f1dbec55b31b4a25d536b5309cc20eda9","0xfa914d99a18dc32d9725b3ef1c50426deb40ec8d0885dac8edcc5bfd6d030016","0xc62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848","0x9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e","0xeba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b","0x9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b","0x19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681","0x9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c"],"values":[{"value":["a"],"treeIndex":10},{"value":["b"],"treeIndex":9},{"value":["c"],"treeIndex":8},{"value":["d"],"treeIndex":7},{"value":["e"],"treeIndex":6},{"value":["f"],"treeIndex":5}]}

0 comments on commit b471e47

Please sign in to comment.