Tutorial 1: Hello World
This Hello World tutorial helps you get started with o1js, 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 zkApp CLI
- Write your smart contract code
- Use a simulated 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!
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.
Create or change to a directory where you have write privileges.
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, selectnone
:? Create an accompanying UI project too? …
next
svelte
nuxt
empty
> noneThe 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 mainThe
zk project
command creates the01-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.Change into the
01-hello-world
directory and list the contents:$ cd 01-hello-world
$ lsThe 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.
Prepare the project
Start by deleting the default files that come with the new project.
To delete the old files:
$ rm src/Add.ts
$ rm src/Add.test.ts
$ rm src/interact.tsNow, create the new files for your project:
$ zk file src/Square
$ touch src/main.tsThe
zk file
command created thesrc/Square.ts
andsrc/Square.test.ts
test files.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.
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.
First, open the Square.ts example file.
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.
All functions used inside a smart contract must operate on o1js 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 'o1js';
These items are:
Field
: The native number type in o1js. You can think of field elements as unsigned integers. Field elements are the most basic type in o1js. All other o1js-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 }
Since this code extends 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 o1js Reference documentation.
Finally, this code adds the update()
function:
14 this.num.set(Field(3));
15 }
16
17 @method async update(square: Field) {
18 const currentState = this.num.get();
19 this.num.requireEquals(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 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 asnum
(for example, 3), then update thenum
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 o1js Reference documentation.
You remember that functions in your smart contract must operate on o1js 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 o1js-compatible data types.
Importantly, data passed as an input to a smart contract method in o1js 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.
Interact 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 interact with the smart contract.
Imports
For this tutorial, the import
statement brings in items from o1js
that you use to interact with your smart contract.
Copy the following lines from main.ts example file into the
src/main.ts
file:1 import { Square } from './Square.js';
2 import { Field, Mina, PrivateKey, AccountUpdate } from 'o1js';These import items are:
Field
: The same o1js unsigned integer type that you learned earlier.Mina
: A simulated 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.
Simulated Local Blockchain
Using a simulated local blockchain speeds up development and tests the behavior of your smart contract locally. Later tutorials cover how to use a lightweight Mina network (Lightnet) to test your zkApp before you deploy to live networks.
- The term simulated local blockchain refers to the local testing blockchain you use in the first phase of testing as described here.
- The term Lightnet is used to describe the lightweight Mina network (Lightnet) that is a more accurate representation of the Mina blockchain. See Testing zkApps with Lightnet.
To initialize your simulated local blockchain, add the following code from the main.ts example file to src/main.ts
:
4 const useProof = false;
5
6 const Local = await Mina.LocalBlockchain({ proofsEnabled: useProof });
7 Mina.setActiveInstance(Local);
8
9 const deployerAccount = Local.testAccounts[0];
10 const deployerKey = deployerAccount.key;
11 const senderAccount = Local.testAccounts[1];
12 const senderKey = senderAccount.key;
Tip: To preserve line numbers in your local main.ts
file, add blank lines as needed after you copy the code snippets.
This simulated local blockchain provides pre-funded accounts. Add these lines to create local test accounts with test MINA (tMINA) to use for this tutorial:
16 // ----------------------------------------------------
17
18 // Create a public/private key pair. The public key is your address and where you deploy the zkApp to
19 const zkAppPrivateKey = PrivateKey.random();
20 const zkAppAddress = zkAppPrivateKey.toPublicKey();
Build and run the smart contract
Now that the Square smart contract is complete, these commands run your project as a simulated 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 thebuild
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 insrc/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 an address on the Mina network where you deploy the zkApp to
- Create an instance of your smart contract
Square
and deploy it tozkAppAddress
- Get the initial state of
Square
after deployment
Comments break down each stage:
21
22 // create an instance of Square - and deploy it to zkAppAddress
23 const zkAppInstance = new Square(zkAppAddress);
24 const deployTxn = await Mina.transaction(deployerAccount, async () => {
25 AccountUpdate.fundNewAccount(deployerAccount);
26 await zkAppInstance.deploy();
27 });
28 await deployTxn.sign([deployerKey, zkAppPrivateKey]).send();
29
30 // get the initial state of Square after deployment
31 const num0 = zkAppInstance.num.get();
32 console.log('state after init:', num0.toString());
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
o1js loaded
state after init: 3
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:
33
34 // ----------------------------------------------------
35
36 const txn1 = await Mina.transaction(senderAccount, async () => {
37 await zkAppInstance.update(Field(9));
38 });
39 await txn1.prove();
40 await txn1.sign([senderKey]).send();
41
42 const num1 = zkAppInstance.num.get();
43 console.log('state after txn1:', num1.toString());
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
The expected output is:
> 01-hello-world@0.1.0 build
> tsc
state after init: 3
state after txn1: 9
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, add these next lines of code from the src/main.ts
file and change the state to 75
in zkAppInstance.update(Field(75))
:
44
45 // ----------------------------------------------------
46
47 try {
48 const txn2 = await Mina.transaction(senderAccount, async () => {
49 await zkAppInstance.update(Field(75));
50 });
51 await txn2.prove();
52 await txn2.sign([senderKey]).send();
53 } catch (error: any) {
54 console.log(error.message);
55 }
56 const num2 = zkAppInstance.num.get();
57 console.log('state after txn2:', num2.toString());
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
state after init: 3
state after txn1: 9
Field.assertEquals(): 75 != 81
state after txn2: 9
And finally, be sure to change your main.ts
file to include the correct update to change the state to 81
in zkAppInstance.update(Field(81))
.
Run this command again:
$ npm run build && node build/src/main.js
The expected output is:
> 01-hello-world@0.1.0 build
> tsc
state after init: 3
state after txn1: 9
state after txn2: 81
Follow along
You can follow along in this video as cryptographer, David Wong, learns how to code a Hello World project.
The video is provided for educational purposes and uses earlier versions of the zkApp CLI and o1js, so there are some differences. The Hello World tutorial always uses the most recent version of the zkApp CLI and o1js.
Conclusion
Congratulations! You have successfully completed all of the steps to build your first zkApp with o1js.
Check out Tutorial 2: Private Inputs and Hash Functions to learn how to use private inputs and hash functions with o1js.
Find more tutorials and resources in the zkApps docs.