Skip to main content

Introduction to zkApps

zkApps are smart contracts that run on the Mina blockchain, powered by zero-knowledge proofs. Unlike traditional smart contracts that execute on-chain, zkApps use an off-chain execution model where computation happens locally and only a proof of correct execution is submitted to the blockchain.

How it works

The SmartContract class exported by o1js is the foundation for building zkApps. It wraps a lot of complex functionality into one developer-friendly package. Fundamentally, the ZkApp is a constraint system just like a ZkProgram. When you call methods, you are creating a proof. When the Mina blockchain confirms your transaction, it is verifying your proof. Unlike Ethereum, there is no on-chain VM, and o1js programs can't be "executed" on Mina. Mina only verifies proofs and applies the relevant state transitions.

Account Updates

In order to interact on chain, ZkApps have to deal with several special types of data. There are public keys of users. There are fees and balances and tokens. There are slots and epochs, and other smart contracts, etc... The language that o1js uses to communicate about all these things is called "Account Updates", and there is a specific article about them here.

info

Smart Contracts interact with the Mina blockchain through AccountUpdates, but there is a limit to how many AccountUpdates can be included in a single transaction. The more account updates are included, the more expensive the transaction is to prove. In order to keep all transaction inclusion proofs small enough to fit into the slot time of ~180 seconds, the limit for account updates is ~7 per transaction. The exact calculation is available here.

Zeko has no account update limit per transaction because they use a centralized sequencer. They can take their time proving expensive transactions because they are sequenced first and then proved.

State

Smart contracts also have state, but only in a limited way. In order to keep the blockchain light, each Smart Contract is limited to 8 Fields of state. Most applications will use these states to store hashes of a larger off-chain storage solution. For summary data like counters, boolean flags, balances, etc... 8 fields is enough.

Interacting with state is done with the .get and .set methods on state variables. This API will generate the correct account updates for you, and hide the associated complexity.

When you .get state, you should always constrain the value you get back from the network. Your proof should only be valid if the network state is as expected when the proof is later verified. To do this, use .getAndRequireEquals() or .get() and .requireEquals(). This API will add preconditions to the Account Update that the state at verification time is the same as the state at proving time.

Methods

ZkApp methods are decorated with @method, which tells o1js that this method ought to be part of the constraint system constructed for the ZkApp. You can add other class methods to a SmartContract that aren't a part of the constraint system like toString(). @methods must be fully provable like a ZkProgram.

Basic zkApp Structure

Every zkApp is built using the SmartContract class from o1js:

export class MyZkApp extends SmartContract {
@state(Field) count = State<Field>();

init() {
super.init();
this.count.set(Field(0));
}

// @method decorator means that this method must make a valid constraint system
// @methods cannot accept any non-provable types as method parameters
@method
async increment(value: Field) {
// o1js allows you to make assertions about the input
value.assertGreaterThan(10);

// getAndRequireEquals generates an AccountUpdate for the SmartContract to assert that the on-chain value is correct
// This is how we can safely access account state off-chain. If the app state changes between proof time, and
// verification time, then this transaction will fail.
const currentCount = this.count.getAndRequireEquals();

// .set() will edit the existing AccountUpdate for the SmartContract to set the new value of the account state
this.count.set(currentCount.add(value));
}

// methods that are not decorated with @method are just normal TS methods. No need to keep it provable.
toNumber() {
const count = this.count.get();
return Number(count.toBigInt());
}
}

Key Concepts

  • State Management: Store state on-chain using the @state decorator
  • Methods: Public methods marked with @method generate proofs when called
  • Proofs: Zero-knowledge proofs validate execution and are submitted to blockchain
  • Permissions: Control who can update state, emit events, and modify contracts