How to code a ZK App (each step explained) so you can create privacy preserving apps

Alysia Tech
8 min readMay 29, 2023

With this tutorial, you’ll go from ZERO to ZK (Zero Knowledge) in just 4 steps. You are going to be coding in Typescript today because zero knowledge (zk) apps on Mina are written using a Typescript library called SnarkyJS.

P.s. this tutorial assumes you’re using a Mac but please google the alternative commands for Windows machines.

You can also watch this tutorial on youtube or continue reading below.

Video tutorial for coding your own ZK app using Typescript and SnarkyJS

1. Set up your developer environment

Before you begin coding, install the Mina zkApp CLI. The Mina zkApp CLI allows you to scaffold, write, test, & deploy zkApps or Mina Protocol using recommended best practices.

CLI stands for Command Line Interface. It is a type of interface that allows users to interact with a computer program or system by typing commands into a text-based terminal.

Open the terminal app and write the following command:

npm install -g zkapp-cli

You can test whether the zkapp-cli works by running this command:

zk help

If you don’t have node installed, install it using this command:

brew update && brew install node

If you get stuck on the node install, follow this guide https://treehouse.github.io/installation-guides/mac/node-mac.html

2. Define your zkSnark Circuit

Let’s create an age verification app which lets users enter their date of birth privately and the app verifies that they are old enough to use the app. If they are, there are able to answer a simple math quiz and update the smart contract with the answer. A ZK Proof is used here so that a user can prove their age without revealing their exact birth date.

2.1 Start Your ZK Project

Now that we have our tools, we are ready to start a project to write our code. Run the following command:

zk project myFirstZKApp

We’ll use the terminal as the UI for this project so make you select nonein the first prompt, “Create an accompanying UI project too”. You can select the defaults for the prompts thereafter.

terminal prompt after you call the command zk project, select none to the questoin “Create an accompanying UI project too”

myFirstZKApp is the project name and can be replaced by any name you choose.

  • This command creates a directory containing a new project template, fully set up & ready for local development.
  • A Git repo will be initialized in the project directory automatically.

2.2 Explore the Project

Let’s explore the project via the terminal, change the directory of the terminal and then list the project files.

cd myFirstZKApp && ls

3. Write Your Smart contract

You can follow the code on github too. https://github.com/alysiahuggins/youtube_tutorials/tree/main/myZKApp

3.1 Creating the smart contract file

To create the smart contract file, type the command:

zk file src/AgeVerifier

Also let’s delete the old files that were downloaded as part of the project

rm src/Add.ts && rm src/Add.test.ts && rm src/interact.ts

There’s a file called src/index.ts. The src/index.ts file contains all of the exports you want to make available for use from outside your smart contract project, for example, the UI.

Open Visual Studio Code and open the project files. Open src/index.ts, and change the imports to have the new file.

import { AgeVerifier } from './AgeVerifier.js';

export { AgeVerifier };

We’ll write the smart contract code in the AgeVerifier smart contract. Let’s import packages and dependencies from snarkyjsto use in your smart contract.

3.2 Writing the Smart Contract code

import {
Field,
CircuitString,
SmartContract,
state,
State,
method,
} from 'snarkyjs';
  • Field: Field is the data type for number used in SnarkyJS. It’s a basic building block for creating other types of data.
  • SmartContract: This class creates zkApp smart contracts.
  • state: This is a way to store data in a zkApp account on the blockchain. It’s used as a decorator to reference state stored on-chain in a zkApp account.
  • State: A class used in zkApp smart contracts to create state stored on-chain in a zkApp account.
  • method: This is a way to create functions in zkApp smart contracts that can be called by users. It’s also used as a decorator.

A class is a template for creating objects. It’s a way to define a new type of data with its own set of attributes (also known as properties) and behaviors (also known as methods).

A decorator allows you to add additional functionality to a function or class without modifying its original code. A decorator is applied by placing the decorator function’s name immediately before the function or class definition, typically using the “@” symbol.

Add the following code which defines the gmApp module and add the state of the file which is the last gm message.

export class AgeVerifier extends SmartContract {
@state(Field) ageLimit = State<Field>();
}

Smart Contracts on Mina can have on chain state but it’s limited to 8 state variables. In other tutorials, we’ll address how to support more variables in the smart contract state.

3.3 Define your zkProof

The next step is to define your zkSnark circuits.

A zkSnark circuit is a type of zero-knowledge proof that allows you to verify that certain computations have been performed correctly, without revealing any sensitive information.

To create a zkApp, you will need to define your zkSnark circuits, which will specify the computations that your application will perform. The Mina Protocol provides a circuit language called Snarky, which you can use to define your circuits.

The circuit will have a constraint which determines validity of the proof. So the constraint will ensure that the input is a date of birth which is an age over the ageLimit, which is 18 in this case. If the person is old enough to use the app, they are allowed to update the ageLimit to any number, 18 or more.

We’ll add this constraint the prover function that we’ll write.

@method verifyAge(userAge: Field, ageLimitUpdate: Field){
const currentAgeLimit = this.ageLimit.getAndAssertEquals();

userAge.assertGreaterThanOrEqual(currentAgeLimit);
ageLimitUpdate.assertGreaterThanOrEqual(Field(18));

this.ageLimit.set(ageLimitUpdate);
}

const currentAgeLimit = this.ageLimit.getAndAssertEquals(); gets the current agelimit which is stored on the smart contract and verifies that the one executing in the zk app client is the one that’s executing on chain.

userAge.assertGreaterThanOrEqual(currentAgeLimit); is where we check that the age that the user entered is indeed above the ageLimit. Once this is condition is satisfied, a valid proof is generated.

ageLimitUpdate.assertGreaterThanOrEqual(Field(18)); makes sure that the new age limit is not less than 18.

3.4 Initialize State

Let’s initialize the smart contract state so that if can be referenced in the prover function that we just wrote called verifyAge. You initialize on-chain state in the init() method.

export class AgeVerifier extends SmartContract {
@state(Field) ageLimit = State<Field>();

init(){
super.init();
this.ageLimit.set(Field(18));
}
}

You must call super.init() to set your entire state to 0. The init() method is called when you deploy your zkApp for the first time. It won’t be called if you upgrade your contract and deploy a second time.

this.ageLimit.set(Field(18)) sets the state variable named ageLimit as 18 which is stored on chain on the smart contract when the smart contract is deployed.

4. Test your ZK App

Unlike Ethereum, Smart Contracts on Mina execute offchain. The result of such an off-chain execution is a transaction, which can be sent to the Mina network to apply the changes made by the smart contract. To test you ZK App, you’ll write tests in the file called AgeVerifier.ts that was automatically generated when you created the AgeVerifier smart contract after typing the command zk file AgeVerifier in step 3.1.

Copy and paste the following code into AgeVerifier.test.ts.

import { AgeVerifier } from './AgeVerifier';
import { Field, Mina, PrivateKey, PublicKey, AccountUpdate } from 'snarkyjs';

/*
* This file specifies how to test the `Add` example smart contract. It is safe to delete this file and replace
* with your own tests.
*
* See https://docs.minaprotocol.com/zkapps for more info.
*/

let proofsEnabled = false;

describe('AgeVerifier', () => {
let deployerAccount: PublicKey,
deployerKey: PrivateKey,
senderAccount: PublicKey,
senderKey: PrivateKey,
zkAppAddress: PublicKey,
zkAppPrivateKey: PrivateKey,
zkApp: AgeVerifier;

beforeAll(async () => {
if (proofsEnabled) await AgeVerifier.compile();
});

beforeEach(() => {
const Local = Mina.LocalBlockchain({ proofsEnabled });
Mina.setActiveInstance(Local);
({ privateKey: deployerKey, publicKey: deployerAccount } =
Local.testAccounts[0]);
({ privateKey: senderKey, publicKey: senderAccount } =
Local.testAccounts[1]);
zkAppPrivateKey = PrivateKey.random();
zkAppAddress = zkAppPrivateKey.toPublicKey();
zkApp = new AgeVerifier(zkAppAddress);
});

async function localDeploy() {
const txn = await Mina.transaction(deployerAccount, () => {
AccountUpdate.fundNewAccount(deployerAccount);
zkApp.deploy();
});
await txn.prove(); //goes through your transaction, and creates proofs for all the account updates that came from method calls
// this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization
await txn.sign([deployerKey, zkAppPrivateKey]).send();
}

it('generates and deploys the `AgeVerifier` smart contract', async () => {
await localDeploy();
const num = zkApp.ageLimit.get();
expect(num).toEqual(Field(18));
});

it('correctly updates the num state on the `AgeVerifier` smart contract', async () => {
await localDeploy();

// update transaction
const txn = await Mina.transaction(senderAccount, () => {
zkApp.verifyAge(Field(20), Field(21));
});
await txn.prove();
await txn.sign([senderKey]).send();

const updatedNum = zkApp.ageLimit.get();
expect(updatedNum).toEqual(Field(21));
});

it('correctly fails when the person is younger than the agelimit ', async () => {
await localDeploy();

// update transaction
try{
const txn = await Mina.transaction(senderAccount, () => {
zkApp.verifyAge(Field(16), Field(20));
});
await txn.prove();
await txn.sign([senderKey]).send();
}catch(e){
console.log(e)
}

const updatedNum = zkApp.ageLimit.get();
expect(updatedNum).toEqual(Field(18));
});

it('correctly fails when the person tries to update the ageLimit to a number that is too low ', async () => {
await localDeploy();

// update transaction
try{
const txn = await Mina.transaction(senderAccount, () => {
zkApp.verifyAge(Field(18), Field(16));
});
await txn.prove();
await txn.sign([senderKey]).send();
}catch(e){
console.log(e)
}

const updatedNum = zkApp.ageLimit.get();
expect(updatedNum).toEqual(Field(18));
});
});
  • proofsEnable = false so that the code is not compiled in the test run. Because we are testing this locally, there’s no need to actually compile the code. When you compile the code, that’s when you create the proofs required for running the methods and the verification key, which is required to deploy your zk app.
  • beforeEach(()=>{}) before each method, we’ll create a Mina blockchain and set an active instance as local, and then we’ll create two accounts. deployerAccount is used for simply deploying the smart contract. senderAccount is used for interacting with the smart contract. The deployer account is being funded so that it can pay gas fees, and then it is going to do ZK app deploy.
  • the localDeploy() function is the code that runs in each test that deploys the smart contract locally. tx.provegoes through your transaction and looks for anything that’s happening and making sure the transaction is valid, such as account updates. The transaction may not only be responsible for validating a proof such as a ZK proof that you create, but they also need to validate other things within the transaction, such as, “does this person have enough funds in their account to be able to deploy this transaction?”, for example.
  • the tests purpose are explained by its labels in the code but in each test, the method verify age is called each time with parameters for test case.

5. What’s Next?

Now that we’ve done that, we’ve literally written a very simple ZK proof. You can deploy the smart contract to the blockchain. To deploy the smart contract to the Mina testnet, berkeley, and send transactions to it. Watch this video from time 13:24.

Also, check out the Mina ZK app incubator programme starting June 14th, 2023. There’s 500K USDC and 500K Mina available in funding 💰 and support 🤝.

Mina ZK Ignite June 2023

--

--