Skip to content

Latest commit

 

History

History
259 lines (190 loc) · 11.2 KB

File metadata and controls

259 lines (190 loc) · 11.2 KB
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';

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.

{ //TODO: // - guide to customizing the query params }

Prerequisites

  • 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-svc service for retrieving proof parameters. You can use the public instance of verificator-svc (https://api.app.rarime.com/) for testing, but it's recommended to deploy your own instance for production use, as described in Setting up verificator-svc.

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)
    );

    // 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.

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:

  • 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.

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.