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 on-chain. By the end of this tutorial, you will have:
- Ensure that you have all the necessary components in place to verify ZK Passport proofs on your chain.
- Deployed the Verificator contract on your chain to handle proof verification.
- Integrated verification logic in your own contract to validate proof public signals.
TODO: add step for setting up the front-end with our new SDK
Prerequisites
- In case you are using an EVM-compatible chain other than Rarimo's L2, you'll need to to set up the 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 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 up verificator-svc.
Step 1: Deploy the Verificator smart contract
TODO: put this contract into an npm package? TODO: guide to customizing the query params
The Verificator contract is responsible for verifying the ZK proofs on-chain. This contract often comes with a precompiled ZK circuit.
-
Obtain the
Verificator
contract source code and add it to your project.contract Verificator {
function verifyProof(
uint[2] calldata _pA,
uint[2][2] calldata _pB,
uint[2] calldata _pC,
uint[23] calldata _pubSignals
) public view returns (bool) {
...
}
} -
Deploy it to your chain and record the address of the newly deployed
Verificator
. You will use this address in your other contracts to verify proofs.
Step 2: Integrate Proof Verification in Your Smart Contract
Once your chain is replicating ZK Registry state and you have a deployed Verificator
contract, you can add ZK proof checks to your own contract logic.
A. Collect public signals and proof from verificator-svc
When a user scans a QR code and submits a proof:
-
Your DApp calls the
getProof
endpoint fromverificator-svc
. -
Receive both
pubSignals
and theproof
from the service:{
"pubSignals": ["12345", "67890", "..."],
"proof": "0xabc123..."
} -
Pass the
pubSignals
as calldata to theverifyProof()
method in the smart contract along theproof
.pragma solidity ^0.8.0;
interface IVerificator {
function verifyProof(
uint256[] calldata pubSignals,
bytes calldata proof
) external view returns (bool);
}
contract MyPassportContract {
IVerificator public verificator;
constructor(address _verificator) {
// this is the address of the deployed Verificator contract from step #1
verificator = IVerificator(_verificator);
}
function verifyPassportProof(
uint256[] calldata pubSignals,
bytes calldata proof
) external {
bool isValid = verificator.verifyProof(pubSignals, proof);
require(isValid, "Passport proof is invalid!");
// If the proof is valid, proceed with your logic
// e.g., granting access, minting an NFT, etc.
}
}The critical part is:
bool isValid = verificator.verifyProof(pubSignals, proof);
require(isValid, "Passport proof is invalid!");Here, your contract delegates ZK proof validation to the deployed
Verificator
contract.
B. Manually assemble the pub signals before passing them to the verifyProof
method
In your smart contract (let's call it MyPassportContract
), you would do something like this:
pragma solidity ^0.8.0;
import {VerifierHelper} from "@solarity/solidity-lib/libs/zkp/snarkjs/VerifierHelper.sol";
interface IVerificator {
function verifyProof(
uint256[] calldata pubSignals,
bytes calldata proof
) external view returns (bool);
}
contract MyPassportContract {
IVerificator public verificator;
constructor(address _verificator) {
// this is the address of the deployed Verificator contract from step #1
verificator = IVerificator(_verificator);
}
function verifyPassportProof(
bytes32 registrationRoot_,
uint256 currentDate_,
uint256 eventId_,
uint256[] memory eventData_,
UserData memory userData_,
VerifierHelper.ProofPoints memory zkPoints_
) external {
uint256[] memory pubSignals_ = new uint256[](PROOF_SIGNALS_COUNT);
pubSignals_[0] = userData_.nullifier; // output, nullifier
pubSignals_[4] = userData_.citizenship;
pubSignals_[9] = proposalEventId; // input, eventId used to scope your proofs
pubSignals_[10] = uint248(uint256(keccak256(abi.encode(eventData_)))); // input, eventData specific to your DApp
pubSignals_[11] = uint256(registrationRoot_); // input, ZK Registry state root at the moment of proving
pubSignals_[12] = SELECTOR; // input, selector specifying the passport fields to be revealed
pubSignals_[13] = currentDate_; // input, currentDate
pubSignals_[15] = identityCreationTimestampUpperBound; // input, timestampUpperbound
pubSignals_[17] = identityCounterUpperBound; // input
pubSignals_[18] = ZERO_DATE; // input, birthDateLowerbound
pubSignals_[19] = proposalRules_.birthDateUpperbound; // input, birthDateUpperbound
pubSignals_[20] = proposalRules_.expirationDateLowerBound; // input, expirationDateLowerbound
pubSignals_[21] = ZERO_DATE; // input, expirationDateUpperbound
require(votingVerifier.verifyProof(pubSignals_, zkPoints_), "Passport proof is invalid!");
// If the proof is valid, proceed with your logic
// e.g., granting access, minting an NFT, etc.
}
}
Here are a couple of code references that demonstrate how to collect proof public signals and pass them to a verification contract:
-
ERC1155ETH.sol Shows how a contract gathers public signals and proof parameters before calling
verifyProof
. -
BioPassportVoting.sol Demonstrates a voting contract that uses ZK proofs for passport-based identity checks.
Conclusion
With these steps:
- Replicate the ZK Registry state using
RegistrationSMTReplicator
. - Get proof parameters from
verificator-svc
to collect the ZK Proof in our DApp. - Deploy your
Verificator
contract to verify the proofs. - Integrate
verifyProof(...)
calls in your own contract logic.
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.