Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
LABS
Guides

Setting Up End-to-End Encrypted Messaging with Zero-Knowledge Proofs

A technical tutorial for developers to implement a private messaging layer using libsignal for encryption and zero-knowledge proofs for anonymous access control.
Chainscore © 2026
introduction
PRIVACY BY DESIGN

Introduction

This guide explains how to build an end-to-end encrypted messaging application enhanced with zero-knowledge proofs for verifiable identity and metadata protection.

End-to-end encryption (E2EE) is the standard for private communication, ensuring only the sender and intended recipient can read messages. However, traditional E2EE systems like Signal or WhatsApp still expose metadata—who is talking to whom, when, and how often. This guide explores how zero-knowledge proofs (ZKPs) can augment E2EE to create a new class of messaging applications that protect both content and context. We'll use practical examples with the zk-SNARK library snarkjs and the Circom circuit language to build verifiable, private interactions.

Zero-knowledge proofs allow one party (the prover) to convince another (the verifier) that a statement is true without revealing any information beyond the validity of the statement itself. In messaging, this enables features like proving you are in a trusted contact list without revealing the list, verifying message integrity without exposing its content, or demonstrating group membership anonymously. These cryptographic primitives move us from simple content secrecy to computational privacy, where even the logic of the interaction can be kept confidential.

We will construct a system where users can send encrypted messages via a relay server. To prevent spam and sybil attacks, the sender must attach a ZKP demonstrating they possess a valid credential—like a proof-of-humanity attestation or a token from a social graph—without disclosing their specific identity. This is implemented through a circuit that checks a cryptographic signature against a public list of authorized keys. The server verifies the proof in milliseconds, learning only that the message is from an authorized user, not which user.

The technical stack for this tutorial includes libsodium for high-performance encryption (XChaCha20-Poly1305), circom for writing the arithmetic circuits that define proof statements, and snarkjs for proof generation and verification. We assume familiarity with JavaScript/Node.js and basic cryptographic concepts. All code will be runnable in a Node.js environment, with clear steps for setting up the required dependencies and libraries.

By the end of this guide, you will have a working prototype of a command-line messaging client that uses E2EE and ZKPs. You'll understand how to design circuits for privacy-preserving authentication, integrate proof generation into a client application, and set up a server that verifies proofs before routing messages. This foundation can be extended to build decentralized, censorship-resistant communication platforms with strong privacy guarantees.

prerequisites
FOUNDATION

Prerequisites

Before building end-to-end encrypted messaging with zero-knowledge proofs, you need a foundational setup. This section covers the essential tools, libraries, and accounts required to follow the tutorial.

To build a zero-knowledge (ZK) encrypted messaging system, you'll need a development environment capable of handling cryptographic operations and smart contract interaction. This includes Node.js (v18 or later) and a package manager like npm or yarn. You will also need a code editor such as Visual Studio Code. The core cryptographic work will be done using the Circom compiler for writing ZK circuits and the snarkjs library for proof generation and verification. For the messaging application's backend logic, we'll use Hardhat or Foundry for Ethereum smart contract development and testing.

You must have a basic understanding of public-key cryptography, hash functions, and elliptic curve operations. Familiarity with the Groth16 or PLONK proving systems is beneficial, as they are commonly used for efficient proof generation. The tutorial will use the BN254 curve (often called the "alt_bn128" curve in Ethereum), which is natively supported by the Ethereum Virtual Machine for precompiled contract verification. Ensure your local setup can compile Circom circuits, which may require installing Rust and performing a one-time setup of the Power of Tau trusted setup ceremony for your circuit's constraints.

For the application layer, you'll need a Web3 wallet like MetaMask to interact with the blockchain. We will deploy a simple registry or inbox contract to a testnet, so acquire test ETH from a faucet for networks like Sepolia or Goerli. The frontend will use a framework like Next.js or React with the ethers.js or viem library for wallet connectivity. All code examples will be in TypeScript for type safety. Clone the starter repository from the Chainscore Labs GitHub which includes the initial circuit structure and contract interfaces to begin.

Finally, understand the core workflow: a sender will encrypt a message, generate a ZK proof that they know the decryption key without revealing it, and post the ciphertext and proof on-chain. A verifier contract will validate the proof, allowing the intended recipient (who holds the private key) to decrypt the message off-chain. This ensures metadata privacy and sender authentication. With these prerequisites in place, you are ready to start implementing the cryptographic protocols and smart contracts that form a trust-minimized, encrypted messaging system.

key-concepts
END-TO-END ENCRYPTION

Core Concepts

Learn the cryptographic foundations and practical implementations for building private messaging systems on-chain using zero-knowledge proofs.

key-concepts-text
PRIVACY ENGINEERING

Setting Up End-to-End Encrypted Messaging with Zero-Knowledge Proofs

A technical guide to building a messaging system where privacy is mathematically guaranteed, not just promised.

End-to-end encrypted messaging is the standard for private communication, but traditional systems like Signal rely on a central server to manage user identities and public keys. This creates a metadata vulnerability: the server knows who is talking to whom. Zero-knowledge proofs (ZKPs) solve this by allowing users to prove they are authorized to send a message without revealing their identity. This guide uses zk-SNARKs (Succinct Non-Interactive Arguments of Knowledge) to construct a system where the only data exposed on-chain is an encrypted message and a proof of valid sender credentials.

The core protocol requires three smart contracts: a Registry for anonymous identity commitments, a Mailbox for storing encrypted payloads, and a Verifier to validate ZK proofs. A user first generates a secret nullifier and a public commitment (e.g., commitment = PoseidonHash(nullifier, publicKey)), registering only the commitment. To send a message, the prover generates a zk-SNARK proof that: 1) they know a nullifier corresponding to a registered commitment, 2) they have correctly encrypted the message for the recipient's public key, and 3) they have not used this nullifier before (preventing replay attacks). The proof is verified on-chain before the encrypted ciphertext is accepted.

For development, we use Circom for circuit design and SnarkJS for proof generation. Below is a simplified Circom circuit template for the sender's proof of membership and encryption. This circuit ensures the public outputs (commitment, encryptedMessage) are correctly derived from the private inputs (nullifier, secretKey, plaintext).

circom
template MessageSender() {
    signal input nullifier;
    signal input secretKey;
    signal input plaintext;
    signal input recipientPubKey;

    signal output commitment;
    signal output encryptedMessage;

    // 1. Recompute commitment from nullifier & public key
    component hash = Poseidon(2);
    hash.in[0] <== nullifier;
    hash.in[1] <== secretKey;
    commitment <== hash.out;

    // 2. Encrypt message (simplified for example)
    // In practice, use a ZK-friendly encryption like MiMC
    component enc = Encrypt(secretKey, recipientPubKey);
    enc.plaintext <== plaintext;
    encryptedMessage <== enc.ciphertext;
}

Deploying this system involves a specific sequence: first deploy the Verifier contract with the generated zk-SNARK proving key, then the Registry and Mailbox. Users interact via a client that handles key management, ZKP generation, and wallet signing. When sending, the client submits a transaction to the Mailbox with the encryptedMessage and proof. The Mailbox calls the Verifier; if valid, it emits an event that the recipient's client watches for to decrypt new messages. This architecture ensures the blockchain never sees sender identity, recipient identity, or message content—only a verifiable proof of legitimate action.

Key considerations for production include selecting ZK-friendly cryptographic primitives like Poseidon hash and MiMC encryption for circuit efficiency, managing gas costs of on-chain verification, and designing a robust identity commitment system to prevent sybil attacks. Projects like zkEmail and Semaphore offer foundational libraries for these components. The result is a messaging layer where privacy is enforced by cryptographic proof, moving beyond trust in any central operator and significantly reducing the metadata footprint inherent in Web3 communication.

CHOOSE YOUR PATH

Implementation Steps

Initial Project Configuration

Start by setting up a Node.js/TypeScript environment. You'll need libraries for elliptic curve cryptography and ZKP circuits.

bash
npm init -y
npm install typescript ts-node @types/node
npm install circomlib snarkjs ethers

For ZKP development, install Circom 2.0 and snarkjs to write and compile circuits. Define a simple circuit in circuits/message_verify.circom that proves knowledge of a private key corresponding to a public key used for encryption.

circom
pragma circom 2.0.0;
include "node_modules/circomlib/circuits/poseidon.circom";

template MessageVerify() {
    signal input privateKey;
    signal input publicKeyHash; // Poseidon hash of public key
    signal output verified;

    component hasher = Poseidon(1);
    hasher.inputs[0] <== privateKey;

    // Verify computed hash matches the claimed public key hash
    verified <== isEqual(hasher.out, publicKeyHash);
}

Compile the circuit and generate the proving/verification keys. Store these keys securely for your application.

LIBRARY SELECTION

Encryption and ZK Library Comparison

A comparison of popular libraries for implementing end-to-end encryption and zero-knowledge proofs in messaging applications.

Feature / Metriclibsignal-protocolzkp-ecdsa (ZoKrates)Semaphore (circom)

Primary Use Case

End-to-end encryption

ZK proof of ECDSA signature

ZK group membership proof

Proof System

Groth16

Groth16 / PLONK

Trusted Setup Required

Proof Generation Time

N/A

< 2 sec (Rinkeby)

< 5 sec (mainnet)

Proof Verification Gas Cost

N/A

~250k gas

~450k gas

Language / Framework

Java, Swift, JavaScript

ZoKrates DSL

circom circuit language

Active Maintenance

Suitable for Message Secrecy

Suitable for Anonymous Auth

code-walkthrough
ZK MESSAGING

Code Walkthrough: Key Exchange and Session Setup

This guide walks through the cryptographic core of establishing a secure, private messaging channel using key exchange and zero-knowledge proofs.

End-to-end encrypted messaging requires a secure method for two parties to establish a shared secret over an insecure channel. We use the X25519 elliptic curve Diffie-Hellman (ECDH) key exchange. Each user generates a long-term identity key pair and an ephemeral key pair for each session. The shared secret is derived by combining one party's private key with the other's public key. This secret forms the basis for symmetric encryption keys, but proving you hold the correct private key without revealing it requires zero-knowledge proofs.

To authenticate the exchange without a trusted server, we implement a Schnorr-based zero-knowledge proof. When User A initiates a session, they generate a proof that they know the private key corresponding to their published public identity key. The proof, π_a, is sent alongside their ephemeral public key. User B can verify this proof using A's public identity key. This step prevents man-in-the-middle attacks by cryptographically binding the session initiator to a proven identity, all while keeping the private key secret.

The responder, User B, must also authenticate themselves. Upon verifying A's proof, B generates their own ephemeral key pair and a corresponding zero-knowledge proof π_b. B then computes the shared secret using their private ephemeral key and A's public ephemeral key. This mutual proof exchange ensures both parties are who they claim to be. The protocol's security relies on the discrete logarithm problem for key exchange and the soundness of the Schnorr proof system for authentication.

Here is a simplified code snippet for the initiator's proof generation using a library like circomlib or snarkjs:

javascript
// Import necessary circuits and libraries
const { schnorr } = require('circomlib');

function generateSessionProof(privateKey, ephemeralPublicKey) {
  // Create a Schnorr proof of knowledge of the private key
  const publicKey = derivePublicKey(privateKey);
  const challenge = hash(ephemeralPublicKey, publicKey);
  const proof = schnorr.prove(privateKey, challenge);
  
  return {
    publicKey: publicKey,
    ephemeralPublicKey: ephemeralPublicKey,
    proof: proof
  };
}

The proof contains a commitment and a response, which the verifier checks against the public key and a reconstructed challenge.

After successful mutual verification, both parties derive the same shared secret: S = X25519(priv_eph_a, pub_eph_b) = X25519(priv_eph_b, pub_eph_a). This 32-byte secret is passed through a Key Derivation Function (KDF) like HKDF to create separate encryption and MAC keys for the session. The session state is then initialized, typically storing these keys, nonce counters, and the peer's public identity key. This setup phase, authenticated by ZK proofs, establishes a confidential channel resistant to eavesdropping and impersonation.

For production systems, consider these critical details: use a robust random number generator for key material, implement proper key rotation by generating new ephemeral keys per session, and include the protocol version and cipher suite in the key derivation context. Audited libraries such as libsignal (used by Signal) provide proven implementations of these patterns. The combination of X25519 and Schnorr proofs offers a strong balance of performance and security for decentralized, private messaging applications.

zk-proof-integration
TUTORIAL

Setting Up End-to-End Encrypted Messaging with Zero-Knowledge Proofs

This guide explains how to implement a private messaging system where users can prove group membership without revealing their identity, using zero-knowledge proofs for access control.

End-to-end encrypted messaging ensures only the sender and recipient can read messages. However, traditional systems often require revealing your identity to a server to join a group chat. Zero-knowledge proofs (ZKPs) solve this by allowing a user to cryptographically prove they belong to a permitted group—like holders of a specific NFT—without disclosing which specific token they own. This creates private, gated communities where access is verified trustlessly. We'll build a conceptual system using Semaphore, a ZKP protocol for anonymous signaling, to demonstrate this.

The core component is a Semaphore group. This is an on-chain Merkle tree where each leaf is a commitment representing a member's identity. To join, a user generates a secret identity (a nullifier and trapdoor), creates a commitment, and submits it to the group contract. The contract updates the Merkle root. Critically, the commitment reveals no information about the user's underlying secret. For our messaging app, the group would consist of commitments from all users permitted to access a specific channel.

When a user wants to send a message, they generate a ZKP. This proof cryptographically demonstrates three things without revealing the prover's identity: 1) They possess a valid secret identity. 2) Their commitment is a valid leaf in the current group Merkle tree. 3) They haven't sent a message with this identity before (using the nullifier). The proof is verified by a smart contract, which also records the nullifier to prevent replay attacks. The message itself can be encrypted with a key derived from the group parameters, ensuring only other group members can decrypt it.

Here's a simplified code snippet using the @semaphore-protocol library to create an identity and generate a proof for a signal (message):

javascript
import { Identity, Group, generateProof } from '@semaphore-protocol/identity';

// 1. User creates a secret identity
const identity = new Identity();
const commitment = identity.generateCommitment();

// 2. Group contract adds commitment, updates Merkle root (off-chain simulation)
const group = new Group();
group.addMember(commitment);
const merkleProof = group.generateMerkleProof(0); // Proof of membership for leaf index 0

// 3. Generate ZKP to send an anonymous signal
const externalNullifier = group.id; // Unique ID for the chat room
const signal = "Hello, group!";
const fullProof = await generateProof(identity, merkleProof, externalNullifier, signal);

The fullProof can be sent to a verifier contract.

For production, you need a verifier smart contract. Semaphore provides pre-compiled SemaphoreVerifier contracts. Your main application contract would store the group Merkle root, accept proofs, and check used nullifiers. Upon successful verification, it emits an event containing the encrypted message. Clients listen for these events and decrypt messages if they are group members. This architecture ensures the server never knows who sent a message or who is in the group, providing strong privacy guarantees.

Key considerations for deployment include managing group membership updates efficiently, choosing a secure encryption layer for the message payload (like XOR with a shared secret derived from the group root), and handling gas costs. This pattern extends beyond messaging to private voting, anonymous feedback systems, and any application requiring privacy-preserving access control. The code and concepts are based on Semaphore v3.0.0.

ZK MESSAGING

Frequently Asked Questions

Common technical questions and troubleshooting for developers implementing end-to-end encrypted messaging with zero-knowledge proofs.

The most common primitive is a zk-SNARK (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge). It allows a prover (the sender) to convince a verifier (the receiver or a relayer) that a statement about an encrypted message is true without revealing the message itself. For example, a sender can prove they know the plaintext that hashes to a specific commitment, or that a ciphertext was correctly encrypted for a recipient's public key, without disclosing the plaintext. This is often implemented using circuits written in frameworks like Circom or Halo2, which compile to the proving systems used by blockchains like Ethereum (e.g., Groth16, PLONK).

ENCRYPTED MESSAGING

Troubleshooting Common Issues

Common technical challenges and solutions for developers implementing end-to-end encrypted messaging with zero-knowledge proofs.

Slow proof generation is a common bottleneck. It's often caused by inefficient circuit design or incorrect configuration.

Primary causes and fixes:

  • Circuit complexity: Minimize non-deterministic operations and use optimal constraints. Profile with tools like snarkjs or circomspect.
  • Witness calculation: Ensure your off-chain witness generator is optimized. Use WebAssembly or native bindings for heavy computations.
  • Proving key size: Larger keys increase time. Use a trusted setup for your specific circuit; reusing generic setups can be suboptimal.
  • Hardware limits: ZK-SNARK proving (e.g., Groth16) is CPU/RAM intensive. For production, consider dedicated servers or proof aggregation services like zkSharding.

Example: A simple chat message circuit in Circom might take 2-3 seconds, while one with complex reputation checks could take 30+ seconds.

conclusion
IMPLEMENTATION SUMMARY

Conclusion and Next Steps

You have successfully implemented a basic end-to-end encrypted messaging system using zero-knowledge proofs for identity verification. This guide covered core concepts and a practical setup.

This tutorial demonstrated how to combine end-to-end encryption (E2EE) with zero-knowledge proofs (ZKPs) to create a messaging system where users can prove their identity without revealing their private key. The core components were: a Solidity ZKPMessenger smart contract for on-chain verification and message storage, a Node.js/TypeScript backend for proof generation and encryption, and a simple frontend for interaction. The Semaphore library was used to generate ZK proofs, while the libsodium-wrappers library handled the XChaCha20-Poly1305 encryption for the message payloads.

The primary security model relies on the separation of identity and data. A user's Semaphore identity commitment is stored on-chain, allowing them to generate a ZKP (via the @semaphore-protocol/proof library) that proves membership in a group or knowledge of a secret without exposing it. The actual message content is encrypted client-side before being sent to the contract. This means the blockchain only stores encrypted ciphertext and verifies proof validity, never seeing the plaintext message or the user's private identity trapdoor.

For production deployment, several critical next steps are required. First, upgrade the cryptographic libraries to their latest audited versions and implement a secure, non-custodial method for users to manage their Semaphore identity. Second, integrate a relayer service to pay gas fees on behalf of users, preserving their privacy by decoupling transaction submission from identity. Third, consider moving from a simple smart contract to a ZK rollup or validium (like those built with StarkEx or zkSync) to reduce on-chain data availability costs for encrypted messages.

To extend this system, you could implement additional features such as group messaging using Semaphore groups, where a proof demonstrates membership in a specific chat. Adding message authentication via signatures alongside encryption would ensure integrity. Furthermore, exploring fully homomorphic encryption (FHE) or multi-party computation (MPC) protocols could enable private computations on encrypted data within the messaging context, opening doors to private group voting or data aggregation.

The code from this guide is a foundational prototype. For further learning, review the official documentation for Semaphore, libsodium, and Ethereum development tools. Experiment with testnets like Sepolia or Holesky before considering mainnet deployment, and always prioritize security audits for any system handling private user data and cryptographic operations.

How to Build End-to-End Encrypted Messaging with ZK Proofs | ChainScore Guides | ChainScore Labs