diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5832939..14fb690 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,4 +13,5 @@ jobs: - name: Set up environment uses: ./.github/actions/setup - run: npm run coverage -- --forbid-only + - run: npm run lint - uses: codecov/codecov-action@v3 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5a43c2d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 120, + "singleQuote": true, + "trailingComma": "all", + "arrowParens": "avoid" +} diff --git a/package-lock.json b/package-lock.json index da65104..d95fc03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "c8": "^7.12.0", "fast-check": "^3.3.0", "mocha": "^10.1.0", + "prettier": "^3.2.5", "ts-node": "^10.9.1", "typescript": "^4.8.4" } @@ -1533,6 +1534,21 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pure-rand": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-5.0.3.tgz", @@ -3002,6 +3018,12 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true + }, "pure-rand": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-5.0.3.tgz", diff --git a/package.json b/package.json index 493ff4d..f6a159c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "prepublishOnly": "npm run clean", "prepare": "tsc", "clean": "rm -rf dist", + "lint": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --check", + "lint:fix": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --write", "test": "mocha", "coverage": "c8 npm run test --" }, @@ -29,6 +31,7 @@ "c8": "^7.12.0", "fast-check": "^3.3.0", "mocha": "^10.1.0", + "prettier": "^3.2.5", "ts-node": "^10.9.1", "typescript": "^4.8.4" } diff --git a/src/core.test.ts b/src/core.test.ts index ac83990..66a5ef3 100644 --- a/src/core.test.ts +++ b/src/core.test.ts @@ -1,7 +1,15 @@ import fc from 'fast-check'; import assert from 'assert/strict'; import { equalsBytes } from 'ethereum-cryptography/utils'; -import { makeMerkleTree, getProof, processProof, getMultiProof, processMultiProof, isValidMerkleTree, renderMerkleTree } from './core'; +import { + makeMerkleTree, + getProof, + processProof, + getMultiProof, + processMultiProof, + isValidMerkleTree, + renderMerkleTree, +} from './core'; import { compareBytes, hex } from './bytes'; import { keccak256 } from 'ethereum-cryptography/keccak'; @@ -49,18 +57,12 @@ describe('core properties', () => { describe('core error conditions', () => { it('zero leaves', () => { - assert.throws( - () => makeMerkleTree([]), - /^Error: Expected non-zero number of leaves$/, - ); + assert.throws(() => makeMerkleTree([]), /^Error: Expected non-zero number of leaves$/); }); it('multiproof duplicate index', () => { const tree = makeMerkleTree(new Array(2).fill(zero)); - assert.throws( - () => getMultiProof(tree, [1, 1]), - /^Error: Cannot prove duplicated index$/, - ); + assert.throws(() => getMultiProof(tree, [1, 1]), /^Error: Cannot prove duplicated index$/); }); it('tree validity', () => { @@ -68,10 +70,7 @@ describe('core error conditions', () => { assert(!isValidMerkleTree([zero, zero]), 'even number of nodes'); assert(!isValidMerkleTree([zero, zero, zero]), 'inner node not hash of children'); - assert.throws( - () => renderMerkleTree([]), - /^Error: Expected non-zero number of nodes$/, - ); + assert.throws(() => renderMerkleTree([]), /^Error: Expected non-zero number of nodes$/); }); it('multiproof invariants', () => { @@ -84,12 +83,8 @@ describe('core error conditions', () => { proofFlags: [true, true, false], }; - assert.throws( - () => processMultiProof(badMultiProof), - /^Error: Broken invariant$/, - ); + assert.throws(() => processMultiProof(badMultiProof), /^Error: Broken invariant$/); }); - }); class PrettyBytes extends Uint8Array { diff --git a/src/core.ts b/src/core.ts index a44c405..b8163ea 100644 --- a/src/core.ts +++ b/src/core.ts @@ -5,20 +5,22 @@ import { throwError } from './utils/throw-error'; const hashPair = (a: Bytes, b: Bytes) => keccak256(concatBytes(...[a, b].sort(compareBytes))); -const leftChildIndex = (i: number) => 2 * i + 1; +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'); -const siblingIndex = (i: number) => i > 0 ? i - (-1) ** (i % 2) : throwError('Root has no siblings'); +const parentIndex = (i: number) => (i > 0 ? Math.floor((i - 1) / 2) : throwError('Root has no parent')); +const siblingIndex = (i: number) => (i > 0 ? i - (-1) ** (i % 2) : throwError('Root has no siblings')); -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 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 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 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')); export function makeMerkleTree(leaves: Bytes[]): Bytes[] { leaves.forEach(checkValidMerkleNode); @@ -33,10 +35,7 @@ export function makeMerkleTree(leaves: Bytes[]): Bytes[] { tree[tree.length - 1 - i] = leaf; } for (let i = tree.length - 1 - leaves.length; i >= 0; i--) { - tree[i] = hashPair( - tree[leftChildIndex(i)]!, - tree[rightChildIndex(i)]!, - ); + tree[i] = hashPair(tree[leftChildIndex(i)]!, tree[rightChildIndex(i)]!); } return tree; @@ -129,7 +128,7 @@ export function processMultiProof(multiproof: MultiProof): Bytes { } if (stack.length + proof.length !== 1) { - throw new Error('Broken invariant'); + throw new Error('Broken invariant'); } return stack.pop() ?? proof.shift()!; @@ -169,10 +168,17 @@ export function renderMerkleTree(tree: Bytes[]): string { const [i, path] = stack.pop()!; lines.push( - path.slice(0, -1).map(p => [' ', '│ '][p]).join('') + - path.slice(-1).map(p => ['└─ ', '├─ '][p]).join('') + - i + ') ' + - bytesToHex(tree[i]!) + path + .slice(0, -1) + .map(p => [' ', '│ '][p]) + .join('') + + path + .slice(-1) + .map(p => ['└─ ', '├─ '][p]) + .join('') + + i + + ') ' + + bytesToHex(tree[i]!), ); if (rightChildIndex(i) < tree.length) { diff --git a/src/standard.test.ts b/src/standard.test.ts index cd1217d..f8f5473 100644 --- a/src/standard.test.ts +++ b/src/standard.test.ts @@ -11,14 +11,10 @@ const makeTree = (s: string, opts: MerkleTreeOptions = {}) => { const l = s.split('').map(c => [c]); const t = StandardMerkleTree.of(l, ['string'], opts); return { l, t }; -} +}; describe('standard merkle tree', () => { - for (const opts of [ - {}, - { sortLeaves: true }, - { sortLeaves: false }, - ]) { + for (const opts of [{}, { sortLeaves: true }, { sortLeaves: false }]) { describe(`with options '${JSON.stringify(opts)}'`, () => { const { l: leaves, t: tree } = makeTree('abcdef', opts); const { l: otherLeaves, t: otherTree } = makeTree('abc', opts); @@ -72,31 +68,31 @@ describe('standard merkle tree', () => { tree.render(), opts.sortLeaves == false ? [ - "0) 23be0977360f08bb0bd7f709a7d543d2cd779c79c66d74e0441919871647de2b", - "├─ 1) 8f7234e8cfe39c08ca84a3a3e3274f574af26fd15165fe29e09cbab742daccd9", - "│ ├─ 3) 03707d7802a71ca56a8ad8028da98c4f1dbec55b31b4a25d536b5309cc20eda9", - "│ │ ├─ 7) eba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b", - "│ │ └─ 8) 9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b", - "│ └─ 4) fa914d99a18dc32d9725b3ef1c50426deb40ec8d0885dac8edcc5bfd6d030016", - "│ ├─ 9) 19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681", - "│ └─ 10) 9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c", - "└─ 2) 7b0c6cd04b82bfc0e250030a5d2690c52585e0cc6a4f3bc7909d7723b0236ece", - " ├─ 5) c62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848", - " └─ 6) 9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e", - ].join("\n") + '0) 23be0977360f08bb0bd7f709a7d543d2cd779c79c66d74e0441919871647de2b', + '├─ 1) 8f7234e8cfe39c08ca84a3a3e3274f574af26fd15165fe29e09cbab742daccd9', + '│ ├─ 3) 03707d7802a71ca56a8ad8028da98c4f1dbec55b31b4a25d536b5309cc20eda9', + '│ │ ├─ 7) eba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b', + '│ │ └─ 8) 9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b', + '│ └─ 4) fa914d99a18dc32d9725b3ef1c50426deb40ec8d0885dac8edcc5bfd6d030016', + '│ ├─ 9) 19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681', + '│ └─ 10) 9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c', + '└─ 2) 7b0c6cd04b82bfc0e250030a5d2690c52585e0cc6a4f3bc7909d7723b0236ece', + ' ├─ 5) c62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848', + ' └─ 6) 9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e', + ].join('\n') : [ - "0) 6deb52b5da8fd108f79fab00341f38d2587896634c646ee52e49f845680a70c8", - "├─ 1) 52426e0f1f65ff7e209a13b8c29cffe82e3acaf3dad0a9b9088f3b9a61a929c3", - "│ ├─ 3) 8076923e76cf01a7c048400a2304c9a9c23bbbdac3a98ea3946340fdafbba34f", - "│ │ ├─ 7) 9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b", - "│ │ └─ 8) 9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c", - "│ └─ 4) 965b92c6cf08303cc4feb7f3e0819c436c2cec17c6f0688a6af139c9a368707c", - "│ ├─ 9) 9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e", - "│ └─ 10) 19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681", - "└─ 2) fd3cf45654e88d1cc5d663578c82c76f4b5e3826bacaa1216441443504538f51", - " ├─ 5) eba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b", - " └─ 6) c62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848", - ].join("\n"), + '0) 6deb52b5da8fd108f79fab00341f38d2587896634c646ee52e49f845680a70c8', + '├─ 1) 52426e0f1f65ff7e209a13b8c29cffe82e3acaf3dad0a9b9088f3b9a61a929c3', + '│ ├─ 3) 8076923e76cf01a7c048400a2304c9a9c23bbbdac3a98ea3946340fdafbba34f', + '│ │ ├─ 7) 9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b', + '│ │ └─ 8) 9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c', + '│ └─ 4) 965b92c6cf08303cc4feb7f3e0819c436c2cec17c6f0688a6af139c9a368707c', + '│ ├─ 9) 9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e', + '│ └─ 10) 19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681', + '└─ 2) fd3cf45654e88d1cc5d663578c82c76f4b5e3826bacaa1216441443504538f51', + ' ├─ 5) eba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b', + ' └─ 6) c62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848', + ].join('\n'), ); }); @@ -108,10 +104,7 @@ describe('standard merkle tree', () => { }); it('reject out of bounds value index', () => { - assert.throws( - () => tree.getProof(leaves.length), - /^Error: Index out of bounds$/, - ); + assert.throws(() => tree.getProof(leaves.length), /^Error: Index out of bounds$/); }); it('reject unrecognized tree dump', () => { @@ -128,10 +121,7 @@ describe('standard merkle tree', () => { values: [{ value: ['0'], treeIndex: 0 }], leafEncoding: ['uint256'], }); - assert.throws( - () => loadedTree1.getProof(0), - /^Error: Merkle tree does not contain the expected value$/, - ); + assert.throws(() => loadedTree1.getProof(0), /^Error: Merkle tree does not contain the expected value$/); const loadedTree2 = StandardMerkleTree.load({ format: 'standard-v1', @@ -139,10 +129,7 @@ describe('standard merkle tree', () => { values: [{ value: ['0'], treeIndex: 2 }], leafEncoding: ['uint256'], }); - assert.throws( - () => loadedTree2.getProof(0), - /^Error: Unable to prove value$/, - ); + assert.throws(() => loadedTree2.getProof(0), /^Error: Unable to prove value$/); }); }); } diff --git a/src/standard.ts b/src/standard.ts index 20b40e1..9d069bf 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -1,6 +1,15 @@ import { equalsBytes, hexToBytes } from 'ethereum-cryptography/utils'; import { Bytes, compareBytes, hex } from './bytes'; -import { getProof, isValidMerkleTree, makeMerkleTree, processProof, renderMerkleTree, MultiProof, getMultiProof, processMultiProof } from './core'; +import { + getProof, + isValidMerkleTree, + makeMerkleTree, + processProof, + renderMerkleTree, + MultiProof, + getMultiProof, + processMultiProof, +} from './core'; import { MerkleTreeOptions, defaultOptions } from './options'; import { checkBounds } from './utils/check-bounds'; import { throwError } from './utils/throw-error'; @@ -21,20 +30,22 @@ export class StandardMerkleTree { private constructor( private readonly tree: Bytes[], - private readonly values: { value: T, treeIndex: number }[], + private readonly values: { value: T; treeIndex: number }[], private readonly leafEncoding: string[], ) { - this.hashLookup = - Object.fromEntries(values.map(({ value }, valueIndex) => [ - hex(standardLeafHash(value, leafEncoding)), - valueIndex, - ])); + this.hashLookup = Object.fromEntries( + values.map(({ value }, valueIndex) => [hex(standardLeafHash(value, leafEncoding)), valueIndex]), + ); } static of(values: T[], leafEncoding: string[], options: MerkleTreeOptions = {}) { const sortLeaves = options.sortLeaves ?? defaultOptions.sortLeaves; - const hashedValues = values.map((value, valueIndex) => ({ value, valueIndex, hash: standardLeafHash(value, leafEncoding) })); + const hashedValues = values.map((value, valueIndex) => ({ + value, + valueIndex, + hash: standardLeafHash(value, leafEncoding), + })); if (sortLeaves) { hashedValues.sort((a, b) => compareBytes(a.hash, b.hash)); @@ -54,11 +65,7 @@ export class StandardMerkleTree { if (data.format !== 'standard-v1') { throw new Error(`Unknown format '${data.format}'`); } - return new StandardMerkleTree( - data.tree.map(hexToBytes), - data.values, - data.leafEncoding, - ); + return new StandardMerkleTree(data.tree.map(hexToBytes), data.values, data.leafEncoding); } static verify(root: string, leafEncoding: string[], leaf: T, proof: string[]): boolean { @@ -66,7 +73,11 @@ export class StandardMerkleTree { return equalsBytes(impliedRoot, hexToBytes(root)); } - static verifyMultiProof(root: string, leafEncoding: string[], multiproof: MultiProof): boolean { + static verifyMultiProof( + root: string, + leafEncoding: string[], + multiproof: MultiProof, + ): boolean { const leafHashes = multiproof.leaves.map(leaf => standardLeafHash(leaf, leafEncoding)); const proofBytes = multiproof.proof.map(hexToBytes); @@ -81,9 +92,9 @@ export class StandardMerkleTree { dump(): StandardMerkleTreeData { return { - format: 'standard-v1', - tree: this.tree.map(hex), - values: this.values, + format: 'standard-v1', + tree: this.tree.map(hex), + values: this.values, leafEncoding: this.leafEncoding, }; } @@ -139,7 +150,7 @@ export class StandardMerkleTree { getMultiProof(leaves: (number | T)[]): MultiProof { // input validity - const valueIndices = leaves.map(leaf => typeof leaf === 'number' ? leaf : this.leafLookup(leaf)); + const valueIndices = leaves.map(leaf => (typeof leaf === 'number' ? leaf : this.leafLookup(leaf))); for (const valueIndex of valueIndices) this.validateValue(valueIndex); // rebuild tree indices and generate proof @@ -153,10 +164,10 @@ export class StandardMerkleTree { // return multiproof in hex format return { - leaves: proof.leaves.map(hash => this.values[this.hashLookup[hex(hash)]!]!.value), - proof: proof.proof.map(hex), + leaves: proof.leaves.map(hash => this.values[this.hashLookup[hex(hash)]!]!.value), + proof: proof.proof.map(hex), proofFlags: proof.proofFlags, - } + }; } verify(leaf: number | T, proof: string[]): boolean {