Skip to main content
info

zkApp programmability is not yet available on the Mina Mainnet. You can get started now by deploying zkApps to the Berkeley Testnet.

Tutorial 1: Hello World

This Hello World tutorial helps you get started with SnarkyJS, zkApps, and programming with zero-knowledge proofs.

In this step-by-step tutorial, you learn to code a zkApp from start to finish.

You will:

  • Write a basic smart contract that stores a number as on-chain state.
  • The contract logic allows this number to be replaced only by its square; for example, 3 -> 9 -> 81, and so on.
  • Create a project using the Mina zkApp CLI
  • Write your smart contract code
  • Use a local Mina blockchain to interact with your smart contract.

Later tutorials introduce more concepts and patterns.

The full source code for this tutorial is provided in the examples/zkapps/01-hello-world directory on GitHub. While you're there, give the /docs2 repository a star so that other zk developers can learn to build a zkApp!

info

To prevent copying line numbers and command prompts as shown in the examples, use the copy code to clipboard button that appears at the top right of the snippet box when you hover over it.

Prerequisites

Ensure your environment meets the Prerequisites for zkApp Developer Tutorials.

In particular, make sure you have the zkApp CLI installed:

$ npm install -g zkapp-cli

Create a new project

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

  1. Create or change to a directory where you have write privileges.

  2. Now, create a project using the zk project command:

    $ zk project 01-hello-world

    The zk project command has the ability to scaffold the UI for your project. For this tutorial, select none:

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

    The expected output is:

    ✔ Create an accompanying UI project too? · none
    ✔ UI: Set up project
    ✔ Initialize Git repo
    ✔ Set up project
    ✔ NPM install
    ✔ NPM build contract
    ✔ Set project name
    ✔ Git init commit

    Success!

    Next steps:
    cd 01-hello-world
    git remote add origin <your-repo-url>
    git push -u origin main

    The zk project command creates the 01-hello-world directory that contains the scaffolding for your project, including tools such as the Prettier code formatting tool, the ESLint static code analysis tool, and the Jest JavaScript testing framework.

  3. Change into the 01-hello-world directory and list the contents:

    $ cd 01-hello-world
    $ ls

    The output shows these results:

    LICENSE
    README.md
    babel.config.cjs
    build
    config.json
    jest-resolver.cjs
    jest.config.js
    keys
    node_modules
    package-lock.json
    package.json
    src
    tsconfig.json

For this tutorial, you run commands from the root of the 01-hello-world directory as you work in the src directory on files that contain the TypeScript code for the smart contract. Each time you make updates, then build or deploy, the TypeScript code is compiled into JavaScript in the build directory.

Preparing the project

Start by deleting the default files that come with the new project.

  1. To delete the old files:

    $ rm src/Add.ts
    $ rm src/Add.test.ts
    $ rm src/interact.ts
  2. Now, create the new files for your project:

    $ zk file src/Square
    $ touch src/main.ts
  • The zk file command created the src/Square.ts and src/Square.test.ts test files.
  • However, this tutorial does not include writing tests, so you just use the main.ts file as a script to interact with the smart contract and observe how it works.

In later tutorials, you learn how to interact with a smart contract from the browser, like a typical end user.

  1. Now, open src/index.ts in a text editor and change it to look like:

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

    The src/index.ts file contains all of the exports you want to make available for consumption from outside your smart contract project, such as from a UI.

Write the zkApp Smart Contract

Now, the fun part! Write your smart contract in the src/Square.ts file.

Line numbers are provided for convenience. A final version of the smart contract is provided in the Square.ts example file.

This part of the tutorial walks you through the Square smart contract code already completed in the src/Square.ts example file.

Copy the example

This tutorial describes each part of the completed code in the Square.ts example file.

  1. First, open the Square.ts example file.

  2. Copy the entire contents of the file into your smart contract in the src/Square.ts file.

Now you are ready to review the imports in the smart contract.

Imports

The import statement brings in other packages and dependencies to use in your smart contract.

info

All functions used inside a smart contract must operate on SnarkyJS compatible data types: Field types and other types built on top of Field types.

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

These items are:

  • Field: The native number type in SnarkyJS. You can think of Field elements as unsigned integers. Field elements are the most basic type in SnarkyJS. All other SnarkyJS-compatible types are built on top of Field elements.
  • SmartContract: The class that creates zkApp smart contracts.
  • state: A convenience decorator used in zkApp smart contracts to create references to 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: A convenience decorator used in zkApp smart contracts to create smart contract methods like functions. Methods that use this decorator are the end user's entry points to interacting with a smart contract.

Smart contract class

Now, review the smart contract in the src/Square.ts file.

The smart contract called Square has one element of on-chain state named num of type Field as defined by following code:

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

zkApps can have up to eight fields of on-chain state. Each field stores up to 32 bytes (technically, 31.875 bytes or 255 bits) of arbitrary data. A later tutorial covers options for off-chain state.

Now, this code adds the init method to set up the initial state of the smart contract on deployment:

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 }

Since you're extending SmartContract that has its own initialization to perform, calling super.init() invokes this function on the base class.

Then, this.num.set(Field(3)) initializes the on-chain state num to a value of 3.

You can optionally specify permissions. See setPermissions in the SnarkyJS Reference documentation.

Finally, this code adds the 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 function name update is arbitrary, but it makes sense for this example. Notice how the @method decorator is used because it is intended to be invoked by end users by using a zkApp UI, or as in this case, the main.ts script.

This method contains the logic by which end users are allowed to update the zkApp's account state on chain.

A zkApp account is an account is on the Mina blockchain where a zkApp smart contract is deployed. A zkApp account has a verification key associated with it.

In this example, the code specifies:

  • If the user provides a number (for example, 9) to the update() method that is the square of the existing on-chain state referred to as num (for example, 3), then update the num value that is stored on-chain to the provided value (in this case, 9).
  • If the user provides a number that does not meet these conditions, they are unable to generate a proof or update the on-chain state.

These update conditions are accomplished by using assertions within the method. When a user invokes a method on a smart contract, all assertions must be true to generate the zero-knowledge proof from that smart contract. The Mina network accepts the transaction and updates the on-chain state only if the attached proof is valid. This assertion is how you can achieve predictable behavior in an off-chain execution model.

Notice that get() and set() methods are used 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() changes the transaction to indicate that changes to this particular on-chain state are updated only when the transaction is received by the Mina network if it contains a valid authorization (usually, a valid authorization is a proof).

The logic also uses the .mul() method for multiplication of the values stored in Field types. You can view all available methods in the SnarkyJS Reference documentation.

You remember that functions in your smart contract must operate on SnarkyJS compatible data types: Field types and other types built on top of Field types. Because a smart contract is really a zero-knowledge circuit, functions from random NPM packages work inside a smart contract only if the functions the contract provides operate on SnarkyJS-compatible data types.

Importantly, data passed as an input to a smart contract method in SnarkyJS is private and never seen by the network.

You can also store data publicly on-chain when needed, like num in this example. A later tutorial covers an example that leverages privacy.

Congratulations, you have reviewed the complete smart contract code.

Interacting with a smart contract

Next, write a script that interacts with your smart contract. As before, the complete main.ts example file is provided. Follow these steps to build the main.ts file so you can can interact with the smart contract.

Imports

For this tutorial, the import statement brings in items from snarkyjs that you use to interact with your smart contract.

  1. Copy the following lines from main.ts example file into the src/main.ts file:
1 import { Square } from './Square.js';
2 import {
3 isReady,
4 shutdown,
5 Field,
6 Mina,
7 PrivateKey,
8 AccountUpdate,
9 } from 'snarkyjs';
10

These import items are:

  • isReady: An asynchronous promise that resolves when SnarkyJS is loaded and ready. This field is required because SnarkyJS contains WebAssembly (Wasm).
  • shutdown: A function that closes the program.
  • Field: The same SnarkyJS unsigned integer type that you learned earlier.
  • Mina: A local Mina blockchain to deploy the smart contract to so you can interact with it as a user would.
  • PrivateKey: A class with functions for manipulating private keys.
  • AccountUpdate: A class that generates a data structure that can update zkApp accounts.

Local Blockchain

Using a local blockchain speeds up development and tests the behavior of your smart contract locally. Later tutorials cover how to deploy a zkApp to live Mina networks.

To initialize your local blockchain, add the following code from the main.ts example file to 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();

This local blockchain also provides pre-funded accounts. These lines create local test accounts with test MINA to use for this tutorial:

const { privateKey: deployerKey, publicKey: deployerAccount } = Local.testAccounts[0];
const { privateKey: senderKey, publicKey: senderAccount } = Local.testAccounts[1];

Tip: To preserve line numbers in your local main.ts file, add blank lines as needed after you copy the code snippets.

Building and running

Now that the Square smart contract is complete, these commands run your project as a local blockchain.

To compile the TypeScript code into JavaScript:

$ npm run build

To run the JavaScript code:

$ node build/src/main.js

You have the option to combine these commands into one line:

npm run build && node build/src/main.js
  • The npm run build command creates JavaScript code in the build directory.
  • The && operator links two commands together. The second command runs only if the first command is successful.
  • The node build/src/main.js command runs the code in src/main.ts.

Initialize your smart contract

To initialize your smart contract, add more code from the main.ts example file to the src/main.ts file.

All smart contracts that you create with the zkApp CLI use similar code:

  • Create a public/private key pair; the public key is your address and where you deploy the zkApp to
  • Create an instance of your smart contract Square and deploy it to zkAppAddress
  • Get the initial state of Square after deployment

Comments 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 your address and where you deploy the zkApp 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();

Try running this command again:

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

The expected output is:

> 01-hello-world@0.1.0 build
> tsc

SnarkyJS loaded
state after init: 3
Shutting down

Update your zkApp account with a transaction

To update your local zkApp account with a transaction, add the following code to the src/main.ts file:

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 of the rules in the update() function that is called on the smart contract, this command succeeds when you run it again:

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

Add a transaction that fails

It's time to do some testing. To add a transaction that fails, add more code from the main.ts example file to the src/main.ts file.

The contract logic allows the number that is stored as on-chain state to be replaced only by its square. Now that num is in state 9, updating is possible only with 81.

To test a failure, update the state to 75 in zkAppInstance.update(Field(75)):

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();

Try running this command again:

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

The expected output is:

$ 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 finally, be sure to change your main.ts file to include 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 command again:

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

The expected output is:

$ 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

Congratulations! You have successfully completed all of the steps to build your first zkApp with SnarkyJS.

Check out Tutorial 2: Private Inputs and Hash Functions to learn how to use private inputs and hash functions with SnarkyJS.

Find more tutorials and resources in the zkApps docs.