This repository offers an integrated setup for developing and testing zero-knowledge proofs using a modern stack designed to streamline your development workflow. It combines the robust functionalities of Hardhat, Circom, and React to provide a cohesive environment for building and deploying smart contracts and zero-knowledge circuits in a production-like frontend interface.
- Circom: Empowers developers with tools for creating and managing zero-knowledge proofs efficiently.
- Hardhat: Utilized for deploying smart contracts, verifying proofs, and testing the contracts in a local development environment.
- React Frontend: A ready-to-use frontend setup to interact with your circuits and contracts, simulating real-world usage.
Before installation, ensure that you have the following software versions installed, as compatibility has been confirmed with:
- Circom v2.1.8
- Solidity Compiler (solc) v0.8.24
- Node.js v18
- pnpm v8.15.5
To set up the development environment, follow these steps:
Install all node dependencies which will also download the required powers of tau base file for zero-knowledge proof:
pnpm install
Note: The installation process involves downloading large files and may take some time.
This repository comes with a nix flake for easy installation of dependencies.
Ensure direnv is installed and then run:
direnv allow
In order to get a shell for the folder with the correct dependencies installed.
The repository demonstrates a basic Multiplier circuit:
pragma circom 2.0.0;
template Multiplier() {
signal input a;
signal input b;
signal output c;
c <== a * b;
}
component main = Multiplier();
There is a basic Prover created to form the base of your protocol sdk:
export class CircomExample {
constructor(
private provider: Provider,
private address: string
) { }
async multiplierProve(a: number, b: number) {
return await generateGroth16Proof({ a, b }, "multiplier");
}
async multiplierVerify(proof: string, c: number) {
const verifier = CircomExample__factory.connect(this.address, this.provider);
return await verifier.multiplierVerify(proof, [c]);
}
}
You can test the circuit using the Hardhat Tests which allow for testing integration logic with the prover:
it("should pass a valid proof", async () => {
const { verifier } = await loadFixture(deployVerifierFixture);
const address = await verifier.getAddress();
const circomExample = new CircomExample(ethers.provider, address);
const proof = await circomExample.multiplierProve(4, 11);
await circomExample.multiplierVerify(proof, 44);
});
it("should fail an invalid proof", async () => {
const { verifier } = await loadFixture(deployVerifierFixture);
const address = await verifier.getAddress();
const circomExample = new CircomExample(ethers.provider, address);
const proof = await circomExample.multiplierProve(4, 10);
await expect(circomExample.multiplierVerify(proof, 44)).to.be.revertedWith(
"invalid proof"
);
});
Lastly you can edit the Frontend React Application to create real experiences for users:
function App() {
const [status, setStatus] = useState<"init" | "error" | "success">("init");
const [proof, setProof] = useState<string>("");
const [provingTime, setProvingTime] = useState<number | undefined>();
const [verifyingTime, setVerifyingTime] = useState<number | undefined>();
async function onSubmit(e: FormEvent<HTMLFormElement>) {
setStatus("init");
setProof("");
e.preventDefault();
const data = new FormData(e.currentTarget);
const a = Number(data.get("a"));
const b = Number(data.get("b"));
const c = Number(data.get("c"));
let time = new Date();
const proof = await example.multiplierProve(a, b);
setProvingTime(Number(new Date()) - Number(time));
setProof(proof);
time = new Date();
try {
await example.multiplierVerify(proof, c);
setStatus("success");
} catch (err) {
setStatus("error");
}
setVerifyingTime(Number(new Date()) - Number(time));
}
return (
<div>
<h2>Prove and Verify your zkSNARK</h2>
<blockquote>a x b = c</blockquote>
<form onSubmit={onSubmit}>
<label htmlFor="a">
a:
<input type="number" name="a" />
</label>
<label>
b:
<input type="number" name="b" />
</label>
<label>
c:
<input type="number" name="c" />
</label>
{status === "init" && <button>Prove and verify</button>}
{status === "error" && (
<button>
Try Again <span className="error">×</span>
</button>
)}
{status === "success" && (
<button>
Proof is valid <span className="success">✓</span>
</button>
)}
{provingTime !== undefined && <div>Proof took {provingTime}ms</div>}
{verifyingTime !== undefined && (
<div>Verification took {verifyingTime}ms</div>
)}
</form>
{proof && <pre>{proof}</pre>}
</div>
);
}
This repository provides several scripts to help streamline the development and testing process:
Script | Description |
---|---|
pnpm build |
Builds the circuits and smart contracts, preparing all necessary elements for testing. |
pnpm test |
Runs tests for both the circuits and the smart contracts to ensure integrity. |
pnpm dev |
Executes a full build, spins up a Hardhat local node, deploys contracts, and launches the React frontend at http://localhost:5173. |
To integrate a new circuit into your project, follow these steps:
-
Create a New Circuit File:
- Initialize a new circuit by creating a
.circom
file at./circuits/new_circuit.circom
.
- Initialize a new circuit by creating a
-
Build the Project:
- Run
pnpm build
to compile the new circuit. This process generates the following files:./compiled/new_circuit_js/**
./compiled/new_circuit.r1cs
./compiled/new_circuit.zkey
./contracts/generated/new_circuit.sol
./ignition/modules/generated/NewCircuitVerifier.ts
- Typechain types for
new_circuit.sol
- Run
-
Modify the Smart Contract:
- Update the
CircomExample.sol
contract to include the new verifier contract. Example changes:import {MultiplierVerifier} from "./generated/multiplier.sol"; +import {NewCircuitVerifier} from "./generated/new_circuit.sol";
- Utilize the new verifier similarly to existing circuits.
- Update the
-
Integrate with Ignite Module:
- An Ignite module
NewCircuitVerifier.ts
will be automatically created. Use it as shown:import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; import MultiplierVerifier from "./generated/MultiplierVerifier"; +import NewCircuitVerifier from "./generated/NewCircuitVerifier"; export default buildModule("CircomExample", (m) => { const { verifier: multiplierVerifier } = m.useModule(MultiplierVerifier); + const { verifier: newCircuitVerifier } = m.useModule(NewCircuitVerifier); // etc... });
- An Ignite module
-
Update Application Logic:
- Adjust your application code to utilize the new circuit. For example, in
./src/index.ts
:await generateGroth16Proof({ a, b }, "new_circuit");
- Adjust your application code to utilize the new circuit. For example, in
-
Revise Frontend As Necessary:
- Make any required updates to the frontend, especially if the API or interaction patterns have changed due to the new circuit.
We welcome contributions from the community! If you'd like to improve the opinionated zero-knowledge starter repo, please feel free to fork the repository, make your changes, and submit a pull request. We appreciate your input!
Ensure you follow the provided coding standards and write tests for new features to maintain the quality of the software.
This project is licensed under the MIT License. Feel free to use, modify, and distribute the code as you see fit.
By leveraging this starter repo, developers can focus more on the creative aspects of their projects while the configuration and setup are streamlined to provide a productive and error-free development environment.