DEV Community

uratmangun
uratmangun Subscriber

Posted on

Step-by-Step Guide to Building a zkApp with O1js

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

  1. Basic knowledge of TypeScript/JavaScript.
  2. Node.js (v18+ recommended).
  3. Familiarity with Mina Protocol concepts (e.g., zk-SNARKs).
  4. 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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Project Structure

my-zkapp/ ├── src/ │ ├── contracts/ # zkApp smart contracts │ ├── tests/ # Test files │ └── index.ts # Main entry (optional) ├── zkapp.config.json # Configuration └── package.json 
Enter fullscreen mode Exit fullscreen mode

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 } } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode
  • 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)); }); }); 
Enter fullscreen mode Exit fullscreen mode

Run tests:

zk test 
Enter fullscreen mode Exit fullscreen mode

6. Deploy to Mina Network

Configure Network

Update zkapp.config.json:

{ "networks": { "berkeley": { "url": "https://proxy.berkeley.minaexplorer.com/graphql", "keyPath": "./keys/berkeley.json" } } } 
Enter fullscreen mode Exit fullscreen mode

Fund Account & Deploy

  1. Get testnet MINA from Mina Faucet.
  2. Deploy:
zk deploy:berkeley src/contracts/NumberUpdate.ts \ --key-file ./keys/berkeley.json 
Enter fullscreen mode Exit fullscreen mode

7. Build a Frontend (React Example)

Install dependencies:

npm install react @mina_ui/core 
Enter fullscreen mode Exit fullscreen mode

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>  ); } 
Enter fullscreen mode Exit fullscreen mode

8. Advanced Features

Add Privacy with @method.private

@method private validateSecret(secret: Field) { Poseidon.hash([secret]).assertEquals(this.account.hash); } 
Enter fullscreen mode Exit fullscreen mode

Gas Optimization

  • Use @method({ gasBudget: 0.1 }) to limit gas costs.
  • Batch proofs using Mina.transactionBatch().

Best Practices

  1. Testing: Cover all circuit branches with unit tests.
  2. Security: Audit constraints to prevent invalid state transitions.
  3. 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)