Building On Mina: A guide to writing smart contract implementing Merkle Trees using 01js

This article will explore Merkle Trees within the Mina ecosystem, demonstrating their practical application with a step-by-step creation of a Voting Zk App.

Introduction

A blockchain consists of a chain of blocks, with each block containing…


This content originally appeared on DEV Community and was authored by Adetayo Lasisi

This article will explore Merkle Trees within the Mina ecosystem, demonstrating their practical application with a step-by-step creation of a Voting Zk App.

Introduction

A blockchain consists of a chain of blocks, with each block containing one or more transactions. Each block is identified by a cryptographic hash representing its contents, including transactions and metadata.

Within a block, transactions are structured using a Merkle tree, where each transaction is represented by a hash. These transaction hashes serve as the leaves of the tree. Each pair of transaction hashes is recursively hashed together until a single root hash, called the Merkle root, is obtained. This Merkle root, along with other block metadata, is then included in the block header, ensuring data integrity and efficient verification.

Image description

In Mina, zkApp accounts can only store a limited amount of data to prevent bloating. Hence, need for a Merkle tree, which allows the referencing of off-chain data by storing only a single hash on-chain, thereby enabling Mina to maintain its succinctness.

The Merkle tree root summarizes and verifies the integrity of a set of data and is used to prove that some data exists in the Merkle tree, the root is considered a compact representation of a large pool of data so this allows an efficient verification and confirmation of data integrity inside the blockchain system.

Another benefit of Merkle trees is the witness, also known as a Merkle proof or Merkle path. The witness is the path from one specific leaf node to the very top of the tree which is the root. Merkle witnesses are proofs of inclusion that prove that one specific piece of data

witness

You can reference large amounts of off-chain data and prove the inclusion of very specific parts of that data with only a small hash - the root - and a witness.

Let's start building.

We'll take the first step by installing the zkapp-cli.

npm install -g zkapp-cli

After we have that installed we'll enter this command to create a project

zk project votingNft

We'll have the choice to select one of the options in the image below, navigate the options and select none

Image description

After that has been selected, the project will be created, after it's done we will navigate to the folder from the terminal using

cd votingNft

We need to remove some files that have been generated along with the project because we won't be using them. With this command, we'll delete the files

cd src
rm Add.test.ts Add.ts interact.ts

Then while still in the src directory, we run this command to create the necessary files we exit the src directory

touch Voting.ts Voting.test.ts

//exit the directory
cd ..

We are ready to start building so let's open the Voting.ts file, we import the necessary packages that we need to get this ball rolling

import { Field, SmartContract, state, State, method, Struct, MerkleWitness, Poseidon, Provable } from "o1js";

We need to define a tree height, The tree height decides how many layers of grouping are needed, affecting how quickly and easily data can be verified and we'll create a corresponding Merkel Witness class

The height states how many leaves we can have when specifying the witness, we'll see this in action when taking a look at the data in the tree

export const treeHeight = 4

export class MerkleWitness4 extends MerkleWitness(treeHeight){}

Now let's create our main Voting smart contract and initialize it

export class Voting extends SmartContract{
    // declare state variables
    @state(Field) voters = State<Field>()
    @state(Field) winningNft = State<Field>()
    @state(Field) winningVotes = State<Field>()

    // root of the merkle tree
    @state(Field) treeRoot = State<Field>()

    init(){
      super.init()
      this.voters.set(Field(1))
    }

   // initialize the state on the contract
       @method async initState(initialRoot: Field){
     this.treeRoot.set(initialRoot)
   }
}

We define the state variables to hold the voters, winningNft, winningVotes and the treeRoot, then we initialize the state of the contract.

Before we continue we need to create a class for the NFT and create a method to vote.

export class NFT extends Struct({nftName: Field, nftCreator: Field, nftVotes: Field}){

//method for the vote
vote(){
  this.nftVotes = this.nftVotes.add(1)
 }
}

We begin by verifying the NFT's presence within the Merkle tree. This is followed by the voting process. Finally, the Merkle tree is updated to reflect any changes made to the NFT. In our Voting smart contract, we create the method voteForNft

export class Voting extends SmartContract{
  // declare state variables
  @state(Field) voters = State<Field>()
  @state(Field) winningNft = State<Field>()
  @state(Field) winningVotes = State<Field>()

  // root of the merkle tree
  @state(Field) treeRoot = State<Field>()
     init(){
       super.init()
       this.voters.set(Field(1))
     }

  // initialize the state of the contract
  @method async initState(initialRoot: Field){
    this.treeRoot.set(initialRoot)
   }

 //method to vote for the nft
 @method async voteForNft(nft: NFT, witness: MerkleWitness4){

  //Get the tree root and see if the nft is in it
  const treeRoot = this.treeRoot.getAndRequireEquals()

  // check to see whether the nft is in the merkle tree, the way we 
  // check to see if the nft is within the tree is by the witness       

  const nftRoot = witness.calculateRoot(Poseidon.hash(NFT.toFields(nft)))

  // check if the nft is in the tree, if it is not in the tree, the transaction will fail and the state will not be updated

   nftRoot.assertEquals(treeRoot)

  //if the nft is in the tree then the nft can be voted on
  nft.vote()

  //update the tree with the new nft hash
  const newNftRoot = witness.calculateRoot(Poseidon.hash(NFT.toFields(nft)))
  this.treeRoot.set(newNftRoot)

//we want to increment the number of voters after a vote has been cast      this.voters.set(this.voters.getAndRequireEquals().add(1))

//we are going to check if the nft has more votes than the current winning nft then we will update the winning nft

  const winningNft = this.winningNft.getAndRequireEquals()
  const winningVotes = this.winningVotes.getAndRequireEquals()

  const newWinningNft = Provable.if(winningVotes.lessThan(nft.nftVotes), nft.nftName, winningNft)
  const newWinningVotes = Provable.if(winningVotes.lessThan(nft.nftVotes), nft.nftName, winningVotes)

this.winningNft.set(newWinningNft)
this.winningVotes.set(newWinningVotes)
  }
}

This method verifies, votes, and updates the winning NFT on the Merkle tree.

  1. First, we verify the nft in the tree by retrieving the Merkle root, hashing with Poseidon, and checking if it exists in the tree via a Merkle witness.

-Poseidon: Poseidon is a hash function for zero knowledge-proof systems, we use it because it only stores field variables but since the nft is a struct we have to pass it as fields

  1. We cast the Vote & Update Tree, If it is valid, we call nft.vote(), and update the Merkle root, setting a new root in this.treeRoot.

  2. We increase the voter count by increasing voters to track the number of participants.

  3. Finally we update the winning NFT, using Provable.if to compare votes, and update winningNft and winningVotes, if the new NFT has more votes.

With this we have our smart contract ready, now we have to test our smart contract.

let's go

We'll navigate to our Voting.test.ts file and start adding some code, the first thing we need to do is import the necessary packages and files

import { AccountUpdate, CircuitString, Field, MerkleTree, Mina, Poseidon, PrivateKey, PublicKey } from 'o1js';
import { treeHeight, NFT, MerkleWitness4, Voting } from './Voting';

Then we set up for the tests with

let proofsEnabled = false;

function createNFT(nftName: string, nftCreator: string) {
    const nft = new NFT({
        nftName: Poseidon.hash(CircuitString.toFields(CircuitString.fromString(nftName))),
        nftCreator: Poseidon.hash(CircuitString.toFields(CircuitString.fromString(nftCreator))),
        nftVotes: Field(0)
    });
    return nft;
}

function createTree(): MerkleTree {
    const tree = new MerkleTree(treeHeight);

    const nft0 = createNFT("nft0", "creator0");
    const nft1 = createNFT("nft1", "creator1");
    const nft2 = createNFT("nft2", "creator2");
    const nft3 = createNFT("nft3", "creator3");

    tree.setLeaf(0n, Poseidon.hash(NFT.toFields(nft0)));
    tree.setLeaf(1n, Poseidon.hash(NFT.toFields(nft1)));
    tree.setLeaf(2n, Poseidon.hash(NFT.toFields(nft2)));
    tree.setLeaf(3n, Poseidon.hash(NFT.toFields(nft3)));

    return tree;
}


describe('Voting', () => {
    let deployerAccount: Mina.TestPublicKey,
    deployerKey: PrivateKey,
    senderAccount: Mina.TestPublicKey,
    senderKey: PrivateKey,
    zkAppAddress: PublicKey,
    zkAppPrivateKey: PrivateKey,
    zkApp: Voting,
    tree: MerkleTree;

    beforeAll(async () => {
        if (proofsEnabled) await Voting.compile();
    });

    beforeEach(async () => {
        const Local = await Mina.LocalBlockchain({ proofsEnabled });
        Mina.setActiveInstance(Local);
        [deployerAccount, senderAccount] = Local.testAccounts;
        deployerKey = deployerAccount.key;
        senderKey = senderAccount.key;

        zkAppPrivateKey = PrivateKey.random();
        zkAppAddress = zkAppPrivateKey.toPublicKey();
        zkApp = new Voting(zkAppAddress);
        tree = createTree();
    });

async function localDeploy() {
        const txn = await Mina.transaction(deployerAccount, async () => {
            AccountUpdate.fundNewAccount(deployerAccount);
            await zkApp.deploy();
            await zkApp.initState(tree.getRoot());
        });
        await txn.prove();
        await txn.sign([deployerKey, zkAppPrivateKey]).send();
    }
})

One thing we notice is the function to create a tree with createTree function, where we have dummy data for the NFTs we want to vote on.

We also have a localDeploy function that we call


This content originally appeared on DEV Community and was authored by Adetayo Lasisi


Print Share Comment Cite Upload Translate Updates
APA

Adetayo Lasisi | Sciencx (2025-02-01T20:15:09+00:00) Building On Mina: A guide to writing smart contract implementing Merkle Trees using 01js. Retrieved from https://www.scien.cx/2025/02/01/building-on-mina-a-guide-to-writing-smart-contract-implementing-merkle-trees-using-01js/

MLA
" » Building On Mina: A guide to writing smart contract implementing Merkle Trees using 01js." Adetayo Lasisi | Sciencx - Saturday February 1, 2025, https://www.scien.cx/2025/02/01/building-on-mina-a-guide-to-writing-smart-contract-implementing-merkle-trees-using-01js/
HARVARD
Adetayo Lasisi | Sciencx Saturday February 1, 2025 » Building On Mina: A guide to writing smart contract implementing Merkle Trees using 01js., viewed ,<https://www.scien.cx/2025/02/01/building-on-mina-a-guide-to-writing-smart-contract-implementing-merkle-trees-using-01js/>
VANCOUVER
Adetayo Lasisi | Sciencx - » Building On Mina: A guide to writing smart contract implementing Merkle Trees using 01js. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/02/01/building-on-mina-a-guide-to-writing-smart-contract-implementing-merkle-trees-using-01js/
CHICAGO
" » Building On Mina: A guide to writing smart contract implementing Merkle Trees using 01js." Adetayo Lasisi | Sciencx - Accessed . https://www.scien.cx/2025/02/01/building-on-mina-a-guide-to-writing-smart-contract-implementing-merkle-trees-using-01js/
IEEE
" » Building On Mina: A guide to writing smart contract implementing Merkle Trees using 01js." Adetayo Lasisi | Sciencx [Online]. Available: https://www.scien.cx/2025/02/01/building-on-mina-a-guide-to-writing-smart-contract-implementing-merkle-trees-using-01js/. [Accessed: ]
rf:citation
» Building On Mina: A guide to writing smart contract implementing Merkle Trees using 01js | Adetayo Lasisi | Sciencx | https://www.scien.cx/2025/02/01/building-on-mina-a-guide-to-writing-smart-contract-implementing-merkle-trees-using-01js/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.