Design of an On-Chain Identity Authentication System Based on zk + Smart Contracts

2025-04-30

I named this system zkgate.fun, aiming to leverage the features of zero-knowledge proofs combined with blockchain to create a small utility.

The main function is to allow users to prove that they belong to a certain group, without revealing their real (on-chain) identity.

Here’s the current design idea: the administrator first has a list of names, which can be an array of Ethereum addresses. Based on this address list, a Merkle Root Hash is computed.

Then, this root hash is submitted to a smart contract.

People included in this list can use a proving key from a Circom circuit to generate a zk proof for themselves, and then submit the zk proof to the smart contract.

On the smart contract side, the verifier.sol generated by the Circom circuit will be used to verify the received zk proof, determine whether the address used to generate the zk proof is included in the Merkle Root Hash, and finally return the result of the verification.

In this way, the administrator doesn’t need to reveal which addresses are in the group, and addresses that belong to the group don’t need to declare their identity. They just need to submit a zk proof generated by the zero-knowledge proof system to prove that they indeed belong to the group.

Next, I will proceed to implement this design from a technical perspective.


Update (2025.05.14)

There is an existing zk protocol supported by the Ethereum Foundation, with a relatively mature toolchain and ecosystem, also designed for identity verification. It’s called Semaphore. You can try out the demo with a frontend interface directly on their official website:

In the first two iterations of zkgate.fun, I did not choose to use Semaphore’s EdDSA account system solution, mainly because I did not want to deviate from Ethereum’s account system, nor did I want to abandon ECDSA. However, in reality, only EdDSA is zk-friendly. It supports signing with Poseidon Hash, allowing signature verification directly within the zk circuit, eliminating the need for the awkward approach of “off-chain signature, on-chain recover.”

I have to admit, from a personal learning perspective, although it has only been a few days, I have already roughly understood the operation process of zk (toolchain). From the perspective of cutting-edge industry technology, it’s impossible for me to do better than Semaphore with just my personal efforts. Even if zkgate.fun were to further develop a frontend interface to visually demonstrate the specific interaction process, at most it would look like Semaphore’s Demo, and technically it would not be as sophisticated as Semaphore.

Therefore, the zkgate.fun project will no longer continue development. The domain will automatically expire after one year and will not be renewed.


Update v0.2.0 (2025.05.13)

This version addresses the issue of verifying address ownership. The basic idea is to separate zk proof from the ownership proof. Off-chain, zk is used to prove the path of the address in the Merkle Root. On-chain, users need to submit a signature of the root signed with their private key, and the signature is then submitted to the blockchain. The contract recovers the address from the signature and compares it with the address information contained in the zk circuit proof.

1. zk proof includes address information -> On-chain verification of zk proof -> Obtain address information from zk proof
2. Sign the root with private key -> Obtain signature on-chain -> Recover address from the signature

3. Check if the address in zk proof == address recovered from signature

Specific code changes in the demo:

  1. The offchain part does not need changes. The script that generates inputs.json already includes the key information in inputs.
  2. In the circuit code, the key in inputs needs to be made public.
  3. The contract code needs to accept the user’s signature as a parameter, and obtain the address recovered from it, to compare with the proof key.
  4. The script that calls the contract needs to sign the root with a private key here, and pass the signature as a parameter when calling the contract.

At this point, the functionality implemented by zkgate.fun allows group admins to avoid publicly disclosing their group member information on-chain. They only need to submit the Merkle Root Hash. For group members, with access to the full member list and a signature from their private key for their address, they can generate a zk proof to verify on-chain that they are indeed part of the group.

In this process, zk hides only one piece of information: the complete list of group members does not need to be publicly disclosed on-chain—only a Merkle Root Hash is required. However, the user’s address cannot be hidden for now, as it must be submitted on-chain for verification.


Update v0.1.0 Version (2025.05.09)

First, let’s correct a previous design mistake: administrators must publicly share their group address list. Otherwise, it’s impossible to generate a Merkle Tree based on the address list, and users won’t be able to locate their address within the tree structure or generate a path proof.

Secondly, I’m happy to report that a very basic demo is now working (smallyunet/zkgate-demo). While this demo is quite rudimentary and doesn’t yet verify address ownership within the circuit, it does demonstrate a functional toolchain.

Here’s how the implementation works:

  1. An off-chain script generates the zk circuit’s inputs.json from the address list and a user’s own address. This input file includes the Merkle Root Hash and the path required to verify node position.
  2. Based on the circuit code, it compiles some binary files, which are used to generate the witness file.
  3. It uses a public ptau file to generate a .zkey file.
  4. From the .zkey file, it exports proof.json, public.json, and verification_key.json. These three JSON files enable offline off-chain verification of the proof’s validity.
  5. It also exports a .sol file, i.e., the smart contract code, from the .zkey file to be deployed on-chain.
  6. By supplying the contents of proof.json and public.json as parameters, one can call the smart contract’s verifyProof function. It returns true if the proof is valid; otherwise, false.

If an address is not in the group list, two scenarios are possible:

  1. If someone tries to generate an inputs.json using an address not in the group list, the circuit will directly reject it and throw an error during proof generation.
  2. If someone tries to submit a fake proof with the correct root for on-chain verification, the proof will fail verification.

Currently, the biggest flaw in this initial demo is that the proof is built using plaintext addresses, such as:

const members = [
  "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
  "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
  "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
];

const proofKey = toField(members[0]);
const { siblings } = await tree.find(proofKey);

This code instructs the zk circuit to check whether members[0] is part of the Merkle tree constructed from the members array — and of course, it is. To construct a proof for a non-member address, one simply needs to replace the proofKey:

const nonMemberAddress = "0x1234567890123456789012345678901234567890";
const proofKey = toField(nonMemberAddress);
const { siblings } = await tree.find(proofKey);

In other words, the members list must be public. Right now, the program can only verify whether an address is in the members array. But even if members[0] is not my address, I can still construct a valid proof with it. So what’s the point of zk?

The next step is to have users sign a message with their private key, then use the zk circuit to recover the address from the signature, and finally check whether the recovered address is in the members array.

Sounds simple, right? But in practice, recovering an address from an ECDSA signature within a zk circuit is not only extremely complex—it’s like trying to build a nuclear reactor out of LEGO. No wonder people say working with zk makes your hair fall out.