| description | Use ZK Passport to verify users on-chain without revealing private data. Learn how to implement the verification process step-by-step. |
|---|---|
| sidebar_label | Verifying on-chain |
import OutLink from "@site/src/components/OutLink"; import IdealImage from '@site/src/components/IdealImage';
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:
- Ensured that you have all the necessary components in place to verify ZK Passport proofs on your chain.
- Deployed the
TD3QueryProofVerifiercontract on your chain to handle proof verification. - Integrated verification logic into your own contract to validate proof public signals.
{ //TODO: // - guide to customizing the query params }
- If you are using an EVM-compatible chain other than Rarimo's L2, you'll need to set up ZK Passport Registry state replication as described in Setting up Cross-Chain Verification.
- Access to the
verificator-svcservice for retrieving proof parameters. You can use the public instance ofverificator-svc(https://api.app.rarime.com/) for testing, but it's recommended to deploy your own instance for production use, as described in Setting upverificator-svc.
The contracts are available in the @rarimo/passport-contracts npm package:
npm install @rarimo/passport-contractsThe 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.
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)
);
// highlight-start
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
);
// highlight-end
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.
{ // TODO: reference to the public signals builder when it's ready }
To learn more about the verification parameters(public signals) and how to build them, check out PublicSignalsBuilder.sol.
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.
First, install the SDK and the viem library in your React project:
npm install viem@^2.31.0 @rarimo/zk-passport-react
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.
Here is a simple age-restricted airdrop DApp that allows users to claim tokens if they meet the age requirement:
- ClaimableToken.sol: smart contract that implements the ZK Passport proof verification.
- on-chain-verification-react: front-end that uses the ZK Passport React component to verify user age and uniqueness.
Live demo is available at StackBlitz.
With these steps:
- Set up the ZK Passport Registry state replication on your chain if needed.
- Deploy the
TD3QueryProofVerifiercontract to facilitate ZK proof verification. - Add proof verification into your DApp's smart contract by inheriting from
AQueryProofExecutorand implementing the required methods. - 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.