Back

Proof of membership using Semaphore

Written by tawago

Dec 17, 2024 · 15 min read

Introduction

In the rapidly evolving landscape of blockchain technology, privacy and scalability remain two of the most critical challenges. Semaphore is a zero-knowledge protocol that enables users to prove membership in a group without revealing their identity. It leverages zero-knowledge proofs to ensure privacy while maintaining trust within decentralized systems. On the other hand, Scroll is a zkEVM-based zkRollup on Ethereum that aims to enhance scalability without compromising on security or decentralization.

Combining Semaphore with Scroll opens up new possibilities for building scalable and privacy-preserving applications on Ethereum. This integration allows developers to create systems where users can authenticate and interact anonymously, all while benefiting from the scalability of zkRollups.

Setup

Since I wanted to make this as easy as possible for beginner blockchain developers, readers can try to deploy the contract and play around with it without learning Hardhat by following the instructions..

First, get your Scroll Sepolia ETH ready by bridging your Ethereum Sepolia ETH: https://sepolia.scroll.io/bridge. It takes up to 20 mins to complete (in my case it was 12 minutes) captionless image

For deployment and RPC node-related tasks, I prefer using MultiBaas by Curvegrid: https://console.curvegrid.com/. You can sign up for free and create up to two deployments on both Scroll Mainnet and Testnet (or other EVM chains).

captionless image

Extra steps: Deploy the three key contracts

When you check out semaphore documentation, you would notice that three key contracts ( Semaphore, SemaphoreVerifier, PoseidonT3) are usually provided by the PSE team. https://docs.semaphore.pse.dev/deployed-contracts At the time I started writing this article, these contracts were not available on Scroll Testnet. (They are now officially available as of v4.7.0) In other words, you have to deploy them on your own if they are not available on which the network you are seeking to develop, or you should wait for the official contracts by the PSE team.

After bits of digging around documents and reading Solidity code, I’ve managed to deploy them as below. (This is only for temporarily uses. Be sure to use the original contracts.)

  • SemaphoreVerifier : 0x76C994dA048f14F4153Ac550cA6f6b327fCE9180
  • PoseidonT3 : 0x5A7242de32803bC5329Ca287167eE726E53b219A
  • Semaphore : 0x0303e10025D7578aC8e4fcCD0249622ac1D17B82

Step by Step to deploy, call the functions, and connect with a frontend app

Step 1: Deploy

captionless image

When you click on the deployment URL on Curvegrid console, you get to your deployment dashboard. From there, click Contracts > On-Chain and then you see nothing in the On-Chain Contracts . Click the plus button.

captionless image

It will show a modal. From there:

  1. Click Deploy Contract
  2. Select Contract from Compilation Artifact
  3. Upload the artifacts/contracts/TestSemaphore.json from my repo.
  4. Label it as you like (I just named it “semophore”) with a version number.
  5. Finally, label your deploying contract with “Sync Events” checked and _verifier argument with the SemaphoreVerifier address above (0x76C…) …and then click “Deploy”… tada!

(The UI should prompt your Metamask to confirm a tx. If you haven’t connected your signer wallet yet, do it from the top right corner.)

…and then click “Deploy”… tada!

Alternatively, instead of uploading your contract artifact or solidity file, you can fetch the contract’s bytecode and ABI for verified contracts. In that case, you can link the official Semaphore contract by following the screenshots below:

Link ContractContract from AddressSearch: 0x06d1530c829366A7fff0069e77c5af6A6FA7db2E and then “Continue” to add the contractKeep the “Contract Address” as is, and name the label “semaphore_official”, check Sync Events and latest block.

After you succeed with your Semaphore deployment, you would see this overview of the contract.

captionless image

Step 2: Interacting with the Contract

On “On-Chain Contracts”, Click “Functions” of your deployed Semaphore contract from the side panel menu.

Once you see the list of functions of Semaphore contract, you can play around with it. In the video below, I’ve created two Semaphore Groups by calling createGroup: one with an argument of null address (no admin group) and the other without argument, which sets the signer address as admin. And then you can call getGroupAdmin to check these admin addresses.

Demo of calling functions on Semaphore contract

Step 3: Connecting with a Frontend App

Okay, now you sort of understand what Semaphore contract can do in terms of its functionalities. When it comes to a real use-case, you would only need to interact with your SemaphoreGroup , which is determined by GroupId when you createGroup .

Curvegrid provides a sample app for on-chain voting contract. I’ve modified its contract to add Semaphore elements to play around with in this article. You can checkout the repo: https://github.com/tawago/scroll-semaphore

Checkout the modified version of the sample app at: https://scroll-semaphore.vercel.app/

In the repo, you can find a contract named [SimpleVoting](https://github.com/tawago/scroll-semaphore/blob/main/contracts/SimpleVoting.sol) . Let’s deploy this contract and start running the frontend app to interact with it. This contract needs an existing Semaphore contract address as one of the constructor arguments so use either TestSemaphore I’ve deployed above or the official one. The contract also sets a “secret code” to prevent total strangers from obtaining membership. The secret code is a string GM! and for the constructor argument, you need to hash it with keccak. (This is very insecure because anyone can check the tx to learn the secret code. In a real use case, this should be probably done in a zkp way)

Constructor arguments for SimpleVoting contract would something like:

  • _numChoices: 4
  • semaphoreAddress: 0x0303e10025D7578aC8e4fcCD0249622ac1D17B82
  • _secretCodeHash: 0xc87a2838ff5cbcb7515eef22d409b3271b26f101f3b1a51873086460417c4454

Deploying SimpleVoting contract

After you’ve successfully deployed, let’s get the API key and whitelist your frontend app in the CORS setting.

  • Go to Admin > API Keys and click “+ New Key” button.
  • Label and Select DApp User then “Create”

captionless image

  • Go to Admin > CORS Originsand click “+ Add Origin” button.
  • Input http://localhost:3000 for local development and then “Continue”

captionless image

Interact with the voting app

From here, we will mostly do things on the frontend app under the repo. git clone if you haven’t yet.

$ cd frontend && yarn $ cp .env.template .env.development edit .env.development to reflect your API key and deployment URL.

$ yarn dev and open http://localhost:3000. You should be able to see the app lik below.

captionless image

Click “Create” to generate you Semaphore Identity! Once you create you Identity, you can check if your Identity’s commitment already exists in the SimpleVoting contract Group; in other words, your membership of the group. It would prompt an alert either telling you that you are a part of the group or not. This is done by calling hasMember(groupId, commitment) function of Semaphore contract. (But you do not want to convince a third party of your membership in this way because you are basically telling your Identity’s commitment.)

captionless image

Let’s join the Group!

  • Connect your wallet. Make sure your current network is “Scroll Sepolia”
  • Enter “GM!‘ in the text field
  • Click “Join” and send a transaction.

If nothing happens when you click “Join”, you must make sure your wallet is connected properly.

Once you obtain a membership, you can cast a vote using your Semaphore Proof!

captionless image

And you might wonder “But… how do I actually generate Semaphore Proof?” and that’s a valid question. I didn’t know either until I created this sample app.

In order to generate a proof of your membership, you basically need all the commitments of the group. You have several options to get the all the commitments of a group.

  1. If you are using the official Semaphore, you can query to the Graph endpoint. For Scroll Semaphore: https://api.studio.thegraph.com/query/14377/semaphore-sepolia/v4.1.0/graphql and query can be as simple below
{ groups(where: { id: 1 }) { id merkleTree { root depth size } admin members(orderBy: index) { identityCommitment } } }
  1. You can add “@semaphore-protocol/data” and import SemaphoreEthers. It has a convenientgetGroupMembers method. Check out the official boilerplate: https://github.com/semaphore-protocol/boilerplate/blob/9dd9518c4fc6eda8864656eb6697f04db25dbef8/apps/web-app/src/context/SemaphoreContext.tsx#L37

  2. MultiBaas indexes Events emitted by the contracts you’ve added. There are two API endpoints that I could use. The first one is List Events endpoint. By using the endpoint, I could simply write a code as below (though this does not take other events like MemberRemoved in consideration)

//https://github.com/tawago/scroll-semaphore/blob/258256b08cf11c1069093f547c712e3841656915/frontend/app/hooks/useMultiBaas.ts#L107-L132 const getCommitmentsFromMemberAddedEvents = async (): Promise<Array<bigint> | null> => { try { const eventSignature = "MemberAdded(uint256,uint256,uint256,uint256)"; const response = await eventsApi.listEvents( undefined, undefined, undefined, undefined, undefined, false, chain, semaphoreAddressLabel, semaphoreContractLabel, eventSignature, 50 ); const events: Event[] = response.data.result.filter(event => event.transaction.contract.addressLabel === votingAddressLabel) const commitments = events .sort((a, b) => new Date(a.triggeredAt).getTime() - new Date(b.triggeredAt).getTime()) .map(item => item.event.inputs[2].value) return commitments; } catch (err) { console.error("Error getting member added events:", err); return null; } };

The other one is Execute Query endpoint. For this endpoint, you first need to set up an event query in the UI (or through another API).

  • Go to Blockchain > Event Queries and click the plus botton in the side menu.
  • Name the query “commitments”
  • Click the plus botton next to “Events”
  • Select “MemberAdded(uint256,uint256,uint256,uint256)”
  • Click “Add Event Field” and add three fields, “groupId”, “identityCommitment” and “triggered_at”
  • Click “Add Filter” — — set “groupId” as Operand — — set “Equal” as Operator — — set your groupId value
  • Click “Save Query”

captionless image

If you’ve successfully created the query, you should be able to see the preview data below the QueryBuilder.

list of the commitments queried from indexed Events on MultiBaas

And then, you could get the commitments as below.

const queryLabel = "commitments"; const response = await eventQueriesApi.executeEventQuery(queryLabel); const commitments = response.data.result.rows.map((row: any) => row.identitycommitment); return commitments;

Now, that you have all the commitments of the group, let’s generate a Semaphore proof (zero knowled proof). The Semaphore proof consists of your identity, the merkle tree proof of your commitment, message and scope. While the message can be just an empty string, setting a right scope is important. Together with your identity’s private key, it generates a nullifier which each user may only generate one valid proof per scope. Though, I’m not sure if this is practical, you can set a periodical datetime as a scope for a session to grant a group member for a login and its expiration.

Internally, lean-imt is used for the merkle tree. With the all commitments and index of your commitment within the group, you can get the merkle tree proof as below:

import { poseidon2 } from "poseidon-lite" import { LeanIMT } from "@zk-kit/lean-imt"; export async function generateMerkleProof( commitmentsArray: bigint[], index: number, ) { const hash = (a, b) => poseidon2([a, b]) // Initialize the Merkle tree const tree = new LeanIMT<bigint>(hash); // Insert the commitments into the tree for (const commitment of commitmentsArray) { tree.insert(commitment); } // Generate the proof for the leaf at the given index const proof = tree.generateProof(index); // The proof object already has the structure we need const merkleProof = { root: proof.root, leaf: proof.leaf, index: proof.index, siblings: proof.siblings, }; return merkleProof; }

In summary, generating a Semaphore Proof looks like below in my sample app. https://github.com/tawago/scroll-semaphore/blob/095194445ccf5d2c5710429ba46730deb3458834/frontend/app/hooks/useMultiBaas.ts#L183-L210

const castVote = useCallback(async (choice: string): Promise<SendTransactionParameters> => { if (!identity) throw Error("No identity") const scope = groupId; // You have two API to get commitments // First choice: Get all events and filter on frontend side // const commitments = await _getCommitmentsFromMemberAddedEvents() // Second choice: Get commitments from a event query const commitments = await _getCommitmentsFromQuery() if (!commitments?.length) throw Error("No members in this group") const index = await callContractFunction("indexOf", [groupId, identity?.commitment.toString()], true) const merkelProof = await generateMerkleProof(commitments, Number(index)) const proof = await generateSemaphoreProof(identity, merkelProof, choice, scope) console.log('generateSemaphoreProof', proof) return await callContractFunction("vote", [ proof.merkleTreeDepth, proof.merkleTreeRoot, proof.nullifier, proof.message, proof.points, ]); }, [callContractFunction, groupId, identity]);

Conclusion: What’s Next?

Overall, interacting with Semaphore Contract and creating a smart contract for an membership application seems very intuitive. The only one-time hassle you may have to go through is when the key Contracts do not exist on the chain for which you are seeking to develop.

More advanced examples could include making the secret code to join in a ZKP manner, adding a feature to open a new ballot with a unique identifier scope, using MACI to make the app a more robust anonymous voting system, adding delegation mechanisms, etc!

All the above ambitious ideas are becoming possible by the advancement of technologies in the family tree of Programmable Cryptography. I’m personally very interested in Liquid Democracy and with ZKP, MPC and FHE, it feels like we can soon upgrade our societies with a new modern decision making algorithm.

Links:

Lastly! This article was encouraged by Scroll’s “Articles Bounty Contest” https://www.levelup.xyz/events/writers-competition-2024q4

© 2024 Scroll Foundation | All rights reserved

Terms of UsePrivacy Policy