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.
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
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
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.
- 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
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.We increase the voter count by increasing
voters
to track the number of participants.Finally we update the winning NFT, using
Provable.if
to compare votes, and updatewinningNft
andwinningVotes
, if the new NFT has more votes.
With this we have our smart contract ready, now we have to test our smart contract.
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

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/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.