所要時間:10~20分

このガイドでは、EVMベースのコントラクト用に提供されたソースコードの使い方を順を追って解説し、Universal Signature Validator(ERC-6492)を組み込んだカスタムオンチェーンメッセージバリデータコントラクトの仕組みや、型付きデータによるアプリ内署名検証の方法を説明します。

これは6つのステップで実現できます:

  1. Builderプロジェクトの作成とアクセスキーの取得
  2. React Viteアプリケーションの初期化
  3. Sequence Walletを使ったユーザーサインイン
  4. EIP712型付きデータでEIP6492署名を生成
  5. EIP712検証とEIP1271バリデーション用コントラクトのデプロイ
  6. 検証・バリデーションコントラクトからのレスポンスを表示

このアプリケーションの全体的な流れは、以下のシーケンス図で確認できます:

デモの全コードはこちらで確認できます。また、デモはこちらからご利用いただけます。

1. Builderプロジェクトの作成とアクセスキーの取得

まず、こちらのガイドに従ってSequence Builderでプロジェクトを作成し、プロジェクトのアクセスキーを取得してください。

2. React Viteアプリケーションの初期化

次に、署名生成やブロックチェーンからの検証レスポンス取得に必要なコードを格納する新しいプロジェクトを初期化します:

pnpm create vite

これで空のプロジェクトが作成され、要素やロジックを追加できるようになります。

3. Sequence Walletを使ったユーザーサインイン

プロジェクトの動作に必要なパッケージをインストールします:

pnpm install 0xsequence ethers

その後、ステップ1で取得したプロジェクトアクセスキーと選択したネットワークで、ユーザーがサインインできるようにします。



import { sequence } from '0xsequence'



function App() {

    sequence.initWallet(PROJECT_ACCESS_KEY, {

        defaultNetwork: 'sepolia',

    });



    const signIn = async () => {

        const wallet = sequence.getWallet()

        const details = await wallet.connect({app: 'sequence signature validation demo'})



        if(details){

            console.log('is signed in')

            console.log(details)

        }

    }



    return (

        ...

        <button onClick={() => signIn()}>sign in</button>

        ...

    )

}

4. EIP712型付きデータでEIP6492署名を生成

次に、typescriptでカスタム型付きデータを定義します。Sequenceのユーティリティライブラリを使ってTypedData型を構築し、namewalletmessageパラメータを持つメッセージ構造を検証します:

この例のVERIFYING_CONTRACT_ADDRESSsepoliaにデプロイしたスマートコントラクトですが、次のステップでこのコントラクトの内容を紹介し、どのネットワークでもデプロイできるように説明します:

import { sequence } from '0xsequence'



interface Person {

  name: string;

  wallet: string;

  message: string;

}



const VERIFYING_CONTRACT_ADDRESS = '0xB81efF8d6700b83B24AA69ABB18Ca8f9F7A356c5'

const CHAIN_ID = 11155111



const submitSignature = () => {

    const wallet = sequence.getWallet()



    const message = 'hey' // message can be dynamic

    const person: Person = {

        name: "user", // name can be dynamic

        wallet: wallet.getAddress(),

        message: message,

    };



    const chainId = CHAIN_ID

    const typedData: sequence.utils.TypedData = {

        domain: {

            // Domain settings must match verifying contract

            name: "Sequence Signature Validation Demo",

            version: "1",

            chainId,

            verifyingContract: VERIFYING_CONTRACT_ADDRESS,

        },

        types: {

            Person: [

                { name: "name", type: "string" },

                { name: "wallet", type: "address" },

                { name: "message", type: "string" },

            ],

        },

        message: person,

        primaryType: "Person",

    };

    ...

}


その後、参照した各プロパティを使って型付きメッセージオブジェクトに署名します:

const wallet = await sequence.getWallet()

const signer = wallet.getSigner(CHAIN_ID);



const signature = await signer.signTypedData(

    typedData.domain,

    typedData.types,

    typedData.message,

    {

        chainId,

        eip6492: true, // enabling signatures for non-deployed wallet contracts

    }

);



console.log("signature", signature);

関数をボタンに紐付け、ユーザーがクリックした後に署名が生成されることを確認してください:

<button onClick={() => submitSignature()}>verify signature</button>

5. EIP712検証とEIP1271バリデーション用コントラクトのデプロイ

ここでは、RemixFoundry などのツール、または Sequence Builder を使ってコントラクトをデプロイするためのソースコードを提供します。

1

Universal Signature Validator(ユニバーサル署名バリデータ)

Universal Signature Validator は、特定のネットワークに一度デプロイすれば、多くのアプリケーションで共有できるため、再利用性と拡張性に優れています。これは、EIP6492 対応ウォレット向けのオフチェーン・オンチェーン両方のスマートコントラクトウォレットで利用できます。

こちらにソースコードがあります。このコードを利用してデプロイしてください。

2

カスタムコントラクトバリファイア

次に紹介するコントラクトは、用途に合わせてカスタマイズできるよう、さまざまな関数について詳しく説明します。まずは、Universal Signature Validator をコンストラクタに渡すという最初のステップで、以下の基本事項から始めましょう。

import {IERC1271} from "./interfaces/IERC1271.sol";

import {IERC6492} from "./interfaces/IERC6492.sol";



import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";



struct Person { // can be customized

    string name;

    address wallet;

    string message;

}



contract EIP712Verifier is EIP712 {

    using ECDSA for bytes32;



    IERC6492 public immutable ERC6492_SIGNATURE_VALIDATOR; // the universal signature validator



    bytes32 private constant _ERC6492_DETECTION_SUFFIX = 0x6492649264926492649264926492649264926492649264926492649264926492;



    // this line of code must be customized to the struct you're verifying

    bytes32 private constant _PERSON_TYPEHASH = keccak256(bytes("Person(string name,address wallet,string message)"));



    constructor(address erc6492SignatureValidator) {

        ERC6492_SIGNATURE_VALIDATOR = IERC6492(erc6492SignatureValidator);

    }

    ...

}


3

署名の検証

次に、署名検証用の関数を用意します。この関数はメッセージハッシュのダイジェストを生成し、署名者を検証します。

/// @dev Verifies the signature of a person.

function verifySignature(address signer, Person memory person, bytes calldata signature)

    external

    returns (bool success)

{

    bytes32 digest = personDigest(person);

    return ERC6492_SIGNATURE_VALIDATOR.isValidSig(signer, digest, signature);

}

カスタム Person ダイジェスト

以下の関数では、渡されたパラメータを使って struct ハッシュを再生成します。必要に応じて、パラメータの種類や数を増減できます。

ダイジェストの構築方法については、EIP712仕様 をご参照ください。

/// @dev Returns the EIP712 hash of a person.

function personDigest(Person memory person) public view returns (bytes32 digest) {

    bytes32 structHash = keccak256(

        abi.encode(_PERSON_TYPEHASH, keccak256(bytes(person.name)), person.wallet, keccak256(bytes(person.message)))

    );

    digest = EIP712._hashTypedDataV4(structHash);

}

署名者の検証

次に、signer アドレス、digestsignature を検証します。EIP6492 署名が渡された場合は universal signature validator を利用し、それ以外は EIP1271 の署名検証を直接行います。

/// @dev Validates the ERC1271 signature of a signer.

function validateSigner(address signer, bytes32 digest, bytes calldata signature) internal returns (bool success) {

    if (signature.length >= 32) {

        bool isCounterfactual =

            bytes32(signature[signature.length - 32:signature.length]) == _ERC6492_DETECTION_SUFFIX;

        if (isCounterfactual) {

            return ERC6492_SIGNATURE_VALIDATOR.isValidSig(signer, digest, signature);

        }

    }



    try IERC1271(signer).isValidSignature(digest, signature) returns (bytes4 magicValue) {

        return magicValue == IERC1271.isValidSignature.selector;

    } catch {}

    return false;

}

これで両方のコントラクトをデプロイする準備ができました。ネットワークを選択してください。

6. 検証コントラクトからのレスポンスを表示

署名を渡し、ethers を使ってデプロイ済みコントラクトを呼び出します。この際、PROJECT_ACCESS_KEY を利用します。

1

プロバイダの作成

プロジェクトアクセスキーを使ってプロバイダを作成します。

import { ethers } from 'ethers'



const CHAIN_HANDLE = 'sepolia'



const provider = new ethers.JsonRpcProvider(

    `https://nodes.sequence.app/${CHAIN_HANDLE}/${PROJECT_ACCESS_KEY}`

);
2

Ethers コントラクトの初期化

ステップ5 で生成された ABI(または Git のソースコード からコピー)をインポートし、プロバイダと検証コントラクトアドレスを指定します。

import { ABI } from "./abi";



const contract = new ethers.Contract(

    VERIFYING_CONTRACT_ADDRESS,

    ABI,

    provider

);
3

署名検証関数をスタティックコール

関数をスタティックコールすることで、トランザクションをチェーンに送信せずにシミュレーションできます。これにより、検証が成功したかどうかの結果が返されます。

const address = await wallet.getAddress()



const person: Person = {

    name: "user",

    wallet: address,

    message: message,

}



const signature = await signer.signTypedData(

    typedData.domain,

    typedData.types,

    typedData.message,

    {

        chainId,

        eip6492: true,

    }

);



const result = await contract.verifySignature.staticCall(

    address,

    person,

    signature

);



console.log(`Signature is ${result ? "valid" : "invalid"}`);

return result;

まとめ

これで、ユーザーが正しい情報に署名していることを保証するさまざまなユースケース(例:ERC20 の使用許可、オフチェーンでの入札、署名済み QR コードによるミントなど)へとアプリケーションを拡張できます。