Exploring Mina Protocol: Building zkApps w/ ‘o1js’

Exploring Mina Protocol: Building zkApps w/ ‘o1js’

A smooth introduction to Mina Protocol

If you are following me on Twitter, you already know that I love Mina Protocol and I published a couple of floods about it.

If not, you can check them out:

Before starting, it is worth noting that this is NOT a paid content.

Contents

  • What is a zero-knowledge proof?

  • Why are ZK proofs important?,

  • What is a zkSNARK?,

  • What is Mina Protocol?

  • zkBridge,

  • What is a zkApp?,

  • General structure of a zkApp,

  • zkApp CLI,

  • How do zkApps work?,

  • Building our first smart contract w/ 'o1js',

  • How to interact?.

What is a zero-knowledge proof?

A zero-knowledge proof is a cryptographic method where one party, the prover, can prove to another party, the verifier, that a statement is true, without conveying any additional information beyond the validity of the statement itself.

In other words, it allows one party to prove to another that they know a certain secret or have certain information without actually revealing the secret or information.

Why are they important?

  • They allow private transactions.

  • They can help to improve blockchain scalability by reducing the computational resources required for verifying transactions.

  • They let us keep confidential information hidden without sacrificing trustlessness.

  • ... and many more.

What is a zkSNARK?

zk-SNARKs represent a form of zero-knowledge proof enabling one party, the prover, to demonstrate knowledge of specific information to another party, the verifier, without revealing the actual content.

This mechanism hinges on specialized cryptographic techniques that enable the prover to generate a compact proof swiftly verifiable by the verifier, all the while persuading them of its validity.

In simpler terms, zk-SNARKs permit the prover to authenticate their awareness of something to the verifier without explicitly disclosing the details. It's akin to proving knowledge of a secret recipe ingredient without explicitly unveiling it. The term "zero-knowledge" emphasizes that the prover can establish their knowledge without imparting any supplementary information beyond this fact.

Moreover, zk-SNARKs are referred to as "succinct" because the evidence produced by the prover is concise enough for rapid verification. This feature is crucial in enabling efficient confirmation by the verifier, obviating the need for extensive computations.

Additionally, zk-SNARKs are described as "non-interactive" since the prover and verifier can independently generate and verify the proof without direct communication. This property renders zk-SNARKs a valuable asset for facilitating secure and anonymous transactions within blockchain systems.

Ultimately, zk-SNARKs serve as a potent cryptographic tool for establishing anonymous and secure transactions on the blockchain. Think of it as possessing a magical "proof machine" capable of demonstrating your knowledge without explicitly disclosing the specifics.

What is Mina Protocol?

Mina is an L1 blockchain based on zero-knowledge proofs (ZKP) with smart contracts written in TypeScript. It is the first cryptocurrency protocol with a succinct blockchain (22KB).

docs.minaprotocol.com

Wait... 22KB? How?

Yes! The entire blockchain state is encapsulated by zkSNARKs. Instead of storing all the data, the network nodes keep this proof. When a new block is generated, a fresh snapshot is created, encompassing the preceding network state and the new block. This lightweight snapshot represents the complete state. This cycle continues with the addition of new blocks. In essence, through the iterative combination of zkSNARKs, the blockchain maintains a consistent size.

What about the historical data? Do we let them disappear?

No! A Mina node has the option to operate as an archive node, storing past transactions. Archive nodes prove beneficial for service operators, like block explorers. However, this historical data isn't necessary for verifying the current consensus state of the protocol.

What is a zkApp?

zkApps, which stands for zero-knowledge apps, denote smart contracts within the Mina Protocol, leveraging zero-knowledge proofs, particularly relying on zkSNARKs.

zkApps employ an execution method situated off the blockchain and primarily rely on an off-chain state framework. This design facilitates confidential computation and state management that can be either private or public.

Hence, the proof is stored on-chain where verification is executed off-chain.

General structure of a zkApp

A zkApp consists of two main ingredients:

  • front-end,

  • smart contract.

Smart contracts for zkApps are written in TypeScript, using 'o1js' (evolution of SnarkyJS).

How do zkApps work?

When building zkApps, the o1js library facilitates the creation of a prover function and a corresponding verifier function during the coding process. The prover function executes custom logic within a user's browser, generating a proof of the executed code. Users interact with the zkApp's interface, providing private and public inputs to the prover function, which in turn produces a zero-knowledge proof. While private inputs aren't needed again, the verifier function, responsible for efficiently validating whether a zero-knowledge proof satisfies all constraints defined in the prover function, must be supplied with public inputs during its operation on the Mina network.

On Mina, the verifier function operates as the final check, ensuring that a zero-knowledge proof adheres to all constraints outlined in the prover function. After the development phase, running the 'npm run build' command compiles the TypeScript code into JavaScript, resulting in the creation of the 'smart_contract.js' file. This file allows developers to execute the prover function and generate a verification key for deploying the smart contract.

Although the prover function runs in a user's browser, the verification key resides on-chain within a specific zkApp account, enabling the Mina network to verify whether a zero-knowledge proof meets all the constraints specified in the prover. The verification key serves as a crucial component for establishing a zkApp account and can be utilized off-chain to verify proofs using the verifier function or the verification key itself.

zkApp CLI

Before starting, please make sure you have already installed the following:

Let us install zkApp CLI now:

npm install -g zkapp-cli

To make sure if zkApp CLI is installed successfully, we can check its version:

zk --version

Now, it's time to create our first zkApp project:

zk project fibonacci

Output:

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

Let's choose 'none' for now.

Success!
Next steps:
  cd fibonacci
  git remote add origin <your-repo-url>
  git push -u origin main

Our project directory is ready! Let us see what we have here:

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

Building our first smart contract w/ o1js

Let us rename src/Add.ts to src/Fibonacci.ts and rewrite the code:

import { Field, SmartContract, state, State, method } from 'o1js';
export class Fibonacci extends SmartContract {
  @state(Field) num1 = State<Field>();
  @state(Field) num2 = State<Field>();
  @state(Field) result = State<Field>();
  init() {
    super.init();
    this.num1.set(Field(0));
    this.num2.set(Field(1));
  }
  @method update() {
    const newResult = this.num1.get().add(this.num2.get());
    this.num1.assertEquals(this.num1.get());
    this.num2.assertEquals(this.num2.get());
    this.result.set(newResult);
    this.num1.set(this.num2.get());
    this.num2.set(newResult);
  }
}

This is our very first smart contract example and let's dive a bit deeply:

  • The code begins with the initialization of a smart contract named 'Fibonacci', implemented using the 'o1js' library for blockchain development.

  • Within the 'Fibonacci' contract, there are three state field variables: 'num1', 'num2', and 'result'. These variables are utilized to maintain the values required for generating the Fibonacci sequence.

  • The 'init' method is responsible for setting the initial values of 'num1' and 'num2' to 0 and 1, respectively, effectively representing the starting point of the Fibonacci sequence.

  • The 'update' method is crucial for generating the subsequent number in the Fibonacci sequence. Here's how the method operates:

    • First, it computes the sum of the current values of 'num1' and 'num2' using the 'add' method.

    • Next, it validates that the values of 'num1' and 'num2' have not changed during the execution to ensure the consistency and reliability of the sequence.

    • Once the validation is complete, it updates the 'result' variable with the newly computed sum.

    • Lastly, it updates the values of 'num1' and 'num2' to prepare for the subsequent iteration. Specifically, 'num1' assumes the value of the prior 'num2', while 'num2' assumes the value of the newly calculated 'result'.

Now let's run the build command:

npm run build

This command compiles our smart contract written in TypeScript to JavaScript.

Now, let's configure it before the deployment:

zk config
  ┌──────────────────────────────────┐
  │  Deploy aliases in config.json   │
  ├────────┬────────┬────────────────┤
  │ Name   │ URL    │ Smart Contract │
  ├────────┴────────┴────────────────┤
  │ None found                       │
  └──────────────────────────────────┘
Enter values to create a deploy alias:
✔ Create a name (can be anything): · fibonacci
✔ Set the Mina GraphQL API URL to deploy to: · https://proxy.berkeley.minaexplorer.com/graphql
✔ Set transaction fee to use when deploying (in MINA): · 0.1
✔ Choose an account to pay transaction fees: · Create a new fee payer key pair
  NOTE: The private key is created on this computer and is stored in plain text.
✔ Create an alias for this account · acc1
✔ Create fee payer key pair at /Users/furkan/.cache/zkapp-cli/keys/acc1.json
✔ Create zkApp key pair at keys/add.json
✔ Add deploy alias to config.json
Success!
Next steps:
  - If this is a testnet, request tMINA at:
    https://faucet.minaprotocol.com/?address=<address>&?explorer=minaexplorer
  - To deploy, run: `zk deploy fibonacci`

Let's cover what we've done above:

  • We set the name as "fibonacci".

  • As we will deploy our smart contract to Berkeley testnet, we put its GraphQL API.

  • We set the transaction fee as 0.1 MINA (could be something else).

  • We created a new account that will deploy the contract and set an alias for it.

After getting some faucet MINA by visiting the suggested faucet link, we can deploy our smart contract to Berkeley network:

zk deploy fibonacci

Output:

✔ Build project
✔ Generate build.json
✔ Choose smart contract
  Only one smart contract exists in the project: Fibonacci
  Your config.json was updated to always use this
  smart contract when deploying to this network.
✔ Generate verification key (takes 1-2 min)
✔ Build transaction
✔ Confirm to send transaction
  Are you sure you want to send (yes/no)? · y
✔ Send to network
Success! Deploy transaction sent.
Next step:
  Your smart contract will be live (or updated)
  as soon as the transaction is included in a block:
  https://berkeley.minaexplorer.com/transaction/<txn-hash>

How to interact?

Let us write a script to check the results:

touch src/main.ts
import { Fibonacci } from './Fibonacci.js';
import {
  Mina,
  PrivateKey,
  AccountUpdate,
} from 'o1js';
console.log('o1js loaded');
const useProof = false;
const Local = Mina.LocalBlockchain({ proofsEnabled: useProof });
Mina.setActiveInstance(Local);
const { privateKey: deployerKey, publicKey: deployerAccount } = Local.testAccounts[0];
const { privateKey: senderKey, publicKey: senderAccount } = Local.testAccounts[1];
// Create a public/private key pair. The public key is your address and where you deploy the zkApp to
const zkAppPrivateKey = PrivateKey.random();
const zkAppAddress = zkAppPrivateKey.toPublicKey();
// create an instance of Fibonacci - and deploy it to zkAppAddress
const zkAppInstance = new Fibonacci(zkAppAddress);
const deployTxn = await Mina.transaction(deployerAccount, () => {
  AccountUpdate.fundNewAccount(deployerAccount);
  zkAppInstance.deploy();
});
await deployTxn.sign([deployerKey, zkAppPrivateKey]).send();
// get the initial state of Fibonacci after deployment
const num0 = zkAppInstance.num1.get();
const num1 = zkAppInstance.num2.get();
console.log('state after init:', num0.toString());
console.log('state after init:', num1.toString());
const txn1 = await Mina.transaction(senderAccount, () => {
  zkAppInstance.update();
});
await txn1.prove();
await txn1.sign([senderKey]).send();
const num2 = zkAppInstance.result.get();
console.log('state after txn1:', num2.toString());
try {
  const txn2 = await Mina.transaction(senderAccount, () => {
    zkAppInstance.update();
  });
  await txn2.prove();
  await txn2.sign([senderKey]).send();
} catch (ex) {
  console.log("Error!");
}
const num3 = zkAppInstance.result.get();
console.log('state after txn2:', num3.toString());
try {
    const txn3 = await Mina.transaction(senderAccount, () => {
      zkAppInstance.update();
    });
    await txn3.prove();
    await txn3.sign([senderKey]).send();
  } catch (ex) {
    console.log("Error!");
  }
  const num4 = zkAppInstance.result.get();
  console.log('state after txn2:', num4.toString());

const txn4 = await Mina.transaction(senderAccount, () => {
  zkAppInstance.update();
});
await txn4.prove();
await txn4.sign([senderKey]).send();
const num5 = zkAppInstance.result.get();
console.log('state after txn3:', num5.toString());
console.log('Shutting down');

This TypeScript code does:

  • start by importing the necessary modules and the 'Fibonacci' smart contract from './Fibonacci.js'.

  • set up the 'o1js' library and initializes a local instance of the Mina blockchain for testing purposes, with the option to enable or disable proofs.

  • define the necessary private and public keys for both the deployer and sender accounts.

  • generate a new public/private key pair, with the public key representing the address where the 'Fibonacci' smart contract will be deployed.

  • create an instance of the 'Fibonacci' smart contract and deploy to the specified address.

  • retrieves and logs the initial state of the 'Fibonacci' contract after deployment, fetching the values of 'num1' and 'num2'.

  • perform a series of transactions is to execute the 'update' method of the 'Fibonacci' contract, with error handling implemented for potential exceptions.

  • log the results of the transactions, displaying the state after each update.

  • finally, log the message 'Shutting down', indicating the end of the script's execution.

Now, let's see what'll happen:

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

Output:

> firstproject@0.1.0 build
> tsc
o1js loaded
state after init: 0
state after init: 1
state after txn1: 1
state after txn2: 2
state after txn2: 3
state after txn3: 5
Shutting down

Just as expected! It printed the initial values of 0 and 1 first. Then printed the next number in each transaction!

Thanks for reading Furkan’s Substack! Subscribe for free to receive new posts and support my work.