Documentation

English
  • English
  • Russian
Mina Overview

info

Please note that zkApp programmability is not yet available on Mina, but zkApps can now be deployed to Berkeley QANet. These docs are a preview of work that is currently in progress.

How to write a zkApp

A zkApp consists of a smart contract and a UI to interact with it. First, we’ll install the Mina zkApp CLI and write a smart contract.

Write a smart contract

Your smart contract will be written using Mina zkApp CLI.

Mina zkApp CLI makes it easy to follow recommended best practices by providing project scaffolding including dependencies such as SnarkyJS, a test framework (Jest), code auto-formatting (Prettier), linting (ES Lint), & more.

Install Mina zkApp CLI

Dependencies:

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

tip

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.

Start a project

Now that you have Mina zkApp CLI installed, you can start with an example or start your own project.

Examples are based on the standard project structure, but with additional files in the /src directory as the only difference.

  1. Install: Run zk example sudoku. This creates a new project and includes the example files (i.e. the smart contract) inside the project’s src/ directory. Type ls & hit enter to see the files that were created or open the directory in a code editor such as VS Code.
  2. Run tests: Run npm run test. Tests are written using Jest. After running this command, you should see all tests pass. You can also run npm run testw to run tests in watch mode, so it will automatically re-run tests when saving changes to your code.
  3. Build the example: Run npm run build. This will compile your TypeScript into JavaScript inside the project’s /build directory.
  4. Deploy to QANet: Run zk config, which will walk you through adding a network alias to your project’s config.json. For Berkeley QANet, we recommend using berkeley as the name, 0.1 for the fee, and https://proxy.berkeley.minaexplorer.com/graphql for the url. Then run zk deploy and follow the prompts. See the Deployment section for further details.

You can view a list of all available examples here.

Option B: Start your own project

  1. Install: Run zk project <myproj>. Type ls and hit enter to see the newly created project structure.
  2. Run tests: Run npm run test. Tests are written using Jest. After running this command, you should see all tests pass. You can also run npm run testw to run tests in watch mode, so it will automatically re-run tests when saving changes to your code.
  3. Build: Run npm run build. This will compile your TypeScript code into JavaScript inside the project’s /build.
  4. Deploy to QANet: Run zk config, which will walk you through adding a network alias to your project’s config.json. For Berkeley QANet, we recommend using berkeley as the name, 0.1 for the fee, and https://proxy.berkeley.minaexplorer.com/graphql for the url. Then run zk deploy and follow the prompts. See the Deployment section for further details.
  5. Deploy to Mainnet: (Coming soon.)

Writing your smart contract’s custom logic

The goal of this section is to explain the concepts that you will need to understand to write a zero-knowledge-based smart contract.

If you haven’t yet read the how zkApps work section, please read it first so that this section makes sense.

SnarkyJS

zkApps are written in TypeScript using SnarkyJS. SnarkyJS is a TypeScript library for writing smart contracts based on zero-knowledge proofs for the Mina Protocol. It is included automatically when creating a new project using the Mina zkApp CLI.

To view the full SnarkyJS reference, please see here.

Concepts

Field elements are the basic unit of data in zero-knowledge proof programming. Each field element can store a number up to almost 256 bits in size. You can think of it as a uint256 in Solidity.

note

For the cryptography inclined, the exact max value that a field can store is: 28,948,022,309,329,048,855,892,746,252,171,976,963,363,056,481,941,560,715,954,676,764,349,967,630,336

For example, in typical programming, you might use:

const sum = 1 + 3.

In SnarkyJS, you would write this as:

const sum = new Field(1).add(new Field(3))

This can be simplified as:

const sum = new Field(1).add(3)

Note that the 3 is auto-promoted to a field type to make this cleaner.

Primitive data types

A couple common data types you may use are:

new Bool(x);   // accepts true or false
new Field(x);  // accepts an integer, or a numeric string if you want to represent a number greater than JavaScript can represent but within the max value that a field can store.
new UInt64(x); // accepts a Field - useful for constraining numbers to 64 bits
new UInt32(x); // accepts a Field - useful for constraining numbers to 32 bits
PrivateKey, PublicKey, Signature; // useful for accounts and signing
new Group(x, y); // a point on our elliptic curve, accepts two Fields/numbers/strings
Scalar; // the corresponding scalar field (different than Field)

In the case of Field and Bool, you can also call the constructor without new:

let x = Field(10);
let b = Bool(true);

info

Support for strings within SnarkyJS will be available soon.

Conditionals

Traditional conditional statements are not yet supported by SnarkyJS:

// this will NOT work
if (foo) {
  x.assertEquals(y);
}

Instead, use SnarkyJS’ built-in Circuit.if() method, which is a ternary operator:

const x = Circuit.if(new Bool(foo), a, b); // behaves like `foo ? a : b`
Functions

Functions work as you would expect in TypeScript. For example:

function addOneAndDouble(x: Field): Field {
  return x.add(1).mul(2);
}
Common methods

Some common methods you will use often are:

let x = new Field(4); // x = 4
x = x.add(3); // x = 7
x = x.sub(1); // x = 6
x = x.mul(3); // x = 18
x = x.div(2); // x = 9
x = x.square(); // x = 81
x = x.sqrt(); // x = 9

let b = x.equals(8); // b = Bool(false)
b = x.greaterThan(8); // b = Bool(true)
b = b.not().or(b).and(b); // b = Bool(true)
b.toBoolean(); // true

let hash = Poseidon.hash([x]); // takes array of Fields, returns Field

let privKey = PrivateKey.random(); // create a private key
let pubKey = PublicKey.fromPrivateKey(privKey); // derive public key
let msg = [hash];
let sig = Signature.create(privKey, msg); // sign a message
sig.verify(pubKey, msg); // Bool(true)

For a full list, see the SnarkyJS reference.

Smart Contract

Let’s put this all together and create a basic smart contract. This where you define your custom logic.

Smart contracts are written by extending the base class SmartContract:

class HelloWorld extends SmartContract {}

A smart contract can contain on-chain state, which is declared as a property on the class with the @state decorator:

class HelloWorld extends SmartContract {
  @state(Field) x = State<Field>();
}

Here, x is of type Field. Only types built out of Field can be used for state variables. In the current design, the state can consist of at most 8 Fields of 32 bytes each. States are initialized with the State() function.

The constructor of a SmartContract is inherited from the base class and should not be overriden. It takes the zkApp account address (a public key) as its only argument:

let zkAppKey = PrivateKey.random();
let zkAppAddress = PublicKey.fromPrivateKey(zkAppKey);

let zkApp = new SmartContract(zkAppAddress);
SnarkyJS Deployment Method

There is one method that every smart contract needs: deploy. It describes how the zkApp account will be initialized when the zkApp is deployed:

class HelloWorld extends SmartContract {
  @state(Field) x = State<Field>();

  deploy(initialBalance: UInt64, x: Field) {
    super.deploy();
    this.balance.addInPlace(initialBalance);
    this.x.set(x);
  }
}

The deploy method needs one default parameter, the initial balance of the zkApp account. The first two lines below are mandatory:

super.deploy();
this.balance.addInPlace(initialBalance);

You are free to add additional parameters, like the initial values of your state. State variables can be initialized during deployment using the .set method on State:

this.x.set(x);

Uninitialized values will be zero.

Methods and state updates

To update your zkApp state or to send MINA, your smart contract will need one or more custom methods.

You declare methods using the @method decorator:

class HelloWorld extends SmartContract {
  @state(Field) x = State<Field>();

  // ...

  @method async increment() {
    const x = await this.x.get();
    this.x.set(x.add(1));
  }
}

The increment() method in our example is async because it calls this.x.get(). This is how you can fetch the current state of the zkApp account from the chain.

The method then sets the new state to x + 1 using this.x.set().

The methods .get() and .set() are defined on every state property.

Assertions

Let's modify the increment() method to accept a parameter:

class HelloWorld extends SmartContract {
  @state(Field) x = State<Field>();

  // ...

  @method async increment(xPlus1: Field) {
    const x = await this.x.get();
    x.add(1).assertEquals(xPlus1);
    this.x.set(xPlus1);
  }
}

Here, after obtaining the current state x, we make an assertion: x.add(1).assertEquals(xPlus1);.

If the assertion fails, SnarkyJS will throw an error and not submit the transaction. On the other hand, if it succeeds, it will become a part of the proof that is sent to the chain and that is verified on-chain.

Because of this, our new version of increment() is guaranteed to behave like the previous version: It can only ever update the state x to x + 1.

Assertions can be incredibly useful to constrain state updates. Common assertions you may use are:

x.assertEquals(y); // x = y
x.assertBoolean(); // x = 0 or x = 1
x.assertLt(y);     // x < y
x.assertLte(y);    // x <= y
x.assertGt(y);     // x > y
x.assertGte(y);    // x >= y

For a full list, see the SnarkyJS reference.

Public and private inputs

While the state of a zkApp is public, method parameters are private.

When a smart contract method is called, its code will execute on the client and generate a proof, which is then sent to the network to update the chain. The proof uses zero-knowledge to hide inputs and details of the computation.

The only way method parameters can be exposed is when the computation explicitly exposes them, as in our last example where the input was directly stored in the public state: this.x.set(xPlus1);

Let's show an example where this is not the case, by defining a new method called incrementSecret():

class HelloWorld extends SmartContract {
  @state(Field) x = State<Field>();

  // ...

  @method async incrementSecret(secret: Field) {
    const x = await this.x.get();
    Poseidon.hash(secret).assertEquals(x);
    this.x.set(Poseidon.hash(secret.add(1)));
  }
}

This time, our input is called secret. We check that the hash of our secret is equal to the current state x. If this is the case, we add 1 to the secret and set x to the hash of that.

When running this successfully, it just proves that the code was run with some input secret whose hash is x, and that the new x will be set to hash(secret + 1). However, the secret itself remains private, because it can't be deduced from it's hash.

Managing State

Updating on-chain state

As we saw before, on-chain state can be managed inside smart contract methods using .get() and .set().

Updating off-chain state

info

Off-chain state is not yet available.

Testing your smart contract

Writing automated tests for your smart contract is of crucial importance. The Jest testing framework is included in all projects created by the Mina zkApp CLI via zk project <name> and zk example <name. We recommend Jest, but any testing framework can be used.

Running tests

To run all test files within your project, run npm run test or npm run testw (for watch mode) from your project’s root directory.

To generate a test coverage report for your project, run npm run coverage. Coverage reports show the % of your code that is tested. The result will be output in your terminal. This can be helpful to ensure your code is well tested.

Writing tests

Creating tests for your smart contract is easy using the Mina zkApp CLI. To scaffold a TypeScript file with a corresponding test file, simply run the command zk file foo. This will generate two files, named foo.ts and foo.test.ts. foo.test.ts is a great place to start writing all your smart contract test code. To write good unit tests, it's vital that you concretely understand the functionality your smart contract provides. An example is shown below of a basic test written in Jest:

describe("foo.test.ts", () => {
  describe("test()", () => {
    // your test here
  });
});

Because we are using Jest, it's helpful to break down all functionality of your smart contract into describe blocks. Mapping out your unit tests like this is also a good way of providing documentation to other developers reading your smart contract. An excellent place to start testing your smart contract is in the areas your smart contract modifies its state. Make sure to verify that your state updates in the way you expect.

For examples of existing tests, we recommend creating a template example using the Mina zkApp CLI via zk project <name> and examining the test file there. You will see a basic example of a few tests that deploy and update the state on a smart contract using Jest.

Learn more

Please see the Jest docs for further information on how to use Jest.

We will be adding blockchain-specific testing functionality in the future. Keep an eye on this section for updates.

Deployment

Add a network alias to config.json

Before deploying, we must first define which network that we will deploy to and give it a name. We do this by creating a network alias within your project's config.json.

First, change into the directory containing your project and then run the following command:

$ zk config

It will ask you to specify a name (can be anything), URL to deploy to, & fee (in MINA) used when sending your deploy transaction. The URL is the Mina GraphQL API that will receive your deploy transaction and broadcast it to the Mina network. Note that this URL is important because it also determines which network you're be deploying to (e.g. QANet, Testnet, Mainnet, etc).

For Berkeley QANet, let's use the following values:

  • Network Name: berkeley
  • URL: https://proxy.berkeley.minaexplorer.com/graphql
  • Fee: 0.1

info

If your project contains multiple smart contracts (e.g. `Foo` and `Bar`) that you intend to deploy to the same network, we recommend following a naming pattern such as `berkeley-foo` and `berkeley-bar` for your network alias names.

You will see the following output:

$ zk config

Add a new network:
✔ Choose a name (can be anything): · berkeley
✔ Set URL to deploy to: · https://proxy.berkeley.minaexplorer.com/graphql
✔ Set transaction fee to use when deploying (in MINA): · 0.1
✔ Create key pair at keys/berkeley.json
✔ Add network to config.json

Success!

Next steps:

- If this is a testnet, request tMINA at:
  https://faucet.minaprotocol.com/qanets?address=<YOUR-ADDRESS>
- To deploy, run: `zk deploy berkeley`

Request funds from the faucet

To deploy your zkApp, you will need some funds to pay for transaction fees.

To get funds on the Berkeley network, use the URL that was shown from the CLI output. Visit https://faucet.minaprotocol.com/qanets?address=<YOUR-ADDRESS> and click Request.

You will have to wait a few minutes for the next block to include your transaction, so you'll have tMINA before proceeding to the next step.

Deploying your smart contract

To deploy your smart contract to the network, run the following command:

$ zk deploy berkeley

When running the deploy command, the zkApp CLI will compute a verification key for your zkApp CLI. Computing the verification key can take 1-2 minutes so please be patient. The zkApp CLI will show you the details of the transaction such as the network name, the URL, and the smart contract that will be deployed..

Finally, enter yes or y when prompted, to confirm and send the transaction.

You will see the following output:

$ zk deploy berkeley
✔ Build project
✔ Generate build.json
✔ Choose smart contract
  Only one smart contract exists in the project: Add
  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>

After a few minutes, the transaction will be included in the next block. To see your changes, search for the address that you used on berkeley.minaexplorer.com.

Writing a UI for your smart contract

To allow users to interact with your smart contract, you’ll typically want to build a website UI.

This UI can be written with any framework (e.g. React, Vue, Svelte, etc.) or plain HTML and JavaScript.

info

This is not yet production ready, but is work in progress.

Adding your smart contract as a dependency of the UI

info

You can integrate your smart contract by publishing it to npm and adding it as a
dependency of your UI.

Publish to npm

The index.ts file is the entry point of your project. This file only imports all smart contract classes you want access to, and exports them. This pattern allows you to specify which smart contracts are available to import when consuming your project from npm within your UI.

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

export { YourSmartContract };
  1. Create an npm account: Create one here if you don't have one yet.
  2. Login: To sign in, enter npm login on the command line. You will be prompted to enter your username, password, and email address.
  3. Publish: To publish your package, enter npm publish on the command line at the root of your smart contract project directory. If the package name already exists on npm, you will get an error. You can change the package name by changing the name property in your package.json.

tip

You can check if a package name already exists on npm using the npm search terminal command. To avoid naming collisions, npm allows to publish scoped packages: @your-username/your-package-name. For additional details about publishing packages including scoped packages, see the full npm reference docs.

Consuming your smart contract in your UI

Once you have published your smart contract to npm, you can easily add it to any UI framework of your choosing by importing the package.

  1. Install your smart contract package: To install your package, run the following npm command: npm install your-package-name from the root of your UI project directory. Or if you published a scoped npm package, run npm install @your-username/your-project-name.
  2. Import your smart contract package into the UI using: import { YourSmartContract } from ‘your-smart-contract’; , where YourSmartContract is the named export that you chose in your smart contract.

tip

For a more performant UI, you may want to render your UI before importing and loading your smart contract. This allows the SnarkyJS wasm workers to perform initialization without blocking the UI.

For example, if your UI is built using React, loading the smart contract in a useEffect, instead of a top level import, will give the UI time to render its components before loading SnarkyJS.

Loading your contract with React

useEffect(() => {
  (async () => {
    const { YourSmartContract } = await import("your-smart-contract");
  })();
}, []);

Loading your contract with Svelte

onMount(async () => {
  const { YourSmartContract } = await import("your-smart-contract");
});

Loading your contract with Vue

onMounted(async () => {
  const { YourSmartContract } = await import("your-smart-contract");
});
Enabling the COOP and COEP headers

To successfully load any SnarkyJS code in your UI, you must set the COOP and COEP headers to set up a cross-origin isolated environment that enables the use of SharedArrayBuffer. SnarkyJS relies on the use of SharedArrayBuffer to enable important WebAssembly features.

There are many alternatives available to enable these headers. For example, if you deploy your UI to products like Vercel or Cloudflare Pages, you can set these headers in a custom configuration file. Otherwise, you can set these headers in the server framework of your choice (e.g., Express for JavaScript).

tip

COOP must be set to same-origin.

COEP must be set to require-corp.

Setting COOP and COEP in your Vercel configuration:

To see more details on deploying to Vercel, see their documentation on vercel.json.

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
        { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
      ]
    }
  ]
}

Setting COOP and COEP in your Cloudflare Pages configuration:

To see more details on deploying to Cloudflare Pages, see their documentation on _headers.

/*
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Embedder-Policy: require-corp