12 KiB
sidebar_position
| sidebar_position |
|---|
| 2 |
Quick setup
Set up a new Hardhat project with Semaphore. Learn how to create and test an Ethereum smart contract that uses zero-knowledge proofs to verify membership.
To check out the code used in this guide, visit the semaphore-quick-setup repository.
- Create a Node.js project
- Install Hardhat
- Install Semaphore contracts and ZK-kit
- Create the Semaphore contract
- Create Semaphore IDs
- Create a Hardhat task that deploys your contract
- Deploy your contract to a local network
Create a Node.js project
-
Follow the Node.js LTS version instructions to install
node(Hardhat may not work with Node.js Current). -
Follow the Yarn instructions to download and install the
yarnpackage manager. -
Create a directory for the project and change to the new directory.
mkdir semaphore-example cd semaphore-example -
In your terminal, run
yarn initto initialize the Node.js project.
Install Hardhat
Hardhat is a development environment you can use to compile, deploy, test, and debug Ethereum software. Hardhat includes the Hardhat Network, a local Ethereum network for development.
-
Use
yarnto install Hardhat:yarn add hardhat --dev -
Use
yarnto runhardhatand create a basic sample project:yarn hardhat # At the prompt, select "Create a basic sample project" # and then enter through the prompts.
Install Semaphore contracts and ZK-kit
@appliedzkp/semaphore-contracts provides a base contract that verifies
Semaphore proofs.
@appliedzkp/@zk-kit provides JavaScript libraries that help developers
build zero-knowledge applications.
To install these dependencies for your project, do the following:
-
Use
yarnto install@appliedzkp/semaphore-contracts:yarn add @appliedzkp/semaphore-contractsFor more detail about Semaphore base contracts, see Contracts. To view the source, see Contracts in the Semaphore repository.
-
Use
yarnto install@appliedzkp/@zk-kit:yarn add @zk-kit/identity @zk-kit/protocols --devFor more information about
@appliedzkp/@zk-kit, see the ZK-kit repository.
Create the Semaphore contract
Create a Greeters contract that imports and extends the Semaphore base contract:
-
In
./contracts, renameGreeter.soltoGreeters.sol. -
Replace the contents of
Greeters.solwith the following://SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@appliedzkp/semaphore-contracts/interfaces/IVerifier.sol"; import "@appliedzkp/semaphore-contracts/base/SemaphoreCore.sol"; /// @title Greeters contract. /// @dev The following code is just a example to show how Semaphore con be used. contract Greeters is SemaphoreCore { // A new greeting is published every time a user's proof is validated. event NewGreeting(bytes32 greeting); // Greeters are identified by a Merkle root. // The offchain Merkle tree contains the greeters' identity commitments. uint256 public greeters; // The external verifier used to verify Semaphore proofs. IVerifier public verifier; constructor(uint256 _greeters, address _verifier) { greeters = _greeters; verifier = IVerifier(_verifier); } // Only users who create valid proofs can greet. // The external nullifier is in this example the root of the Merkle tree. function greet( bytes32 _greeting, uint256 _nullifierHash, uint256[8] calldata _proof ) external { _verifyProof(_greeting, greeters, _nullifierHash, greeters, _proof, verifier); // Prevent double-greeting (nullifierHash = hash(root + identityNullifier)). // Every user can greet once. _saveNullifierHash(_nullifierHash); emit NewGreeting(_greeting); } }
Create Semaphore IDs
Semaphore identity commitments (Semaphore IDs) represent user identities and are the leaves of the Merkle trees in the protocol.
Create a ./static folder and add a ./static/identityCommitments.json file that
contains the following array of IDs:
[
"9426253249246138013650573474062059446203468399013007463704855436559640562175",
"6200634377081441056179822649025268043304989981899916286941956069781421654881",
"19706772421195815860043593475869058320994241404138740034486179990871964981523"
]
:::info
To generate the IDs for this example, we used @zk-kit/identity
(with a message strategy) to create messages.
Then, in Metamask, we signed the messages with the first 3 Ethereum accounts
of the Hardhat dev wallet.
:::
Create a Hardhat task that deploys your contract
Hardhat lets you write tasks
that automate building and deploying smart contracts and dApps.
To create a task that deploys the Greeters contract, do the following.
-
Use
yarnto install@zk-kit/incremental-merkle-treeandcircomlibjs@0.0.8:yarn add @zk-kit/incremental-merkle-tree circomlibjs@0.0.8 --dev@zk-kit/incremental-merkle-treeandcircomlibjs@0.0.8let you create off-chain Merkle trees. For more information, see the ZK-kit repository -
Use
yarnto installhardhat-dependency-compiler:yarn add hardhat-dependency-compiler --devhardhat-dependency-compilercompiles Solidity contracts and dependencies. -
Create a
tasksfolder and add a./tasks/deploy.jsfile that contains the following:const { IncrementalMerkleTree } = require("@zk-kit/incremental-merkle-tree") const { poseidon } = require("circomlibjs") const identityCommitments = require("../static/identityCommitments.json") const { task, types } = require("hardhat/config") task("deploy", "Deploy a Greeters contract") .addOptionalParam("logs", "Print the logs", true, types.boolean) .setAction(async ({ logs }, { ethers }) => { const VerifierContract = await ethers.getContractFactory("Verifier") const verifier = await VerifierContract.deploy() await verifier.deployed() logs && console.log(`Verifier contract has been deployed to: ${verifier.address}`) const GreetersContract = await ethers.getContractFactory("Greeters") const tree = new IncrementalMerkleTree(poseidon, 20, BigInt(0), 2) for (const identityCommitment of identityCommitments) { tree.insert(identityCommitment) } const greeters = await GreetersContract.deploy(tree.root, verifier.address) await greeters.deployed() logs && console.log(`Greeters contract has been deployed to: ${greeters.address}`) return greeters }) -
In your
hardhat.config.jsfile, add the following:require("@nomiclabs/hardhat-waffle") require("hardhat-dependency-compiler") require("./tasks/deploy") // Your deploy task. module.exports = { solidity: "0.8.4", dependencyCompiler: { // It allows Hardhat to compile the external Verifier.sol contract. paths: ["@appliedzkp/semaphore-contracts/base/Verifier.sol"] } }
Test your smart contract
hardhat-waffle
lets you write tests with the Waffle test framework
and Chai assertions.
-
Use
yarnto install thehardhat-waffleplugin and dependencies for smart contract tests:yarn add -D @nomiclabs/hardhat-waffle 'ethereum-waffle@^3.0.0' \ @nomiclabs/hardhat-ethers 'ethers@^5.0.0' chai -
Download the Semaphore snark build files and copy them to the
./staticfolder. Your application and tests must pass these static files to Semaphore to create zero-knowledge proofs. -
Replace the contents of
./test/sample-test.jswith the following test:const { Strategy, ZkIdentity } = require("@zk-kit/identity") const { generateMerkleProof, Semaphore } = require("@zk-kit/protocols") const identityCommitments = require("../static/identityCommitments.json") const { expect } = require("chai") const { run, ethers } = require("hardhat") describe("Greeters", function () { let contract let signers before(async () => { contract = await run("deploy", { logs: false }) signers = await ethers.getSigners() }) describe("# greet", () => { const wasmFilePath = "./static/semaphore.wasm" const finalZkeyPath = "./static/semaphore_final.zkey" it("Should greet", async () => { const message = await signers[0].signMessage("Sign this message to create your identity!") const identity = new ZkIdentity(Strategy.MESSAGE, message) const identityCommitment = identity.genIdentityCommitment() const greeting = "Hello world" const bytes32Greeting = ethers.utils.formatBytes32String(greeting) const merkleProof = generateMerkleProof(20, BigInt(0), identityCommitments, identityCommitment) const witness = Semaphore.genWitness( identity.getTrapdoor(), identity.getNullifier(), merkleProof, merkleProof.root, greeting ) const fullProof = await Semaphore.genProof(witness, wasmFilePath, finalZkeyPath) const solidityProof = Semaphore.packToSolidityProof(fullProof.proof) const nullifierHash = Semaphore.genNullifierHash(merkleProof.root, identity.getNullifier()) const transaction = contract.greet(bytes32Greeting, nullifierHash, solidityProof) await expect(transaction).to.emit(contract, "NewGreeting").withArgs(bytes32Greeting) }) }) }) -
Run the following
yarncommands to compile and test your contract:yarn hardhat compile yarn hardhat test
Deploy your contract to a local network
To deploy your contract in a local Hardhat network (and use it in your dApp), run the following yarn commands:
yarn hardhat node
yarn hardhat deploy --network localhost # In another tab.
For a more complete demo that provides a starting point for your dApp, see semaphore-boilerplate.