zkTLSプロトコルを使用したアプリケーション開発【zkPass編】

ponta

ponta

· 13 min read
Thumbnail

株式会社Pontechのぽんたです。PontechではzkTLSを用いたアプリケーションの開発に着目・注力しています。

zkTLSについての考察と期待する理由

zkTLSのプロトコルを使用したアプリケーションの開発方法について記事を書きます。今回はzkPass編です。

今回開発するアプリケーション

StackupというDeveloper向けのLearn&Earnプラットフォームで提供されていたミッションがとても分かりやすく、公式チュートリアル以上に充実していたので、そちらに沿ってやっていきます。

Quest 2 - Understand how to use the Extension zkPass JS-SDK

Quest 3 - Using zkPass In A Practical Example


ざっくり言うとNotionの特定のOrganizationに入れる人だけがオンチェーンからSecretの文字列を参照できるというデモです。

zkPassのダッシュボードでの設定

zkPass dev center にアクセスし、ウォレットでログインします。

Projectを作成します。開発環境でテストするため、Domainはlocalhostに設定します。

Image

カスタムスキーマの設定

次にSchemaを作成していきます。Schemaの設定には以下の2つのBrowser Extensionが必要です。

zkPass Schema Validatorは、構築されたJSONスキーマが正しく機能していることを検証します。JSONスキーマは、証明生成のために取得する外部データソースを記述するための「仕様」です。JSONスキーマについての詳細は次のステップで説明されます。

zkPass Transgate拡張機能は、証明生成が行われる場所です。これはエンドユーザーにもインストールしてもらう必要があります。

JSON Shcema

JSONスキーマ(または単にスキーマ)は、以下の3つの主要な要素で構成されます

  1. 検証条件に基づいて必要なAPIリクエスト
  2. 検証に必要なフィールドを抽出するためのAPIレスポンス内のパス
  3. 検証のためのアサーション

以下はスキーマの詳細な定義です:

  • issuer: データソースの名前(例: Twitter、Discordなど)。
  • desc: スキーマの詳細な説明。
  • website: 指定されたAPIが含まれるウェブページのリンク。このウェブページは、ユーザーが検証を開始するとブラウザタブでTransGate拡張機能によって表示されます。
  • APIs: 指定されたデータを返すRESTful APIのリスト。配列として定義されており、複数のAPIを含むことができます。
    • host: APIのホスト。
    • intercept: APIの詳細情報を定義。TransGate拡張機能は、ここで定義されたAPIに対して3P-TLSを実行します。
      • url: APIのURLの絶対パス。
      • method: APIのHTTPメソッド。
      • query: HTTPリクエストヘッダー内のクエリパラメータ。
        • keyとvalue: クエリのキーと値を指定。例: URL https://...?needBalanceDetail=true の場合、"needBalanceDetail": "true"と定義。
        • verify: キーと値がゼロ知識検証を受ける必要がある場合、値はtrueまたはfalseとする。
      • header: HTTPリクエストヘッダー内のヘッダーパラメータ。
        • keyとvalue: ヘッダーのキーと値を指定。例: ヘッダーgraphql-operation: AvatarMenuQuery の場合、"graphql-operation": "AvatarMenuQuery"と定義。
        • verify: キーと値がゼロ知識検証を受ける必要がある場合、値はtrueまたはfalseとする。
      • body: HTTPリクエスト内のボディ。
        • keyとvalue: ボディのキーと値を指定。
        • verify: キーと値がゼロ知識検証を受ける必要がある場合、値はtrueまたはfalseとする。
    • assert: 検証条件を定義。
      • key: JSON内で検証されるデータフィールドを選択。キーは「|」で区切られます。
      • value: 上記で定義されたキーに関連付けられたデータと比較する値。
      • operation: アサーション条件(例: >, <, =, >=, <=, !=, in, contains)。
    • nullifier: データソース内のJSONで一意の識別子フィールドを選択(例: uid、idなど)。
  • HRCondition: 各アサーションの説明。
  • tips: データソースのウェブサイトの右下に表示されるユーザーへのヒント。

nullifierinterceptは次の用途で重要です。

  • nullifier: ユーザーの一意の識別子を提供するため、Sybil攻撃を防止します。
  • intercept: 外部データソースを取得し、APIエンドポイントを指し示すために必要であり、証明を生成するために使用されます。

zkPass Dev Centerの使用方法

zkPass Dev Centerを使用すると、開発者は一意のアプリIDを生成し、プリセットまたはゼロからJSONスキーマを作成できます。各JSONスキーマには一意のスキーマIDが割り当てられます。

  • アプリID: zkPassをアプリに統合するために使用されます。各アプリIDにはスキーマとスキーマIDのリストが関連付けられています。スキーマIDに対して間違ったアプリIDを使用すると、zkPass統合が無効になります。
  • データソース: ユーザーは外部のプライベートデータソースを使用して証明を生成したり、証明生成に利用したりすることができます。

どのように動作するのかを理解するために、カスタムJSONスキーマを作成するためのデータポイントを選択してみましょう。

データポイントの選択

ゼロ知識証明(zkProofs)に使用できる任意のデータポイントを選択できます。これらのデータポイントは、クライアント側でAPIコールを行い、サーバー側のデータを取得します。

今回のクエストでは、Notionアプリをこれらのデータポイントのソースとして使用します。以下に、その利用方法を説明します。

データポイントを有効活用する方法

例えば、次のようなテストを実行できます:

  • 例1: Notionページを所有していることを証明する → この場合、Notionアカウントを所有していることにもなります(プロセスでログインが必要だからです)。
  • 例2: 特定のNotionページにアクセスできることを証明する → 所有権は関係なく、そのページにアクセス権があることを示します。
Image

データはJSON形式で提供されます。注目すべきフィールドはuserHasExplicitAccessで、ユーザーが指定されたNotionページにアクセスできるかを確認するために使用されます。

もしNotionページ上に「リクエストアクセス」ボタンが表示されている場合、それはそのページへのアクセス権がないことを意味します。ただし、これはあくまでデモンストレーション用の例です。このクエストのステップの目的は、証明に使用できるデータポイントをどのように探すかを学ぶことにあります。

使用できるデータについての理解が深まったところで、次に進んでカスタムスキーマを作成しましょう。

{
  "issuer": "Notion",
  "desc": "A productivity and note-taking web application",
  "website": "https://www.notion.so/xxxxxxx",
  "APIs": [
    {
      "host": "www.notion.so",
      "intercept": {
        "url": "api/v3/getUserAnalyticsSettings",
        "method": "POST"
      },
      "nullifier": "user_id"
    },
    {
      "host": "www.notion.so",
      "intercept": {
        "url": "api/v3/getPublicPageData",
        "method": "POST"
      },
      "assert": [
        {
          "key": "userHasExplicitAccess",
          "value": "true",
          "operation": "=",
          "verify": true
        }
      ]
    }
  ],
  "HRCondition": [
    "Notion Has Page Access"
  ],
  "tips": {
    "message": "Login to your Notion Account and select any page you want to check you have access to. Wait for the page to fully load."
  }
}

次に、JSONスキーマエディタの右下付近にある「Check Schema」を選択します。スキーマ内のターゲットウェブサイトフィールドが正しく設定されていれば、指定されたNotionページにリダイレクトされます。リダイレクト後、「Check Pass」ボタンが表示されたモーダルが表示されます。検証が完了するまで待ちます。APIエンドポイントがスキーマ内で正しく設定されている場合、以下のスクリーンショットのような一致が確認されます。「Check Pass」ボタンをクリックしてください。

Image

最後に、zkPass Dev Centerのページで**「Submit」**をクリックし、新しいスキーマを登録します。

Image

Vite+Reactでフロントエンドの作成

Vite + React プロジェクトを準備します。以下のコマンドを実行して、プロジェクトテンプレートを作成してください。

npm create vite@latest zkpass-capstone -- --template react-swc-ts
cd zkpass-capstone
npm install
npm install @zkpass/transgate-js-sdk vite-plugin-node-polyfills web3 ethers

npm run dev

http://127.0.0.1:5173に立ち上がります。

App.tsxを編集していきます。

import { type FormEvent, useEffect, useState } from "react";
import "./App.css";
import TransgateConnect from "@zkpass/transgate-js-sdk";
import type { Result } from "@zkpass/transgate-js-sdk/lib/types";
import { ethers } from "ethers";

const App = () => {
	const [appId, setAppId] = useState<string>("xxxxxxxxxxx");
	const [schemaId, setSchemaId] = useState<string>("xxxxxxxxx");
	const [result, setResult] = useState<Result | undefined>(undefined);
	return (
		<div className="app">
			<form
				className="form"
				onSubmit={(e) => requestVerifyMessage(e, appId, schemaId)}
			>
				<label htmlFor="app-id">
					AppId:
					<input
						id="app-id"
						type="text"
						placeholder="Your App ID"
						value={appId}
						onChange={(e) => setAppId(e.target.value)}
					/>
				</label>
				<label htmlFor="schema-id">
					SchemaId:
					<input
						id="schema-id"
						type="text"
						placeholder="Your App ID"
						value={schemaId}
						onChange={(e) => setSchemaId(e.target.value)}
					/>
				</label>
				<button type="submit">Start Verification</button>
				{result !== undefined ? (
					<pre>Result: {JSON.stringify(result, null, 2)}</pre>
				) : (
					""
				)}
			</form>
		</div>
	);
}

export default App;

appIdschemaIdには初期状態のプレースホルダ値がすでに割り当てられています。ご自身のappIdとschemaIdにこれらの「xxxxxxxxxxx」を置き換えてください

検証ロジックを書き始める前に、プロジェクトのルートディレクトリに移動してください。その後、vite.config.tsファイルを編集します。内容をすべて以下のコードスニペットに置き換えてください。

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import { nodePolyfills } from "vite-plugin-node-polyfills";

// https://vitejs.dev/config/
export default defineConfig({
	build: {
		modulePreload: {
			polyfill: true,
		},
	},
	server: {
		port: 5173,
		host: "127.0.0.1",
	},
	plugins: [
		nodePolyfills({
			globals: {
				Buffer: true,
			},
		}),
		react(),
	],
});

App.tsxに戻り、編集していきます。

export type TransgateError = {
	message: string,
	code: number
}
const requestVerifyMessage = async (
		e: FormEvent,
		appId: string,
		schemaId: string,
	) => {
		e.preventDefault();
		try {
			const connector = new TransgateConnect(appId);
			const isAvailable = await connector.isTransgateAvailable();

			if (isAvailable) {
				const provider = window.ethereum ? new ethers.BrowserProvider(window.ethereum) : null;
				const signer = await provider?.getSigner()
				const recipient = await signer?.getAddress()
				const res = (await connector.launch(schemaId, recipient)) as Result;
				console.log("Result", res);
              const verifiedResult = connector.verifyProofMessageSignature(
					"evm",
					schemaId,
					res
				);

				if (verifiedResult) {
					alert("Verified Result");
					setResult(res);
				}

} else {
				console.log(
					"Please install zkPass Transgate from https://chromewebstore.google.com/detail/zkpass-transgate/afkoofjocpbclhnldmmaphappihehpma",
				);
			}
		} catch (error) {
			const transgateError = error as TransgateError;
			alert(`Transgate Error: ${transgateError.message}`);
			console.log(transgateError);
		}
	};

vite-env.d.ts. を以下に置き換えます。

/// <reference types="vite/client" />

import type { Eip1193Provider } from "ethers/providers";

declare global {
	interface Window {
		ethereum?: Eip1193Provider | null;
	}
}

実際に試してみます。

ImageImage

コントラクトの開発

forge init secretをプロジェクトのRootディレクトリで実行します。secretフォルダができます。

touch src/{Proof,ProofVerifier,Attestation,GetSecret}.sol


以下のスマートコントラクトは、Proof.solProofVerifier.sol、およびAttestation.solです。これらのスマートコントラクトは、オンチェーンでデータを検証および確認するために一緒に使用されます。そのためには、生成された証明を確認する必要があります。以下は、証明の生成時に得られる重要なフィールドのAPIリファレンスです。

証明に関連する重要なフィールド

  • allocatorAddress (string): アロケーターノードのアドレス。
  • allocatorSignature (string): アロケーターノードによるタスクメタデータの署名。
  • publicFields (Object): スキーマで定義された公開フィールドの値。
  • publicFieldsHash (string): 公開フィールド値のハッシュ。
  • taskId (string): アロケーターノードによって割り当てられたタスクの一意のID。
  • uHash (string): データソース内のユーザー一意IDのハッシュ値。
  • validatorAddress (string): バリデーターノードのアドレス。
  • validatorSignature (string): バリデーターノードによる検証結果の署名。

また、証明を生成するためのデータを取得する特定のAPIエンドポイント(例: Notion API)を指し示すJSONスキーマのスキーマIDが必要です。このスキーマIDはzkPass Dev Centerで生成されます。

各スマートコントラクトの機能

  • Proof.sol
    • Proof構造体を含み、証明に必要なフィールドを保持します。
  • ProofVerifier.sol
    • バリデータアドレスを検証するロジックを含みます。
  • Attestation.sol
    • 検証済みの証明を記録し、証明の正確性を保証します。また、証明が再検証された、または再検証可能であることを証明します(アテステーション)。

これらのスマートコントラクトを次のステップで記述していきます。


Proof.sol

// SPDX-License-Identifier: APACHE-2.0
pragma solidity ^0.8.20;

struct Proof {
    bytes32 taskId;
    bytes32 schemaId;
    bytes32 uHash;
    address recipient;
    bytes32 publicFieldsHash;
    address validator;
    bytes allocatorSignature;
    bytes validatorSignature;
}

ProofVerifier.sol

// SPDX-License-Identifier: APACHE-2.0
pragma solidity ^0.8.20;

import {Proof} from "./Proof.sol";

contract ProofVerifier {
    address public defaultAllocator = 0x19a567b3b212a5b35bA0E3B600FbEd5c2eE9083d;

    constructor() {}

    function verify(Proof memory _proof) public view returns (bool) {
        return (
            verifyAllocatorSignature(_proof.taskId, _proof.schemaId, _proof.validator, _proof.allocatorSignature)
                && verifyValidatorSignature(
                    _proof.taskId,
                    _proof.schemaId,
                    _proof.uHash,
                    _proof.recipient,
                    _proof.publicFieldsHash,
                    _proof.validator,
                    _proof.validatorSignature
                )
        );
    }

    function verifyAllocatorSignature(
        bytes32 _taskId,
        bytes32 _schemaId,
        address _validator,
        bytes memory _allocatorSignature
    ) public view returns (bool) {
        bytes32 hashed = keccak256(abi.encode(_taskId, _schemaId, _validator));
        address allocator = recoverSigner(prefixed(hashed), _allocatorSignature);

        return (allocator == defaultAllocator);
    }

    function verifyValidatorSignature(
        bytes32 _taskId,
        bytes32 _schemaId,
        bytes32 _uHash,
        address _recipient,
        bytes32 _publicFieldsHash,
        address _validator,
        bytes memory _validatorSignature
    ) public pure returns (bool) {
        bytes32 hashed = keccak256(abi.encode(_taskId, _schemaId, _uHash, _publicFieldsHash, _recipient));
        address validator = recoverSigner(prefixed(hashed), _validatorSignature);

        return (validator == _validator);
    }

    function prefixed(bytes32 hash) private pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }

    function recoverSigner(bytes32 _hash, bytes memory _signature) private pure returns (address signer) {
        require(_signature.length == 65, "Invalid signature length");
        bytes32 r;
        bytes32 s;
        uint8 v;
        assembly {
            r := mload(add(_signature, 0x20))
            s := mload(add(_signature, 0x40))
            v := byte(0, mload(add(_signature, 0x60)))
        }
        if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
            revert("SignatureValidator#recoverSigner: invalid signature 's' value");
        }

        if (v != 27 && v != 28) {
            revert("SignatureValidator#recoverSigner: invalid signature 'v' value");
        }

        signer = ecrecover(_hash, v, r, s);
        // Prevent signer from being 0x0
        require(signer != address(0x0), "SignatureValidator#recoverSigner: INVALID_SIGNER");
        return signer;
    }
}

Attestation.sol

// SPDX-License-Identifier: APACHE-2.0
pragma solidity ^0.8.20;

import {Proof} from "./Proof.sol";
import {ProofVerifier} from "./ProofVerifier.sol";

struct Attestation {
    bytes32 uid;
    bytes32 schema;
    bytes32 uHash;
    address recipient;
    bytes32 publicFieldsHash;
}

contract SampleAttestation is ProofVerifier {
    mapping(bytes32 uid => Attestation) private attestations;
    mapping(address => bytes32 uid) private attestedAddresses;

    string private secret = "bad Secret";

    constructor() ProofVerifier() {}

    function attest(bytes memory _proofAsBytes) public returns (string memory) {
        Proof memory _proof = abi.decode(_proofAsBytes, (Proof));
        require(verify(_proof), "verify proof fail");

        Attestation memory attestation = Attestation({
            uid: 0,
            schema: _proof.schemaId,
            uHash: _proof.uHash,
            recipient: _proof.recipient,
            publicFieldsHash: _proof.publicFieldsHash
        });

        bytes32 uid;
        uint32 bump = 0;
        while (true) {
            uid = getUID(attestation, bump);
            if (attestations[uid].uid == 0) {
                break;
            }

            unchecked {
                ++bump;
            }
        }

        attestation.uid = uid;

        attestations[uid] = attestation;
        attestedAddresses[_proof.recipient] = uid;
        return secret;
    }

    function getAttestationFromAddress(address _recipient) public view returns (Attestation memory) {
        return attestations[attestedAddresses[_recipient]];
    }

    function getAttestation(bytes32 uid) public view returns (Attestation memory) {
        return attestations[uid];
    }

    function getUID(Attestation memory attestation, uint32 bump) private pure returns (bytes32) {
        return keccak256(
            abi.encodePacked(
                attestation.schema, attestation.uHash, attestation.recipient, attestation.publicFieldsHash, bump
            )
        );
    }
}

Attestation.sol の概要

Attestation.sol は、証明が検証されるたびに、**証明されたデータの正確性を保証する「アテステータ」**として機能します。このコントラクトは、ProofVerifierを利用して署名を検証し、attest関数を通じて検証済み証明の記録(アテステーション)を作成します。

主な特徴

アテステーションの一意性:
各アテステーションにはgetUID関数から生成される新しいUIDが割り当てられます。これにより、すべてのアテステーションは一意であることが保証されます。

秘密の返却:
アテステーションが生成されると、同じattest関数の最後のreturn文で秘密(secret)が返されます。

オンチェーン記録:
このコードは、すべての検証済み証明をオンチェーンに記録し、それらの証明が改ざんされないことを保証するために重要です。

GetSecret.sol

// SPDX-License-Identifier: MPL-2.0
pragma solidity ^0.8.20;

import {Proof} from "./Proof.sol";

contract GetSecret {
    address public proxy;

    string private secret = "bad Secret";

    event Response(bool success, string secret, address recipient);

    constructor(address _proxy) {
        proxy = _proxy;
    }

    function getSecret() public view returns (string memory) {
        return secret;
    }

    function assignSecret(Proof calldata _proof) public returns (string memory) {
        require(msg.sender == _proof.recipient, "Sender address do not match with recipient!");
        (bool success, bytes memory secretData) =
            proxy.call(abi.encodeWithSignature("attest(bytes)", abi.encode(_proof)));
        if (success) {
            secret = abi.decode(secretData, (string));
            emit Response(true, secret, msg.sender);
            return secret;
        }
        revert("Nuh uh!");
    }
}

本来はテストケースを書くべきですが、この記事では省略します。

scriptsディレクトリにDeployer.s.solを作成します。

// SPDX-License-Identifier: MPL-2.0
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {GetSecret} from "../src/GetSecret.sol";

contract Deployer is Script {
    GetSecret public getSecret;
    address public proxyContract = 0x75982eBc73a9Fc60d4CBbb4be8CF301bfC032976;

    function run() external {
        // reads our .env file
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);
        getSecret = new GetSecret(proxyContract);
        vm.stopBroadcast();
    }
}

Deployer.s.solを実行してコントラクトをデプロイします。

source .env
forge script --chain sepolia script/Deployer.s.sol:Deployer --rpc-url ${SEPOLIA_RPC_URL} --broadcast -vvvv

フロントエンドにコントラクトとのインタラクションを実装していきます。

npm install @wagmi/cli wagmi [email protected] @tanstack/react-query


wagmi.config.tsを作成し、以下のコードを実装します。

import { defineConfig } from "@wagmi/cli";
import { foundry, react } from "@wagmi/cli/plugins";

const toTitleCase = (str: string) => {
        return str
                .toLocaleLowerCase()
                .replace(/\b\w/g, (char: string) => char.toUpperCase());
};

export default defineConfig({
        out: "src/generated.ts",
        contracts: [],
        plugins: [
                foundry({
                        project: "./secret",
                }),
                react({
                        getHookName(options) {
                                return `use${toTitleCase(options.type)}${options.contractName}${options.itemName}`;
                        },

                }),
        ],
});

npx @wagmi/cli generate
を実行するとgenerated.tsがsrcディレクトリに生成されます。

main.tsxを以下に置き換えます。

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { WagmiProvider } from "wagmi";
import { http, createConfig } from 'wagmi'
import { sepolia } from 'wagmi/chains'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient();

const config = createConfig({
	chains: [sepolia],
	transports: {
		[sepolia.id]: http(),
	},
})

const root = document.getElementById("root") ?? new HTMLElement();
createRoot(root).render(
	<StrictMode>
		<QueryClientProvider client={queryClient}>
			<WagmiProvider config={config}><App /></WagmiProvider>
		</QueryClientProvider>
	</StrictMode>,
);

App.tsxを以下に置き換えます。

import { type FormEvent, useEffect, useState } from "react";
import "./App.css";
import TransgateConnect from "@zkpass/transgate-js-sdk";
import type { Result } from "@zkpass/transgate-js-sdk/lib/types";
import { ethers } from "ethers";
import { useReadGetSecretGetSecret, useWriteGetSecretAssignSecret } from "./generated";
import { readContract } from "viem/actions";

export type TransgateError = {
	message: string;
	code: number;
};

export type Proof = {
	taskId: `0x${string}`,
	schemaId: `0x${string}`,
	uHash: `0x${string}`,
	recipient: `0x${string}`,
	publicFieldsHash: `0x${string}`,
	validator: `0x${string}`,
	allocatorSignature: `0x${string}`,
	validatorSignature: `0x${string}`
}

const contractAddress = "";

const App = () => {
	let chainParams: Proof;
	const [appId, setAppId] = useState<string>(
		"xxxxxx",
	);
	const [schemaId, setSchemaId] = useState<string>(
		"xxxxxx",
	);
	const [result, setResult] = useState<Result | undefined>(undefined);
	const [secret, setSecret] = useState<string | undefined>("0x");
	const { writeContractAsync, isPending } = useWriteGetSecretAssignSecret();
	const { data, isPending: isPendingRead, refetch } = useReadGetSecretGetSecret({
		address: contractAddress
	});

	useEffect(() => {
		if (!isPending || !isPendingRead) {
			setSecret(data ?? "");
		}
	}, [isPending, data])


	const requestVerifyMessage = async (
		e: FormEvent,
		appId: string,
		schemaId: string,
	) => {
		e.preventDefault();
		try {
			const connector = new TransgateConnect(appId);
			const isAvailable = await connector.isTransgateAvailable();

			if (isAvailable) {
				const provider = window.ethereum ? new ethers.BrowserProvider(window.ethereum) : null;
				const signer = await provider?.getSigner()
				const recipient = await signer?.getAddress()
				const res = (await connector.launch(schemaId, recipient)) as Result;
				console.log("Result", res);

				const validatedResult = connector.verifyProofMessageSignature(
					"evm",
					schemaId,
					res
				);

				if (validatedResult) {
					alert("Validated Result");
					console.log(res);
					setResult(res);
					const taskId = ethers.hexlify(ethers.toUtf8Bytes(res.taskId)) as `0x${string}` // to hex
					const schemaIdHex = ethers.hexlify(ethers.toUtf8Bytes(schemaId)) as `0x${string}`// to hex
					if (recipient) {
						chainParams = {
							taskId,
							schemaId: schemaIdHex,
							uHash: res.uHash as `0x${string}`,
							recipient: recipient as `0x${string}`,
							publicFieldsHash: res.publicFieldsHash as `0x${string}`,
							validator: res.validatorAddress as `0x${string}`,
							allocatorSignature: res.allocatorSignature as `0x${string}`,
							validatorSignature: res.validatorSignature as `0x${string}`,
						}
						await writeContractAsync({
							address: contractAddress,
							args: [chainParams]
						});
						await refetch();
					}
				}

			} else {
				console.log(
					"Please install zkPass Transgate from https://chromewebstore.google.com/detail/zkpass-transgate/afkoofjocpbclhnldmmaphappihehpma",
				);
			}
		} catch (error) {
			const transgateError = error as TransgateError;
			alert(`Transgate Error: ${transgateError.message}`);
			console.log(transgateError);
		}
	};

	return (
		<div className="app">
			<form
				className="form"
				onSubmit={(e) => requestVerifyMessage(e, appId, schemaId)}
			>
				<label htmlFor="app-id">
					AppId:
					<input
						id="app-id"
						type="text"
						placeholder="Your App ID"
						value={appId}
						onChange={(e) => setAppId(e.target.value)}
					/>
				</label>
				<label htmlFor="schema-id">
					SchemaId:
					<input
						id="schema-id"
						type="text"
						placeholder="Your App ID"
						value={schemaId}
						onChange={(e) => setSchemaId(e.target.value)}
					/>
				</label>
				<button type="submit">Start Verification</button>
				{result !== undefined ? (<>
					<pre>Result: {JSON.stringify(result, null, 2)}</pre>
					<h1>Secret: {secret}</h1>
				</>) : (
					""
				)}
			</form>
		</div>
	);
};

export default App;

以上で開発は完了です!

NotionアカウントでVerificationを行ってみましょう。


以下の手順で、ウォレットアドレスが正常にアテステーションされたことを確認してください。

  1. リンク先にアクセス
    Sepolia Etherscan に移動します。
  2. コントラクトの読み取り
    ページ上の「Read Contract」タブをクリックします。
  3. 手順に従う
    スクリーンショットに示された手順に従って操作します。
    • 必要なフィールドに正しいウォレットアドレスを入力します。
    • Call ボタンをクリックして、アテステーションの状態を確認します。

これにより、アテステーションが正常に記録されていることを確認できます。

Image

まとめ

zkPassを用いた開発は、Schemaのカスタマイズが一番の難関と思われます。Reclaim Protocolと比較すると難易度が高く、検証できるデータの柔軟性も低い印象です。一方、ブラウザ拡張機能の性質上、ユーザーに対して複数回Proofを発行してもらうユースケースでは、既存ブラウザ内でログイン済みのサービスからProofを生成できるので、都度ログイン作業をしてもらう必要のあるReclaim ProtocolよりUXが良い印象です。

株式会社Pontechでは、Dapps開発の受託を請け負っております。本記事のように採用する技術やサードパーティのサービスの検討からお手伝いすることが可能です。

Dapps開発に関するご相談がございましたら、ぜひ当社までお問い合わせください。

関連記事

zkTLSの現在地と実用化に向けた課題

zkTLSについての考察と期待する理由

ponta

About ponta

2019年からEthereumを中心にDapp開発に従事。スキーとNBAとTWICEが好き。

Copyright © 2025 Pontech.Inc