From 3af75a4e01b3ce31c38421095c0f0921b6e6602b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 Feb 2024 21:17:47 +0100 Subject: [PATCH 01/12] Implement custom nodeHash in core and SimpleMerkleTree --- src/core.ts | 30 ++++++------ src/hashes.ts | 14 ++++++ src/merkletree.ts | 18 ++++--- src/simple.test.ts | 118 ++++++++++++++++++++++++++++----------------- src/simple.ts | 26 ++++++---- src/standard.ts | 14 +++--- 6 files changed, 137 insertions(+), 83 deletions(-) create mode 100644 src/hashes.ts diff --git a/src/core.ts b/src/core.ts index b523ba7..d5011dd 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): 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 ?? standardNodeHash)(tree[leftChildIndex(i)]!, tree[rightChildIndex(i)]!); } return tree; @@ -43,14 +41,14 @@ export function getProof(tree: BytesLike[], index: number): HexString[] { proof.push(toHex(tree[siblingIndex(index)]!)); index = parentIndex(index); } - return proof; + return proof.map(node => toHex(node)); } -export function processProof(leaf: BytesLike, proof: BytesLike[]): HexString { +export function processProof(leaf: BytesLike, proof: BytesLike[], nodeHash?: NodeHash): HexString { checkValidMerkleNode(leaf); proof.forEach(checkValidMerkleNode); - return toHex(proof.reduce(hashPair, leaf)); + return toHex(proof.reduce(nodeHash ?? standardNodeHash, 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): 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 ?? standardNodeHash)(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): 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 ?? standardNodeHash)(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 5d79c5e..60c96fa 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) => ({ value, valueIndex, hash: leafHash(value) })); @@ -61,7 +64,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()) { @@ -89,7 +95,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 { @@ -165,10 +171,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 af40c8b..de83488 100644 --- a/src/simple.test.ts +++ b/src/simple.test.ts @@ -2,9 +2,12 @@ import assert from 'assert/strict'; import { HashZero as zero } from '@ethersproject/constants'; import { keccak256 } from '@ethersproject/keccak256'; import { SimpleMerkleTree } from './simple'; +import { BytesLike, HexString, concat, compare } from './bytes'; + +const reverseHashPair = (a: BytesLike, b: BytesLike): HexString => keccak256(concat([a, b].sort(compare).reverse())); describe('simple merkle tree', () => { - for (const opts of [{}, { sortLeaves: true }, { sortLeaves: false }]) { + for (const opts of [{}, { sortLeaves: true }, { sortLeaves: false }, { nodeHash: reverseHashPair }]) { describe(`with options '${JSON.stringify(opts)}'`, () => { const leaves = 'abcdef'.split('').map(c => keccak256(Buffer.from(c))); const otherLeaves = 'abc'.split('').map(c => keccak256(Buffer.from(c))); @@ -24,7 +27,7 @@ describe('simple merkle tree', () => { assert(tree.verify(id, proof1)); assert(tree.verify(leaf, proof1)); - assert(SimpleMerkleTree.verify(tree.root, leaf, proof1)); + assert(SimpleMerkleTree.verify(tree.root, leaf, proof1, opts.nodeHash)); } }); @@ -33,7 +36,7 @@ describe('simple merkle tree', () => { const invalidProof = otherTree.getProof(leaf); assert(!tree.verify(leaf, invalidProof)); - assert(!SimpleMerkleTree.verify(tree.root, leaf, invalidProof)); + assert(!SimpleMerkleTree.verify(tree.root, leaf, invalidProof, opts.nodeHash)); }); it('generates valid multiproofs', () => { @@ -44,7 +47,7 @@ describe('simple merkle tree', () => { assert.deepEqual(proof1, proof2); assert(tree.verifyMultiProof(proof1)); - assert(SimpleMerkleTree.verifyMultiProof(tree.root, proof1)); + assert(SimpleMerkleTree.verifyMultiProof(tree.root, proof1, opts.nodeHash)); } }); @@ -52,44 +55,62 @@ describe('simple merkle tree', () => { const multiProof = otherTree.getMultiProof(leaves.slice(0, 3)); assert(!tree.verifyMultiProof(multiProof)); - assert(!SimpleMerkleTree.verifyMultiProof(tree.root, multiProof)); + assert(!SimpleMerkleTree.verifyMultiProof(tree.root, multiProof, opts.nodeHash)); }); it('renders tree representation', () => { - assert.equal( - tree.render(), - opts.sortLeaves == false - ? [ - '0) 0x9012f1e18a87790d2e01faace75aaaca38e53df437cdce2c0552464dda4af49c', - '├─ 1) 0x68203f90e9d07dc5859259d7536e87a6ba9d345f2552b5b9de2999ddce9ce1bf', - '│ ├─ 3) 0xd253a52d4cb00de2895e85f2529e2976e6aaaa5c18106b68ab66813e14415669', - '│ │ ├─ 7) 0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3', - '│ │ └─ 8) 0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2', - '│ └─ 4) 0x805b21d846b189efaeb0377d6bb0d201b3872a363e607c25088f025b0c6ae1f8', - '│ ├─ 9) 0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510', - '│ └─ 10) 0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb', - '└─ 2) 0xf0b49bb4b0d9396e0315755ceafaa280707b32e75e6c9053f5cdf2679dcd5c6a', - ' ├─ 5) 0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483', - ' └─ 6) 0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761', - ].join('\n') - : [ - '0) 0x1b404f199ea828ec5771fb30139c222d8417a82175fefad5cd42bc3a189bd8d5', - '├─ 1) 0xec554bdfb01d31fa838d0830339b0e6e8a70e0d55a8f172ffa8bebbf8e8d5ba0', - '│ ├─ 3) 0x434d51cfeb80272378f4c3a8fd2824561c2cad9fce556ea600d46f20550976a6', - '│ │ ├─ 7) 0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510', - '│ │ └─ 8) 0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761', - '│ └─ 4) 0x7dea550f679f3caab547cbbc5ee1a4c978c8c039b572ba00af1baa6481b88360', - '│ ├─ 9) 0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb', - '│ └─ 10) 0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2', - '└─ 2) 0xaf46af0745b433e1d5bed9a04b1fdf4002f67a733c20db2fca5b2af6120d9bcb', - ' ├─ 5) 0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3', - ' └─ 6) 0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483', - ].join('\n'), - ); + const expected = // standard hash + unsorted + ( + !opts.nodeHash && opts.sortLeaves === false + ? [ + '0) 0x9012f1e18a87790d2e01faace75aaaca38e53df437cdce2c0552464dda4af49c', + '├─ 1) 0x68203f90e9d07dc5859259d7536e87a6ba9d345f2552b5b9de2999ddce9ce1bf', + '│ ├─ 3) 0xd253a52d4cb00de2895e85f2529e2976e6aaaa5c18106b68ab66813e14415669', + '│ │ ├─ 7) 0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3', + '│ │ └─ 8) 0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2', + '│ └─ 4) 0x805b21d846b189efaeb0377d6bb0d201b3872a363e607c25088f025b0c6ae1f8', + '│ ├─ 9) 0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510', + '│ └─ 10) 0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb', + '└─ 2) 0xf0b49bb4b0d9396e0315755ceafaa280707b32e75e6c9053f5cdf2679dcd5c6a', + ' ├─ 5) 0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483', + ' └─ 6) 0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761', + ] + : // sortLeaves = true | undefined --- standard hash + sorted + !opts.nodeHash + ? [ + '0) 0x1b404f199ea828ec5771fb30139c222d8417a82175fefad5cd42bc3a189bd8d5', + '├─ 1) 0xec554bdfb01d31fa838d0830339b0e6e8a70e0d55a8f172ffa8bebbf8e8d5ba0', + '│ ├─ 3) 0x434d51cfeb80272378f4c3a8fd2824561c2cad9fce556ea600d46f20550976a6', + '│ │ ├─ 7) 0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510', + '│ │ └─ 8) 0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761', + '│ └─ 4) 0x7dea550f679f3caab547cbbc5ee1a4c978c8c039b572ba00af1baa6481b88360', + '│ ├─ 9) 0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb', + '│ └─ 10) 0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2', + '└─ 2) 0xaf46af0745b433e1d5bed9a04b1fdf4002f67a733c20db2fca5b2af6120d9bcb', + ' ├─ 5) 0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3', + ' └─ 6) 0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483', + ] + : // non standard hash + [ + '0) 0x8f0a1adb058c628fa4ce2e7bd26024180b888fec77087d4e5ee6890746e9c6ec', + '├─ 1) 0xb9f5a6bc1b75fadcd9765163dfc8d4865d1608337a2a310ff51fecb431faaee4', + '│ ├─ 3) 0x37d657e93dfbae50b18241610418794b51124af5ca872f1b56c08490cb2905ac', + '│ │ ├─ 7) 0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510', + '│ │ └─ 8) 0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761', + '│ └─ 4) 0xed90ef72e95e6692b91b020dc6cb5c4db9dc149a496799c4318fa8075960c48e', + '│ ├─ 9) 0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb', + '│ └─ 10) 0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2', + '└─ 2) 0x138c55cca8f6430d75b6bbcea643a7afa8ee74c22643ad76723ecafd4fcd21d4', + ' ├─ 5) 0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3', + ' └─ 6) 0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483', + ] + ).join('\n'); + + assert.equal(tree.render(), expected); }); it('dump and load', () => { - const recoveredTree = SimpleMerkleTree.load(tree.dump()); + const recoveredTree = SimpleMerkleTree.load(tree.dump(), opts.nodeHash); recoveredTree.validate(); assert.deepEqual(tree, recoveredTree); @@ -100,9 +121,9 @@ describe('simple merkle tree', () => { }); it('reject invalid leaf size', () => { - const invalidLeaf = [zero + '00']; // 33 bytes (all zero) + const invalidLeaf = zero + '00'; // 33 bytes (all zero) assert.throws( - () => SimpleMerkleTree.of(invalidLeaf, opts), + () => SimpleMerkleTree.of([invalidLeaf], opts), `InvalidArgumentError: ${invalidLeaf} is not a valid 32 bytes object (pos: 0)`, ); }); @@ -122,16 +143,25 @@ describe('simple merkle tree', () => { ); }); + it('reject standard tree dump with a custom hash', () => { + assert.throws( + () => SimpleMerkleTree.load({ format: 'simple-v1' } as any, reverseHashPair), + /^InvalidArgumentError: Data does not expect a custom node hashing function$/, + ); + }); + + it('reject custom tree dump without a custom hash', () => { + assert.throws( + () => SimpleMerkleTree.load({ format: 'simple-v1', hash: 'custom' } as any), + /^InvalidArgumentError: Data expects a custom node hashing function$/, + ); + }); + it('reject malformed tree dump', () => { const loadedTree1 = SimpleMerkleTree.load({ format: 'simple-v1', tree: [zero], - values: [ - { - value: '0x0000000000000000000000000000000000000000000000000000000000000001', - treeIndex: 0, - }, - ], + values: [{ value: '0x0000000000000000000000000000000000000000000000000000000000000001', treeIndex: 0 }], }); assert.throws(() => loadedTree1.getProof(0), /^InvariantError: Merkle tree does not contain the expected value$/); diff --git a/src/simple.ts b/src/simple.ts index 94f87e8..32b85a6 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -3,10 +3,12 @@ import { BytesLike, HexString, toHex } from './bytes'; import { MultiProof, processProof, processMultiProof } from './core'; import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; import { MerkleTreeOptions } from './options'; +import { NodeHash } from './hashes'; import { validateArgument } from './utils/errors'; export interface SimpleMerkleTreeData extends MerkleTreeData { format: 'simple-v1'; + hash?: 'custom'; } export function formatLeaf(value: BytesLike): HexString { @@ -14,22 +16,27 @@ 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: MerkleTreeOptions & { nodeHash?: NodeHash } = {}): 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', + ); + + return new SimpleMerkleTree(data.tree, data.values, formatLeaf, nodeHash); } - 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 +44,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { format: 'simple-v1', tree: this.tree, values: this.values.map(({ value, treeIndex }) => ({ value: toHex(value), treeIndex })), + hash: this.nodeHash ? 'custom' : undefined, }; } } diff --git a/src/standard.ts b/src/standard.ts index e15ef77..739463b 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -1,19 +1,14 @@ -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 { +export type StandardMerkleTreeData = MerkleTreeData & { format: 'standard-v1'; leafEncoding: string[]; -} - -export function standardLeafHash(types: string[], value: T): HexString { - return keccak256(keccak256(defaultAbiCoder.encode(types, value))); -} +}; export class StandardMerkleTree extends MerkleTreeImpl { protected constructor( @@ -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); } @@ -40,6 +36,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { } 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 +45,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { leafEncoding: string[], multiproof: MultiProof, ): boolean { + // use default nodeHash (standardNodeHash) for processMultiProof return ( toHex(root) === processMultiProof({ From efb2399b46567724ecfdd2b590ee7fbbb4272be8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 Feb 2024 21:20:16 +0100 Subject: [PATCH 02/12] Apply suggestions from code review --- src/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core.ts b/src/core.ts index d5011dd..c91cb54 100644 --- a/src/core.ts +++ b/src/core.ts @@ -41,7 +41,7 @@ export function getProof(tree: BytesLike[], index: number): HexString[] { proof.push(toHex(tree[siblingIndex(index)]!)); index = parentIndex(index); } - return proof.map(node => toHex(node)); + return proof; } export function processProof(leaf: BytesLike, proof: BytesLike[], nodeHash?: NodeHash): HexString { From 27ccfc0642cc0ee1d206d066339ce0efd14417a2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 Feb 2024 22:21:46 +0100 Subject: [PATCH 03/12] Apply suggestions from code review --- src/standard.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/standard.ts b/src/standard.ts index 739463b..cd49460 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -5,10 +5,10 @@ import { MerkleTreeOptions } from './options'; import { standardLeafHash } from './hashes'; import { validateArgument } from './utils/errors'; -export type StandardMerkleTreeData = MerkleTreeData & { +export interface StandardMerkleTreeData extends MerkleTreeData { format: 'standard-v1'; leafEncoding: string[]; -}; +} export class StandardMerkleTree extends MerkleTreeImpl { protected constructor( From 0ef30b039f193e960139c591ed961a28972cbd15 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 28 Feb 2024 09:25:48 +0100 Subject: [PATCH 04/12] Address comments from code review --- src/core.ts | 16 ++++++++-------- src/simple.ts | 6 +++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/core.ts b/src/core.ts index c91cb54..c02d267 100644 --- a/src/core.ts +++ b/src/core.ts @@ -16,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[], nodeHash?: NodeHash): HexString[] { +export function makeMerkleTree(leaves: BytesLike[], nodeHash: NodeHash = standardNodeHash): HexString[] { leaves.forEach(checkValidMerkleNode); validateArgument(leaves.length !== 0, 'Expected non-zero number of leaves'); @@ -27,7 +27,7 @@ export function makeMerkleTree(leaves: BytesLike[], nodeHash?: NodeHash): HexStr tree[tree.length - 1 - i] = toHex(leaf); } for (let i = tree.length - 1 - leaves.length; i >= 0; i--) { - tree[i] = (nodeHash ?? standardNodeHash)(tree[leftChildIndex(i)]!, tree[rightChildIndex(i)]!); + tree[i] = nodeHash(tree[leftChildIndex(i)]!, tree[rightChildIndex(i)]!); } return tree; @@ -44,11 +44,11 @@ export function getProof(tree: BytesLike[], index: number): HexString[] { return proof; } -export function processProof(leaf: BytesLike, proof: BytesLike[], nodeHash?: NodeHash): HexString { +export function processProof(leaf: BytesLike, proof: BytesLike[], nodeHash: NodeHash = standardNodeHash): HexString { checkValidMerkleNode(leaf); proof.forEach(checkValidMerkleNode); - return toHex(proof.reduce(nodeHash ?? standardNodeHash, leaf)); + return toHex(proof.reduce(nodeHash, leaf)); } export interface MultiProof { @@ -96,7 +96,7 @@ export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof< }; } -export function processMultiProof(multiproof: MultiProof, nodeHash?: NodeHash): HexString { +export function processMultiProof(multiproof: MultiProof, nodeHash: NodeHash = standardNodeHash): HexString { multiproof.leaves.forEach(checkValidMerkleNode); multiproof.proof.forEach(checkValidMerkleNode); @@ -116,7 +116,7 @@ export function processMultiProof(multiproof: MultiProof, nodeHash?: const a = stack.shift(); const b = flag ? stack.shift() : proof.shift(); invariant(a !== undefined && b !== undefined); - stack.push((nodeHash ?? standardNodeHash)(a, b)); + stack.push(nodeHash(a, b)); } invariant(stack.length + proof.length === 1); @@ -124,7 +124,7 @@ export function processMultiProof(multiproof: MultiProof, nodeHash?: return toHex(stack.pop() ?? proof.shift()!); } -export function isValidMerkleTree(tree: BytesLike[], nodeHash?: NodeHash): boolean { +export function isValidMerkleTree(tree: BytesLike[], nodeHash: NodeHash = standardNodeHash): boolean { for (const [i, node] of tree.entries()) { if (!isValidMerkleNode(node)) { return false; @@ -137,7 +137,7 @@ export function isValidMerkleTree(tree: BytesLike[], nodeHash?: NodeHash): boole if (l < tree.length) { return false; } - } else if (compare(node, (nodeHash ?? standardNodeHash)(tree[l]!, tree[r]!))) { + } else if (compare(node, nodeHash(tree[l]!, tree[r]!))) { return false; } } diff --git a/src/simple.ts b/src/simple.ts index 32b85a6..86da528 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -11,12 +11,16 @@ export interface SimpleMerkleTreeData extends MerkleTreeData { hash?: 'custom'; } +export interface SimpleMerkleTreeOptions extends MerkleTreeOptions { + nodeHash?: NodeHash; +} + export function formatLeaf(value: BytesLike): HexString { return defaultAbiCoder.encode(['bytes32'], [value]); } export class SimpleMerkleTree extends MerkleTreeImpl { - static of(values: BytesLike[], options: MerkleTreeOptions & { nodeHash?: NodeHash } = {}): SimpleMerkleTree { + 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); } From ee4e65a2915c9ea94b361e3465a5405d3253baf6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 29 Feb 2024 11:14:25 +0100 Subject: [PATCH 05/12] fix lint --- src/simple.test.ts | 67 ++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/src/simple.test.ts b/src/simple.test.ts index 3e62024..584af03 100644 --- a/src/simple.test.ts +++ b/src/simple.test.ts @@ -16,12 +16,9 @@ const options = fc.record({ nodeHash: fc.oneof(fc.constant(undefined), fc.constant(reverseHashPair)), }); -const tree = fc.tuple(leaves, options).chain(([leaves, options]) => - fc.tuple( - fc.constant(SimpleMerkleTree.of(leaves, options)), - fc.constant(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)), @@ -41,26 +38,34 @@ 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, 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( + '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, 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( + '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, options, indices]) => { const proof1 = tree.getMultiProof(indices.map(e => e.index)); @@ -71,13 +76,17 @@ testProp('generates valid multiproofs', [treeAndLeaves], (t, [tree, options, ind t.true(SimpleMerkleTree.verifyMultiProof(tree.root, proof1, options.nodeHash)); }); -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( + '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', From 579986bd388a4b2753d6810cf194fa1a9fc06f27 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 29 Feb 2024 11:18:17 +0100 Subject: [PATCH 06/12] do not include 'hash: undefined' in dumps if no custom hash is used --- src/simple.test.ts.md | 2 -- src/simple.test.ts.snap | Bin 4291 -> 4282 bytes src/simple.ts | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/simple.test.ts.md b/src/simple.test.ts.md index cc42078..ec437f4 100644 --- a/src/simple.test.ts.md +++ b/src/simple.test.ts.md @@ -116,7 +116,6 @@ Generated by [AVA](https://avajs.dev). { format: 'simple-v1', - hash: undefined, tree: [ '0x6e12fdaf4d30f09cb298cbae1fefca45e012f15c4cbe029971591354cb02fb93', '0xec92620e0f47fd0c9d042839fefe36d967f496d3d35ac93cea868b19cd2e02ee', @@ -198,7 +197,6 @@ Generated by [AVA](https://avajs.dev). { format: 'simple-v1', - hash: undefined, tree: [ '0xe63fd9d6327adc6f47acceb58d2d9842ecd428ec4d6dda7904e6b828bae3c636', '0xf05c245c04fb0566cc96741aefb7bfb3b56029f920d5e310e45a96139c32fd67', diff --git a/src/simple.test.ts.snap b/src/simple.test.ts.snap index 5b0c74b021990f89c2cfa25a61dcb26ded254df9..9383c9fdf8c5d3ed58c477d9073dc9da6da22a95 100644 GIT binary patch delta 2608 zcmYk5c|2768^_N%GnP@R7a?3E`*KORu7-&=WUB~WGFi$Na_5>cwwbQFxhzhuj z3yuC`FWy`R0o5bF3NqM4)IVNYrO_|mVtn_>td+1cq4 zH{=f6X|BmB+Ml|7k97X%?xUVit2B5_QP$r6sQqLb+e6&~iJG3jn6T!y{ITUoGX3=^ zmsmPevC3=|lwp(lR)?MvV}?qiUQ(L)Gb}-qJ2R=9Lh6hp6$`m&t9d4#O8h`+;S$!f zoAAqBzJ23~>r<3w;cGgYw_!V>=FTK`RQ*@N}@WPdL9OLumiM}Z7sN(0h(%<+}|0-T4B7Q)N%vM zx;xJ4V+Z5ymRl-Y7iJz>VZ9zZRR?8^zOOlZtL3$igNkODykV;O_5AYM0|&$DXZB0c z;T(@}Ed`s?Hd*(ayj=UQKe@H%a9i+c+_!&S?rk}bn`}DIPOj| zWv&|k`QFOy_osVCQ+?V^Sry8Bf*IgLk;PrTHk&>Vd7--w~|7y@ypXLmMX)3 z5vSZcY#vW#s@%f-iEB%M3n3#2Z3g#39#HOGSPNl@`0j>*Pa^aXK7u$n9$^4t;l~Ig zXx?edlDr}coRidn+}7Z$k_M=4N@y;tCWtN%*Gd}5C}zHM`jM|HO)Z6IBy~Xwyi3Ye z^a+0{1JD7!A*Bae!c|gk$nIc`FEAvn3ueFu(t6-i_>_qHFi}JXTqvy%V&HCRH}Dm# zjM9NB8(=$>9)vZA!%!{|)&s6X>58^7QTpHvEG45W8Z?p7C;ju8WvY?S;`P~X%+(%Q zQpGleEO{%Um4W14z(F7p0@Qu|8msHS6|R{@6O_^t=(CK<_DMryleWoSqE_6ep|O_g zdW)!!$zweAjacGTR|zGln_uB*XnHrxvcEJmPRP& zfSVFDC6W^0Z3?`XARlT1s+y%229?0g&--Y+9W)05uo)VnP(*W|gVtY}(5wg`CK1Kv z-v?S_P;A6~(7$>60`@-0BuTLADx24b@uZdSk-}?lt`&_;OiX;6@TTG=*t6b%qBA)~ zhvu6FMEgR`5vY?{z`QS%cp0@Ypks$6LF2zMP2(0ZuWWsqB^0FwT|Np+4;`S&Oo@c!0Pa1Vz|GL<&ThXe?LoV z^v?6uHQ~u|(F#!{7rMI}-MNf2?@oVL#_=xZ+&ja6m{6u47>;S(S(d@X8e1;v;qIv$H9X0vU?6Ai<>K|(C=t{);R96K< zw69xGKJ&s=+(8JPH58lJ5lMQOMw|%=m>XKT=CHI8e0e&BTxw#tfCT07l^B?we90GP8 zxY-`r{y<}wD8#If+{3s1VgFY(RV3QEqNPE;AwhUg!QS@z#>hRmlr`K7e%^63F3r=b zFt6cd>BIis@vjxGp&?NcbGk3g>qy-(@?+V)skJC>Px->wRXitaGwLuJR~919A1e~) zC*Qe-PCdKb^(tGXGwcPa7pFvC$azzk*9iX>xUZjf1-J(X7SRZ*Qre2MRq?UJE zmev++YQ+uGRvW)H(2DaP|F8Is#ysMpIv79m07%++0JKETtAlGaGcbPh0kFX|1J{~@ zpl|7-`};-$a{5Roa$3F<8M^Nfseb3ezj(+ZQ^N*Wc`FtaQ*~gSzokga{t{K(tyq!* z`%tZXLF#(qf(osC%li>0%cjXY9n=Ov|`)!4w6{2jvsLl>n@>L2P9!XB;0(tTU4p)FY ihPaYG`0$z~r(>PTy%A*p3`@Ho+md@HujqsT!2bX^;Sq`e delta 2617 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 nJm77N`Eb`zAA-&k2_C+w2T { format: 'simple-v1', tree: this.tree, values: this.values.map(({ value, treeIndex }) => ({ value: toHex(value), treeIndex })), - hash: this.nodeHash ? 'custom' : undefined, + ...(this.nodeHash ? { hash: 'custom'} : {}), }; } } From 57aca96ed136e415322ae07004e6601152444d76 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 29 Feb 2024 14:51:42 +0100 Subject: [PATCH 07/12] fix lint --- src/simple.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/simple.ts b/src/simple.ts index 08c8acc..77c1697 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -48,7 +48,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'} : {}), + ...(this.nodeHash ? { hash: 'custom' } : {}), }; } } From cc0810b264daed659b33775f4a25a9b5fcbd8fad Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 29 Feb 2024 14:59:46 +0100 Subject: [PATCH 08/12] more precise test --- src/simple.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/simple.test.ts b/src/simple.test.ts index 584af03..9e418b2 100644 --- a/src/simple.test.ts +++ b/src/simple.test.ts @@ -117,7 +117,7 @@ testProp('dump and load', [tree], (t, [tree, options]) => { const recoveredTree = SimpleMerkleTree.load(dump, options.nodeHash); recoveredTree.validate(); - t.is(dump.hash === undefined, options.nodeHash === undefined); + 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()); From 7ee1181bafc44297d7ee6286a9838f5392508c2c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 4 Mar 2024 11:59:53 +0100 Subject: [PATCH 09/12] validate tree at construction --- src/simple.test.ts | 52 +++++++++++++++++++++++++------------------- src/simple.ts | 4 +++- src/standard.test.ts | 36 ++++++++++++++++-------------- src/standard.ts | 5 ++++- 4 files changed, 57 insertions(+), 40 deletions(-) diff --git a/src/simple.test.ts b/src/simple.test.ts index 9e418b2..41cc1cf 100644 --- a/src/simple.test.ts +++ b/src/simple.test.ts @@ -1,10 +1,11 @@ import { test, testProp, fc } from '@fast-check/ava'; import { HashZero as zero } from '@ethersproject/constants'; -import { SimpleMerkleTree } from './simple'; import { keccak256 } from '@ethersproject/keccak256'; +import { SimpleMerkleTree } from './simple'; import { BytesLike, HexString, concat, compare } from './bytes'; -const reverseHashPair = (a: BytesLike, b: BytesLike): HexString => keccak256(concat([a, b].sort(compare).reverse())); +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'; @@ -13,7 +14,7 @@ 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()), - nodeHash: fc.oneof(fc.constant(undefined), fc.constant(reverseHashPair)), + nodeHash: fc.oneof(fc.constant(undefined), fc.constant(reverseNodeHash)), }); const tree = fc @@ -94,8 +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: reverseHashPair }).render()); - t.snapshot(SimpleMerkleTree.of(leaves, { sortLeaves: false, nodeHash: reverseHashPair }).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 }, ); @@ -106,8 +107,8 @@ 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: reverseHashPair }).dump()); - t.snapshot(SimpleMerkleTree.of(leaves, { sortLeaves: false, nodeHash: reverseHashPair }).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 }, ); @@ -115,7 +116,7 @@ testProp( testProp('dump and load', [tree], (t, [tree, options]) => { const dump = tree.dump(); const recoveredTree = SimpleMerkleTree.load(dump, options.nodeHash); - recoveredTree.validate(); + recoveredTree.validate(); // already done in load t.is(dump.hash, options.nodeHash ? 'custom' : undefined); t.is(tree.root, recoveredTree.root); @@ -128,6 +129,12 @@ 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]), { @@ -148,22 +155,23 @@ 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({ + t.throws(() => 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')); + }), new InvariantError('Merkle tree is invalid')); }); diff --git a/src/simple.ts b/src/simple.ts index 77c1697..ff1d550 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -32,7 +32,9 @@ export class SimpleMerkleTree extends MerkleTreeImpl { nodeHash ? 'Data does not expect a custom node hashing function' : 'Data expects a custom node hashing function', ); - return new SimpleMerkleTree(data.tree, data.values, formatLeaf, nodeHash); + const tree = new SimpleMerkleTree(data.tree, data.values, formatLeaf, nodeHash); + tree.validate(); + return tree; } static verify(root: BytesLike, leaf: BytesLike, proof: BytesLike[], nodeHash?: NodeHash): boolean { diff --git a/src/standard.test.ts b/src/standard.test.ts index 2d622d4..d88c5cf 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,23 @@ 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 cd49460..c69488d 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -32,7 +32,10 @@ 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 { From 6ff294dcb94b0307a7c88fb94a2acecd12d945dc Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 4 Mar 2024 12:08:02 +0100 Subject: [PATCH 10/12] fix lint --- src/simple.test.ts | 39 ++++++++++++++++++++++----------------- src/standard.test.ts | 26 ++++++++++++++------------ 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/simple.test.ts b/src/simple.test.ts index 41cc1cf..c9811ac 100644 --- a/src/simple.test.ts +++ b/src/simple.test.ts @@ -130,7 +130,7 @@ testProp('reject out of bounds value index', [tree], (t, [tree]) => { }); // 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) => { +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')); }); @@ -156,22 +156,27 @@ test('reject unrecognized tree dump', t => { test('reject malformed tree dump', t => { t.throws( - () => SimpleMerkleTree.load({ - format: 'simple-v1', - tree: [zero], - values: [ - { - value: '0x0000000000000000000000000000000000000000000000000000000000000001', - treeIndex: 0, - }, - ], - }), - new InvariantError('Merkle tree does not contain the expected value') + () => + SimpleMerkleTree.load({ + format: 'simple-v1', + tree: [zero], + values: [ + { + value: '0x0000000000000000000000000000000000000000000000000000000000000001', + treeIndex: 0, + }, + ], + }), + new InvariantError('Merkle tree does not contain the expected value'), ); - t.throws(() => SimpleMerkleTree.load({ - format: 'simple-v1', - tree: [zero, zero, zero], - values: [{ value: zero, treeIndex: 2 }], - }), new InvariantError('Merkle tree is invalid')); + 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/standard.test.ts b/src/standard.test.ts index d88c5cf..164f3e2 100644 --- a/src/standard.test.ts +++ b/src/standard.test.ts @@ -111,22 +111,24 @@ test('reject unrecognized tree dump', t => { test('reject malformed tree dump', t => { t.throws( - () => StandardMerkleTree.load({ - format: 'standard-v1', - tree: [zero], - values: [{ value: ['0'], treeIndex: 0 }], - leafEncoding: ['uint256'], - }), + () => + 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'], - }), + () => + StandardMerkleTree.load({ + format: 'standard-v1', + tree: [zero, zero, keccak256(keccak256(zero))], + values: [{ value: ['0'], treeIndex: 2 }], + leafEncoding: ['uint256'], + }), new InvariantError('Merkle tree is invalid'), ); }); From a9ae8306a90d7c8334376e8b24c2d374e76cf223 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 4 Mar 2024 09:32:16 -0600 Subject: [PATCH 11/12] Move validation to MerkleTree constructor --- src/merkletree.ts | 1 + src/simple.ts | 1 - src/standard.ts | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index eda1b81..69a92e3 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -49,6 +49,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { 'Leaf values cannot be numbers', ); this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree[treeIndex], valueIndex])); + this.validate(); } protected static prepare( diff --git a/src/simple.ts b/src/simple.ts index ff1d550..9029d80 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -33,7 +33,6 @@ export class SimpleMerkleTree extends MerkleTreeImpl { ); const tree = new SimpleMerkleTree(data.tree, data.values, formatLeaf, nodeHash); - tree.validate(); return tree; } diff --git a/src/standard.ts b/src/standard.ts index c69488d..434b5a2 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -34,7 +34,6 @@ export class StandardMerkleTree extends MerkleTreeImpl { validateArgument(data.leafEncoding !== undefined, 'Expected leaf encoding'); const tree = new StandardMerkleTree(data.tree, data.values, data.leafEncoding); - tree.validate(); return tree; } From 8722ae2fa4f25264d2bb7dece1cc135307ad51a4 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 4 Mar 2024 10:12:44 -0600 Subject: [PATCH 12/12] Revert "Move validation to MerkleTree constructor" This reverts commit a9ae8306a90d7c8334376e8b24c2d374e76cf223. --- src/merkletree.ts | 1 - src/simple.ts | 1 + src/standard.ts | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index 69a92e3..eda1b81 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -49,7 +49,6 @@ export abstract class MerkleTreeImpl implements MerkleTree { 'Leaf values cannot be numbers', ); this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree[treeIndex], valueIndex])); - this.validate(); } protected static prepare( diff --git a/src/simple.ts b/src/simple.ts index 9029d80..ff1d550 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -33,6 +33,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { ); const tree = new SimpleMerkleTree(data.tree, data.values, formatLeaf, nodeHash); + tree.validate(); return tree; } diff --git a/src/standard.ts b/src/standard.ts index 434b5a2..c69488d 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -34,6 +34,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { validateArgument(data.leafEncoding !== undefined, 'Expected leaf encoding'); const tree = new StandardMerkleTree(data.tree, data.values, data.leafEncoding); + tree.validate(); return tree; }