Building a Private Voting zkApp on Mina Protocol using o1js

I have created the guide for implementation of private voting app as zk app. I have opted for zk app as zkApps leverage the power of zero-knowledge proofs (ZKPs) to ensure data integrity and security while maintaining user privacy. By allowing computations to be verified without revealing the underlying data, zkApps create an ideal environment for private voting, where voters can cast their choices securely without exposing their identities or preferences. I am using o1js as it is a TypeScript library designed to simplify the development of zkApps (zero-knowledge applications) on the Mina Protocol. It provides tools to create privacy-preserving applications without requiring deep expertise in cryptography or zero-knowledge proofs (ZKPs) I’ll walk through the implementation of a Private Voting zkApp using Mina Protocol. I will cover setup, development, testing and deployment phase here in the contract. First I installed zk-app-cli which provides CLI interface for scaffolding zk-apps and outlines the process of creating, developing and managing zk-apps. It automatically installs and configures all the dependencies required for zkApp development, including Mina’s o1js library, making the setup seamless. The CLI integrates with tools like TypeScript and Mina’s LocalBlockchain out of the box, providing a consistent environment for building zkApps. he CLI includes commands for compiling zkApps and deploying them to either a local blockchain instance (for testing) or the Mina Testnet/Mainnet. Then I setup private-voting app by following the command and setup the folder This creates the instance of the project with sample src files add.ts, index.ts, add.test.ts and interact.ts. Since it is boiler plate so I have amended the code as per the project. import { Field, SmartContract, state, State, method } from 'o1js'; import { Provable } from 'o1js'; export class PrivateVoting extends SmartContract { @state(Field) totalVotesA = State(); @state(Field) totalVotesB = State(); init() { super.init(); this.totalVotesA.set(Field(0)); this.totalVotesB.set(Field(0)); } @method async vote(option: Field, proof: Field): Promise { // Simplified voter proof verification (replace with ZKP circuit) proof.assertEquals(Field(1)); // Only allow valid voters // Ensure on-chain state consistency before modifying votes this.totalVotesA.requireEquals(this.totalVotesA.get()); this.totalVotesB.requireEquals(this.totalVotesB.get()); if (option.equals(Field(0))) { const currentVotes = this.totalVotesA.get(); this.totalVotesA.set(currentVotes.add(1)); } else if (option.equals(Field(1))) { const currentVotes = this.totalVotesB.get(); this.totalVotesB.set(currentVotes.add(1)); } else { throw new Error('Invalid voting option'); } } @method async getResults(): Promise { // Add precondition to ensure consistency with on-chain state this.totalVotesA.requireEquals(this.totalVotesA.get()); this.totalVotesB.requireEquals(this.totalVotesB.get()); const votesA = this.totalVotesA.get(); const votesB = this.totalVotesB.get(); // Log results (or you can return them if needed) Provable.log(votesA); Provable.log(votesB); } } This code reflects the simple voting system which uses state variables for tracking votes and it initializes both vote counters to 0 when the contract is deployed. Users call the vote method with their choice and proof of eligibility. The contract verifies the proof, updates the appropriate vote counter, and rejects invalid options. A Field value representing the voting option (e.g., 0 for option A, 1 for option B). A Field value representing a voter's proof of eligibility (simplified here as Field(1)). It ensures that only valid voters (those with a proof of Field(1)) can vote. In a real-world scenario, this would involve verifying a cryptographic zero-knowledge proof (ZKP). Before modifying votes, the contract ensures that the state variables (totalVotesA and totalVotesB) match their on-chain values using requireEquals(). Depending on the option (Field(0) for A or Field(1) for B), it increments the appropriate vote counter (totalVotesA or totalVotesB). It will throw an error if the option is neither 0 nor 1. This needs to be updated in Add.ts file which then can be renamed. The zkApp (Zero-Knowledge Application) implements the logic for private voting where votes for Option A and Option B are cast and counted, but the vote itself is kept private using zero-knowledge proofs (ZKPs). The zkApp contract uses snarkyjs to verify the proof that accompanies the vote. If the proof is valid, the vote is accepted (either for Option A or Option B). Through zero-knowledge proofs, the zkApp contract ensures that the vote is valid while hiding the vote's content. The contract will not know who voted or how they voted, only that the vote is legitimate. The contract verifies the proof sent by the interact.

Jan 12, 2025 - 19:33
 0
Building a Private Voting zkApp on Mina Protocol using o1js

I have created the guide for implementation of private voting app as zk app. I have opted for zk app as zkApps leverage the power of zero-knowledge proofs (ZKPs) to ensure data integrity and security while maintaining user privacy. By allowing computations to be verified without revealing the underlying data, zkApps create an ideal environment for private voting, where voters can cast their choices securely without exposing their identities or preferences. I am using o1js as it is a TypeScript library designed to simplify the development of zkApps (zero-knowledge applications) on the Mina Protocol. It provides tools to create privacy-preserving applications without requiring deep expertise in cryptography or zero-knowledge proofs (ZKPs)

I’ll walk through the implementation of a Private Voting zkApp using Mina Protocol.

I will cover setup, development, testing and deployment phase here in the contract.

First I installed zk-app-cli which provides CLI interface for scaffolding zk-apps and outlines the process of creating, developing and managing zk-apps.

It automatically installs and configures all the dependencies required for zkApp development, including Mina’s o1js library, making the setup seamless. The CLI integrates with tools like TypeScript and Mina’s LocalBlockchain out of the box, providing a consistent environment for building zkApps. he CLI includes commands for compiling zkApps and deploying them to either a local blockchain instance (for testing) or the Mina Testnet/Mainnet.

Image description

Then I setup private-voting app by following the command and setup the folder

Image description

This creates the instance of the project with sample src files add.ts, index.ts, add.test.ts and interact.ts. Since it is boiler plate so I have amended the code as per the project.

import { Field, SmartContract, state, State, method } from 'o1js';
import { Provable } from 'o1js';


export class PrivateVoting extends SmartContract {
  @state(Field) totalVotesA = State<Field>();
  @state(Field) totalVotesB = State<Field>();

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

  @method async vote(option: Field, proof: Field): Promise<void> {
    // Simplified voter proof verification (replace with ZKP circuit)
    proof.assertEquals(Field(1)); // Only allow valid voters

      // Ensure on-chain state consistency before modifying votes
  this.totalVotesA.requireEquals(this.totalVotesA.get());
  this.totalVotesB.requireEquals(this.totalVotesB.get());

    if (option.equals(Field(0))) {
      const currentVotes = this.totalVotesA.get();
      this.totalVotesA.set(currentVotes.add(1));
    } else if (option.equals(Field(1))) {
      const currentVotes = this.totalVotesB.get();
      this.totalVotesB.set(currentVotes.add(1));
    } else {
      throw new Error('Invalid voting option');
    }
  }

  @method async getResults(): Promise<void> {
    // Add precondition to ensure consistency with on-chain state
    this.totalVotesA.requireEquals(this.totalVotesA.get());
    this.totalVotesB.requireEquals(this.totalVotesB.get());

    const votesA = this.totalVotesA.get();
    const votesB = this.totalVotesB.get();

    // Log results (or you can return them if needed)
    Provable.log(votesA);
    Provable.log(votesB);
  }
}

This code reflects the simple voting system which uses state variables for tracking votes and it initializes both vote counters to 0 when the contract is deployed. Users call the vote method with their choice and proof of eligibility. The contract verifies the proof, updates the appropriate vote counter, and rejects invalid options. A Field value representing the voting option (e.g., 0 for option A, 1 for option B).
A Field value representing a voter's proof of eligibility (simplified here as Field(1)). It ensures that only valid voters (those with a proof of Field(1)) can vote. In a real-world scenario, this would involve verifying a cryptographic zero-knowledge proof (ZKP). Before modifying votes, the contract ensures that the state variables (totalVotesA and totalVotesB) match their on-chain values using requireEquals(). Depending on the option (Field(0) for A or Field(1) for B), it increments the appropriate vote counter (totalVotesA or totalVotesB). It will throw an error if the option is neither 0 nor 1. This needs to be updated in Add.ts file which then can be renamed. The zkApp (Zero-Knowledge Application) implements the logic for private voting where votes for Option A and Option B are cast and counted, but the vote itself is kept private using zero-knowledge proofs (ZKPs). The zkApp contract uses snarkyjs to verify the proof that accompanies the vote. If the proof is valid, the vote is accepted (either for Option A or Option B). Through zero-knowledge proofs, the zkApp contract ensures that the vote is valid while hiding the vote's content. The contract will not know who voted or how they voted, only that the vote is legitimate. The contract verifies the proof sent by the interact.ts to ensure the vote is valid before updating the on-chain state (i.e., vote tallies).

import { PrivateKey, PublicKey,Mina, Field } from 'o1js';
import { PrivateVoting } from './Add';

const Local = await Mina.LocalBlockchain();
Mina.setActiveInstance(Local);

const deployerAccount = PrivateKey.random();  // Generate a random PrivateKey for deployer
const deployerKey = deployerAccount;  // Use deployer's PrivateKey

const feePayerPublicKey = deployerAccount.toPublicKey();
const feePayerSpec = feePayerPublicKey;



const zkAppPrivateKey = PrivateKey.random();
const zkAppAddress = zkAppPrivateKey.toPublicKey();

const votingApp = new PrivateVoting(zkAppAddress);

async function deploy() {
  console.log('Deploying Private Voting zkApp...');

  const tx = await Mina.transaction(feePayerSpec, async () => {
    votingApp.deploy();
  });
  await tx.sign([deployerKey]).send();
  console.log('Deployment Complete!');
}

deploy();


I then updated index.ts which basically reflects the process of deploying a Private Voting zkApp on Mina's local blockchain using the o1js library. It generates a random private key for the deployer and zkApp. A transaction is then created to deploy the PrivateVoting zkApp on Mina's local blockchain. The transaction is then signed with the deployer's private key and sent for execution.

import { PrivateKey, PublicKey,Mina, Field } from 'o1js';
import { PrivateVoting } from './Add';

const zkAppAddress = ''; // Replace with your zkApp's public key
const votingApp = new PrivateVoting(PublicKey.fromBase58(zkAppAddress));

async function castVote(option: Field, proof: Field) {
  console.log(`Casting vote for option ${option.toString()}...`);
  const tx = await Mina.transaction(async () => {
    votingApp.vote(option, proof);
  });
  await tx.send();
  console.log('Vote cast successfully!');
}

async function getResults() {
  const results = votingApp.getResults();
  console.log('Results:', results);
}

// Example usage
(async () => {
  await castVote(Field(0), Field(1)); // Cast vote for Option A
  await castVote(Field(1), Field(1)); // Cast vote for Option B
  await getResults();
})();

I then updated interact.ts which reflects the process explaining how to interact with a Private Voting zkApp (Zero-Knowledge Application) built on Mina Protocol using o1js (the Mina SDK). It creates instance of the zkapp by allowing interaction with the Private Voting contract deployed on the Mina blockchain. The castVote() function sends a vote for one of two options (Option A or Option B) to the zkApp, including a zero-knowledge proof to verify the validity of the vote. The getResults() function fetches the current vote tally from the zkApp and logs the results. It allows to simulate a private voting process where users can cast votes and then see the results, all while utilizing zero-knowledge proofs for privacy and integrity of the voting process. It is serving as proof generation where a user submits their vote along with a proof that is generated by a zero-knowledge proof circuit.

Image description

After successful building of the contract by running npm run build. I then tested the contract through this script which is add.test.ts

import { PrivateVoting } from './Add';
import { Field, Mina, PrivateKey, UInt64 } from 'o1js';

const Local = await Mina.LocalBlockchain();
Mina.setActiveInstance(Local);

const deployerAccount = PrivateKey.random();
const deployerKey = deployerAccount;

const zkAppPrivateKey = PrivateKey.random();
const zkAppAddress = zkAppPrivateKey.toPublicKey();

describe('PrivateVoting', () => {
  let votingApp: PrivateVoting;

  beforeAll(async () => {
    // Add the deployer account with sufficient balance to the local blockchain
    Local.addAccount(deployerKey.toPublicKey(), UInt64.from(10000000000).toString()); // Add deployer account with balance

    // Add zkApp account with sufficient balance
    Local.addAccount(zkAppAddress, UInt64.from(10000000000).toString()); // Add zkApp account with balance

    // Initialize the zkApp
    votingApp = new PrivateVoting(zkAppAddress);

    // Compile the zkApp to generate the verification key
    await PrivateVoting.compile();
  });

  it('should deploy successfully', async () => {
    // Create the transaction for deploying the contract
    const tx = await Mina.transaction(
      { sender: deployerKey.toPublicKey(), fee: UInt64.from(1000000).toString() }, // Convert fee to string
      async () => {
        votingApp.deploy();
      }
    );

    // Sign with both the deployer's private key (for transaction fee) and the zkApp private key (for state change)
    await tx.sign([deployerKey, zkAppPrivateKey]).send();

    // Fetch the zkApp account using getAccount after deploying
    let zkAppAccount;
    try {
      zkAppAccount = await Mina.getAccount(zkAppAddress);
    } catch (error: unknown) {
      if (error instanceof Error) {
        throw new Error('Error fetching zkApp account: ' + error.message);
      } else {
        throw new Error('An unknown error occurred while fetching the zkApp account.');
      }
    }

    // Check if the account was found and is initialized
    if (!zkAppAccount) {
      throw new Error('zkApp account not found');
    }

    // Add the preconditions here
    votingApp.totalVotesA.requireEquals(Field(0)); // Ensure totalVotesA is 0
    votingApp.totalVotesB.requireEquals(Field(0)); // Ensure totalVotesB is 0

    // Check initial state of votes using deep equality comparison
    expect(votingApp.totalVotesA.get()).toEqual(Field(0));
    expect(votingApp.totalVotesB.get()).toEqual(Field(0));
  });

});

It is a test suite for the PrivateVoting zkApp on the Mina blockchain. It uses the Jest testing framework to verify the deployment and initial state of the voting application. This will verify the deployment and initial state of the PrivateVoting zkApp contract, ensuring that the votes start at 0 for both options (A and B).

Now it is time for deployment phase. I initiated it by writing zk config as the command which asks for app name, where to deploy, graphql link which is available on devnet, keys and the transaction cost as highlighted below

Image description

I then deployed the zk app on devnet and it appears on minascan. It takes approx 32 block confirmations.

Image description

This is the minascan explorer Link
Image description

I then proceeded with creating UI and interacting the zk app with the UI.

Image description

In conclusion, building zkApps on Mina Protocol offers a robust framework for developing privacy-preserving decentralized applications. By leveraging O1js, developers can focus on high-level logic and user interaction while abstracting away the complexities of cryptographic proof generation.

This article reflects building a PrivateVoting contract using O1js, where users can cast votes while ensuring that their identity remains confidential. This process is underpinned by a proof that verifies the validity of a user's vote, using ZKPs to protect voter privacy.