Skip to main content
info

Please note that zkApp programmability is not yet available on Mina Mainnet, but zkApps can now be deployed to Berkeley Testnet.

note

This tutorial was last tested with Mina zkApp CLI 0.6.4 and SnarkyJS 0.8.0.

Tutorial 1: Hello World

Overview

In this tutorial, we will code a zkApp step by step, from start to finish.

We will write a basic smart contract that stores a number as on-chain state and contains logic to only allow this number to be replaced by its square (e.g. 2 -> 4 -> 16...). We will create this project using the Mina zkApp CLI, write our smart contract code, and then use a local Mina blockchain to interact with it.

Later tutorials will introduce further concepts and patterns. But we hope this helps you get started with SnarkyJS, zkApps, and programming with zero knowledge proofs. Then you can go further by reading the zkApps docs and additional tutorials.

You can find the full source code for this example here.

Setup

First, install the Mina zkApp CLI, if you haven’t already.

Dependencies

You'll need the following installed to use the zkApp CLI:

  • NodeJS 16+ (or 14 using --experimental-wasm-threads)
  • NPM 6+
  • Git 2+

If you have an older version installed, we suggest installing a newer version using the package manager for your system: Homebrew (Mac), Chocolatey (Windows), or apt/yum/etc (Linux). On Linux, you may need to install a recent NodeJS version via NodeSource (deb or rpm), as recommended by the NodeJS Project.

Installation

$ npm install -g zkapp-cli

To confirm it is installed, run:

$ zk --version

This tutorial has been tested as of Mina zkApp CLI version 0.6.4 and SnarkyJS 0.8.0.

Create a new project

Now that you have the tooling installed, we can start building our application.

First, create a new project using:

$ zk project 01-hello-world

This will create a directory named 01-hello-world containing scaffolding for our project, including tools such as Prettier, ESLint, & Jest.

The zkApp CLI will ask if you would like to create an acompnanying ui project. Select none for this tutorial.

? Create an accompanying UI project too? …
next
svelte
nuxt
empty
❯ none

Let's change into this directory and list the contents to see what it created:

$ cd 01-hello-world
$ ls
LICENSE build jest.config.js package-lock.json tsconfig.json
README.md config.json keys package.json
babel.config.cjs jest-resolver.cjs node_modules src

We will be working mostly within the src directory, which will contain the TypeScript code for our smart contract. When we build or deploy (which automatically builds for us), our TypeScript will be compiled into JavaScript inside the build directory.

Preparing the project

We will start by creating files for our project and deleting the default files that come with the new project.

First, delete the old files. Run:

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

And generate the new files for our project. Enter:

$ zk file src/Square
$ touch src/main.ts

The zk file <name> command created both src/Square.ts and src/Square.test.ts for us, but we won't worry about writing tests in this tutorial. We'll use main.ts as a script to interact with the smart contract and observe how it works. In later tutorials, we will see how to interact with a smart contract from the browser, like a typical end user, but for now we'll just use our main.ts script to do this.

Now, let's open src/index.ts in a text editor and change it to look like below. This file contains all exports we want to make available for consumption from outside our smart contract project, such as from a UI.

1 import { Square } from './Square.js';
2
3 export { Square };

Building and running

info

The following commands are included for reference, and will fail if you run them at this point. This is because we haven't written the Square smart contract yet.

To compile the TypeScript code into JavaScript, and run the JavaScript code, you'll type:

$ npm run build
$ node build/src/main.js

The first line creates JavaScript code in the build directory, while the second line runs the code in src/main.ts.

You can also combine these together into one line, such as:

$ npm run build && node build/src/main.js

This will run main if the build is successful.

Write the zkApp Smart Contract

Now, the fun part! Let's write our smart contract: src/Square.ts. Line numbers are provided for convenience. A final version of what we're writing can be found here.

tip

This tutorial will walk through the Square smart contract linked above. We recommend that you copy and paste the entire file from that link, and then follow the tutorial with the finished code in place.

You can also copy and paste the code snippets into your project as you go - to avoid inserting the line numbers into your smart contract, copy these code snippets using the button provided. It will appear at the top right of the snippet box when you mouseover it.

Imports

First, open src/Square.ts in your editor, then add the following at the top of the file:

1 import {
2 Field,
3 SmartContract,
4 state,
5 State,
6 method,
7 } from 'snarkyjs';

What each of these are:

  • Field: The native "number" type in SnarkyJS. You can think of these as unsigned integers. These are the most basic type in SnarkyJS and are what all other SnarkyJS-compatible types are built on top of.
  • SmartContract: The class that creates zkApp smart contracts.
  • state: a convenience decorator used within zkApp smart contracts to create references to state stored on chain in a zkApp account.
  • State: a class used within zkApp smart contracts to create state stored on chain in a zkApp account.
  • method: a convenience decorator used within zkApp smart contracts to create new smart contract methods (i.e. functions). Methods that uses this decorator are the end user's entry points to interacting with our smart contract.

Smart contract class

Now, we will write the smart contract. Write the following in your file:

8
9 export class Square extends SmartContract {
10 @state(Field) num = State<Field>();
11
12 }

This creates a new smart contract called Square with one element of on-chain state named num of type Field. zkApps can have up to 8 fields worth of on-chain state, each storing up to 32 bytes (technically, 31.875 bytes or 255 bit) of arbitrary data. A later tutorial will discuss options for off-chain state.

Now, let's add our init method:

8
9 export class Square extends SmartContract {
10 @state(Field) num = State<Field>();
11
12 init() {
13 super.init();
14 this.num.set(Field(3));
15 }
16
17 }

This method sets up the initial state of the smart contract on deployment.

Since we're extending SmartContract, which has its own initialization to perform, we also need to call super.init() to invoke this function on the base class.

Then we initialize our on-chain state, num, to a value of 3.

We can also optionally specify permissions here.

Lastly, we will add our update function:

14     this.num.set(Field(3));
15 }
16
17 @method update(square: Field) {
18 const currentState = this.num.get();
19 this.num.assertEquals(currentState);
20 square.assertEquals(currentState.mul(currentState));
21 this.num.set(square);
22 }
23 }

The name update is arbitrary, but makes sense for our example. Notice that we use the @method decorator because we intend for this method to be invoked by end users, such as via a zkApp UI or our main.ts script in this case.

This method will contain our logic by which end users are allowed to update our zkApp's account state on chain. In this example, we are saying that if the user provides a number (e.g. 9) to the update() method that is the square of the existing on-chain state referred to as num (e.g. 3), then we'll update the num value stored on chain to the provided value (e.g. 9). If the user provides a number that does not meet these conditions, they will not be able to generate a proof or update the on-chain state.

This is accomplished through using "assertions" within our method. When a user invokes a method on a smart contract, all assertions must be true in order to generate the zero knowledge proof from that smart contract; the Mina network will only accept the transaction and update the on-chain state if the attached proof is valid. This is how we can achieve predictable behavior in an off-chain execution model.

Notice that we have get() and set() methods for retrieving and setting on-chain state. A smart contract retrieves the on-chain account state when it is first invoked if at least one get() exists within it. Similarly, using set() will change the transaction to indicate we want to change this particular on-chain state, but it will only be updated when the transaction is received by the Mina network if it contains a valid authorization (i.e. a proof usually).

Our logic also uses the .mul() method for multiplication of our values stored in fields. You can view all available methods in the SnarkyJS reference. Keep in mind that all functions used inside your smart contract must operate on SnarkyJS compatible data types (e.g. Fields and other types built on top of Fields). This is to say, functions from random NPM packages won't work inside our smart contract, because it's really a zero-knowledge circuit, unless the functions it provides operate on SnarkyJS-compatible data types.

Importantly, data passed as an input to smart contract method in SnarkyJS is private and never seen by the network. But you can also store data publicly on-chain when needed, such as we do with our num in this example. A later tutorial will cover an example leveraging privacy.

This completes the smart contract!

Interacting with our smart contract

Next, we will write a script that interacts with our smart contract, so we can easily test it out, for purposes of this tutorial.

Open up src/main.ts in your editor. A complete version of this file can be found here. Again, we recommend that you copy this whole file over to begin with, and then work through the tutorial steps with the complete file in place.

Imports

Add the following:

1 import { Square } from './Square.js';
2 import {
3 isReady,
4 shutdown,
5 Field,
6 Mina,
7 PrivateKey,
8 AccountUpdate,
9 } from 'snarkyjs';
10

What each of these imports are:

  • isReady: an asynchronous promise that tells us when SnarkyJS is loaded and ready. This is necessary because SnarkyJS contains WASM.
  • shutdown: a function that closes our program.
  • Field: SnarkyJS' unsigned integer type, that we've seen above.
  • Mina: A local Mina blockchain. We will deploy our smart contract to this in order to interact with it as a user would.
  • PrivateKey: a class with functions for manipulating private keys.
  • AccountUpdate: a class that generates a data structure referred to as an AccountUpdate that can update zkApp accounts.

Local Blockchain

Now add the following code to your src/main.ts:

11 await isReady;
12
13 console.log('SnarkyJS loaded')
14
15 const useProof = false;
16
17 const Local = Mina.LocalBlockchain({ proofsEnabled: useProof });
18 Mina.setActiveInstance(Local);
19 const { privateKey: deployerKey, publicKey: deployerAccount } = Local.testAccounts[0];
20 const { privateKey: senderKey, publicKey: senderAccount } = Local.testAccounts[1];
21
22 console.log('Shutting down')
23
24 await shutdown();

Now when you run

$ npm run build && node build/src/main.js

your main function should run!

In production, you'll deploy your zkApp to Mina network. But this "local blockchain" allows us to speed up development and test the behavior of our smart contract locally. This local blockchain also provides pre-funded accounts (e.g. the deployerAccount above). Later tutorials will discuss how to deploy your zkApp to live Mina networks, such as Berkeley Testnet.

note

zkApp programmability is currently available on Berkeley Testnet, Mina's public testnet, which is in its final stages of testing before Mainnet.

Initializing our smart contract

Now, let's expand the main.ts file to initialize our smart contract. Comments are provided to break down each stage.

19 const { privateKey: deployerKey, publicKey: deployerAccount } = Local.testAccounts[0];
20 const { privateKey: senderKey, publicKey: senderAccount } = Local.testAccounts[1];
21
22 // ----------------------------------------------------
23
24 // Create a public/private key pair. The public key is our address and where we will deploy to
25 const zkAppPrivateKey = PrivateKey.random();
26 const zkAppAddress = zkAppPrivateKey.toPublicKey();
27
28 // create an instance of Square - and deploy it to zkAppAddress
29 const zkAppInstance = new Square(zkAppAddress);
30 const deployTxn = await Mina.transaction(deployerAccount, () => {
31 AccountUpdate.fundNewAccount(deployerAccount);
32 zkAppInstance.deploy();
33 });
34 await deployTxn.sign([deployerKey, zkAppPrivateKey]).send();
35
36 // get the initial state of Square after deployment
37 const num0 = zkAppInstance.num.get();
38 console.log('state after init:', num0.toString());
39
40 // ----------------------------------------------------
41
42 console.log('Shutting down')
43
44 await shutdown();

The above code will be similar for any smart contract that you create.

Try running this now with: npm run build && node build/src/main.js. The output should be:

$ npm run build && node build/src/main.js
...
SnarkyJS loaded
state after init: 3
Shutting down

Updating our zkApp account with a transaction

Now let's try updating our local zkApp account with a transaction! Add the following:

38 console.log('state after init:', num0.toString());
39
40 // ----------------------------------------------------
41
42 const txn1 = await Mina.transaction(senderAccount, () => {
43 zkAppInstance.update(Field(9));
44 });
45 await txn1.prove();
46 await txn1.sign([senderKey]).send();
47
48 const num1 = zkAppInstance.num.get();
49 console.log('state after txn1:', num1.toString());
50
51 // ----------------------------------------------------
52
53 console.log('Shutting down')
54
55 await shutdown();

This code creates a new transaction that attempts to update the field to the value 9. Because this follows the rules in the update() function that we are calling on the smart contract, this should pass. And if you run it, it should!

Use npm run build && node build/src/main.js again to run it:

$ npm run build && node build/src/main.js
...
SnarkyJS loaded
state after init: 3
state after txn1: 9
Shutting down

Now let's try adding a transaction that should fail - updating the state to 75. Now that num is in state 9, updating should only be possible with 81.

49 console.log('state after txn1:', num1.toString());
50
51 // ----------------------------------------------------
52
53 try {
54 const txn2 = await Mina.transaction(senderAccount, () => {
55 zkAppInstance.update(Field(75));
56 });
57 await txn2.prove();
58 await txn2.sign([senderKey]).send();
59 } catch (ex: any) {
60 console.log(ex.message);
61 }
62 const num2 = zkAppInstance.num.get();
63 console.log('state after txn2:', num2.toString());
64
65 // ----------------------------------------------------
66
67 console.log('Shutting down')
68
69 await shutdown();

Run this again with npm run build && node build/src/main.js. The output should be:

$ npm run build && node build/src/main.js
...
SnarkyJS loaded
state after init: 3
state after txn1: 9
assert_equal: 75 != 81
state after txn2: 9
Shutting down

And lastly, to show the correct update:

63 console.log('state after txn2:', num2.toString());
64
65 // ----------------------------------------------------
66
67 const txn3 = await Mina.transaction(senderAccount, () => {
68 zkAppInstance.update(Field(81));
69 });
70 await txn3.prove();
71 await txn3.sign([senderKey]).send();
72
73 const num3 = zkAppInstance.num.get();
74 console.log('state after txn3:', num3.toString());
75
76 // ----------------------------------------------------
77
78 console.log('Shutting down');
79
80 await shutdown();

Run this again with npm run build && node build/src/main.js. The output should be:

$ npm run build && node build/src/main.js
...
SnarkyJS loaded
state after init: 3
state after txn1: 9
assert_equal: 75 != 81
state after txn2: 9
state after txn3: 81
Shutting down

Conclusion

Congrats! We have finished building our first zkApp with SnarkyJS.

Checkout Tutorial 2 to learn how to use private inputs and hash functions with SnarkyJS.