zkvault-basic is a minimal, functional zero-knowledge proof project based on zkSNARKs, designed to help developers understand the fundamental workflow of zk applications—including circuit writing, proof generation, and smart contract verification.
This project guides you through building a complete zk application, from ciruits build setup and contract compile to testing and deployment, CLI operations.
zkvault-basic is designed as a learning and sharing tool to replicate a typical anonymous deposit/withdrawal scenario:
- Use zkSNARK to enable anonymous deposits and withdrawals to arbitrary wallets
- Combine Circom and Solidity to construct a full zero-knowledge workflow
- Emphasize minimal implementation, focusing on core concepts for easier understanding
This project is a simplified version of Tornado Cash’s basic mechanism—an ideal reference for getting started with zk app development.
- The user generates a random secret
- A commitment is derived from this secret
- ETH of a fixed denomination is deposited into CONTRACT using this commitment, enabling anonymous deposit
- Provide the original deposit secret
- Generate the corresponding zero-knowledge proof
- Use any wallet to execute the withdrawal and transfer funds to any desired address
The purpose of zkvault is to achieve the following three core characteristics:
- Consistency
- Security
- Privacy (Not implemented yet)
We will explain these three characteristics by detailing the deposit and withdrawal processes.
- APP prepares to call CONTRACT function
deposit(commitment)- APP randomly generates a
secret(the user ensures it is kept private) - APP calculates the parameter
commitment = hash(secret)needed for CONTRACT functiondeposit.commitmentis public.
- APP randomly generates a
- CONTRACT function
deposit(commitment)handles the process:- Ensures that the user's deposit amount (
msg.value) is correct. - Saves the
commitmentand marks it asDEPOSITEDstatus.
- Ensures that the user's deposit amount (
-
APP prepares to call CONTRACT function
withdraw(pA, pB, pC, pubSignals)- Parameter explanation:
(pA, pB, pC, pubSignals)are standard zkSNARK proof parameters.pubSignals[0] = commitment,pubSignals[1] = recipient
- APP uses the
secretsaved during deposit and the specifiedrecipientto call zk-circuitWithdraw(public: commitment, public: recipient, private: secret), generating the zkProof (standard components:pA,pB,pC,pubSignals). - The zkProof generated by zk-circuit binds
commitment,recipient, and the algorithm that calculatescommitment, ensuring that any modification of values in the zkProof cannot be validated by CONTRACT . - If the
commitmentin the zkProof for thewithdraw(i.e.,pubSignals[0]) does not match thecommitmentfrom the deposit, CONTRACT will reject thewithdraw— either the verifier fails or thecommitmentstatus cannot be properly recognized. This ensures Consistency. - Additionally, because the zkProof binds the
recipient, even if an unexpected failure (e.g., insufficient gas) causes the execution of CONTRACT functionwithdrawto fail, other users cannot steal funds by modifying therecipientparameter. Changing therecipientwould require the APP to regenerate the zkProof, and to do so, thesecretis needed. This ensures Security.
- Parameter explanation:
-
CONTRACT function
withdraw(pA, pB, pC, pubSignals)handles the process:- Checks the
commitmentstatus. - Calls the
verifierto validate the zkProof (pA,pB,pC,pubSignals).- The
verifieris the corresponding contract generated when APP compiles zk-circuit.
- The
- Checks the
Throughout the deposit/withdraw process, to ensure Consistency, both deposit and withdraw expose the commitment, which means the account addresses for performing the deposit and withdrawal are linked. This results in a lack of Privacy.
.
├── circuits/ # Circom ZK circuits
│ └── ZkVaultBasic.circom # Main circuit implementing deposit/withdraw logic
│
├── contracts/ # Solidity smart contracts
│ ├── ZkVaultBasic.sol # ZK-enabled Vault contract
│ └── ZkVaultBasicVerifier.sol # Auto-generated Groth16 verifier contract
│
├── scripts/ # CLI and deployment scripts
│ ├── cli.ts # Command-line interface for deposit/withdraw testing
│ └── deploy.ts # Deploys contracts to local or testnet environments
│
├── test/ # Tests for circuits and contracts
│ ├── utils.hex.test.ts # Unit tests for hex encoding utilities
│ ├── utils.pedersen.test.ts # Unit tests for Pedersen hash implementation
│ ├── ZkVaultBasic.circom.test.ts # Tests for circuit correctness and witness verification
│ └── ZkVaultBasic.sol.test.ts # Tests for smart contract behavior and proof validation
│
├── types/ # Type declarations for external JS/TS libraries
│ ├── circom_tester.d.ts # Types for circom_tester (circuit tester wrapper)
│ └── ffjavascript.d.ts # Types for ffjavascript (bigint/buffer utils)
│
├── utils/ # Utility modules used across scripts and tests
│ ├── hex.ts # Hex encoding/decoding helpers
│ └── pedersen.ts # Pedersen hash implementation (compatible with circom)
│
├── .mocharc.json # Mocha testing framework configuration
├── hardhat.config.ts # Hardhat config for smart contract compilation/deployment
├── package.json # Project dependencies and scripts
└── tsconfig.json # TypeScript configuration- Install Node.js (recommended version: v22)
- Install Circom 2
Installation guide: https://docs.circom.io/getting-started/installation/circom --version
- Install Anvil (local testnet tool)
Installation: https://github.com/foundry-rs/foundry
npm installGenerate r1cs and wasm files:
npm run buildPrecondition: Download powersOfTau28_hez_final_12.ptau and place it in the project root.
- Download: https://storage.googleapis.com/zkevm/ptau/powersOfTau28_hez_final_12.ptau
- If the link is broken, refer to iden3/snarkjs
Run setup to generate proving key, verifying key, and Solidity verifier:
npm run setupCompile Vault and Verifier contracts to EVM-compatible bytecode:
npm run compileIncludes Circom circuit tests and Solidity contract tests:
npm run testDefault endpoint: http://127.0.0.1:8545:
npm run srvCreate a .env file in the root directory, e.g.:
NETWORK = "test"
NODE_URL = "http://127.0.0.1:8545"
MNEMONIC = "<your test mnemonic>"Deploy the contract with 1 ETH denomination:
npm run deploy -- --denomination 1npm run cli -- depositThe command will print a secret — store it securely.
Withdraw using the previously printed secret:
npm run cli -- withdraw --secret <your-secret>Upgrade to zkvault-classic by implementing a Merkle Tree to fully decouple deposit and withdrawal addresses.
This project uses the MIT license. See LICENSE for details.