The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Semaphore is a zero-knowledge (zk) protocol that allows users to prove their membership in a group and send messages such as votes or feedback without revealing their identity.
It also provides a simple mechanism to prevent double-signaling, which means that the same proof cannot be verified twice.
Privacy remains a significant challenge in the digital world, with existing solutions often being limited, hard to extend, and overly complex. These limitations make it difficult to create privacy-preserving applications and prevent users from securely interacting without exposing their identities. Semaphore addresses these issues by enabling the sharing of anonymous messages, solving the critical need for privacy while maintaining transparency.
Implementations MUST provide:
An identity serves as a unique identifier for users.
A group is a collection of identities represented in a structured format.
An anonymous proof of membership enables users to prove their inclusion in a group without revealing their identity.
A value designed to be a unique identifier for the zk proof. It is generated using the identity.
After joining a group (an identity is added to a group) and proving that they are part of it, users can also send an anonymous message.
Semaphore Flow:
There are some Semaphore implementations such as Semaphore v3 and Semaphore v4 both developed by PSE. While they follow the same system components, they have differences. To learn more about the differences, you can read Appendix A: Semaphore v3 and v4 implementations.
The implementation section will refer to Semaphore v4 implemented by PSE. The same logic can be applied to implement Semaphore v4 with other tech stacks such as Noir or Rust.
Semaphore's zk functionality is implemented using Circom + Snarkjs + Groth16.
The identity of a user in the Semaphore protocol. A Semaphore identity consists of an EdDSA public/private key pair and a commitment. Semaphore uses an EdDSA implementation based on Baby Jubjub and Poseidon.
The Identity Commitment is the public Semaphore identity value used in Semaphore groups. Semaphore uses the Poseidon hash function to create the identity commitment from the Semaphore identity public key.
A Semaphore group is a Merkle tree in which each leaf is an identity commitment for a user.
Semaphore uses the LeanIMT implementation, which is an optimized binary incremental Merkle tree. The tree nodes are calculated using Poseidon. To learn more about it you can read the LeanIMT paper.
merkleProofLength
: Length of the Merkle Proof (Siblings Length) used to calculate the Merkle Root.merkleProofSiblings
: Merkle Proof Siblings used to calculate the Merkle Root.merkleProofIndices
: Merkle Proof Indices used to calculate the Merkle Root.secret
: The secret is the scalar generated from the EdDSA private key. Using the secret scalar instead of the private key allows this circuit to skip steps 1, 2, 3 in the generation of the public key defined in RFC 8032, making the circuit more efficient and simple. See the Semaphore identity package to know more about how the identity is generated.message
: The value the user shares when voting, confirming, sending a text message, etc.scope
: A value used like a topic on which users can generate a valid proof only once. The scope is supposed to be used to generate the nullifier.EdDSA public key generation: The EdDSA public key is derived from the secret
using Baby Jubjub. The public key is a point with two coordinates.
Identity Commitment generation: Calculate the hash of the public key. This hash is the Identity Commitment.
Proof of membership verification: The Merkle root passed as output must be equal to that calculated within the circuit through the inputs of the Merkle proof.
Nullifier generation: The nullifier is generated by calculating the hash of the scope
and the secret
.
Dummy Square: As the message is not really used within the circuit, the square applied to it is a way to force Circom's compiler to add a constraint and prevent its value from being changed by an attacker. More information in this article by the Geometry team. This dummy square is tied to Circom and may not be necessary when using other technologies.
merkleRoot
: Merkle Root of the LeanIMT.nullifier
: A value designed to be a unique identifier for the zk proof. It is used to prevent the same zk proof from being used twice. In Semaphore, the nullifier is the hash of the scope and secret value of the user's Semaphore identity.When using the same scope for an identity, the resulting nullifier remains the same because the same hash is generated. To obtain different nullifiers for the same identity (allowing users to share multiple zk proofs) users must use a different scope each time.
Snarkjs is used to generate the proof.
const { proof, publicSignals } = await groth16.fullProve(
{
secret: identity.secretScalar,
merkleProofLength,
merkleProofIndices,
merkleProofSiblings,
scope: hash(scope),
message: hash(message)
},
wasm,
zkey
)
return {
merkleTreeDepth,
merkleTreeRoot: merkleProof.root.toString(),
nullifier: publicSignals[1],
message: message.toString() as NumericString,
scope: scope.toString() as NumericString,
points: packGroth16Proof(proof)
}
To learn more about the proof generation, see the proof generation code.
{
merkleTreeDepth: 10,
merkleTreeRoot: '4990292586352433503726012711155167179034286198473030768981544541070532815155',
nullifier: '17540473064543782218297133630279824063352907908315494138425986188962403570231',
message: '32745724963520510550185023804391900974863477733501474067656557556163468591104',
scope: '37717653415819232215590989865455204849443869931268328771929128739472152723456',
points: [
'21668337069844646813015291115284438234607322052337623326830707330064154913250',
'5484905467799538881631237123282286864306155680753671338313686933143657835972',
'16129789229127169079253218689550197285028424883172925653046098078118792423164',
'20777706122379854993524659601832014684665489694335277047215897593373874956681',
'6697558559751679943942291885282718275907555268106795371542167431979105110434',
'19709269142703129641057076037387702381970592578248722843989118216760760132874',
'17493422037248079872314969622558990504818232868144223100447800353776555945950',
'20398320346518400096197920333973312490517624241764728676355205902471625807914'
]
}
Proof verification works both off-chain and on-chain (on EVM-compatible chains where the Groth16 Semaphore verifier can be deployed).
Semaphore v4 is currently deployed on many networks, the full list can be found in the Semaphore Docs.
Snarkjs is used to verify the proof.
return groth16.verify(
verificationKey,
[merkleTreeRoot, nullifier, hash(message), hash(scope)],
unpackGroth16Proof(points)
)
To learn more about the proof verification in TypeScript/JavaScript, see the proof verification code.
Snarkjs is used to generate a Solidity verifier per tree depth. Then, all verifiers are merged into a single one. To know more about the verifier see the Semaphore Verifier.
return
verifier.verifyProof(
[proof.points[0], proof.points[1]],
[[proof.points[2], proof.points[3]], [proof.points[4], proof.points[5]]],
[proof.points[6], proof.points[7]],
[proof.merkleTreeRoot, proof.nullifier, _hash(proof.message), _hash(proof.scope)],
proof.merkleTreeDepth
);
These are the key components of Semaphore v4's implementation. When implementing Semaphore v4 with a different tech stack, these components should be rewritten, updated, or reused as needed.
For the implementation of Semaphore v4 it is RECOMMENDED to:
Benchmarks for the LeanIMT can be found in the LeanIMT paper.
Benchmarks for Semaphore are available in the Semaphore documentation.
The protocol MUST guarantee:
Semaphore v4 introduces two significant protocol updates:
The main protocol components, libraries, and contracts remain largely unchanged, with some adjustments to parameters and functions. However, Semaphore v4 is not backward compatible with the Semaphore v3.
The new data structure improves over the old one in terms of efficiency and storage with two major changes.
These two changes together make adding new members to the group much more efficient, reducing both off-chain processing time and on-chain gas costs.
In Semaphore, the term message (also known as signal) refers to the value a user shares when performing actions such as voting, confirming, sending a text message, and more.
The act of sharing a message (e.g., a text message or vote).
zk-artifacts are the collection of files generated during:
.r1cs
, .wasm
for circuit definition)..zkey
)In this document, zk-artifacts specifically refer to:
.wasm
.zkey
These zk-artifacts are used for:
.wasm
, .zkey
).EVM-compatible chains are blockchain networks that support the Ethereum Virtual Machine (EVM), allowing them to run Ethereum-based smart contracts and decentralized applications (dApps) without modification. These chains maintain compatibility with Ethereum’s tooling, such as wallets, development frameworks, and infrastructure, enabling seamless deployment and interoperability across ecosystems.
zk-friendly refers to functions or data structures optimized for efficient computation in zero-knowledge proofs. They reduce constraints in proving systems like zk-SNARKs, lowering costs and proof sizes. Examples include Poseidon (hash function) and LeanIMT (data structure).ˇ