From 29f611e12386b645e31daab8726c0b0da25e7e8f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 4 Mar 2024 18:23:01 +0100 Subject: [PATCH] Support custom node hash in SimpleMerkleTree (#39) Co-authored-by: ernestognw --- src/core.ts | 28 +++--- src/hashes.ts | 14 +++ src/merkletree.ts | 18 ++-- src/simple.test.ts | 137 ++++++++++++++++--------- src/simple.test.ts.md | 216 ++++++++++++++++++++++++++++++++++++++++ src/simple.test.ts.snap | Bin 2791 -> 4282 bytes src/simple.ts | 32 ++++-- src/standard.test.ts | 38 ++++--- src/standard.ts | 15 +-- 9 files changed, 399 insertions(+), 99 deletions(-) create mode 100644 src/hashes.ts diff --git a/src/core.ts b/src/core.ts index b523ba7..c02d267 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,9 +1,7 @@ -import { keccak256 } from '@ethersproject/keccak256'; -import { BytesLike, HexString, toHex, toBytes, concat, compare } from './bytes'; +import { BytesLike, HexString, toHex, toBytes, compare } from './bytes'; +import { NodeHash, standardNodeHash } from './hashes'; import { invariant, throwError, validateArgument } from './utils/errors'; -const hashPair = (a: BytesLike, b: BytesLike): HexString => keccak256(concat([a, b].sort(compare))); - const leftChildIndex = (i: number) => 2 * i + 1; const rightChildIndex = (i: number) => 2 * i + 2; const parentIndex = (i: number) => (i > 0 ? Math.floor((i - 1) / 2) : throwError('Root has no parent')); @@ -18,7 +16,7 @@ const checkLeafNode = (tree: unknown[], i: number) => void (isLeafNode(tree, i) const checkValidMerkleNode = (node: BytesLike) => void (isValidMerkleNode(node) || throwError('Merkle tree nodes must be Uint8Array of length 32')); -export function makeMerkleTree(leaves: BytesLike[]): HexString[] { +export function makeMerkleTree(leaves: BytesLike[], nodeHash: NodeHash = standardNodeHash): HexString[] { leaves.forEach(checkValidMerkleNode); validateArgument(leaves.length !== 0, 'Expected non-zero number of leaves'); @@ -29,7 +27,7 @@ export function makeMerkleTree(leaves: BytesLike[]): HexString[] { tree[tree.length - 1 - i] = toHex(leaf); } for (let i = tree.length - 1 - leaves.length; i >= 0; i--) { - tree[i] = hashPair(tree[leftChildIndex(i)]!, tree[rightChildIndex(i)]!); + tree[i] = nodeHash(tree[leftChildIndex(i)]!, tree[rightChildIndex(i)]!); } return tree; @@ -46,11 +44,11 @@ export function getProof(tree: BytesLike[], index: number): HexString[] { return proof; } -export function processProof(leaf: BytesLike, proof: BytesLike[]): HexString { +export function processProof(leaf: BytesLike, proof: BytesLike[], nodeHash: NodeHash = standardNodeHash): HexString { checkValidMerkleNode(leaf); proof.forEach(checkValidMerkleNode); - return toHex(proof.reduce(hashPair, leaf)); + return toHex(proof.reduce(nodeHash, leaf)); } export interface MultiProof { @@ -68,7 +66,7 @@ export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof< 'Cannot prove duplicated index', ); - const stack = indices.concat(); // copy + const stack = Array.from(indices); // copy const proof = []; const proofFlags = []; @@ -98,7 +96,7 @@ export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof< }; } -export function processMultiProof(multiproof: MultiProof): HexString { +export function processMultiProof(multiproof: MultiProof, nodeHash: NodeHash = standardNodeHash): HexString { multiproof.leaves.forEach(checkValidMerkleNode); multiproof.proof.forEach(checkValidMerkleNode); @@ -111,14 +109,14 @@ export function processMultiProof(multiproof: MultiProof): HexString 'Provided leaves and multiproof are not compatible', ); - const stack = multiproof.leaves.concat(); // copy - const proof = multiproof.proof.concat(); // copy + const stack = Array.from(multiproof.leaves); // copy + const proof = Array.from(multiproof.proof); // copy for (const flag of multiproof.proofFlags) { const a = stack.shift(); const b = flag ? stack.shift() : proof.shift(); invariant(a !== undefined && b !== undefined); - stack.push(hashPair(a, b)); + stack.push(nodeHash(a, b)); } invariant(stack.length + proof.length === 1); @@ -126,7 +124,7 @@ export function processMultiProof(multiproof: MultiProof): HexString return toHex(stack.pop() ?? proof.shift()!); } -export function isValidMerkleTree(tree: BytesLike[]): boolean { +export function isValidMerkleTree(tree: BytesLike[], nodeHash: NodeHash = standardNodeHash): boolean { for (const [i, node] of tree.entries()) { if (!isValidMerkleNode(node)) { return false; @@ -139,7 +137,7 @@ export function isValidMerkleTree(tree: BytesLike[]): boolean { if (l < tree.length) { return false; } - } else if (compare(node, hashPair(tree[l]!, tree[r]!))) { + } else if (compare(node, nodeHash(tree[l]!, tree[r]!))) { return false; } } diff --git a/src/hashes.ts b/src/hashes.ts new file mode 100644 index 0000000..fcda8b6 --- /dev/null +++ b/src/hashes.ts @@ -0,0 +1,14 @@ +import { defaultAbiCoder } from '@ethersproject/abi'; +import { keccak256 } from '@ethersproject/keccak256'; +import { BytesLike, HexString, concat, compare } from './bytes'; + +export type LeafHash = (leaf: T) => HexString; +export type NodeHash = (left: BytesLike, right: BytesLike) => HexString; + +export function standardLeafHash(types: string[], value: T): HexString { + return keccak256(keccak256(defaultAbiCoder.encode(types, value))); +} + +export function standardNodeHash(a: BytesLike, b: BytesLike): HexString { + return keccak256(concat([a, b].sort(compare))); +} diff --git a/src/merkletree.ts b/src/merkletree.ts index bff1b1f..eda1b81 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -12,6 +12,7 @@ import { } from './core'; import { MerkleTreeOptions, defaultOptions } from './options'; +import { LeafHash, NodeHash } from './hashes'; import { validateArgument, invariant } from './utils/errors'; export interface MerkleTreeData { @@ -40,7 +41,8 @@ export abstract class MerkleTreeImpl implements MerkleTree { protected constructor( protected readonly tree: HexString[], protected readonly values: MerkleTreeData['values'], - public readonly leafHash: MerkleTree['leafHash'], + public readonly leafHash: LeafHash, + protected readonly nodeHash?: NodeHash, ) { validateArgument( values.every(({ value }) => typeof value != 'number'), @@ -52,7 +54,8 @@ export abstract class MerkleTreeImpl implements MerkleTree { protected static prepare( values: T[], options: MerkleTreeOptions = {}, - leafHash: MerkleTree['leafHash'], + leafHash: LeafHash, + nodeHash?: NodeHash, ): [tree: HexString[], indexedValues: MerkleTreeData['values']] { const sortLeaves = options.sortLeaves ?? defaultOptions.sortLeaves; const hashedValues = values.map((value, valueIndex) => ({ @@ -65,7 +68,10 @@ export abstract class MerkleTreeImpl implements MerkleTree { hashedValues.sort((a, b) => compare(a.hash, b.hash)); } - const tree = makeMerkleTree(hashedValues.map(v => v.hash)); + const tree = makeMerkleTree( + hashedValues.map(v => v.hash), + nodeHash, + ); const indexedValues = values.map(value => ({ value, treeIndex: 0 })); for (const [leafIndex, { valueIndex }] of hashedValues.entries()) { @@ -93,7 +99,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { validate(): void { this.values.forEach((_, i) => this._validateValueAt(i)); - invariant(isValidMerkleTree(this.tree), 'Merkle tree is invalid'); + invariant(isValidMerkleTree(this.tree, this.nodeHash), 'Merkle tree is invalid'); } leafLookup(leaf: T): number { @@ -171,10 +177,10 @@ export abstract class MerkleTreeImpl implements MerkleTree { } private _verify(leafHash: BytesLike, proof: BytesLike[]): boolean { - return this.root === processProof(leafHash, proof); + return this.root === processProof(leafHash, proof, this.nodeHash); } private _verifyMultiProof(multiproof: MultiProof): boolean { - return this.root === processMultiProof(multiproof); + return this.root === processMultiProof(multiproof, this.nodeHash); } } diff --git a/src/simple.test.ts b/src/simple.test.ts index b5cafe2..c9811ac 100644 --- a/src/simple.test.ts +++ b/src/simple.test.ts @@ -1,23 +1,36 @@ import { test, testProp, fc } from '@fast-check/ava'; import { HashZero as zero } from '@ethersproject/constants'; +import { keccak256 } from '@ethersproject/keccak256'; import { SimpleMerkleTree } from './simple'; +import { BytesLike, HexString, concat, compare } from './bytes'; + +const reverseNodeHash = (a: BytesLike, b: BytesLike): HexString => keccak256(concat([a, b].sort(compare).reverse())); +const otherNodeHash = (a: BytesLike, b: BytesLike): HexString => keccak256(reverseNodeHash(a, b)); // double hash + import { toHex } from './bytes'; import { InvalidArgumentError, InvariantError } from './utils/errors'; const leaf = fc.uint8Array({ minLength: 32, maxLength: 32 }).map(toHex); const leaves = fc.array(leaf, { minLength: 1 }); -const options = fc.record({ sortLeaves: fc.oneof(fc.constant(undefined), fc.boolean()) }); +const options = fc.record({ + sortLeaves: fc.oneof(fc.constant(undefined), fc.boolean()), + nodeHash: fc.oneof(fc.constant(undefined), fc.constant(reverseNodeHash)), +}); -const tree = fc.tuple(leaves, options).map(([leaves, options]) => SimpleMerkleTree.of(leaves, options)); +const tree = fc + .tuple(leaves, options) + .chain(([leaves, options]) => fc.tuple(fc.constant(SimpleMerkleTree.of(leaves, options)), fc.constant(options))); const treeAndLeaf = fc.tuple(leaves, options).chain(([leaves, options]) => fc.tuple( fc.constant(SimpleMerkleTree.of(leaves, options)), + fc.constant(options), fc.nat({ max: leaves.length - 1 }).map(index => ({ value: leaves[index]!, index })), ), ); const treeAndLeaves = fc.tuple(leaves, options).chain(([leaves, options]) => fc.tuple( fc.constant(SimpleMerkleTree.of(leaves, options)), + fc.constant(options), fc .uniqueArray(fc.nat({ max: leaves.length - 1 })) .map(indices => indices.map(index => ({ value: leaves[index]!, index }))), @@ -26,41 +39,55 @@ const treeAndLeaves = fc.tuple(leaves, options).chain(([leaves, options]) => fc.configureGlobal({ numRuns: process.env.CI ? 10000 : 100 }); -testProp('generates a valid tree', [tree], (t, tree) => { +testProp('generates a valid tree', [tree], (t, [tree]) => { t.notThrows(() => tree.validate()); }); -testProp('generates valid single proofs for all leaves', [treeAndLeaf], (t, [tree, { value: leaf, index }]) => { - const proof1 = tree.getProof(index); - const proof2 = tree.getProof(leaf); - - t.deepEqual(proof1, proof2); - t.true(tree.verify(index, proof1)); - t.true(tree.verify(leaf, proof1)); - t.true(SimpleMerkleTree.verify(tree.root, leaf, proof1)); -}); +testProp( + 'generates valid single proofs for all leaves', + [treeAndLeaf], + (t, [tree, options, { value: leaf, index }]) => { + const proof1 = tree.getProof(index); + const proof2 = tree.getProof(leaf); + + t.deepEqual(proof1, proof2); + t.true(tree.verify(index, proof1)); + t.true(tree.verify(leaf, proof1)); + t.true(SimpleMerkleTree.verify(tree.root, leaf, proof1, options.nodeHash)); + }, +); -testProp('rejects invalid proofs', [treeAndLeaf, tree], (t, [tree, { value: leaf }], otherTree) => { - const proof = tree.getProof(leaf); - t.false(otherTree.verify(leaf, proof)); - t.false(SimpleMerkleTree.verify(otherTree.root, leaf, proof)); -}); +testProp( + 'rejects invalid proofs', + [treeAndLeaf, tree], + (t, [tree, options, { value: leaf }], [otherTree, otherOptions]) => { + const proof = tree.getProof(leaf); + t.false(otherTree.verify(leaf, proof)); + t.false(SimpleMerkleTree.verify(otherTree.root, leaf, proof, options.nodeHash)); + t.false(SimpleMerkleTree.verify(otherTree.root, leaf, proof, otherOptions.nodeHash)); + }, +); -testProp('generates valid multiproofs', [treeAndLeaves], (t, [tree, indices]) => { +testProp('generates valid multiproofs', [treeAndLeaves], (t, [tree, options, indices]) => { const proof1 = tree.getMultiProof(indices.map(e => e.index)); const proof2 = tree.getMultiProof(indices.map(e => e.value)); t.deepEqual(proof1, proof2); t.true(tree.verifyMultiProof(proof1)); - t.true(SimpleMerkleTree.verifyMultiProof(tree.root, proof1)); + t.true(SimpleMerkleTree.verifyMultiProof(tree.root, proof1, options.nodeHash)); }); -testProp('rejects invalid multiproofs', [treeAndLeaves, tree], (t, [tree, indices], otherTree) => { - const multiProof = tree.getMultiProof(indices.map(e => e.index)); - - t.false(otherTree.verifyMultiProof(multiProof)); - t.false(SimpleMerkleTree.verifyMultiProof(otherTree.root, multiProof)); -}); +testProp( + 'rejects invalid multiproofs', + [treeAndLeaves, tree], + (t, [tree, options, indices], [otherTree, otherOptions]) => { + const multiProof = tree.getMultiProof(indices.map(e => e.index)); + + t.false(otherTree.verifyMultiProof(multiProof)); + t.false(SimpleMerkleTree.verifyMultiProof(otherTree.root, multiProof, options.nodeHash)); + t.false(SimpleMerkleTree.verifyMultiProof(otherTree.root, multiProof, otherOptions.nodeHash)); + }, +); testProp( 'renders tree representation', @@ -68,6 +95,8 @@ testProp( (t, leaves) => { t.snapshot(SimpleMerkleTree.of(leaves, { sortLeaves: true }).render()); t.snapshot(SimpleMerkleTree.of(leaves, { sortLeaves: false }).render()); + t.snapshot(SimpleMerkleTree.of(leaves, { sortLeaves: true, nodeHash: reverseNodeHash }).render()); + t.snapshot(SimpleMerkleTree.of(leaves, { sortLeaves: false, nodeHash: reverseNodeHash }).render()); }, { numRuns: 1, seed: 0 }, ); @@ -78,24 +107,34 @@ testProp( (t, leaves) => { t.snapshot(SimpleMerkleTree.of(leaves, { sortLeaves: true }).dump()); t.snapshot(SimpleMerkleTree.of(leaves, { sortLeaves: false }).dump()); + t.snapshot(SimpleMerkleTree.of(leaves, { sortLeaves: true, nodeHash: reverseNodeHash }).dump()); + t.snapshot(SimpleMerkleTree.of(leaves, { sortLeaves: false, nodeHash: reverseNodeHash }).dump()); }, { numRuns: 1, seed: 0 }, ); -testProp('dump and load', [tree], (t, tree) => { - const recoveredTree = SimpleMerkleTree.load(tree.dump()); - recoveredTree.validate(); +testProp('dump and load', [tree], (t, [tree, options]) => { + const dump = tree.dump(); + const recoveredTree = SimpleMerkleTree.load(dump, options.nodeHash); + recoveredTree.validate(); // already done in load + t.is(dump.hash, options.nodeHash ? 'custom' : undefined); t.is(tree.root, recoveredTree.root); t.is(tree.render(), recoveredTree.render()); t.deepEqual(tree.entries(), recoveredTree.entries()); t.deepEqual(tree.dump(), recoveredTree.dump()); }); -testProp('reject out of bounds value index', [tree], (t, tree) => { +testProp('reject out of bounds value index', [tree], (t, [tree]) => { t.throws(() => tree.getProof(-1), new InvalidArgumentError('Index out of bounds')); }); +// We need at least 2 leaves for internal node hashing to come into play +testProp('reject loading dump with wrong node hash', [fc.array(leaf, { minLength: 2 })], (t, leaves) => { + const dump = SimpleMerkleTree.of(leaves, { nodeHash: reverseNodeHash }).dump(); + t.throws(() => SimpleMerkleTree.load(dump, otherNodeHash), new InvariantError('Merkle tree is invalid')); +}); + test('reject invalid leaf size', t => { const invalidLeaf = '0x000000000000000000000000000000000000000000000000000000000000000000'; t.throws(() => SimpleMerkleTree.of([invalidLeaf]), { @@ -116,22 +155,28 @@ test('reject unrecognized tree dump', t => { }); test('reject malformed tree dump', t => { - const loadedTree1 = SimpleMerkleTree.load({ - format: 'simple-v1', - tree: [zero], - values: [ - { - value: '0x0000000000000000000000000000000000000000000000000000000000000001', - treeIndex: 0, - }, - ], - }); - t.throws(() => loadedTree1.getProof(0), new InvariantError('Merkle tree does not contain the expected value')); + t.throws( + () => + SimpleMerkleTree.load({ + format: 'simple-v1', + tree: [zero], + values: [ + { + value: '0x0000000000000000000000000000000000000000000000000000000000000001', + treeIndex: 0, + }, + ], + }), + new 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 }], - }); - t.throws(() => loadedTree2.getProof(0), new InvariantError('Unable to prove value')); + t.throws( + () => + SimpleMerkleTree.load({ + format: 'simple-v1', + tree: [zero, zero, zero], + values: [{ value: zero, treeIndex: 2 }], + }), + new InvariantError('Merkle tree is invalid'), + ); }); diff --git a/src/simple.test.ts.md b/src/simple.test.ts.md index 2f47b43..ec437f4 100644 --- a/src/simple.test.ts.md +++ b/src/simple.test.ts.md @@ -58,6 +58,58 @@ Generated by [AVA](https://avajs.dev). ├─ 13) 0xf26f6ac933beb3e3608203578396131902729b62338aae6b4e1a767ae30c8b76␊ └─ 14) 0x223dc038a27b04f90f865157aefbee4e661bdcf6a35cf443f57daae5e6eb3e10` +> Snapshot 3 + + `0) 0xd37427b3c449bf13d7e7d79cf805d2c0cabc7d23f7af86e8c24342a65a38117f␊ + ├─ 1) 0x23ef2b407ee96466ab7c1a264a943f0c6472fea32e2b954988e875cec381fc37␊ + │ ├─ 3) 0x3c80438df7d87a81d85c2cc336c339bf6eb1b7a4de3f40d1049a69df5ad27508␊ + │ │ ├─ 7) 0x3424aa6830d46cca88e092afbf281f0ee77e434130ee44d01427d47b31a43a85␊ + │ │ │ ├─ 15) 0x9f71c4166a5f935a4bb399392a0e19c8f6f6d70f9a57a5f49b76fd723b694795␊ + │ │ │ └─ 16) 0x8c11d27a6e9c74765e13271dc8aeae29070438ee6ee872b94347de321c114657␊ + │ │ └─ 8) 0x1b9b2b9ef84eba851dafa78967746275701a1cc21a36a83b99595b44a197da6f␊ + │ │ ├─ 17) 0x88a73a1171ee3428809bb0a61a2e3c5d1cbf719910d939ae243ec2859fdad36a␊ + │ │ └─ 18) 0x46cfb9f0d1f86847e17e8975657bb314b1a3b6bb89e4a55f7d6f4c2fdf0e5422␊ + │ └─ 4) 0x3b1d26649340543d5b5a6155c84e67f44fa55809b0f362f66bf2ade281a07ea5␊ + │ ├─ 9) 0x18650562a2744d49344010252b3e3104da144aa6737c4e730d59712909631fc7␊ + │ │ ├─ 19) 0x2fac6f51addc8d004b3ff871d87c51837a4d881adb95b2a27820dbcafa94e7b8␊ + │ │ └─ 20) 0x223dc038a27b04f90f865157aefbee4e661bdcf6a35cf443f57daae5e6eb3e10␊ + │ └─ 10) 0x1faef214093f94529d3d8238dc6597ca6068c9d22a6ecfd3f7ccc25ed7b726df␊ + │ ├─ 21) 0x1d3d2f27573475675df2df1fb427256b05d4fb3c1b60baa7f58e479719daa0aa␊ + │ └─ 22) 0x0229437bdff01e90d3cd2c5dc9d3253d2c13bcb59c0b1caf0862bac2bd829db1␊ + └─ 2) 0x28b144b0df394fbee7f318e0d0747f5b20cfa05fe2b6aa20437a9c4fc1c65ceb␊ + ├─ 5) 0xcb4307038b5d39b0653ac9fb0bba4a43601307bc4844b72694dde953435db74d␊ + │ ├─ 11) 0xf26f6ac933beb3e3608203578396131902729b62338aae6b4e1a767ae30c8b76␊ + │ └─ 12) 0xe6c6ce5eb372e601197002c965ee5d050cb2925dbb89822c5c8f27585a9b5a96␊ + └─ 6) 0xdcaac046ba8c026de89bdcaeb2d8b5ddf762ec899f86060a802a00371fd7d4f2␊ + ├─ 13) 0xc36697399f86481780854e17b7cbc50072efd74da5bfe81a712db493185c82ec␊ + └─ 14) 0xaa504056e189fd775322d63793b5a16fa1207441aa1a32439deb249fcdf54c3b` + +> Snapshot 4 + + `0) 0xfdcc17bd4cc4ebba0cc671d3c52b68cd13e695f9dc3c4e7e4c8faf7ce0f8e371␊ + ├─ 1) 0x4a8b703c889b2fd6cceb57d51a2960c4c7c283707f8d8080d93d08ad9dea922e␊ + │ ├─ 3) 0x5dc10e6d5380d7c6dc6f5f042eda9974cbb412236f79686e40a052fabc12c049␊ + │ │ ├─ 7) 0x5d23ab436576466a1fc3cd310e096e1cade67473d398c6a43c9952ff31e1e75a␊ + │ │ │ ├─ 15) 0x9f71c4166a5f935a4bb399392a0e19c8f6f6d70f9a57a5f49b76fd723b694795␊ + │ │ │ └─ 16) 0xe6c6ce5eb372e601197002c965ee5d050cb2925dbb89822c5c8f27585a9b5a96␊ + │ │ └─ 8) 0xa1d2e032a606b18bbb030451c6664ea03e4ab569f7318aa126a2a0002b1178b2␊ + │ │ ├─ 17) 0xaa504056e189fd775322d63793b5a16fa1207441aa1a32439deb249fcdf54c3b␊ + │ │ └─ 18) 0x88a73a1171ee3428809bb0a61a2e3c5d1cbf719910d939ae243ec2859fdad36a␊ + │ └─ 4) 0xd9eaefae907251e252eb2ed5c8963cb568aef567615ea3e006233374997eba5a␊ + │ ├─ 9) 0x553394a4ded1e4b00817083b9fdeecd889ea3b7d9c19046afcf1e4eb9199d55d␊ + │ │ ├─ 19) 0x8c11d27a6e9c74765e13271dc8aeae29070438ee6ee872b94347de321c114657␊ + │ │ └─ 20) 0x0229437bdff01e90d3cd2c5dc9d3253d2c13bcb59c0b1caf0862bac2bd829db1␊ + │ └─ 10) 0xdc812b2fa70fff382e84e45bee14d08893c2ff6279019ee95b04ce76be6f95f8␊ + │ ├─ 21) 0x46cfb9f0d1f86847e17e8975657bb314b1a3b6bb89e4a55f7d6f4c2fdf0e5422␊ + │ └─ 22) 0x1d3d2f27573475675df2df1fb427256b05d4fb3c1b60baa7f58e479719daa0aa␊ + └─ 2) 0xf31eacd3933ab8ffac3fe2218dd35f33773df8b0b883d4466421c60b2c5b499d␊ + ├─ 5) 0xe2728c3f6cfeeed85f6b1276ac1076feafaa56464605e0fdeb3a189ed02e77e3␊ + │ ├─ 11) 0xc36697399f86481780854e17b7cbc50072efd74da5bfe81a712db493185c82ec␊ + │ └─ 12) 0x2fac6f51addc8d004b3ff871d87c51837a4d881adb95b2a27820dbcafa94e7b8␊ + └─ 6) 0x08e34c58c9ac0aba2ad2e76375558139169fbc57883d8895841499566b41df21␊ + ├─ 13) 0xf26f6ac933beb3e3608203578396131902729b62338aae6b4e1a767ae30c8b76␊ + └─ 14) 0x223dc038a27b04f90f865157aefbee4e661bdcf6a35cf443f57daae5e6eb3e10` + ## dump (with seed=0) > Snapshot 1 @@ -221,3 +273,167 @@ Generated by [AVA](https://avajs.dev). }, ], } + +> Snapshot 3 + + { + format: 'simple-v1', + hash: 'custom', + tree: [ + '0xd37427b3c449bf13d7e7d79cf805d2c0cabc7d23f7af86e8c24342a65a38117f', + '0x23ef2b407ee96466ab7c1a264a943f0c6472fea32e2b954988e875cec381fc37', + '0x28b144b0df394fbee7f318e0d0747f5b20cfa05fe2b6aa20437a9c4fc1c65ceb', + '0x3c80438df7d87a81d85c2cc336c339bf6eb1b7a4de3f40d1049a69df5ad27508', + '0x3b1d26649340543d5b5a6155c84e67f44fa55809b0f362f66bf2ade281a07ea5', + '0xcb4307038b5d39b0653ac9fb0bba4a43601307bc4844b72694dde953435db74d', + '0xdcaac046ba8c026de89bdcaeb2d8b5ddf762ec899f86060a802a00371fd7d4f2', + '0x3424aa6830d46cca88e092afbf281f0ee77e434130ee44d01427d47b31a43a85', + '0x1b9b2b9ef84eba851dafa78967746275701a1cc21a36a83b99595b44a197da6f', + '0x18650562a2744d49344010252b3e3104da144aa6737c4e730d59712909631fc7', + '0x1faef214093f94529d3d8238dc6597ca6068c9d22a6ecfd3f7ccc25ed7b726df', + '0xf26f6ac933beb3e3608203578396131902729b62338aae6b4e1a767ae30c8b76', + '0xe6c6ce5eb372e601197002c965ee5d050cb2925dbb89822c5c8f27585a9b5a96', + '0xc36697399f86481780854e17b7cbc50072efd74da5bfe81a712db493185c82ec', + '0xaa504056e189fd775322d63793b5a16fa1207441aa1a32439deb249fcdf54c3b', + '0x9f71c4166a5f935a4bb399392a0e19c8f6f6d70f9a57a5f49b76fd723b694795', + '0x8c11d27a6e9c74765e13271dc8aeae29070438ee6ee872b94347de321c114657', + '0x88a73a1171ee3428809bb0a61a2e3c5d1cbf719910d939ae243ec2859fdad36a', + '0x46cfb9f0d1f86847e17e8975657bb314b1a3b6bb89e4a55f7d6f4c2fdf0e5422', + '0x2fac6f51addc8d004b3ff871d87c51837a4d881adb95b2a27820dbcafa94e7b8', + '0x223dc038a27b04f90f865157aefbee4e661bdcf6a35cf443f57daae5e6eb3e10', + '0x1d3d2f27573475675df2df1fb427256b05d4fb3c1b60baa7f58e479719daa0aa', + '0x0229437bdff01e90d3cd2c5dc9d3253d2c13bcb59c0b1caf0862bac2bd829db1', + ], + values: [ + { + treeIndex: 21, + value: '0x1d3d2f27573475675df2df1fb427256b05d4fb3c1b60baa7f58e479719daa0aa', + }, + { + treeIndex: 18, + value: '0x46cfb9f0d1f86847e17e8975657bb314b1a3b6bb89e4a55f7d6f4c2fdf0e5422', + }, + { + treeIndex: 22, + value: '0x0229437bdff01e90d3cd2c5dc9d3253d2c13bcb59c0b1caf0862bac2bd829db1', + }, + { + treeIndex: 16, + value: '0x8c11d27a6e9c74765e13271dc8aeae29070438ee6ee872b94347de321c114657', + }, + { + treeIndex: 17, + value: '0x88a73a1171ee3428809bb0a61a2e3c5d1cbf719910d939ae243ec2859fdad36a', + }, + { + treeIndex: 14, + value: '0xaa504056e189fd775322d63793b5a16fa1207441aa1a32439deb249fcdf54c3b', + }, + { + treeIndex: 12, + value: '0xe6c6ce5eb372e601197002c965ee5d050cb2925dbb89822c5c8f27585a9b5a96', + }, + { + treeIndex: 15, + value: '0x9f71c4166a5f935a4bb399392a0e19c8f6f6d70f9a57a5f49b76fd723b694795', + }, + { + treeIndex: 20, + value: '0x223dc038a27b04f90f865157aefbee4e661bdcf6a35cf443f57daae5e6eb3e10', + }, + { + treeIndex: 11, + value: '0xf26f6ac933beb3e3608203578396131902729b62338aae6b4e1a767ae30c8b76', + }, + { + treeIndex: 19, + value: '0x2fac6f51addc8d004b3ff871d87c51837a4d881adb95b2a27820dbcafa94e7b8', + }, + { + treeIndex: 13, + value: '0xc36697399f86481780854e17b7cbc50072efd74da5bfe81a712db493185c82ec', + }, + ], + } + +> Snapshot 4 + + { + format: 'simple-v1', + hash: 'custom', + tree: [ + '0xfdcc17bd4cc4ebba0cc671d3c52b68cd13e695f9dc3c4e7e4c8faf7ce0f8e371', + '0x4a8b703c889b2fd6cceb57d51a2960c4c7c283707f8d8080d93d08ad9dea922e', + '0xf31eacd3933ab8ffac3fe2218dd35f33773df8b0b883d4466421c60b2c5b499d', + '0x5dc10e6d5380d7c6dc6f5f042eda9974cbb412236f79686e40a052fabc12c049', + '0xd9eaefae907251e252eb2ed5c8963cb568aef567615ea3e006233374997eba5a', + '0xe2728c3f6cfeeed85f6b1276ac1076feafaa56464605e0fdeb3a189ed02e77e3', + '0x08e34c58c9ac0aba2ad2e76375558139169fbc57883d8895841499566b41df21', + '0x5d23ab436576466a1fc3cd310e096e1cade67473d398c6a43c9952ff31e1e75a', + '0xa1d2e032a606b18bbb030451c6664ea03e4ab569f7318aa126a2a0002b1178b2', + '0x553394a4ded1e4b00817083b9fdeecd889ea3b7d9c19046afcf1e4eb9199d55d', + '0xdc812b2fa70fff382e84e45bee14d08893c2ff6279019ee95b04ce76be6f95f8', + '0xc36697399f86481780854e17b7cbc50072efd74da5bfe81a712db493185c82ec', + '0x2fac6f51addc8d004b3ff871d87c51837a4d881adb95b2a27820dbcafa94e7b8', + '0xf26f6ac933beb3e3608203578396131902729b62338aae6b4e1a767ae30c8b76', + '0x223dc038a27b04f90f865157aefbee4e661bdcf6a35cf443f57daae5e6eb3e10', + '0x9f71c4166a5f935a4bb399392a0e19c8f6f6d70f9a57a5f49b76fd723b694795', + '0xe6c6ce5eb372e601197002c965ee5d050cb2925dbb89822c5c8f27585a9b5a96', + '0xaa504056e189fd775322d63793b5a16fa1207441aa1a32439deb249fcdf54c3b', + '0x88a73a1171ee3428809bb0a61a2e3c5d1cbf719910d939ae243ec2859fdad36a', + '0x8c11d27a6e9c74765e13271dc8aeae29070438ee6ee872b94347de321c114657', + '0x0229437bdff01e90d3cd2c5dc9d3253d2c13bcb59c0b1caf0862bac2bd829db1', + '0x46cfb9f0d1f86847e17e8975657bb314b1a3b6bb89e4a55f7d6f4c2fdf0e5422', + '0x1d3d2f27573475675df2df1fb427256b05d4fb3c1b60baa7f58e479719daa0aa', + ], + values: [ + { + treeIndex: 22, + value: '0x1d3d2f27573475675df2df1fb427256b05d4fb3c1b60baa7f58e479719daa0aa', + }, + { + treeIndex: 21, + value: '0x46cfb9f0d1f86847e17e8975657bb314b1a3b6bb89e4a55f7d6f4c2fdf0e5422', + }, + { + treeIndex: 20, + value: '0x0229437bdff01e90d3cd2c5dc9d3253d2c13bcb59c0b1caf0862bac2bd829db1', + }, + { + treeIndex: 19, + value: '0x8c11d27a6e9c74765e13271dc8aeae29070438ee6ee872b94347de321c114657', + }, + { + treeIndex: 18, + value: '0x88a73a1171ee3428809bb0a61a2e3c5d1cbf719910d939ae243ec2859fdad36a', + }, + { + treeIndex: 17, + value: '0xaa504056e189fd775322d63793b5a16fa1207441aa1a32439deb249fcdf54c3b', + }, + { + treeIndex: 16, + value: '0xe6c6ce5eb372e601197002c965ee5d050cb2925dbb89822c5c8f27585a9b5a96', + }, + { + treeIndex: 15, + value: '0x9f71c4166a5f935a4bb399392a0e19c8f6f6d70f9a57a5f49b76fd723b694795', + }, + { + treeIndex: 14, + value: '0x223dc038a27b04f90f865157aefbee4e661bdcf6a35cf443f57daae5e6eb3e10', + }, + { + treeIndex: 13, + value: '0xf26f6ac933beb3e3608203578396131902729b62338aae6b4e1a767ae30c8b76', + }, + { + treeIndex: 12, + value: '0x2fac6f51addc8d004b3ff871d87c51837a4d881adb95b2a27820dbcafa94e7b8', + }, + { + treeIndex: 11, + value: '0xc36697399f86481780854e17b7cbc50072efd74da5bfe81a712db493185c82ec', + }, + ], + } diff --git a/src/simple.test.ts.snap b/src/simple.test.ts.snap index 5c19c980ea52845c530208e0db4bb5c8df562be4..9383c9fdf8c5d3ed58c477d9073dc9da6da22a95 100644 GIT binary patch literal 4282 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 { format: 'simple-v1'; + hash?: 'custom'; +} + +export interface SimpleMerkleTreeOptions extends MerkleTreeOptions { + nodeHash?: NodeHash; } export function formatLeaf(value: BytesLike): HexString { @@ -14,22 +20,29 @@ export function formatLeaf(value: BytesLike): HexString { } export class SimpleMerkleTree extends MerkleTreeImpl { - static of(values: BytesLike[], options: MerkleTreeOptions = {}): SimpleMerkleTree { - const [tree, indexedValues] = MerkleTreeImpl.prepare(values, options, formatLeaf); - return new SimpleMerkleTree(tree, indexedValues, formatLeaf); + static of(values: BytesLike[], options: SimpleMerkleTreeOptions = {}): SimpleMerkleTree { + const [tree, indexedValues] = MerkleTreeImpl.prepare(values, options, formatLeaf, options.nodeHash); + return new SimpleMerkleTree(tree, indexedValues, formatLeaf, options.nodeHash); } - static load(data: SimpleMerkleTreeData): SimpleMerkleTree { + static load(data: SimpleMerkleTreeData, nodeHash?: NodeHash): SimpleMerkleTree { validateArgument(data.format === 'simple-v1', `Unknown format '${data.format}'`); - return new SimpleMerkleTree(data.tree, data.values, formatLeaf); + validateArgument( + (nodeHash == undefined) !== (data.hash == 'custom'), + nodeHash ? 'Data does not expect a custom node hashing function' : 'Data expects a custom node hashing function', + ); + + const tree = new SimpleMerkleTree(data.tree, data.values, formatLeaf, nodeHash); + tree.validate(); + return tree; } - static verify(root: BytesLike, leaf: BytesLike, proof: BytesLike[]): boolean { - return toHex(root) === processProof(formatLeaf(leaf), proof); + static verify(root: BytesLike, leaf: BytesLike, proof: BytesLike[], nodeHash?: NodeHash): boolean { + return toHex(root) === processProof(formatLeaf(leaf), proof, nodeHash); } - static verifyMultiProof(root: BytesLike, multiproof: MultiProof): boolean { - return toHex(root) === processMultiProof(multiproof); + static verifyMultiProof(root: BytesLike, multiproof: MultiProof, nodeHash?: NodeHash): boolean { + return toHex(root) === processMultiProof(multiproof, nodeHash); } dump(): SimpleMerkleTreeData { @@ -37,6 +50,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { format: 'simple-v1', tree: this.tree, values: this.values.map(({ value, treeIndex }) => ({ value: toHex(value), treeIndex })), + ...(this.nodeHash ? { hash: 'custom' } : {}), }; } } diff --git a/src/standard.test.ts b/src/standard.test.ts index 2d622d4..164f3e2 100644 --- a/src/standard.test.ts +++ b/src/standard.test.ts @@ -85,7 +85,7 @@ testProp( testProp('dump and load', [tree], (t, tree) => { const recoveredTree = StandardMerkleTree.load(tree.dump()); - recoveredTree.validate(); + recoveredTree.validate(); // already done in load t.is(tree.root, recoveredTree.root); t.is(tree.render(), recoveredTree.render()); @@ -110,19 +110,25 @@ test('reject unrecognized tree dump', t => { }); test('reject malformed tree dump', t => { - const loadedTree1 = StandardMerkleTree.load({ - format: 'standard-v1', - tree: [zero], - values: [{ value: ['0'], treeIndex: 0 }], - leafEncoding: ['uint256'], - }); - t.throws(() => loadedTree1.getProof(0), new 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'], - }); - t.throws(() => loadedTree2.getProof(0), new InvariantError('Unable to prove value')); + t.throws( + () => + StandardMerkleTree.load({ + format: 'standard-v1', + tree: [zero], + values: [{ value: ['0'], treeIndex: 0 }], + leafEncoding: ['uint256'], + }), + new InvariantError('Merkle tree does not contain the expected value'), + ); + + t.throws( + () => + StandardMerkleTree.load({ + format: 'standard-v1', + tree: [zero, zero, keccak256(keccak256(zero))], + values: [{ value: ['0'], treeIndex: 2 }], + leafEncoding: ['uint256'], + }), + new InvariantError('Merkle tree is invalid'), + ); }); diff --git a/src/standard.ts b/src/standard.ts index e15ef77..c69488d 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -1,9 +1,8 @@ -import { keccak256 } from '@ethersproject/keccak256'; -import { defaultAbiCoder } from '@ethersproject/abi'; import { BytesLike, HexString, toHex } from './bytes'; import { MultiProof, processProof, processMultiProof } from './core'; import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; import { MerkleTreeOptions } from './options'; +import { standardLeafHash } from './hashes'; import { validateArgument } from './utils/errors'; export interface StandardMerkleTreeData extends MerkleTreeData { @@ -11,10 +10,6 @@ export interface StandardMerkleTreeData extends MerkleTreeData< leafEncoding: string[]; } -export function standardLeafHash(types: string[], value: T): HexString { - return keccak256(keccak256(defaultAbiCoder.encode(types, value))); -} - export class StandardMerkleTree extends MerkleTreeImpl { protected constructor( protected readonly tree: HexString[], @@ -29,6 +24,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { leafEncoding: string[], options: MerkleTreeOptions = {}, ): StandardMerkleTree { + // use default nodeHash (standardNodeHash) const [tree, indexedValues] = MerkleTreeImpl.prepare(values, options, leaf => standardLeafHash(leafEncoding, leaf)); return new StandardMerkleTree(tree, indexedValues, leafEncoding); } @@ -36,10 +32,14 @@ export class StandardMerkleTree extends MerkleTreeImpl { static load(data: StandardMerkleTreeData): StandardMerkleTree { 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); + + const tree = new StandardMerkleTree(data.tree, data.values, data.leafEncoding); + tree.validate(); + return tree; } static verify(root: BytesLike, leafEncoding: string[], leaf: T, proof: BytesLike[]): boolean { + // use default nodeHash (standardNodeHash) for processProof return toHex(root) === processProof(standardLeafHash(leafEncoding, leaf), proof); } @@ -48,6 +48,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { leafEncoding: string[], multiproof: MultiProof, ): boolean { + // use default nodeHash (standardNodeHash) for processMultiProof return ( toHex(root) === processMultiProof({