はじめに
ブロックチェーン技術は、その透明性と改ざん防止性が鍵となり、多くの分野で革新的な解決策を生み出しています。しかし、その分散性と匿名性は、Sybil攻撃という一つの問題を引き起こす可能性があります。Sybil攻撃とは、一人のユーザーが多数のIDを作成し、ネットワークやシステムを悪用する攻撃のことを指します。プライベートやコンソーシアムブロックチェーンでは、中央集権的な管理によりSybil攻撃への耐性がありますが、パブリックブロックチェーンでは一人のユーザーが鍵ペアを無制限に生成できるため、Sybil攻撃耐性を付与することは困難でした。
そこで登場したのが、World IDという革新的なシステムです。World IDは、Sybil-resistanceを付与する初めての現実的なプロダクトであり、さらに、ゼロ知識証明の仕組みを用いることで、ユーザーが秘密鍵を管理しなくても、NFTを受け取ったり、投票に参加することができるという新しい可能性を開きました。
本記事では、World ID証明を実現するスマートコントラクトとその利用方法について、ソースコードを交えて解説していきます。
World ID証明を実現するスマートコントラクト
World ID証明は、ブロックチェーン技術とゼロ知識証明(ZKP)を活用して、ユーザーが特定の条件を満たしていることを証明しつつ、そのユーザーのアイデンティティを秘密に保つことを可能にします。World ID証明を実現するスマートコントラクトは、下記コンポーネントから構成されます。
-
World ID Router
World ID Routerは、サービスのコントラクトが呼び出すコントラクトで、Identity ManagerやState Bridge contract(L2 Chain)にルーティングする役割を持ちます。検証時に呼び出す引数に基づいて正しいIdentity Managerコントラクト(Ethereum main chain)、またはState Bridge契約(L2 Chains)にルーティングします。このコントラクトはプロキシ化されているため、ルーティング先のコントラクトがアップグレードされても、サービスコントラクト側のコードを変更する必要がありません。 -
Identity Manager
Identity ManagerはEthereum上にのみデプロイされます。World IDで受け入れられる各資格タイプ(虹彩認証、電話認証等)ごとに一つのコントラクトがデプロイされています。
Identity ManagerはSemaphoreインスタンスの管理を担当します。WorldcoinのサインアップシーケンサーはIdentity Managerを呼び出してアイデンティティをMerkle treeに追加し、誰でもverifyProof関数(検証に使用する関数)を呼び出してWorld IDの証明を検証することができます。 -
State Bridges
Ethereum上では、各Identity Managerごとに一つのState Bridge契約がデプロイされます。これはMerkle rootを他のチェーンに公開し、証明を複数のチェーンで検証できるようにします。
他のサポートされているチェーン(現在はOptimismとPolygon)でも、各資格タイプごとに一つのState Bridgeコントラクトが存在します。これらのコントラクトはEthereum State BridgeからMerkle rootを受け取り、そのチェーン上で証明を検証するためのverifyProof関数を公開しています。
スマートコントラクトの関係図は下記のようになります。
以上が、World ID証明を実現するための主要なスマートコントラクトの概要です。これらのコントラクトは、ユーザーの匿名性を保ちつつ、特定のアクションの一意性を保証するための重要な要素を提供します。
World ID証明を活用したスマートコントラクト
今回解説するスマートコントラクトは下記の公式サンプルです。
World ID認証されたユーザーのアクションを検証するもので、ソースコードの全体は下記の通りです。
(2023/7/31時点のソースコード)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import { ByteHasher } from './helpers/ByteHasher.sol';
import { IWorldID } from './interfaces/IWorldID.sol';
contract Contract {
using ByteHasher for bytes;
///////////////////////////////////////////////////////////////////////////////
/// ERRORS ///
//////////////////////////////////////////////////////////////////////////////
/// @notice Thrown when attempting to reuse a nullifier
error InvalidNullifier();
/// @dev The World ID instance that will be used for verifying proofs
IWorldID internal immutable worldId;
/// @dev The contract's external nullifier hash
uint256 internal immutable externalNullifier;
/// @dev The World ID group ID (always 1)
uint256 internal immutable groupId = 1;
/// @dev Whether a nullifier hash has been used already. Used to guarantee an action is only performed once by a single person
mapping(uint256 => bool) internal nullifierHashes;
/// @param _worldId The WorldID instance that will verify the proofs
/// @param _appId The World ID app ID
/// @param _actionId The World ID action ID
constructor(IWorldID _worldId, string memory _appId, string memory _actionId) {
worldId = _worldId;
externalNullifier = abi.encodePacked(abi.encodePacked(_appId).hashToField(), _actionId).hashToField();
}
/// @param signal An arbitrary input from the user, usually the user's wallet address (check README for further details)
/// @param root The root of the Merkle tree (returned by the JS widget).
/// @param nullifierHash The nullifier hash for this proof, preventing double signaling (returned by the JS widget).
/// @param proof The zero-knowledge proof that demonstrates the claimer is registered with World ID (returned by the JS widget).
/// @dev Feel free to rename this method however you want! We've used `claim`, `verify` or `execute` in the past.
function verifyAndExecute(address signal, uint256 root, uint256 nullifierHash, uint256[8] calldata proof) public {
// First, we make sure this person hasn't done this before
if (nullifierHashes[nullifierHash]) revert InvalidNullifier();
// We now verify the provided proof is valid and the user is verified by World ID
worldId.verifyProof(
root,
groupId,
abi.encodePacked(signal).hashToField(),
nullifierHash,
externalNullifier,
proof
);
// We now record the user has done this, so they can't do it again (proof of uniqueness)
nullifierHashes[nullifierHash] = true;
// Finally, execute your logic here, for example issue a token, NFT, etc...
// Make sure to emit some kind of event afterwards!
}
}
ソースコード解説
公式サンプルの主要なコードを解説していきます。
import { IWorldID } from './interfaces/IWorldID.sol';
こちらではverifyProofメソッドを使うために、World IDコントラクトのinterfaceをimportしています。
/// @dev Whether a nullifier hash has been used already. Used to guarantee an action is only performed once by a single person
mapping(uint256 => bool) internal nullifierHashes;
2重実行を防止するために、nullifierHashのmapを定義する部分です。
nullifierHashとは、ゼロ知識証明(Zero-Knowledge Proofs、ZKP)とSemaphoreというプロトコルに使われるもので、特に匿名性を保つための一連の操作において重要な役割を果たします。
Semaphoreは、ユーザーが特定の条件を満たすこと(人間であること、特定のグループに所属していること等)を証明しつつ、そのユーザーのアイデンティティを秘密に保つことを可能にするプロトコルです。これは、ゼロ知識証明を使用して実現されます。
nullifierHashは、Semaphoreの証明の一部で、特定のアクションやイベントを一意に識別するために使用されます。これにより、同じユーザーが同じアクションを繰り返し行う(つまり、ダブルスペンディングを行う)ことを防ぐことができます。
たとえば、投票システムでは、nullifierHashは特定の投票イベントを識別するために使用されます。これにより、システムは同じユーザーが同じ投票イベントに対して2回以上投票することを防ぐことができます。これは、ユーザーが匿名であるため、通常の方法でダブルスペンディングを防ぐことができないからです。
このように、nullifierHashは、匿名性を保つ一方で、特定のアクションの一意性を保証するために重要な役割を果たします。
/// @dev The World ID group ID (always 1)
uint256 internal immutable groupId = 1;
groupIdはユーザーの区分を表します。1はOrbによって認証されたユーザー、つまり虹彩認証済のユーザーです。0は電話認証されたユーザー区分に相当します。
/// @param _worldId The WorldID instance that will verify the proofs
/// @param _appId The World ID app ID
/// @param _actionId The World ID action ID
constructor(IWorldID _worldId, string memory _appId, string memory _actionId) {
worldId = _worldId;
externalNullifier = abi.encodePacked(abi.encodePacked(_appId).hashToField(), _actionId).hashToField();
}
こちらはコンストラクタです。appIdとactionId(World ID Developer Portalで作成)を元に、externalNullifierのハッシュを生成しています。externalNullifierはSemaphoreで実行されるActionの識別子であり、そのkeccak256ハッシュがWorld ID RouterのverifyProofメソッドに渡されます。この例では、すべてのユーザーが同じActionを実行するので、ガスを節約するためにコンストラクタで設定しています。
/// @param signal An arbitrary input from the user, usually the user's wallet address (check README for further details)
/// @param root The root of the Merkle tree (returned by the JS widget).
/// @param nullifierHash The nullifier hash for this proof, preventing double signaling (returned by the JS widget).
/// @param proof The zero-knowledge proof that demonstrates the claimer is registered with World ID (returned by the JS widget).
/// @dev Feel free to rename this method however you want! We've used `claim`, `verify` or `execute` in the past.
function verifyAndExecute(address signal, uint256 root, uint256 nullifierHash, uint256[8] calldata proof) public {
// First, we make sure this person hasn't done this before
if (nullifierHashes[nullifierHash]) revert InvalidNullifier();
World ID認証されたユーザーのみが1回のみロジックを実行することを意図して作られた関数の定義部です。
同じnullifierHashが使われていたらRevertし、エラー通知します。これにより、同一ユーザーによる同一Actionの2重実行を防止しています。
// We now verify the provided proof is valid and the user is verified by World ID
worldId.verifyProof(
root,
groupId,
abi.encodePacked(signal).hashToField(),
nullifierHash,
externalNullifier,
proof
);
こちらはWorld ID証明を検証する部分です。verifyProofメソッドの引数は下記の通りです。
- root: 検証対象のWorld IDのルートです。IDKitウィジェットから取得され、そのまま渡されます。
- groupId: ユーザー区分です。Orbで認証されたユーザーの場合は1、電話で認証されたユーザーの場合は0になります。
- signal: 検証する信号です。サンプルコードではコントラクトのアドレスが設定されています。
- nullifierHash: 匿名ユーザーIDです。IDKitウィジェットから取得され、そのまま渡されます。
- action: 検証するアクションです。
- proof: 検証する証明です。IDKitウィジェットから取得されますが、uint256[8]に変換してから渡す必要があります。
// We now record the user has done this, so they can't do it again (proof of uniqueness)
nullifierHashes[nullifierHash] = true;
// Finally, execute your logic here, for example issue a token, NFT, etc...
// Make sure to emit some kind of event afterwards!
}
検証が完了したら、nullifierHashのmapを更新し、2重実行できないようにします。その後、目的のロジック(NFT発行や投票など)を実行します。
サンプルコードの解説は以上です。
おわりに
この記事を通じて、World ID証明を実現するための主要なスマートコントラクトとその機能について解説しました。これらのスマートコントラクトは、ユーザーの匿名性を保ちつつ、特定のアクションの一意性を保証するための重要な要素を提供します。また、World IDは、Sybil攻撃への耐性を持つとともに、ユーザーがethereumアカウントを持たなくても、NFTを受け取ったり、投票に参加することができるという新しい可能性を開きました。これらの技術は、ブロックチェーンの分散化と匿名性を活かしつつ、その問題点を克服するための有力な手段となり得ます。今後もブロックチェーン技術の進化とともに、これらの技術がどのように進化し、どのように活用されていくのかを注視していきたいと思います。
P.S.
関連記事を貼っておきます。