Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SimpleMerkleTree #36

Merged
merged 38 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
302fa2d
Simple MerkleTree alternative
ernestognw Feb 23, 2024
f24e205
wip
Amxx Feb 23, 2024
30d150c
Use generic MerkleTreeImpl
Amxx Feb 23, 2024
21399c1
MerkleTree interface
Amxx Feb 23, 2024
eabc28e
explicit type
Amxx Feb 23, 2024
6eaf646
use bind instead of curation
Amxx Feb 23, 2024
d9678a6
fix default
Amxx Feb 23, 2024
034ce88
newline
Amxx Feb 23, 2024
2da68b1
add prettier (config ported from @openzeppelin/contracts)
Amxx Feb 23, 2024
b2d34fc
more generic multiproof: input support all BytesLike, output is expli…
Amxx Feb 24, 2024
bcfc765
Merge branch 'master' into support-raw-leaves-inheritance
Amxx Feb 24, 2024
62ab129
remove empty file
Amxx Feb 24, 2024
ec339e1
Apply PR suggestions
ernestognw Feb 25, 2024
826ffab
Unify all error interfaces
ernestognw Feb 25, 2024
ada28f1
Unify errors
ernestognw Feb 25, 2024
b73e3c4
fix naming
Amxx Feb 26, 2024
72057f3
Update src/merkletree.ts
Amxx Feb 26, 2024
de39370
rename leafHasher to leafHash, and make mark it as public
Amxx Feb 26, 2024
1be7ece
rewrite checks
Amxx Feb 26, 2024
5e69b25
remove duplicate test
Amxx Feb 26, 2024
de68246
simplify
Amxx Feb 26, 2024
e13943e
naming consistency
Amxx Feb 26, 2024
e5e035e
refactor
Amxx Feb 26, 2024
64b7dfa
split value validation and leafHashing
Amxx Feb 26, 2024
5d5deb8
deduplicate type definition
Amxx Feb 26, 2024
2ce3d55
specify type
Amxx Feb 26, 2024
1ad4309
fix lint
Amxx Feb 26, 2024
b13748a
refactor
Amxx Feb 26, 2024
fd1cf28
Nits
ernestognw Feb 26, 2024
a87db4c
Merge branch 'master' into support-raw-leaves-inheritance
Amxx Feb 26, 2024
d2b6e97
Add dumps
Amxx Feb 26, 2024
84e96d5
fix name
Amxx Feb 26, 2024
640e378
remove duplicate
Amxx Feb 26, 2024
6578293
up
Amxx Feb 26, 2024
1b587e8
Exclude number type support from MerkleTree
ernestognw Feb 27, 2024
a0e8b20
Address comments from @frangio
Amxx Feb 27, 2024
f48455e
fix lint
Amxx Feb 27, 2024
5d1ceb7
types → interfaces
Amxx Feb 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 1.0.6

- Added an option to disable leaf sorting.
- Added `SimpleMerkleTree` class that supports `bytes32` leaves with no extra hashing.

## 1.0.5

Expand Down
Empty file added index.test.ts
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
Empty file.
1,731 changes: 172 additions & 1,559 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"license": "MIT",
"dependencies": {
"@ethersproject/abi": "^5.7.0",
"ethereum-cryptography": "^1.1.2"
"@ethersproject/bytes": "^5.7.0",
"@ethersproject/constants": "^5.7.0",
"@ethersproject/keccak256": "^5.7.0"
},
"devDependencies": {
"@types/mocha": "^10.0.0",
Expand Down
29 changes: 15 additions & 14 deletions src/bytes.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { bytesToHex } from 'ethereum-cryptography/utils';
import type { BytesLike, Hexable } from "@ethersproject/bytes";

export type Bytes = Uint8Array;
type Hex = BytesLike | Hexable | number | bigint;
type HexString = string;
ernestognw marked this conversation as resolved.
Show resolved Hide resolved

export function compareBytes(a: Bytes, b: Bytes): number {
const n = Math.min(a.length, b.length);
import {
isBytesLike,
arrayify as toBytes,
hexlify as toHex,
concat,
isBytes,
} from "@ethersproject/bytes";
ernestognw marked this conversation as resolved.
Show resolved Hide resolved

for (let i = 0; i < n; i++) {
if (a[i] !== b[i]) {
return a[i]! - b[i]!;
}
}

return a.length - b.length;
function compare(a: Hex, b: Hex): number {
const diff = BigInt(toHex(a)) - BigInt(toHex(b));
return diff > 0 ? 1 : diff < 0 ? -1 : 0;
}

export function hex(b: Bytes): string {
return '0x' + bytesToHex(b);
}
export type { Hex, HexString, BytesLike };
export { isBytesLike, toBytes, toHex, concat, compare, isBytes };
23 changes: 7 additions & 16 deletions src/core.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import fc from 'fast-check';
import assert from 'assert/strict';
import { equalsBytes } from 'ethereum-cryptography/utils';
import { HashZero as zero } from '@ethersproject/constants';
import { keccak256 } from '@ethersproject/keccak256';
import { makeMerkleTree, getProof, processProof, getMultiProof, processMultiProof, isValidMerkleTree, renderMerkleTree } from './core';
import { compareBytes, hex } from './bytes';
import { keccak256 } from 'ethereum-cryptography/keccak';
import { toHex, compare } from './bytes';

const zero = new Uint8Array(32);

const leaf = fc.uint8Array({ minLength: 32, maxLength: 32 }).map(x => PrettyBytes.from(x));
const leaf = fc.uint8Array({ minLength: 32, maxLength: 32 }).map(toHex);
const leaves = fc.array(leaf, { minLength: 1 });
const leavesAndIndex = leaves.chain(xs => fc.tuple(fc.constant(xs), fc.nat({ max: xs.length - 1 })));
const leavesAndIndices = leaves.chain(xs => fc.tuple(fc.constant(xs), fc.uniqueArray(fc.nat({ max: xs.length - 1 }))));
Expand All @@ -25,7 +23,7 @@ describe('core properties', () => {
const proof = getProof(tree, treeIndex);
const leaf = leaves[leafIndex]!;
const impliedRoot = processProof(leaf, proof);
return equalsBytes(root, impliedRoot);
return root === impliedRoot;
}),
);
});
Expand All @@ -41,7 +39,7 @@ describe('core properties', () => {
if (leafIndices.length !== proof.leaves.length) return false;
if (leafIndices.some(i => !proof.leaves.includes(leaves[i]!))) return false;
const impliedRoot = processMultiProof(proof);
return equalsBytes(root, impliedRoot);
return root === impliedRoot;
}),
);
});
Expand Down Expand Up @@ -79,7 +77,7 @@ describe('core error conditions', () => {
const tree = makeMerkleTree([leaf, zero]);

const badMultiProof = {
leaves: [128, 129].map(n => keccak256(Uint8Array.of(n))).sort(compareBytes),
leaves: [128, 129].map(n => keccak256(Uint8Array.of(n))).sort(compare),
proof: [leaf, leaf],
proofFlags: [true, true, false],
};
Expand All @@ -89,11 +87,4 @@ describe('core error conditions', () => {
/^Error: Broken invariant$/,
);
});

});

class PrettyBytes extends Uint8Array {
[fc.toStringMethod]() {
return hex(this);
}
}
51 changes: 24 additions & 27 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { keccak256 } from 'ethereum-cryptography/keccak';
import { concatBytes, bytesToHex, equalsBytes } from 'ethereum-cryptography/utils';
import { Bytes, compareBytes } from './bytes';
import { keccak256 } from '@ethersproject/keccak256';
import { BytesLike, HexString, toHex, toBytes, concat, compare } from './bytes';
import { throwError } from './utils/throw-error';

const hashPair = (a: Bytes, b: Bytes) => keccak256(concatBytes(...[a, b].sort(compareBytes)));
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;
Expand All @@ -13,24 +12,22 @@ const siblingIndex = (i: number) => i > 0 ? i - (-1) ** (i % 2) : throwEr
const isTreeNode = (tree: unknown[], i: number) => i >= 0 && i < tree.length;
const isInternalNode = (tree: unknown[], i: number) => isTreeNode(tree, leftChildIndex(i));
const isLeafNode = (tree: unknown[], i: number) => isTreeNode(tree, i) && !isInternalNode(tree, i);
const isValidMerkleNode = (node: Bytes) => node instanceof Uint8Array && node.length === 32;
const isValidMerkleNode = (node: BytesLike) => toBytes(node).length === 32;

const checkTreeNode = (tree: unknown[], i: number) => void (isTreeNode(tree, i) || throwError('Index is not in tree'));
const checkInternalNode = (tree: unknown[], i: number) => void (isInternalNode(tree, i) || throwError('Index is not an internal tree node'));
const checkLeafNode = (tree: unknown[], i: number) => void (isLeafNode(tree, i) || throwError('Index is not a leaf'));
const checkValidMerkleNode = (node: Bytes) => void (isValidMerkleNode(node) || throwError('Merkle tree nodes must be Uint8Array of length 32'));
const checkValidMerkleNode = (node: BytesLike) => void (isValidMerkleNode(node) || throwError('Merkle tree nodes must be Uint8Array of length 32'));

export function makeMerkleTree(leaves: Bytes[]): Bytes[] {
export function makeMerkleTree(leaves: BytesLike[]): HexString[] {
leaves.forEach(checkValidMerkleNode);

if (leaves.length === 0) {
throw new Error('Expected non-zero number of leaves');
}

const tree = new Array<Bytes>(2 * leaves.length - 1);
const tree = new Array<HexString>(2 * leaves.length - 1);

for (const [i, leaf] of leaves.entries()) {
tree[tree.length - 1 - i] = leaf;
tree[tree.length - 1 - i] = toHex(leaf);
}
for (let i = tree.length - 1 - leaves.length; i >= 0; i--) {
tree[i] = hashPair(
Expand All @@ -42,31 +39,31 @@ export function makeMerkleTree(leaves: Bytes[]): Bytes[] {
return tree;
}

export function getProof(tree: Bytes[], index: number): Bytes[] {
export function getProof(tree: BytesLike[], index: number): HexString[] {
checkLeafNode(tree, index);

const proof = [];
while (index > 0) {
proof.push(tree[siblingIndex(index)]!);
index = parentIndex(index);
}
return proof;
return proof.map(node => toHex(node));
}
Amxx marked this conversation as resolved.
Show resolved Hide resolved

export function processProof(leaf: Bytes, proof: Bytes[]): Bytes {
export function processProof(leaf: BytesLike, proof: BytesLike[]): HexString {
checkValidMerkleNode(leaf);
proof.forEach(checkValidMerkleNode);

return proof.reduce(hashPair, leaf);
return toHex(proof.reduce(hashPair, leaf));
}

export interface MultiProof<T, L = T> {
leaves: L[];
proof: T[];
export interface MultiProof {
leaves: BytesLike[];
proof: HexString[];
proofFlags: boolean[];
}

export function getMultiProof(tree: Bytes[], indices: number[]): MultiProof<Bytes> {
export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof {
indices.forEach(i => checkLeafNode(tree, i));
indices.sort((a, b) => b - a);

Expand Down Expand Up @@ -98,13 +95,13 @@ export function getMultiProof(tree: Bytes[], indices: number[]): MultiProof<Byte
}

return {
leaves: indices.map(i => tree[i]!),
proof,
leaves: indices.map(i => tree[i]!).map(node => toHex(node)),
proof: proof.map(node => toHex(node)),
Amxx marked this conversation as resolved.
Show resolved Hide resolved
proofFlags,
};
}

export function processMultiProof(multiproof: MultiProof<Bytes>): Bytes {
export function processMultiProof(multiproof: MultiProof): HexString {
multiproof.leaves.forEach(checkValidMerkleNode);
multiproof.proof.forEach(checkValidMerkleNode);

Expand Down Expand Up @@ -132,10 +129,10 @@ export function processMultiProof(multiproof: MultiProof<Bytes>): Bytes {
throw new Error('Broken invariant');
}

return stack.pop() ?? proof.shift()!;
return toHex(stack.pop() ?? proof.shift()!);
}

export function isValidMerkleTree(tree: Bytes[]): boolean {
export function isValidMerkleTree(tree: BytesLike[]): boolean {
for (const [i, node] of tree.entries()) {
if (!isValidMerkleNode(node)) {
return false;
Expand All @@ -148,15 +145,15 @@ export function isValidMerkleTree(tree: Bytes[]): boolean {
if (l < tree.length) {
return false;
}
} else if (!equalsBytes(node, hashPair(tree[l]!, tree[r]!))) {
} else if (node !== hashPair(tree[l]!, tree[r]!)) {
return false;
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
}
}

return tree.length > 0;
}

export function renderMerkleTree(tree: Bytes[]): string {
export function renderMerkleTree(tree: BytesLike[]): HexString {
if (tree.length === 0) {
throw new Error('Expected non-zero number of nodes');
}
Expand All @@ -172,7 +169,7 @@ export function renderMerkleTree(tree: Bytes[]): string {
path.slice(0, -1).map(p => [' ', '│ '][p]).join('') +
path.slice(-1).map(p => ['└─ ', '├─ '][p]).join('') +
i + ') ' +
bytesToHex(tree[i]!)
toHex(tree[i]!)
);

if (rightChildIndex(i) < tree.length) {
Expand Down
9 changes: 9 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import assert from "assert/strict";
import { SimpleMerkleTree, StandardMerkleTree } from ".";

describe("index properties", () => {
it("classes are exported", () => {
assert.notEqual(SimpleMerkleTree, undefined);
assert.notEqual(StandardMerkleTree, undefined);
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { SimpleMerkleTree } from './simple';
export { StandardMerkleTree } from './standard';
14 changes: 10 additions & 4 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
// MerkleTree building options
export type MerkleTreeOptions = Partial<{
// SimpleMerkleTree building options
export type SimpleMerkleTreeOptions = Partial<{
/** Enable or disable sorted leaves. Sorting is strongly recommended for multiproofs. */
sortLeaves: boolean;
}>;

// Recommended (default) options.
// StandardMerkleTree building options
export type StandardMerkleTreeOptions = SimpleMerkleTreeOptions & {
/** ABI Encoding for leaf values. */
leafEncoding: string[];
};

// Recommended (default) SimpleMerkleTree options.
// - leaves are sorted by default to facilitate onchain verification of multiproofs.
export const defaultOptions: Required<MerkleTreeOptions> = {
export const defaultOptions: Required<SimpleMerkleTreeOptions> = {
sortLeaves: true,
};
Loading
Loading