Skip to main content

Guide: Setting up on-chain user verification with ZK Passport

This tutorial walks you through the process of setting up on-chain verification of ZK Passport proofs. By the end of this tutorial, you will have:

  1. Ensured that you have all the necessary components in place to verify ZK Passport proofs on your chain.
  2. Deployed the TD3QueryProofVerifier contract on your chain to handle proof verification.
  3. Integrated verification logic into your own contract to validate proof public signals.

Prerequisites

1. Setting up the smart contract for on-chain verification

1.1 Add the Rarimo Passport Contracts library to your project

The contracts are available in the @rarimo/passport-contracts npm package:

npm install @rarimo/passport-contracts

1.2 Deploy the TD3QueryProofVerifier smart contract

The TD3QueryProofVerifier contract is responsible for verifying ZK proofs on-chain. This contract often comes with a precompiled ZK circuit.

Import the TD3QueryProofVerifier contract from the Rarimo Passport Contracts library:

import {TD3QueryProofVerifier } from "@rarimo/passport-contracts/sdk/verifier/TD3QueryProofVerifier.sol";

Then deploy it to your chain and record the address of the newly deployed TD3QueryProofVerifier . You will use this address in your other contracts to verify proofs.

1.3 Integrate proof verification into your smart contract

Once your chain is replicating ZK Registry state and you have a deployed TD3QueryProofVerifier contract, you can add ZK proof checks to your own contract logic.

Below is a skeleton of the DApp that relies on the Query Proof Verification to perform some actions (token mint, grant a role, etc.).

pragma solidity ^0.8.28;

import {IPoseidonSMT} from "@rarimo/passport-contracts/interfaces/state/IPoseidonSMT.sol";
import {AQueryProofExecutor} from "@rarimo/passport-contracts/sdk/AQueryProofExecutor.sol";
import {PublicSignalsBuilder} from "@rarimo/passport-contracts/sdk/lib/PublicSignalsBuilder.sol";

contract MyPassportContract is AQueryProofExecutor {
struct UserData {
uint256 nullifier;
uint256 identityCreationTimestamp;
}

mapping(uint256 => bool) public usedNullifiers;

constructor(address registrationSMT_, address verifier_) {
__AQueryProofExecutor_init(registrationSMT_, verifier_);
}

// Called before proof verification
function _beforeVerify(bytes32, uint256, bytes memory userPayload_) public override {
(address user, UserData memory userData) = abi.decode(
userPayload_,
(address, UserData)
);

require(!usedNullifiers[userData.nullifier], "Nullifier already used");
usedNullifiers[userData.nullifier] = true;
}

// Called after successful proof verification
function _afterVerify(bytes32, uint256, bytes memory userPayload_) public override {
(address user, UserData memory userData) = abi.decode(
userPayload_,
(address, UserData)
);

// Grant access, mint tokens, or perform other actions
// Example: grantAccess(user);
}

// Builds the public signals for verification
function _buildPublicSignals(
bytes32,
uint256 currentDate_,
bytes memory userPayload_
) public override returns (uint256 dataPointer_) {
(address user, UserData memory userData) = abi.decode(
userPayload_,
(address, UserData)
);

// Query proof verification logic here

return dataPointer_;
}
}

The critical parts are the three required override methods:

  // Called before proof verification
function _beforeVerify(bytes32 registrationRoot_, uint256 currentDate_, bytes memory userPayload_) public override { /* ... */ }

// Called after successful proof verification
function _afterVerify(bytes32 registrationRoot_, uint256 currentDate_, bytes memory userPayload_) public override { /* ... */ }

// Builds the public signals for verification
function _buildPublicSignals(bytes32 registrationRoot_, uint256 currentDate_, bytes memory userPayload_)
public override returns (uint256 dataPointer_) { /* ... */ }

Above, your contract inherits from AQueryProofExecutor, which handles the ZK proof validation internally.

Below, you can see detailed structure for the _buildPublicSignals function. Which is the core of the SDK, where you setup the constraints for identity verification.

function _buildPublicSignals(
bytes32,
uint256 currentDate_,
bytes memory userPayload_
) public override returns (uint256 dataPointer_) {
(address user, UserData memory userData) = abi.decode(
userPayload_,
(address, UserData)
);

uint256 identityCreationTimestampUpperBound = getIdentityCreationTimestampUpperBound();
uint256 identityCounterUpperBound = type(uint32).max;

if (userData.identityCreationTimestamp > 0) {
identityCreationTimestampUpperBound = userData.identityCreationTimestamp;
identityCounterUpperBound = 1;
}

dataPointer_ = PublicSignalsBuilder.newPublicSignalsBuilder(SELECTOR, userData.nullifier);
dataPointer_.withEventIdAndData(getEventId(user), getEventData());
dataPointer_.withCurrentDate(currentDate_, 1 days);
dataPointer_.withTimestampLowerboundAndUpperbound(0, identityCreationTimestampUpperBound);
dataPointer_.withBirthDateLowerboundAndUpperbound(
PublicSignalsBuilder.ZERO_DATE,
BIRTHDAY_UPPERBOUND
);
dataPointer_.withIdentityCounterLowerbound(0, identityCounterUpperBound);
dataPointer_.withExpirationDateLowerboundAndUpperbound(
currentDate_,
PublicSignalsBuilder.ZERO_DATE
);

return dataPointer_;
}

// Helper functions
function getIdentityCreationTimestampUpperBound() public view returns (uint256) {
return accessStartTimestamp - IPoseidonSMT(getRegistrationSMT()).ROOT_VALIDITY();
}

function getEventId(address user) public view returns (uint256) {
// Implementation depends on your application
return uint256(keccak256(abi.encodePacked(block.chainid, address(this), user)));
}

function getEventData() public view returns (uint256) {
// Implementation depends on your application
return uint256(uint248(uint256(keccak256(abi.encodePacked(/* some data*/)))));
}

The client application calls the function execute(registrationRoot, currentDate, userPayload, zkPoints_) to perform the actions and verify the Query Proof.

To learn more about the verification parameters(public signals) and how to build them, check out PublicSignalsBuilder.sol.

2. Adding verification to your DApp front-end

The @rarimo/zk-passport-react package provides a QR code that users can scan to initiate the verification process. This component handles the collection of user data, proof generation, and on-chain verification.

2.1 Install the ZK Passport React package

First, install the SDK and the viem library in your React project:

npm install viem@^2.31.0 @rarimo/zk-passport-react

2.2 Add the QR code component to your DApp

To render the ZK Passport QR code in your DApp, you can use the ZkPassportQrCode component from the @rarimo/zk-passport-react package:

import ZkPassportQrCode from '@rarimo/zk-passport-react'
import { mainnet } from 'viem/chains' // Import the chain you are using

const requestId = 'account-1'
const apiUrl = 'https://api.app.rarime.com' // or your own instance of `verificator-svc`
const contractAddress = '<your_contract_address>' // Address of the deployed TD3QueryProofVerifier contract from Step 1.2
const receiverAddress = '<your_receiver_address>' // User's wallet address
const chain = mainnet

return (
<ZkPassportQrCode
apiUrl={apiUrl}
requestId={requestId}
verificationOptions={{
contractAddress,
receiverAddress,
chain,
}}
qrProps={{ size: 256 }}
onStatusChange={status => console.log(status)}
onSuccess={proof => console.log(proof)}
onError={error => console.error(error)}
/>
)

To learn more about the ZK Passport React component, check out the ZK Passport React component documentation.

Reference implementation

Here is a simple age-restricted airdrop DApp that allows users to claim tokens if they meet the age requirement:

Live demo is available at StackBlitz.

Conclusion

With these steps:

  1. Set up the ZK Passport Registry state replication on your chain if needed.
  2. Deploy the TD3QueryProofVerifier contract to facilitate ZK proof verification.
  3. Add proof verification into your DApp's smart contract by inheriting from AQueryProofExecutor and implementing the required methods.
  4. Integrate proof verification into your DApp's front-end using the ZK Passport React component.

We've set up on-chain verification of ZK Passport proofs. This allows you to verify user identities without revealing sensitive data, ensuring privacy and security in your application.