Skip to main content
info

Please note that zkApp programmability is not yet available on Mina Mainnet, but zkApps can now be deployed to Berkeley Testnet.

note

This tutorial was last tested with SnarkyJS 0.8.0.

Tutorial 2: Private Inputs and Hash Functions

Overview

In our previous tutorial, Hello World, we saw how to build a basic zkApp smart contract in SnarkyJS, with a single state variable, that could be updated if a user knew the square of that number.

In this tutorial, we will discuss private inputs and hash functions.

With a zkApp, a user's local device generates one or more zero knowledge proofs, which are then verified by the Mina network. Each method in a SnarkyJS smart contract corresponds to constructing a proof.

As such, all inputs to a smart contract are private by default, and never seen by the blockchain, unless the developer chooses to store those values as on-chain state in the zkApp account.

We will build a smart contract with a piece of private state, that can be modified if a user knows the private state.

Setup

The following steps assume you've installed dependencies to your machine as described in the previous tutorial — if not, please do that first.

Now, setup a new project with:

$ zk project 02-private-inputs-and-hash-functions

Delete the default generated files by running:

$ rm src/Add.ts
$ rm src/Add.test.ts
$ rm src/interact.ts

And create new files:

$ zk file src/IncrementSecret
$ touch src/main.ts

And lastly, change index.ts to:

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

export { IncrementSecret };
tip

You can find the complete source code of this project here. We recommend that you copy the entire main.ts and IncrementSecret.ts files into your project now, and follow this tutorial with the code in place.

Writing our smart contract

Now we'll build the smart contract for our application.

Begin by writing:

  1 import { Field, SmartContract, state, State, method, Poseidon } from 'snarkyjs';
2
3 export class IncrementSecret extends SmartContract {
4 @state(Field) x = State<Field>();
5 }

This just adds the basic setup for our smart contract — see Tutorial 01 Hello World for more on this.

Now, we will add an initState() method. This is intended to run once to set up the initial state on the zkApp account.

...
5
6 @method initState(salt: Field, firstSecret: Field) {
7 this.x.set(Poseidon.hash([ salt, firstSecret ]));
8 }
9 }

Our initState() method accepts our secret, and a value called a "salt", which we will discuss later.

Note that these inputs to our initState() method are private to whomever initializes the contract. Nobody looking at the zkApp account on the chain can see or know what values firstSecret or salt actually are.

Next we will add a method to update our state:

...
9
10 @method incrementSecret(salt: Field, secret: Field) {
11 const x = this.x.get();
12 this.x.assertEquals(x);
13
14 Poseidon.hash([ salt, secret ]).assertEquals(x);
15 this.x.set(Poseidon.hash([ salt, secret.add(1) ]));
16 }
17 }

Mina uses the Poseidon hash function, which has been optimized for fast performance inside zero knowledge proof systems. The Poseidon hash function takes in an array of Fields, and returns a single Field as output.

In this smart contract, we use both a secret number, and a second Field, known as a "salt".

In the incrementSecret() method, we check that the hash of the salt and 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 the salt and this new secret. SnarkyJS creates a proof of this fact, and a JSON description of the state updates to be made on the zkApp account, such as to store our new hash value, which together form a transaction that can be sent to the Mina network to update the zkApp account.

Because zkApp smart contracts are run off chain, our salt and secret remain private and are never transmitted anywhere. Only the result, updating x on-chain state to hash([ salt, secret + 1]) is revealed. Because the salt and secret can't be deduced from their hash, they remain private.

But why the extra salt argument?

The extra salt argument is added to avoid a possible attack on our smart contract. If we were to just use secret, it would be vulnerable to discovery by an attacker. To do this, our attacker could try hashing likely secrets and then check if the hash matches the hash stored in the smart contract. If the hash were to match, then the attacker would have known they had discovered the secret. This is particularly concerning if the secret is likely to be within a particular subset of possible values, say between 1 and 10,000. In that case, with just 10,000 hashes, the attacker could discover our secret.

What we can do to fix this, is add a second value as an input to our hash function, known as a "salt". The salt will be known only to us and is typically random for optimal security. Using this second value as an additional input to our hash function ensures that an attacker cannot simply brute force attack our secret by generating hashes for likely values of it, because too many possibilities exist.

Main

Our src/main.ts is similar to our last tutorial — for a full version of src/main.ts, see here.

Key parts to discuss though, are initializing our contract, and using the poseidon hash.

Our smart contract initialization this time will be:

...
24 const salt = Field.random();
...
33 const deployTxn = await Mina.transaction(deployerAccount, () => {
34 AccountUpdate.fundNewAccount(deployerAccount);
35 zkAppInstance.deploy();
36 zkAppInstance.initState(salt, Field(750));
37 });
...

Note that the initState() method accepts the salt and our secret, in this case the number 750.

And here is how one can create a user transaction to update the on-chain state:

...
47 const txn1 = await Mina.transaction(senderAccount, () => {
48 zkAppInstance.incrementSecret(salt, Field(750));
49 });
...

We call the zkApp smart contract with both the salt and the secret itself, the number 750. Because zkApp smart contracts are executed locally, neither the secret nor the salt will make it into the transaction.

Instead, the transaction will include only the proof that the update was called in such a way that all assertions passed and an update to the on-chain state x where we store the hash value. After the transaction is processed by the Mina network, x will be the value of Poseidon.hash([ salt, Field(750).add(1) ]), but the underlying salt and secret will not be revealed.

Try running main for yourself:

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

The output should look something like this:

...
SnarkyJS loaded
state after init: 3116464240601550031577632290308565252747064306168758166756574536757280262269
state after txn1: 15333363135506653312218020664441564145350761288169575380089681962972642150348
Shutting down

The state strings will be different though, because we used Field.random() to generate the salt.

Conclusion

Congrats! We have finished building a smart contract that uses privacy and hash functions.

Checkout Tutorial 3 to learn how to deploy zkApps to a live network.