Onchain Verification
ZKPassport allows you to verify passport and national ID proofs directly on EVM chains. As of today, our verifier is only deployed on Ethereum Sepolia. If you need a specific chain, please reach out to us.
The following guide explains how to use ZKPassport proofs in your smart contracts.
Overview
Onchain verification enables your smart contracts to:
- Verify user identity proofs without requiring centralized servers
- Create unique identifiers linked to real-world identity
- Apply age, nationality, or other ID-based restrictions to on-chain actions
Verifier Contract
ZKPassport maintains a deployed ZKPassportVerifier
contract on Sepolia that handles all proof verification. Your contracts will interact with this verifier.
Integration Steps
1. Build your query for onchain verification
import { ZKPassport } from "@zkpassport/sdk";
const zkPassport = new ZKPassport("your-domain.com");
// Create a request with your app details
const queryBuilder = await zkPassport.request({
name: "Your App Name",
// A path to your app's logo
logo: "https://your-domain.com/logo.png",
// A description of the purpose of the request
purpose: "Doing something",
// Optional scope for the user's unique identifier
scope: "my-scope",
// To verify proofs on EVM chains, you need to set the mode to "compressed-evm"
mode: "compressed-evm",
// Specify the EVM chain where the proof will be verified, for now only `ethereum_sepolia` is supported
evmChain: "ethereum_sepolia",
});
// Build your query with the required attributes or conditions you want to verify
const {
url,
requestId,
onRequestReceived,
onGeneratingProof,
onProofGenerated,
onResult,
onReject,
onError,
} = queryBuilder
// Disclose the user's nationality
.disclose("nationality")
// Disclose the user's document type
.disclose("document_type")
// Verify the user's age is greater than or equal to 18
.gte("age", 18)
// Bind the user's address to the proof
.bind("user_address", "0x1234567890123456789012345678901234567890")
// Bind custom data to the proof
.bind("custom_data", "my-custom-data")
// Finalize the query
.done();
2. Get Verifier Contract Details
Use the SDK's getSolidityVerifierDetails
method to get verifier contract details:
const {
// The address of the deployed verifier contract
address,
// The function name to call on the verifier contract
functionName,
// The ABI of the verifier contract
abi,
} = zkPassport.getSolidityVerifierDetails("ethereum_sepolia");
3. Generate Verification Parameters from Proof
When a user completes a verification in your app, use getSolidityVerifierParameters
to prepare the parameters to pass to the verifier contract:
let proof: ProofResult;
// Use the proofResult from the onProofGenerated callback to get the proof
onProofGenerated((proofResult: ProofResult) => {
proof = proofResult;
});
onResult(
({
uniqueIdentifier,
verified,
result,
}: {
uniqueIdentifier: string;
verified: boolean;
result: QueryResult;
}) => {
if (!verified) {
// If the proof is not verified, save yourself some gas and return straight away
console.log("Proof is not verified");
return;
}
// Get the verification parameters
const verifierParams = zkPassport.getSolidityVerifierParameters({
proof: proof,
// Use the same scope as the one you specified with the request function
scope: "my-scope",
// Enable dev mode if you want to use mock passports, otherwise keep it false
devMode: false,
});
// Get the wallet provider
const walletProvider = await getWalletProvider();
// Verify the proof on-chain
// The function is defined in the next steps below
await verifyOnChain(
verifierParams,
walletProvider,
// Use the document type to determine if the proof is for an ID card or passport
result.document_type.disclose.result !== "passport"
);
}
);
4. Create Your Smart Contract
Create a contract that interacts with the verifier:
pragma solidity ^0.8.21;
enum ProofType {
DISCLOSE,
AGE,
BIRTHDATE,
EXPIRY_DATE,
NATIONALITY_INCLUSION,
NATIONALITY_EXCLUSION,
ISSUING_COUNTRY_INCLUSION,
ISSUING_COUNTRY_EXCLUSION
}
struct ProofVerificationParams {
bytes32 vkeyHash;
bytes proof;
bytes32[] publicInputs;
bytes committedInputs;
uint256[] committedInputCounts;
uint256 validityPeriodInDays;
string domain;
string scope;
bool devMode;
}
interface IZKPassportVerifier {
// Verify the proof
function verifyProof(ProofVerificationParams calldata params) external returns (bool verified, bytes32 uniqueIdentifier);
// Get the inputs for the age proof
function getAgeProofInputs(bytes calldata committedInputs, uint256[] calldata committedInputCounts) external view returns (uint256 currentDate, uint8 minAge, uint8 maxAge);
// Get the inputs for the disclose proof
function getDiscloseProofInputs(
bytes calldata committedInputs,
uint256[] calldata committedInputCounts
) external pure returns (bytes memory discloseMask, bytes memory discloseBytes);
// Get the disclosed data from the proof
function getDisclosedData(
bytes calldata discloseBytes,
bool isIDCard
) external view returns (
string memory name,
string memory issuingCountry,
string memory nationality,
string memory gender,
string memory birthDate,
string memory expiryDate,
string memory documentNumber,
string memory documentType
);
// Get the inputs for the nationality/issuing country inclusion and exclusion proofs
function getCountryProofInputs(
bytes calldata committedInputs,
uint256[] calldata committedInputCounts,
ProofType proofType
) external pure returns (string[] memory countryList);
// Get the inputs for the birthdate and expiry date proofs
function getDateProofInputs(
bytes calldata committedInputs,
uint256[] calldata committedInputCounts,
ProofType proofType
) external pure returns (uint256 currentDate, uint256 minDate, uint256 maxDate);
// Get the inputs for the bind proof
function getBindProofInputs(
bytes calldata committedInputs,
uint256[] calldata committedInputCounts
) external pure returns (bytes memory data);
// Get the bound data from the raw data returned by the getBindProofInputs function
function getBoundData(bytes calldata data) external view returns (address userAddress, string memory customData);
// Verify the scope of the proof
function verifyScopes(bytes32[] calldata publicInputs, string calldata domain, string calldata scope) external view returns (bool);
}
contract YourContract {
IZKPassportVerifier public zkPassportVerifier;
// Map users to their verified unique identifiers
mapping(address => bytes32) public userIdentifiers;
constructor(address _verifierAddress) {
zkPassportVerifier = IZKPassportVerifier(_verifierAddress);
}
function register(ProofVerificationParams calldata params, bool isIDCard) public returns (bytes32) {
// Verify the proof
(bool verified, bytes32 uniqueIdentifier) = zkPassportVerifier.verifyProof(params);
require(verified, "Proof is invalid");
// Check the proof was generated using your domain name (scope) and the subscope
// you specified
require(
zkPassportVerifier.verifyScopes(params.publicInputs, "your-domain.com", "my-scope"),
"Invalid scope"
);
// Get the age condition checked in the proof
(uint256 currentDate, uint8 minAge, uint8 maxAge) = zkPassportVerifier.getAgeProofInputs(
params.committedInputs,
params.committedInputCounts
);
// Make sure the date used for the proof makes sense
require(block.timestamp >= currentDate, "Date used in proof is in the future");
// This is the condition for checking the age is 18 or above
// Max age is set to 0 and therefore ignored in the proof, so it's equivalent to no upper limit
// Min age is set to 18, so the user needs to be at least 18 years old
require(minAge == 18 && maxAge == 0, "User needs to be above 18");
// Get the disclosed bytes of data from the proof
(, bytes memory disclosedBytes) = zkPassportVerifier.getDiscloseProofInputs(
params.committedInputs,
params.committedInputCounts
);
// Get the nationality from the disclosed data and ignore the rest
// Passing the disclosed bytes returned by the previous function
// this function will format it for you so you can use the data you need
(, , string memory nationality, , , , , ) = zkPassportVerifier.getDisclosedData(
disclosedBytes,
isIDCard
);
// Get the raw data bound to the proof
// This is the data you bound to the proof using the bind method in the query builder
bytes memory data = zkPassportVerifier.getBindProofInputs(
params.committedInputs,
params.committedInputCounts
);
// Use the getBoundData function to get the formatted data
// which includes the user's address and any custom data
(address userAddress, string memory customData) = zkPassportVerifier.getBoundData(data);
// Make sure the user's address is the one that is calling the contract
require(userAddress == msg.sender, "Not the expected sender");
// You could also check the custom data if you bound any to the proof
require(customData == "my-custom-data", "Invalid custom data");
// If you didn't specify any custom data, make sure the string is empty
// require(bytes(customData).length == 0, "Custom data should be empty");
// Store the unique identifier
userIdentifiers[msg.sender] = uniqueIdentifier;
return uniqueIdentifier;
}
// Your contract functionality using the verification
function restrictedFunction() public view {
require(userIdentifiers[msg.sender] != bytes32(0), "Not verified");
// Function logic for verified users
}
}
5. Use the SDK to connect your frontend and your smart contract
Connect your frontend to the smart contract using the SDK:
import { ZKPassport } from "@zkpassport/sdk";
import { createWalletClient, createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
import { custom } from "viem/wallet";
async function verifyOnChain(proofResult, walletProvider, isIDCard) {
const zkPassport = new ZKPassport("your-domain.com");
// Get verification parameters
const verifierParams = zkPassport.getSolidityVerifierParameters({
proof: proofResult,
// Use the same scope as the one you specified with the request function
scope: "my-scope",
// Enable dev mode if you want to use mock passports, otherwise keep it false
devMode: false,
});
// Create wallet client
const walletClient = createWalletClient({
chain: sepolia,
transport: custom(walletProvider),
});
// Get the account
const [account] = await walletClient.getAddresses();
// Create a public client
const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
});
// Call your contract with the verification parameters
const hash = await walletClient.writeContract({
address: YOUR_CONTRACT_ADDRESS,
abi: YOUR_CONTRACT_ABI,
functionName: "register",
args: [verifierParams, isIDCard],
account,
});
// Wait for the transaction
await publicClient.waitForTransactionReceipt({ hash });
console.log("Verification completed on-chain!");
}