feat: add incremental-merkle-tree.sol pkg

This commit is contained in:
cedoor
2022-02-01 18:47:22 +01:00
parent dfd4b1ce01
commit e363474547
14 changed files with 10989 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
COINMARKETCAP_API_KEY=
REPORT_GAS=false

View File

@@ -0,0 +1,21 @@
{
"extends": "solhint:recommended",
"plugins": ["prettier"],
"rules": {
"code-complexity": ["error", 7],
"compiler-version": ["error", ">=0.8.0"],
"const-name-snakecase": "off",
"no-empty-blocks": "off",
"constructor-syntax": "error",
"func-visibility": ["error", { "ignoreConstructors": true }],
"max-line-length": ["error", 120],
"not-rely-on-time": "off",
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
],
"reason-string": ["warn", { "maxLength": 64 }]
}
}

View File

@@ -0,0 +1 @@
contracts/Verifier.sol

View File

@@ -0,0 +1,68 @@
<p align="center">
<h1 align="center">
Incremental Merkle Trees (Solidity)
</h1>
<p align="center">Incremental Merkle tree smart contracts.</p>
</p>
<p align="center">
<a href="https://github.com/appliedzkp/zk-kit">
<img src="https://img.shields.io/badge/project-zk--kit-blue.svg?style=flat-square">
</a>
<a href="https://github.com/appliedzkp/zk-kit/blob/main/LICENSE">
<img alt="Github license" src="https://img.shields.io/github/license/appliedzkp/zk-kit.svg?style=flat-square">
</a>
<a href="https://www.npmjs.com/package/@zk-kit/incremental-merkle-tree.sol">
<img alt="NPM version" src="https://img.shields.io/npm/v/@zk-kit/incremental-merkle-tree.sol?style=flat-square" />
</a>
<a href="https://npmjs.org/package/@zk-kit/incremental-merkle-tree.sol">
<img alt="Downloads" src="https://img.shields.io/npm/dm/@zk-kit/incremental-merkle-tree.sol.svg?style=flat-square" />
</a>
<a href="https://bundlephobia.com/package/@zk-kit/incremental-merkle-tree.sol">
<img alt="npm bundle size (scoped)" src="https://img.shields.io/bundlephobia/minzip/@zk-kit/incremental-merkle-tree.sol" />
</a>
<a href="https://eslint.org/">
<img alt="Linter eslint" src="https://img.shields.io/badge/linter-eslint-8080f2?style=flat-square&logo=eslint" />
</a>
<a href="https://prettier.io/">
<img alt="Code style prettier" src="https://img.shields.io/badge/code%20style-prettier-f8bc45?style=flat-square&logo=prettier" />
</a>
</p>
<div align="center">
<h4>
<a href="https://discord.gg/9B9WgGP6YM">
🗣️ Chat &amp; Support
</a>
</h4>
</div>
---
## 🛠 Install
### npm or yarn
Install the `@zk-kit/incremental-merkle-tree.sol` package with npm:
```bash
npm i @zk-kit/incremental-merkle-tree.sol --save
```
or yarn:
```bash
yarn add @zk-kit/incremental-merkle-tree.sol
```
## 📜 Usage
## Contacts
### Developers
- e-mail : me@cedoor.dev
- github : [@cedoor](https://github.com/cedoor)
- website : https://cedoor.dev

View File

@@ -0,0 +1,10 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
library PoseidonT3 {
function poseidon(uint256[2] memory) public pure returns (uint256) {}
}
library PoseidonT6 {
function poseidon(uint256[5] memory) public pure returns (uint256) {}
}

View File

@@ -0,0 +1,156 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import {PoseidonT3} from "./Hashes.sol";
// Each incremental tree has certain properties and data that will
// be used to add new leaves.
struct IncrementalTreeData {
uint8 depth; // Depth of the tree (levels - 1).
uint256 root; // Root hash of the tree.
uint256 numberOfLeaves; // Number of leaves of the tree.
mapping(uint256 => uint256) zeroes; // Zero hashes used for empty nodes (level -> zero hash).
// The nodes of the subtrees used in the last addition of a leaf (level -> [left node, right node]).
mapping(uint256 => uint256[2]) lastSubtrees; // Caching these values is essential to efficient appends.
}
/// @title Incremental binary Merkle tree.
/// @dev The incremental tree allows to calculate the root hash each time a leaf is added, ensuring
/// the integrity of the tree.
library IncrementalBinaryTree {
uint8 internal constant MAX_DEPTH = 32;
uint256 internal constant SNARK_SCALAR_FIELD =
21888242871839275222246405745257275088548364400416034343698204186575808495617;
/// @dev Initializes a tree.
/// @param self: Tree data.
/// @param depth: Depth of the tree.
/// @param zero: Zero value to be used.
function init(
IncrementalTreeData storage self,
uint8 depth,
uint256 zero
) public {
require(
depth > 0 && depth <= MAX_DEPTH,
"IncrementalBinaryTree: tree depth must be between 1 and 32"
);
self.depth = depth;
for (uint8 i = 0; i < depth; i++) {
self.zeroes[i] = zero;
zero = PoseidonT3.poseidon([zero, zero]);
}
self.root = zero;
}
/// @dev Inserts a leaf in the tree.
/// @param self: Tree data.
/// @param leaf: Leaf to be inserted.
function insert(IncrementalTreeData storage self, uint256 leaf) public {
require(
leaf < SNARK_SCALAR_FIELD,
"IncrementalBinaryTree: leaf must be < SNARK_SCALAR_FIELD"
);
require(
self.numberOfLeaves < 2**self.depth,
"IncrementalBinaryTree: tree is full"
);
uint256 index = self.numberOfLeaves;
uint256 hash = leaf;
for (uint8 i = 0; i < self.depth; i++) {
if (index % 2 == 0) {
self.lastSubtrees[i] = [hash, self.zeroes[i]];
} else {
self.lastSubtrees[i][1] = hash;
}
hash = PoseidonT3.poseidon(self.lastSubtrees[i]);
index /= 2;
}
self.root = hash;
self.numberOfLeaves += 1;
}
/// @dev Removes a leaf from the tree.
/// @param self: Tree data.
/// @param leaf: Leaf to be removed.
/// @param proofSiblingNodes: Array of the sibling nodes of the proof of membership.
/// @param proofPath: Path of the proof of membership.
function remove(
IncrementalTreeData storage self,
uint256 leaf,
uint256[] memory proofSiblingNodes,
uint8[] memory proofPath
) public {
require(
verify(self, leaf, proofSiblingNodes, proofPath),
"IncrementalBinaryTree: leaf is not part of the tree"
);
uint256 hash = self.zeroes[0];
for (uint8 i = 0; i < self.depth; i++) {
if (proofPath[i] == 0) {
if (proofSiblingNodes[i] == self.lastSubtrees[i][1]) {
self.lastSubtrees[i][0] = hash;
}
hash = PoseidonT3.poseidon([hash, proofSiblingNodes[i]]);
} else {
if (proofSiblingNodes[i] == self.lastSubtrees[i][0]) {
self.lastSubtrees[i][1] = hash;
}
hash = PoseidonT3.poseidon([proofSiblingNodes[i], hash]);
}
}
self.root = hash;
}
/// @dev Verify if the path is correct and the leaf is part of the tree.
/// @param self: Tree data.
/// @param leaf: Leaf to be removed.
/// @param proofSiblingNodes: Array of the sibling nodes of the proof of membership.
/// @param proofPath: Path of the proof of membership.
/// @return True or false.
function verify(
IncrementalTreeData storage self,
uint256 leaf,
uint256[] memory proofSiblingNodes,
uint8[] memory proofPath
) private view returns (bool) {
require(
leaf < SNARK_SCALAR_FIELD,
"IncrementalBinaryTree: leaf must be < SNARK_SCALAR_FIELD"
);
require(
proofPath.length == self.depth &&
proofSiblingNodes.length == self.depth,
"IncrementalBinaryTree: length of path is not correct"
);
uint256 hash = leaf;
for (uint8 i = 0; i < self.depth; i++) {
require(
proofSiblingNodes[i] < SNARK_SCALAR_FIELD,
"IncrementalBinaryTree: sibling node must be < SNARK_SCALAR_FIELD"
);
if (proofPath[i] == 0) {
hash = PoseidonT3.poseidon([hash, proofSiblingNodes[i]]);
} else {
hash = PoseidonT3.poseidon([proofSiblingNodes[i], hash]);
}
}
return hash == self.root;
}
}

View File

@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "./IncrementalBinaryTree.sol";
contract Test {
using IncrementalBinaryTree for IncrementalTreeData;
event TreeCreated(bytes32 id, uint8 depth);
event LeafInserted(bytes32 indexed treeId, uint256 leaf, uint256 root);
event LeafRemoved(bytes32 indexed treeId, uint256 leaf, uint256 root);
mapping(bytes32 => IncrementalTreeData) public trees;
function createTree(bytes32 _id, uint8 _depth) external {
require(trees[_id].depth == 0, "Test: tree already exists");
trees[_id].init(_depth, 0);
emit TreeCreated(_id, _depth);
}
function insertLeaf(bytes32 _treeId, uint256 _leaf) external {
require(trees[_treeId].depth != 0, "Test: tree does not exist");
trees[_treeId].insert(_leaf);
emit LeafInserted(_treeId, _leaf, trees[_treeId].root);
}
function removeLeaf(
bytes32 _treeId,
uint256 _leaf,
uint256[] memory _proofSiblings,
uint8[] memory _proofPathIndices
) external {
require(trees[_treeId].depth != 0, "Test: tree does not exist");
trees[_treeId].remove(_leaf, _proofSiblings, _proofPathIndices);
emit LeafRemoved(_treeId, _leaf, trees[_treeId].root);
}
}

View File

@@ -0,0 +1,28 @@
import "@nomiclabs/hardhat-ethers"
import "@nomiclabs/hardhat-waffle"
import { config as dotenvConfig } from "dotenv"
import "hardhat-gas-reporter"
import { HardhatUserConfig } from "hardhat/config"
import { resolve } from "path"
import "solidity-coverage"
import { config } from "./package.json"
import "./tasks/deploy-test"
dotenvConfig({ path: resolve(__dirname, "./.env") })
const hardhatConfig: HardhatUserConfig = {
solidity: config.solidity,
paths: {
sources: config.paths.contracts,
tests: config.paths.tests,
cache: config.paths.cache,
artifacts: config.paths.build.contracts
},
gasReporter: {
currency: "USD",
enabled: process.env.REPORT_GAS === "true",
coinmarketcap: process.env.COINMARKETCAP_API_KEY
}
}
export default hardhatConfig

View File

@@ -0,0 +1,74 @@
{
"name": "@zk-kit/incremental-merkle-tree.sol",
"version": "0.1.0",
"description": "Incremental Merkle tree smart contracts.",
"license": "MIT",
"files": [
"contracts/",
"!contracts/Test.sol",
"README.md"
],
"keywords": [
"blockchain",
"ethereum",
"hardhat",
"smart-contracts",
"solidity",
"libraries",
"merkle-tree",
"incremental-merkle-tree"
],
"repository": "git@github.com:appliedzkp/zk-kit.git",
"homepage": "https://github.com/appliedzkp/zk-kit/tree/main/packages/incremental-merkle-tree.sol",
"author": {
"name": "Omar Desogus",
"email": "me@cedoor.dev",
"url": "https://cedoor.dev"
},
"scripts": {
"start": "hardhat node",
"compile": "hardhat compile",
"deploy:test": "hardhat deploy:test",
"test": "hardhat test",
"test:report-gas": "REPORT_GAS=true hardhat test",
"test:coverage": "hardhat coverage",
"test:prod": "yarn lint && yarn coverage",
"lint": "solhint 'contracts/**/*.sol'"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.4",
"@nomiclabs/hardhat-waffle": "^2.0.2",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.1.0",
"@zk-kit/incremental-merkle-tree": "^0.4.0",
"chai": "^4.3.5",
"circomlibjs": "^0.0.8",
"dotenv": "^14.3.2",
"ethereum-waffle": "^3.4.0",
"ethers": "^5.5.3",
"hardhat": "^2.8.3",
"hardhat-gas-reporter": "^1.0.7",
"mocha": "^8.4.0",
"prettier": "^2.5.1",
"prettier-plugin-solidity": "^1.0.0-beta.19",
"solhint": "^3.3.6",
"solhint-plugin-prettier": "^0.0.5",
"solidity-coverage": "^0.7.18"
},
"config": {
"solidity": {
"version": "0.8.4"
},
"paths": {
"contracts": "./contracts",
"circuit": "./circuit",
"tests": "./test",
"cache": "./cache",
"build": {
"snark": "./build/snark",
"contracts": "./build/contracts",
"typechain": "./build/typechain"
}
}
}
}

View File

@@ -0,0 +1,62 @@
import { poseidon_gencontract as poseidonContract } from "circomlibjs"
import { Contract } from "ethers"
import { task, types } from "hardhat/config"
task("deploy:test", "Deploy a Test contract")
.addOptionalParam<boolean>("logs", "Print the logs", true, types.boolean)
.setAction(async ({ logs }, { ethers }): Promise<Contract> => {
const poseidonT3ABI = poseidonContract.generateABI(2)
const poseidonT3Bytecode = poseidonContract.createCode(2)
const [signer] = await ethers.getSigners()
const PoseidonLibT3Factory = new ethers.ContractFactory(
poseidonT3ABI,
poseidonT3Bytecode,
signer
)
const poseidonT3Lib = await PoseidonLibT3Factory.deploy()
await poseidonT3Lib.deployed()
if (logs) {
console.info(
`PoseidonT3 library has been deployed to: ${poseidonT3Lib.address}`
)
}
const IncrementalBinaryTreeLibFactory = await ethers.getContractFactory(
"IncrementalBinaryTree",
{
libraries: {
PoseidonT3: poseidonT3Lib.address
}
}
)
const incrementalBinaryTreeLib =
await IncrementalBinaryTreeLibFactory.deploy()
await incrementalBinaryTreeLib.deployed()
if (logs) {
console.info(
`IncrementalBinaryTree library has been deployed to: ${incrementalBinaryTreeLib.address}`
)
}
const ContractFactory = await ethers.getContractFactory("Test", {
libraries: {
IncrementalBinaryTree: incrementalBinaryTreeLib.address
}
})
const contract = await ContractFactory.deploy()
await contract.deployed()
if (logs) {
console.info(`Test contract has been deployed to: ${contract.address}`)
}
return contract
})

View File

@@ -0,0 +1,214 @@
import { expect } from "chai"
import { Contract } from "ethers"
import { ethers, run } from "hardhat"
import { createTree } from "./utils"
/* eslint-disable jest/valid-expect */
describe("Test", () => {
let contract: Contract
const treeId = ethers.utils.formatBytes32String("treeId")
const leaf = BigInt(1)
const depth = 16
before(async () => {
contract = await run("deploy:test", { logs: false })
})
it("Should not create a tree with a depth > 32", async () => {
const transaction = contract.createTree(treeId, 33)
await expect(transaction).to.be.revertedWith(
"IncrementalBinaryTree: tree depth must be between 1 and 32"
)
})
it("Should create a tree", async () => {
const transaction = contract.createTree(treeId, depth)
await expect(transaction)
.to.emit(contract, "TreeCreated")
.withArgs(treeId, depth)
})
it("Should not create a tree with an existing id", async () => {
const transaction = contract.createTree(treeId, depth)
await expect(transaction).to.be.revertedWith("Test: tree already exists")
})
it("Should not insert a leaf if the tree does not exist", async () => {
const treeId = ethers.utils.formatBytes32String("treeId2")
const transaction = contract.insertLeaf(treeId, leaf)
await expect(transaction).to.be.revertedWith("Test: tree does not exist")
})
it("Should not insert a leaf if its value is > SNARK_SCALAR_FIELD", async () => {
const leaf = BigInt(
"21888242871839275222246405745257275088548364400416034343698204186575808495618"
)
const transaction = contract.insertLeaf(treeId, leaf)
await expect(transaction).to.be.revertedWith(
"IncrementalBinaryTree: leaf must be < SNARK_SCALAR_FIELD"
)
})
it("Should insert a leaf in a tree", async () => {
const transaction = contract.insertLeaf(treeId, leaf)
await expect(transaction)
.to.emit(contract, "LeafInserted")
.withArgs(
treeId,
leaf,
"16211261537006706331557500769845541584780950636316907182067421710925347020533"
)
})
it("Should not insert a leaf if the tree is full", async () => {
const treeId = ethers.utils.formatBytes32String("tinyTree")
await contract.createTree(treeId, 1)
await contract.insertLeaf(treeId, leaf)
await contract.insertLeaf(treeId, leaf)
const transaction = contract.insertLeaf(treeId, leaf)
await expect(transaction).to.be.revertedWith(
"IncrementalBinaryTree: tree is full"
)
})
it("Should not remove a leaf if the tree does not exist", async () => {
const treeId = ethers.utils.formatBytes32String("none")
const transaction = contract.removeLeaf(treeId, leaf, [0, 1], [0, 1])
await expect(transaction).to.be.revertedWith("Test: tree does not exist")
})
it("Should not remove a leaf if its value is > SNARK_SCALAR_FIELD", async () => {
const leaf = BigInt(
"21888242871839275222246405745257275088548364400416034343698204186575808495618"
)
const transaction = contract.removeLeaf(treeId, leaf, [0, 1], [0, 1])
await expect(transaction).to.be.revertedWith(
"IncrementalBinaryTree: leaf must be < SNARK_SCALAR_FIELD"
)
})
it("Should remove a leaf", async () => {
const treeId = ethers.utils.formatBytes32String("hello")
const tree = createTree(depth, 3)
tree.delete(0)
await contract.createTree(treeId, depth)
await contract.insertLeaf(treeId, BigInt(1))
await contract.insertLeaf(treeId, BigInt(2))
await contract.insertLeaf(treeId, BigInt(3))
const { siblings, pathIndices, root } = tree.createProof(0)
const transaction = contract.removeLeaf(
treeId,
BigInt(1),
siblings.map((s) => s[0]),
pathIndices
)
await expect(transaction)
.to.emit(contract, "LeafRemoved")
.withArgs(treeId, BigInt(1), root)
})
it("Should remove another leaf", async () => {
const treeId = ethers.utils.formatBytes32String("hello")
const tree = createTree(depth, 3)
tree.delete(0)
tree.delete(1)
const { siblings, pathIndices, root } = tree.createProof(1)
const transaction = contract.removeLeaf(
treeId,
BigInt(2),
siblings.map((s) => s[0]),
pathIndices
)
await expect(transaction)
.to.emit(contract, "LeafRemoved")
.withArgs(treeId, BigInt(2), root)
})
it("Should not remove a leaf that does not exist", async () => {
const treeId = ethers.utils.formatBytes32String("hello")
const tree = createTree(depth, 3)
tree.delete(0)
tree.delete(1)
const { siblings, pathIndices } = tree.createProof(0)
const transaction = contract.removeLeaf(
treeId,
BigInt(4),
siblings.map((s) => s[0]),
pathIndices
)
await expect(transaction).to.be.revertedWith(
"IncrementalBinaryTree: leaf is not part of the tree"
)
})
it("Should insert a leaf in a tree after a removal", async () => {
const treeId = ethers.utils.formatBytes32String("hello")
const tree = createTree(depth, 4)
tree.delete(0)
tree.delete(1)
const transaction = contract.insertLeaf(treeId, BigInt(4))
await expect(transaction)
.to.emit(contract, "LeafInserted")
.withArgs(treeId, BigInt(4), tree.root)
})
it("Should insert 4 leaves and remove them all", async () => {
const treeId = ethers.utils.formatBytes32String("complex")
const tree = createTree(depth, 4)
await contract.createTree(treeId, depth)
for (let i = 0; i < 4; i += 1) {
await contract.insertLeaf(treeId, BigInt(i + 1))
}
for (let i = 0; i < 4; i += 1) {
tree.delete(i)
const { siblings, pathIndices } = tree.createProof(i)
await contract.removeLeaf(
treeId,
BigInt(i + 1),
siblings.map((s) => s[0]),
pathIndices
)
}
const { root } = await contract.trees(treeId)
expect(root).to.equal(tree.root)
})
})

View File

@@ -0,0 +1,18 @@
import { IncrementalMerkleTree } from "@zk-kit/incremental-merkle-tree"
import { poseidon } from "circomlibjs"
/* eslint-disable import/prefer-default-export */
export function createTree(
depth: number,
numberOfNodes = 0,
zeroValue = BigInt(0),
arity = 2
): IncrementalMerkleTree {
const tree = new IncrementalMerkleTree(poseidon, depth, zeroValue, arity)
for (let i = 0; i < numberOfNodes; i += 1) {
tree.insert(BigInt(i + 1))
}
return tree
}

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"resolveJsonModule": true
},
"include": ["tasks/**/*", "test/**/*"],
"files": ["hardhat.config.ts"]
}

File diff suppressed because it is too large Load Diff