Zero-knowledge applications (zkApps) enable privacy-preserving decentralized apps on Mina Protocol. This tutorial uses O1js (formerly SnarkyJS), a TypeScript library for building zk-SNARK circuits, to create a simple zkApp.
Prerequisites
- Basic knowledge of TypeScript/JavaScript.
- Node.js (v18+ recommended).
- Familiarity with Mina Protocol concepts (e.g., zk-SNARKs).
- Terminal/CLI proficiency.
1. Environment Setup
Install Dependencies
npm install -g zkapp-cli # Mina zkApp CLI tool
npm install -g typescript ts-node # TypeScript tools
2. Initialize Project
Create a new zkApp project using the Mina CLI:
zk create my-zkapp --template simple # Use the "simple" template
cd my-zkapp
npm install
Project Structure
my-zkapp/
├── src/
│ ├── contracts/ # zkApp smart contracts
│ ├── tests/ # Test files
│ └── index.ts # Main entry (optional)
├── zkapp.config.json # Configuration
└── package.json
3. Write a zkApp Contract
Create src/contracts/NumberUpdate.ts
:
import {
SmartContract,
State,
state,
method,
PublicKey,
PrivateKey,
} from 'o1js';
export class NumberUpdate extends SmartContract {
@state(Field) number = State<Field>(); // On-chain state
init() {
super.init();
this.number.set(Field(0)); // Initialize state
}
// Method to update the number with a constraint
@method updateNumber(newNumber: Field) {
const currentNumber = this.number.get();
this.number.assertEquals(currentNumber); // Verify current state
newNumber.assertLessThan(Field(100)); // Custom constraint: new number < 100
this.number.set(newNumber); // Update state
}
}
Key Concepts
-
@state
: Declares on-chain state. -
@method
: Defines a zk-SNARK circuit (private computation). -
Field
: A primitive for finite field arithmetic.
4. Compile the Contract
Compile to generate proofs and AVM bytecode:
zk compile src/contracts/NumberUpdate.ts
- Compilation may take 2-10 minutes (generates zk-SNARK keys).
5. Write Tests
Create src/tests/NumberUpdate.test.ts
:
import { Test, expect } from 'zken';
import { NumberUpdate } from '../contracts/NumberUpdate';
import { Field, PrivateKey } from 'o1js';
describe('NumberUpdate', () => {
let zkApp: NumberUpdate;
let deployer: PrivateKey;
beforeAll(async () => {
deployer = PrivateKey.random(); // Test account
});
beforeEach(() => {
zkApp = new NumberUpdate(deployer.toPublicKey());
});
it('updates number correctly', async () => {
await zkApp.compile(); // Ensure contract is compiled
// Deploy
const tx = await Mina.transaction(deployer, () => {
zkApp.deploy();
zkApp.updateNumber(Field(42)); // Update to 42
});
await tx.prove(); // Generate proof
await tx.sign([deployer]).send(); // Submit to testnet
// Verify on-chain state
expect(zkApp.number.get()).toEqual(Field(42));
});
});
Run tests:
zk test
6. Deploy to Mina Network
Configure Network
Update zkapp.config.json
:
{
"networks": {
"berkeley": {
"url": "https://proxy.berkeley.minaexplorer.com/graphql",
"keyPath": "./keys/berkeley.json"
}
}
}
Fund Account & Deploy
- Get testnet MINA from Mina Faucet.
- Deploy:
zk deploy:berkeley src/contracts/NumberUpdate.ts \
--key-file ./keys/berkeley.json
7. Build a Frontend (React Example)
Install dependencies:
npm install react @mina_ui/core
Example component (src/index.tsx
):
import { useState } from 'react';
import { NumberUpdate } from './contracts/NumberUpdate';
import { Mina, PublicKey } from 'o1js';
export default function App() {
const [number, setNumber] = useState<number>(0);
const updateNumber = async () => {
const mina = Mina.connect('https://berkeley.minaexplorer.com');
const contractAddress = PublicKey.fromBase58('YOUR_DEPLOYED_ADDRESS');
const contract = new NumberUpdate(contractAddress);
const tx = await Mina.transaction({ sender: contractAddress }, () => {
contract.updateNumber(Field(number));
});
await tx.prove();
await tx.send();
};
return (
<div>
<input type="number" onChange={(e) => setNumber(Number(e.target.value))} />
<button onClick={updateNumber}>Update Privately</button>
</div>
);
}
8. Advanced Features
Add Privacy with @method.private
@method private validateSecret(secret: Field) {
Poseidon.hash([secret]).assertEquals(this.account.hash);
}
Gas Optimization
- Use
@method({ gasBudget: 0.1 })
to limit gas costs. - Batch proofs using
Mina.transactionBatch()
.
Best Practices
- Testing: Cover all circuit branches with unit tests.
- Security: Audit constraints to prevent invalid state transitions.
-
Gas Costs: Optimize complex circuits with
@method.runUnchecked
.
Conclusion
You’ve built a zkApp that updates a number with privacy guarantees! Expand by:
- Adding more complex business logic.
- Integrating with off-chain oracles.
- Exploring token standards (e.g., zkTokens).
Resources:
Top comments (0)