diff --git a/contracts/src/bridge/merkle/MerkleProof.sol b/contracts/src/bridge/merkle/MerkleProof.sol new file mode 100644 index 000000000..cdfa3835e --- /dev/null +++ b/contracts/src/bridge/merkle/MerkleProof.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +/** + * @title MerkleProof + * @author Shotaro N. - + * @dev A set of funcitons to verify merkle proofs. + */ +contract MerkleProof { + /** @dev Validates membership of leaf in merkle tree with merkle proof. + * @param proof The merkle proof. + * @param leaf The leaf to validate membership in merkle tree. + * @param merkleRoot The root of the merkle tree. + */ + function validateProof( + bytes32[] memory proof, + bytes32 leaf, + bytes32 merkleRoot + ) internal pure returns (bool) { + return (merkleRoot == calculateRoot(proof, leaf)); + } + + /** @dev Validates membership of leaf in merkle tree with merkle proof. + * @param proof The merkle proof. + * @param data The data to validate membership in merkle tree. + * @param merkleRoot The root of the merkle tree. + */ + function validateProof( + bytes32[] memory proof, + bytes memory data, + bytes32 merkleRoot + ) public pure returns (bool) { + return validateProof(proof, keccak256(data), merkleRoot); + } + + /** @dev Calculates merkle root from proof. + * @param proof The merkle proof. + * @param leaf The leaf to validate membership in merkle tree.. + */ + function calculateRoot(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { + uint256 proofLength = proof.length; + require(proofLength <= 64, "Invalid Proof"); + bytes32 h = leaf; + for (uint256 i = 0; i < proofLength; i++) { + bytes32 proofElement = proof[i]; + // effecient hash + if (proofElement > h) + assembly { + mstore(0x00, h) + mstore(0x20, proofElement) + h := keccak256(0x00, 0x40) + } + else + assembly { + mstore(0x00, proofElement) + mstore(0x20, h) + h := keccak256(0x00, 0x40) + } + } + return h; + } +} diff --git a/contracts/src/bridge/merkle/MerkleTreeHistory.sol b/contracts/src/bridge/merkle/MerkleTreeHistory.sol new file mode 100644 index 000000000..6d7059e24 --- /dev/null +++ b/contracts/src/bridge/merkle/MerkleTreeHistory.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +/** + * @title MerkleTreeHistory + * @author Shotaro N. - + * @dev An efficient append only merkle tree with history. + */ +contract MerkleTreeHistory { + // ***************************** // + // * Storage * // + // ***************************** // + + // merkle tree representation + // supports 2^64 messages. + bytes32[64] public branch; + uint256 public count; + + // block number => merkle root history + mapping(uint256 => bytes32) private history; + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /** @dev Append data into merkle tree. + * `O(log(n))` where + * `n` is the number of leaves. + * Note: Although each insertion is O(log(n)), + * Complexity of n insertions is O(n). + * @param data The data to insert in the merkle tree. + */ + function append(bytes memory data) public { + bytes32 leaf = keccak256(data); + count += 1; + uint256 size = count; + uint256 hashBitField = (size ^ (size - 1)) & size; + + for (uint256 height = 0; height < 64; height++) { + if ((hashBitField & 1) == 1) { + branch[height] = leaf; + return; + } + bytes32 node = branch[height]; + // effecient hash + if (node > leaf) + assembly { + mstore(0x00, leaf) + mstore(0x20, node) + leaf := keccak256(0x00, 0x40) + } + else + assembly { + mstore(0x00, node) + mstore(0x20, leaf) + leaf := keccak256(0x00, 0x40) + } + hashBitField /= 2; + } + } + + /** @dev Saves the merkle root state in history and resets. + * `O(log(n))` where + * `n` is the number of leaves. + */ + function reset() internal { + history[block.number] = getMerkleRoot(); + count = 0; + } + + /** @dev Gets the merkle root history + * `O(log(n))` where + * `n` is the number of leaves. + * @param blocknumber requested blocknumber. + */ + function getMerkleRootHistory(uint256 blocknumber) public view returns (bytes32) { + if (blocknumber == block.number) return getMerkleRoot(); + + return history[blocknumber]; + } + + /** @dev Gets the current merkle root. + * `O(log(n))` where + * `n` is the number of leaves. + */ + function getMerkleRoot() public view returns (bytes32) { + bytes32 node; + uint256 size = count; + uint256 height = 0; + bool isFirstHash = true; + while (size > 0) { + // avoid redundant calculation + if ((size & 1) == 1) { + if (isFirstHash) { + node = branch[height]; + isFirstHash = false; + } else { + bytes32 hash = branch[height]; + // effecient hash + if (hash > node) + assembly { + mstore(0x00, node) + mstore(0x20, hash) + node := keccak256(0x00, 0x40) + } + else + assembly { + mstore(0x00, hash) + mstore(0x20, node) + node := keccak256(0x00, 0x40) + } + } + } + size /= 2; + height++; + } + return node; + } +} diff --git a/contracts/test/bridge/merkle/MerkleTree.ts b/contracts/test/bridge/merkle/MerkleTree.ts new file mode 100644 index 000000000..1bc941eca --- /dev/null +++ b/contracts/test/bridge/merkle/MerkleTree.ts @@ -0,0 +1,243 @@ +// Shamelessly adapted from OpenZeppelin-contracts test utils +import { keccak256, bufferToHex, toBuffer } from "ethereumjs-util"; +import { soliditySha3, Mixed } from "web3-utils"; +import { ethers } from "hardhat"; +import { soliditySha256 } from "ethers/lib/utils"; + + +const isNil = (value: unknown): boolean => value === null || value === undefined; + +// Merkle tree called with 32 byte hex values +export class MerkleTree { + private elements: Buffer[]; + private layers: Buffer[][]; + + /** + * Creates a Merkle Tree from an array of hex strings. + * @param leafNodes An array of 32-byte hex strings. + */ + constructor(leafNodes: string[]) { + // Deduplicate elements + this.elements = leafNodes.reduce((acc, el) => (isNil(el) ? acc : [...acc, toBuffer(el)]), [] as Buffer[]); + + // Create layers + this.layers = MerkleTree.getLayers(this.elements); + } + + /** + * Creates a leaf node from any number of args. + * This is the equivalent of `keccak256(abi.encodePacked(first, ...rest))` on Solidity. + * @param data The data to be transformed into a node. + * @return node The `sha3` (A.K.A. `keccak256`) hash of `first, ...params` as a 32-byte hex string. + */ + public static makeLeafNode(data: string): string { + const result = ethers.utils.keccak256(data); + + if (!result) { + throw new Error("Leaf node must not be empty"); + } + + return result; + } + + /** + * Deduplicates buffers from an element + * @param elements An array of buffers containing the leaf nodes. + * @return dedupedElements The array of buffers without duplicates. + */ + private static bufDedup(elements: Buffer[]): Buffer[] { + return elements.filter((el, idx) => { + return idx === 0 || !elements[idx - 1].equals(el); + }); + } + + /** + * Gets the layers of the Merkle Tree by combining the nodes 2-by-2 until the root. + * @param elements An array of buffers containing the leaf nodes. + * @returns layers The layers of the merkle tree + */ + private static getLayers(elements: Buffer[]): Buffer[][] { + if (elements.length === 0) { + return [[Buffer.from("")]]; + } + + const layers: Buffer[][] = []; + layers.push(elements); + + // Get next layer until we reach the root + while (layers[layers.length - 1].length > 1) { + layers.push(MerkleTree.getNextLayer(layers[layers.length - 1])); + } + + return layers; + } + + /** + * Gets the next layers of the Merkle Tree. + * @param elements An array of buffers containing the nodes. + * @returns layer The next layer of the merkle tree. + */ + private static getNextLayer(elements: Buffer[]): Buffer[] { + return elements.reduce((layer, el, idx, arr) => { + if (idx % 2 === 0) { + // Hash the current element with its pair element + const item = MerkleTree.combinedHash(el, arr[idx + 1]); + if (item) { + layer.push(item); + } + } + + return layer; + }, [] as Buffer[]); + } + + /** + * Gets the hash of the combination of 2 nodes. + * @param first The first element. + * @param second The second element. + * @returns hash The next layer of the merkle tree. + */ + private static combinedHash(first: null, second: null): null; + private static combinedHash(first: Buffer | null, second: Buffer): Buffer; + private static combinedHash(first: Buffer, second: Buffer | null): Buffer; + private static combinedHash(first: Buffer | null, second: Buffer | null): Buffer | null { + if (!first) { + return second; + } + if (!second) { + return first; + } + + return keccak256(MerkleTree.sortAndConcat(first, second)); + } + + /** + * Sorts and concatenates an arbitrary number of buffers + * @param ...args The buffers to sort and concat. + * @returns concatedBuffer The concatenated buffer. + */ + private static sortAndConcat(...args: Buffer[]): Buffer { + return Buffer.concat([...args].sort(Buffer.compare)); + } + + /** + * Gets the root of the merkle tree. + * @return root The merkle root as a Buffer. + */ + public getRoot(): Buffer { + return this.layers[this.layers.length - 1][0]; + } + + /** + * Gets the merkle proof for a given element. + * @param el The element to search for the proof. + * @return proof The merkle proof. + */ + public getProof(el: Buffer): Buffer[] { + let idx = MerkleTree.bufIndexOf(el, this.elements); + + if (idx === -1) { + throw new Error("Element does not exist in the merkle tree"); + } + + return this.layers.reduce((proof, layer) => { + const pairElement = MerkleTree.getPairElement(idx, layer); + + if (pairElement) { + proof.push(pairElement); + } + + idx = Math.floor(idx / 2); + + return proof; + }, []); + } + + /** + * Gets the merkle proof for a given element. + * @param el The element to search for the proof. + * @return pos The position of the element in the array or -1 if not found. + */ + private static bufIndexOf(el: Buffer, arr: Buffer[]): number { + let hash; + + // Convert element to 32 byte hash if it is not one already + if (el.length !== 32) { + hash = keccak256(el); + } else { + hash = el; + } + + for (let i = 0; i < arr.length; i++) { + if (hash.equals(arr[i])) { + return i; + } + } + + return -1; + } + + /** + * Gets the related pair element from the given layer. + * @param idx The index of the element. + * @param layer The layer of the merkle tree. + * @return pairEl The pair element. + */ + private static getPairElement(idx: number, layer: Buffer[]): Buffer | null { + const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1; + + if (pairIdx < layer.length) { + return layer[pairIdx]; + } else { + return null; + } + } + + /** + * Gets the root of the merkle tree as hex. + * @return The merkle root as a 0x-prefixed 32-byte hex string. + */ + public getHexRoot(): string { + return bufferToHex(this.getRoot()); + } + + /** + * Gets the merkle proof for a given element as a 32-byte hex string. + * @param el The element to search for the proof. + * @return proof The merkle proof as an array of 32-byte hex strings. + */ + public getHexProof(el: string): string[] { + const proof = this.getProof(toBuffer(el)); + + return MerkleTree.bufArrToHexArr(proof); + } + + /** + * Converts an array of buffers to an array of 0x-prefixed 32-byte strings. + * @param arr The array of buffers. + * @return hexArr The array of string. + */ + private static bufArrToHexArr(arr: Buffer[]): string[] { + if (arr.some((el) => !Buffer.isBuffer(el))) { + throw new Error("Array is not an array of buffers"); + } + + return arr.map((el) => bufferToHex(el)); + } + + /** + * Returns the number of leaf nodes in the Merkle Tree. + * @returns width The tree width. + */ + public getWidth(): number { + return this.elements.length; + } + + /** + * Returns the number of layers in the Merkle Tree. + * @returns height The tree height. + */ + public getHeight(): number { + return this.layers.length; + } +} diff --git a/contracts/test/bridge/merkle/index.ts b/contracts/test/bridge/merkle/index.ts new file mode 100644 index 000000000..c6af5edd6 --- /dev/null +++ b/contracts/test/bridge/merkle/index.ts @@ -0,0 +1,72 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { toBuffer } from "ethereumjs-util"; +import { soliditySha3 } from "web3-utils"; +import { MerkleTree } from "./MerkleTree"; + +/** + * Adapted from OpenZeppelin MerkleProof contract. + * + * @see {https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/MerkleProof.sol} + * @param proof The merkle path from `leaf` to `root`. + * @param root The root of the merkle tree. + * @param leaf The leaf node. + * @return valid Whether the proof is valid or not. + */ + function verify(proof: string[], root: string, leaf: string) { + return ( + root === + proof.reduce( + (computedHash: string, proofElement: string, currentIndex: number): string => + Buffer.compare(toBuffer(computedHash), toBuffer(proofElement)) <= 0 + ? (soliditySha3(computedHash, proofElement) as string) + : (soliditySha3(proofElement, computedHash) as string), + leaf + ) + ); +} + +describe("Merkle", function () { + describe("Sanity tests", async () => { + + let merkleTreeHistory, merkleProof; + let data,nodes,mt; + let rootOnChain,rootOffChain, proof; + + before("Deploying", async () => { + const merkleTreeHistoryFactory = await ethers.getContractFactory("MerkleTreeHistory"); + const merkleProofFactory = await ethers.getContractFactory("MerkleProof"); + merkleTreeHistory = await merkleTreeHistoryFactory.deploy(); + merkleProof = await merkleProofFactory.deploy(); + await merkleTreeHistory.deployed(); + await merkleProof.deployed(); + }); + + it("Merkle Root verification", async function () { + data = [ + "0x00", + "0x01", + "0x03", + ]; + nodes = []; + for (var message of data) { + await merkleTreeHistory.append(message); + nodes.push(MerkleTree.makeLeafNode(message)); + } + mt = new MerkleTree(nodes); + rootOffChain = mt.getHexRoot(); + rootOnChain = await merkleTreeHistory.getMerkleRoot(); + expect(rootOffChain == rootOnChain).equal(true); + }); + it("Should correctly verify all nodes in the tree", async () => { + for (var message of data) { + const leaf = ethers.utils.keccak256(message); + proof = mt.getHexProof(leaf); + const validation = await merkleProof.validateProof(proof, message,rootOnChain); + expect(validation).equal(true); + expect(verify(proof, rootOffChain, leaf)).equal(true); + } + }); + }); +}); \ No newline at end of file