Building Secure, Scalable, and Private DApps with Decentralized Identity Management
In the rapidly evolving landscape of Web3, the need for decentralized applications (DApps) that can scale while maintaining security and user privacy has never been more crucial. As developers, we are tasked with creating solutions that meet these demands and enhance the user experience in the decentralized world. This is where the combination of Privado ID and Cartesi comes into play.
Privado ID provides a decentralized identity management solution that ensures user data remains private and secure, while Cartesi’s off-chain computation capabilities allow DApps to scale effectively without overloading the blockchain. In this article, we’ll explore how to integrate these powerful protocols to build scalable and secure DApps.
Why Privado ID and Cartesi?
The Power of Decentralized Identity Management
Privado ID is designed to manage user identities in a decentralized manner, ensuring that sensitive data is never exposed or compromised. By using Privado ID, DApps can authenticate users securely while maintaining full decentralization — a key requirement in the Web3 ecosystem.
Scalability with Cartesi’s Off-Chain Computation
Cartesi enhances the scalability of DApps by offloading complex computations from the blockchain to a fully deterministic Linux Alt-VM. This not only improves the performance and scalability of your DApp but also provides programmability allowing users to build DApps in the framework of their choice (python, typescript..).
How to Build a Scalable DApp with Privado ID and Cartesi
Let’s dive into the practical implementation. We’ll use the Cartesi Privado repository as our base and break it down to understand how to integrate Privado ID for user authentication.
note: A prior understanding of cartesi-rollups & privado-id is expected due to the limited scope of this article.
Step 1: Clone the Repository and Set Up Your Environment
First, clone the Cartesi Privado repository and navigate to the project directory.
git clone https://github.com/jjhbk/cartesi-privado
cd cartesi-privado
Ensure that Docker is installed on your machine, as Cartesi relies on Docker containers for off-chain computations. Follow this guide to get started.
Step 2: Explore the Project Structure
Understanding the project structure is key to successful integration:
/backend
: Contains the Cartesi Dapp backend written in typescript/frontend
: frontend built with NextJS to verify user Identity (age>18 years) & start interacting with Cartesi backend/on-chain-verification
: Smart contracts that allow on-chain verification of zk proofs generated by PrivadoID & whitelist a user on Cartesi backend. It also contains Hardhat scripts to deploy & interact with these contracts/server
: An express server that allows off-chain verification of ZK-Proofs generated by Privado ID & authenticates user sessions.
Step 3: Understanding the Authentication Flow
- Privado ID wallet Setup:
- Download the Polygon ID mobile app on the Google Play or Apple app store.
- Open the app and set a PIN for security
2. Verifiable Credential Issuance:
A credential is any information about an identity attested by a reputed issuer. In this case, we will use a credential that attests to the DOB of a user and use that to conditionally check if the user’s age is above 18 years. For simplicity, we will use a self-issued credential
- Issue yourself a Credential of the type
KYC Age Credential Merklized
from the Polygon ID Issuer Sandbox - Import JSON schema v3
- Issue credential https://issuer-ui.polygonid.me/credentials/issue using the credential link and fetch credentials to mobile by scanning the QR code displayed.
3. Authentication request:
- The DApp creator creates a custom request on an Identity based on a Verifiable Credential using the query-builder in this case (userage>18).
- The request is registered on Universal Verifier Smart Contract & is mapped to a unique request ID.
- The front end embeds the request in a QR code and serves it to the user to generate a Proof.
- The user scans the QR code from his Privado ID wallet App to generate Proof and Submit it to an Onchain/Offchain Verifier (Universal Verifier Contract)
4. Verification:
- The submitted ZK proof is verified by the Universal Verifier Smart Contract of Privado ID and mapped against user address & requestID.
- The User then requests the DApp to whitelist his Ethereum Address from the front end. The front end sends a request to the Cartesi-Verifier Smart contract.
- The Cartesi-Verifier queries the Universal Verifier smart contract to check if the user has submitted valid proof for a given requestID.
- If the result is true, then an input is sent to the Cartesi Rollups Input Box smart contract to whitelist the User.
- The Cartesi Backend adds the user’s ETH address to the Whitelist to authenticate any future requests.
To get a better understanding of the Entire flow view this video of DecentraSign a DApp built using this architecture.
Step 4: Implementing the WhiteList on the Cartesi backend
Let’s look at the Cartesi backend to implement a whitelist:
import { createApp } from "@deroll/app";
import { decodeFunctionData, encodeAbiParameters, encodePacked, parseAbi, stringToHex, getAddress } from "viem";
//deroll is a High Level framework to bootstrap Cartesi Dapps// create application
const app = createApp({ url: "http://127.0.0.1:5004" });// Define a Map to store the Eth Address of authenticated Users
const WhiteList = new Map<string, boolean>();// define application ABI
const abi = parseAbi([
"function checkWhiteList(address user)",
"function addToWhiteList(address user)"
]);// handle input encoded as ABI function call
app.addAdvanceHandler(async (data) => {
const { functionName, args } = decodeFunctionData({ abi, data: data.payload });
switch (functionName) {
case "checkWhiteList":
const [user] = args;
console.log(`checking whitelist status of user: ${user} `);
if (WhiteList.get(String(user))) {
app.createReport({ payload: stringToHex(`user : ${user} is white listed`) });
} else {
app.createReport({ payload: stringToHex(`user: ${user} is not whitelisted please verify your identity first using privado ID`) });
}
return "accept";
case "addToWhiteList":if (getAddress(data.metadata.msg_sender) === getAddress("0x70c0dE66524a14a55BDb18D00a50e32648dCAa4c")) {
const [user] = args;
console.log(`adding ${user} to whitelist`);
WhiteList.set(user, true);
app.createNotice({ payload: stringToHex(`user: ${user} has been whitelisted`) })
return "accept"
}
app.createReport({ payload: stringToHex("your identity is not verified scan the qr code to verify your identity") });
return "reject"
}
});// start app
app.start().catch((e) => {
console.log(e);
process.exit(1);
});
This Dapp template has 2 functions to add & check the whitelist status of a user. The DApp adds a user to the whitelist if it receives an input from the Cartesi-Verifier smart contract which is deployed on PolygonAmoy testnet at 0x70c0dE66524a14a55BDb18D00a50e32648dCAa4c
Step 5: On-Chain Components
Create a custom request & register it on the Universal Verifier Smart Contract at /on-chain-verification/scripts/set-urequest.js
.
- A sample request:
{
id: "7f38a193-0918-4a48-9fac-36adfdb8b542",
typ: "application/iden3comm-plain-json",
type: "https://iden3-communication.io/proofs/1.0/contract-invoke-request",
thid: "7f38a193-0918-4a48-9fac-36adfdb8b542",
body: {
reason: "Cartesi Verification",
transaction_data: {
contract_address: "0x70696036CA1868B42155b06235F95549667Eb0BE",
method_id: "b68967e2",
chain_id: 80002,
network: "polygon-amoy",
},
scope: [
{
id: 1717138699,
circuitId: "credentialAtomicQuerySigV2OnChain",
query: {
allowedIssuers: ["*"],
context:
"https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld",
credentialSubject: {
birthday: {
$lt: 20020101,
},
},
type: "KYCAgeCredential",
},
},
],
},
}
Let’s break down the parameters of this request:
{
id: <unique request id>,
typ: <application messaging type>,
type: <type of request onchain/offchain>,
thid: <unique request id>,
body: {
reason: <dapp name>,
transaction_data: {
contract_address: <universal verifier smart contract address>,
method_id: <submit zkp proof function selector>,
chain_id: <chain id of the network>,
network: <network name>,
},
scope: [
{
id: <unique request id registered on the universal verifier smart contract>,
circuitId: <Type of verifier Circuit id>,
query: {
allowedIssuers: <List of trusted issuers>,
context:
<jsonld context of the verifiable credential>
credentialSubject: {
birthday: {
<your condition>
},
},
type: <type of credential>,
},
},
],
},
}
- Script to register your custom request on the universal verifier smart contract:
note: only modify the code inside the main() function according to your custom request
const { Web3 } = require("web3");
const { poseidon } = require("@iden3/js-crypto");
const { SchemaHash } = require("@iden3/js-iden3-core");
const { prepareCircuitArrayValues } = require("@0xpolygonid/js-sdk");
const UniversalVerifier = require("../abi/UniversalVerifier.json");
const { ethers } = require("hardhat");
// Put your values here
const CARTESI_U_VERIFIER_ADDRESS = "0x70c0dE66524a14a55BDb18D00a50e32648dCAa4c";
const VALIDATOR_ADDRESS = "0x8c99F13dc5083b1E4c16f269735EaD4cFbc4970d";
const UNIVERSAL_VERIFIER_ADDRESS = "0x70696036CA1868B42155b06235F95549667Eb0BE";
const Operators = {
NOOP: 0, // No operation, skip query verification in circuit
EQ: 1, // equal
LT: 2, // less than
GT: 3, // greater than
IN: 4, // in
NIN: 5, // not in
NE: 6, // not equal
};
function packValidatorParams(query, allowedIssuers = []) {
let web3 = new Web3(Web3.givenProvider || "ws://localhost:8545");
return web3.eth.abi.encodeParameter(
{
CredentialAtomicQuery: {
schema: "uint256",
claimPathKey: "uint256",
operator: "uint256",
slotIndex: "uint256",
value: "uint256[]",
queryHash: "uint256",
allowedIssuers: "uint256[]",
circuitIds: "string[]",
skipClaimRevocationCheck: "bool",
claimPathNotExists: "uint256",
},
},
{
schema: query.schema,
claimPathKey: query.claimPathKey,
operator: query.operator,
slotIndex: query.slotIndex,
value: query.value,
queryHash: query.queryHash,
allowedIssuers: allowedIssuers,
circuitIds: query.circuitIds,
skipClaimRevocationCheck: query.skipClaimRevocationCheck,
claimPathNotExists: query.claimPathNotExists,
},
);
}
function coreSchemaFromStr(schemaIntString) {
const schemaInt = BigInt(schemaIntString);
return SchemaHash.newSchemaHashFromInt(schemaInt);
}
function calculateQueryHashV2(
values,
schema,
slotIndex,
operator,
claimPathKey,
claimPathNotExists,
) {
const expValue = prepareCircuitArrayValues(values, 64);
const valueHash = poseidon.spongeHashX(expValue, 6);
const schemaHash = coreSchemaFromStr(schema);
const quaryHash = poseidon.hash([
schemaHash.bigInt(),
BigInt(slotIndex),
BigInt(operator),
BigInt(claimPathKey),
BigInt(claimPathNotExists),
valueHash,
]);
return quaryHash;
}
async function main() {
// you can run https://go.dev/play/p/oB_oOW7kBEw to get schema hash and claimPathKey using YOUR schema
const schemaBigInt = "74977327600848231385663280181476307657";
const type = "KYCAgeCredential";
const schemaUrl =
"https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld";
// merklized path to field in the W3C credential according to JSONLD schema e.g. birthday in the KYCAgeCredential under the url "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld"
const schemaClaimPathKey =
"20376033832371109177683048456014525905119173674985843915445634726167450989630";
const requestId = 1717138699;
const query = {
requestId,
schema: schemaBigInt,
claimPathKey: schemaClaimPathKey,
operator: Operators.LT,
slotIndex: 0,
value: [20020101, ...new Array(63).fill(0)], // for operators 1-3 only first value matters
circuitIds: ["credentialAtomicQuerySigV2OnChain"],
skipClaimRevocationCheck: false,
claimPathNotExists: 0,
};
query.queryHash = calculateQueryHashV2(
query.value,
query.schema,
query.slotIndex,
query.operator,
query.claimPathKey,
query.claimPathNotExists,
).toString();
const invokeRequestMetadata = {
id: "7f38a193-0918-4a48-9fac-36adfdb8b542",
typ: "application/iden3comm-plain-json",
type: "https://iden3-communication.io/proofs/1.0/contract-invoke-request",
thid: "7f38a193-0918-4a48-9fac-36adfdb8b542",
body: {
reason: "Cartesi Verification",
transaction_data: {
contract_address: UNIVERSAL_VERIFIER_ADDRESS,
method_id: "b68967e2",
chain_id: 80002,
network: "polygon-amoy",
},
scope: [
{
id: query.requestId,
circuitId: query.circuitIds[0],
query: {
allowedIssuers: ["*"],
context: schemaUrl,
credentialSubject: {
birthday: {
$lt: query.value[0],
},
},
type,
},
},
],
},
};
try {
// ############### Use this code to set request in Universal Verifier ############
const universalVerifier = await ethers.getContractAt(
UniversalVerifier.abi,
UNIVERSAL_VERIFIER_ADDRESS,
);
// You can call this method on behalf of any signer which is supposed to be request controller
const tx = await universalVerifier.setZKPRequest(requestId, {
metadata: JSON.stringify(invokeRequestMetadata),
validator: VALIDATOR_ADDRESS,
data: packValidatorParams(query),
});
console.log("Request set", tx);
} catch (e) {
console.log("error: ", e);
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
set request using: npx hardhat run on-chain-verification/scrips/set-urequest.js --network amoy
- Cartesi Verifier Smart Contract:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {PrimitiveTypeUtils} from "@iden3/contracts/lib/PrimitiveTypeUtils.sol";
import {ICircuitValidator} from "@iden3/contracts/interfaces/ICircuitValidator.sol";
import {UniversalVerifier} from "@iden3/contracts/verifiers/UniversalVerifier.sol";
interface IInputBox {
/// @notice Emitted when an input is added to a DApp's input box.
/// @param dapp The address of the DApp
/// @param inputIndex The index of the input in the input box
/// @param sender The address that sent the input
/// @param input The contents of the input
/// @dev MUST be triggered on a successful call to `addInput`.
event InputAdded(
address indexed dapp,
uint256 indexed inputIndex,
address sender,
bytes input
);
/// @notice Add an input to a DApp's input box.
/// @param _dapp The address of the DApp
/// @param _input The contents of the input
/// @return The hash of the input plus some extra metadata
/// @dev MUST fire an `InputAdded` event accordingly.
/// Input larger than machine limit will raise `InputSizeExceedsLimit` error.
function addInput(
address _dapp,
bytes calldata _input
) external returns (bytes32);
/// @notice Get the number of inputs in a DApp's input box.
/// @param _dapp The address of the DApp
/// @return Number of inputs in the DApp's input box
function getNumberOfInputs(address _dapp) external view returns (uint256);
/// @notice Get the hash of an input in a DApp's input box.
/// @param _dapp The address of the DApp
/// @param _index The index of the input in the DApp's input box
/// @return The hash of the input at the provided index in the DApp's input box
/// @dev `_index` MUST be in the interval `[0,n)` where `n` is the number of
/// inputs in the DApp's input box. See the `getNumberOfInputs` function.
function getInputHash(
address _dapp,
uint256 _index
) external view returns (bytes32);
}
interface IWhiteList {
function addToWhiteList(address user) external;
}
contract CartesiUVerifier {
uint64 public constant REQUEST_ID = 1717138697;
UniversalVerifier public verifier;
address public constant INPUT_BOX_ADDRESS =
0x59b22D57D4f067708AB0c00552767405926dc768;
constructor(UniversalVerifier verifier_) {
verifier = verifier_;
}
function whiteList(
uint64 _requestID,
address _dapp
) external returns (bytes32) {
require(
verifier.getProofStatus(msg.sender, _requestID).isVerified,
"only identities who provided sig or mtp proof for transfer requests are allowed to receive tokens"
);
return
IInputBox(INPUT_BOX_ADDRESS).addInput(
_dapp,
abi.encodeCall(IWhiteList.addToWhiteList, (msg.sender))
);
}
}
This contract verifies the user’s identity by checking the validity of their proof with Privado ID & sends an input to the Input Box smart Contract of Cartesi Rollups.
Step 6: Frontend
- The frontend contains a template to interact with the Cartesi backend we will focus only on serving the authentication request to the user for on-chain verification in this article:
"use client";
import injectedModule from "@web3-onboard/injected-wallets";
import { init } from "@web3-onboard/react";
import configFile from "./config.json";
import { Network } from "./network";
import QRCode from "react-qr-code";
import { useState } from "react";
import { v4 as uuidv4 } from "uuid";
//import { verifyMessage } from "ethers/lib/utils";
import { encodeFunctionData, parseAbi } from "viem";
const styles = {
root: {
color: "#2C1752",
fontFamily: "sans-serif",
textAlign: "center",
},
title: {
color: "#7B3FE4",
},
};
const ngrok_url = "<your_ngrok_url>";
const amoy_rpc_url = "<your_rpc_url>";
// update with your contract address
const deployedContractAddress = "0x70c0dE66524a14a55BDb18D00a50e32648dCAa4c";
const id = uuidv4();
// more info on query based requests: https://0xpolygonid.github.io/tutorials/wallet/proof-generation/types-of-auth-requests-and-proofs/#query-based-request
const amoyUqrProofJson = {
id: "7f38a193-0918-4a48-9fac-36adfdb8b542",
typ: "application/iden3comm-plain-json",
type: "https://iden3-communication.io/proofs/1.0/contract-invoke-request",
thid: "7f38a193-0918-4a48-9fac-36adfdb8b542",
body: {
reason: "Cartesi Verification",
transaction_data: {
contract_address: "0x70696036CA1868B42155b06235F95549667Eb0BE",
method_id: "b68967e2",
chain_id: 80002,
network: "polygon-amoy",
},
scope: [
{
id: 1717138699,
circuitId: "credentialAtomicQuerySigV2OnChain",
query: {
allowedIssuers: ["*"],
context:
"https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld",
credentialSubject: {
birthday: {
$lt: 20020101,
},
},
type: "KYCAgeCredential",
},
},
],
},
};
const config = configFile;
const injected = injectedModule();
init({
wallets: [injected],
chains: Object.entries(config).map(([k, v]: [string, any], i) => ({
id: k,
token: v.token,
label: v.label,
rpcUrl: v.rpcUrl,
})),
appMetadata: {
name: "Cartesi-Privado Verifier",
icon: "<svg><svg/>",
description: "Cartesi Dapp with PrivadoID Verification",
recommendedInjectedWallets: [
{ name: "MetaMask", url: "https://metamask.io" },
],
},
});
export default function Home() {
const [qrcode, setQrcode] = useState(amoyUqrProofJson);
return (
<div>
<h1>Cartesi-Privado Verifier</h1>
<Network />
<div>
<h2>
Verify your Age Using Privado ID to start Interacting with Cartesi
DApp greater than 18 years of age
</h2>
<p>
Scan the QR code with your Polygon ID Wallet APP to prove your age &
start Interacting
</p>
<div>
<QRCode
level="Q"
style={{ marginLeft: 50, alignContent: "center", width: 256 }}
value={JSON.stringify(qrcode)}
/>
</div>
<br />
<br />
{/*
<button onClick={getAuthRequest}>Get Qr-code</button>
<br />
<br />
<button
onClick={() => {
setQrcode(onchain_qr_proof_request);
}}
>
Get on chain Qr-code
</button>
<p>
Polygonscan:{" "}
<a
href={`https://mumbai.polygonscan.com/token/${deployedContractAddress}`}
target="_blank"
>
ERC20TokenAddress
</a>
</p>
*/}
</div>
</div>
);
}
This code embeds the request in a QR code to be served to the user.
Step 7: Deploying Your Scalable DApp
Follow the instructions in the Cartesi-Deployment-guide build a Cartesi image and generate the templateHash. If you want to deploy a CartesiDApp to a custom network like Polygon Amoy use the deploy_dapp.js
script.
note:
const { ethers } = require("hardhat");
const SelfhostedDappFactory = require('../abi/SelfHostedApplicationFactory.json');
async function main() {
const owner = <your Ethereum address>;
let selfhostedDappFactory = await ethers.getContractAt(SelfhostedDappFactory.abi, SelfhostedDappFactory.address);
let templateHash = <unique-template-hash of Cartesi Dapp>;
let salt = <unique hash to provide entropy>;
try {
const txId = await selfhostedDappFactory.deployContracts(owner, owner, templateHash, salt);
const txId = await selfhostedDappFactory.calculateAddresses(owner, owner, templateHash, salt);
console.log("dapp created set: ", txId);
} catch (e) {
console.log("error: ", e);
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Conclusion
In the world of Web3, building DApps that are scalable, secure, and user-friendly is paramount. By integrating Privado ID with Cartesi, you can create a DApp that not only meets these demands but exceeds them. The combination of decentralized identity management and off-chain computation opens new avenues for developers, enabling the creation of next-generation applications that truly leverage the power of blockchain.
Whether you’re developing a new DApp or enhancing an existing one, the tools provided by Privado ID and Cartesi are invaluable. Explore the Cartesi Privado repository today, and take your DApp development to the next level.
A sample DApp built with this template DecentraSign:
Repo: https://github.com/jjhbk/decentraSign
Live Demo: https://decentra-sign-frontend.vercel.app/
Video Demo: https://drive.google.com/file/d/1vfqxyS7KILsdzY_lqxIWFKKrmD7MLXC2/view