はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、Avalanche内の別チェーンで発行されたイベントを、安全に取り込み利用するための標準的な仕組みを提案しているACP108についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

ACP108では、Avalanche内の任意のL1ブロックチェーンからEVMイベントをインポートするための、スマートコントラクトの標準インターフェースと抽象的な実装を定義しています。 具体的には、Avalanche Warp Messaging(AWM)を利用し、異なるチェーン間でのイベント参照を可能にします。 これにより、各チェーンが直接相互作用をしなくても、他のAvalancheのL1チェーンで発行されたイベントやステートの変更をスマートコントラクト上で扱えるようになります。

動機

Avalanche Warp Messagingは、corethやsubnet-evmといったコンポーネントに実装されており、すでに「認証済みのブロックハッシュ」を取得する仕組みを提供しています。 この認証済みブロックハッシュは、そのブロックがAvalanche内で正式に承認されたことを保証します。 さらに、ACP75(Acceptance Proofs)によって提案された「承認証明」を利用することで、そのブロック内で発生したイベントやステート変更を検証できるようになります。

しかし現状では、この「認証済みブロックハッシュ」をスマートコントラクト内でどのように扱うべきかについての明確な標準が存在しません。 そのため、アプリケーション開発者がこの仕組みを使ってクロスチェーンのデータを扱おうとすると、以下のような課題が発生します。

  • ブロックハッシュと一致するエンコード済みのブロックヘッダをコントラクトに渡す必要がある
  • そのブロックヘッダに含まれるstate rootやreceipts rootに基づいてMerkle証明を検証する必要がある

これらをアプリケーション開発者が1から実装するのは、処理が複雑で手間がかかり、開発効率を下げる要因となっています。

そこで、標準インターフェースと抽象コントラクトを用意して、以下のような処理を共通化すれば、開発者は簡単に他のAvalancheのL1チェーンのデータを利用できるようになります。

  • 認証済みブロックハッシュの扱い
  • Merkle証明の検証

これによって、クロスチェーンアプリケーションの構築がシンプルになり、Avalanche上でのエコシステム拡大につながります。

仕様

ACP108は、Avalanche内の別チェーンで発生したEVMのイベントを、WarpMessaging(チェーン間メッセージ機構)と「認証済みブロックハッシュ」を基盤として、安全にインポートするための標準interfaceと抽象実装を定義します。 目的は、開発者がブロックヘッダやMerkle証明(ブロック内データの包含証明)といった低レイヤの複雑な処理を自前で実装することなく、他チェーンのイベントを自チェーンのコントラクトで利用できるようにすることです。

EventImportingInterfaceの意図

importEventは、Avalanche内の別チェーンで発行された1件のEVMイベントを、ターゲット側のコントラクトに取り込むための関数です。 イベントは「ソースチェーンのID」、「ブロックヘッダ」、「トランザクション番号(ブロック内のトランザクションの番号)」、「イベント番号(トランザクション内のイベントの番号)」によって一意に特定されます。

取り込みの際には、次の確認を行います。

  1. 与えられたblockHeaderから計算したハッシュ値が、そのチェーンで認証済みのブロックハッシュと一致すること。
  2. blockHeaderに含まれるreceiptsroot(ブロック内すべてのトランザクションレシートをまとめたMerkleルート)に対して、指定されたreceiptProofが正しく、対象トランザクションのレシートが確かに含まれていること。

これらが確認できた場合、レシート内のlogIndexで指定されたイベントログを取り込み、最終的にEventImportedイベントを発行して通知します。この通知には、「どのチェーンのどのブロックで、どのコントラクトが発行したイベントなのか」という情報が含まれます。

importEvent

js
function importEvent(
    bytes32 sourceBlockchainID,
    bytes calldata blockHeader,
    uint256 txIndex,
    bytes[] calldata receiptProof,
    uint256 logIndex
) external;

役割の整理

項目 役割・意味 ポイント
sourceBlockchainID ソース側ブロックチェーンの識別子 このIDに紐づく認証済みブロックハッシュとblockHeaderの一致を確認します。
blockHeader ソース側ブロックのEVMブロックヘッダ(エンコード済み) デコードし、hash(blockHeader)が認証済みハッシュと一致すること、receiptsrootを取得することに使います。
txIndex ブロック内のトランザクションの位置(0始まり) receiptsTrie におけるキーを特定します。
receiptProof txIndexのレシートがreceiptsrootに含まれることを示すMerkle証明 検証結果はエンコード済みトランザクションレシート本体です。
logIndex レシート内のどのログ(イベント)を取り込むかの位置 取り込むLogの1つを特定します。

必須検証

検証 目的 成功条件の例
ブロックハッシュ認証 ソースブロックがAvalanche内で承認済みであることの確認 hash(blockHeader)が「認証済みブロックハッシュ」に一致すること
receiptProof検証 txIndexのレシートがreceiptsrootに含まれることの確認 receiptProofを検証するとそのレシートが復元できること
ログ抽出 目的のイベントログを取り出す レシート中のlogs[logIndex]が存在し取得できること

ブロックハッシュ認証方式の選択肢

このinterfaceはWarpprecompileの使用を強制しません。 実装は以下の2通りが想定されています。

方式 説明 利点
取引内でWarpprecompileを直接呼び出す importEvent呼び出しトランザクションで、Warpprecompileから認証済みブロックハッシュを取得して照合します。 外部状態の依存が少なく、自己完結でわかりやすいです。
外部コントラクトで事前認証し参照 外部のBlockHashRegistryのようなコントラクトに、認証済みハッシュを保存し、importEventではその照会のみ行います。 1度の認証を複数回使い回せます。オラクル等の代替認証機構にも対応しやすいです。

外部参照のイメージコードは以下になります。

js
bool valid = blockHashRegistry.checkAuthenticatedBlockHash(
    sourceBlockchainID,
    keccak256(blockHeader) // ← 正: keccak256
);
require(valid, "Invalid block header");

EventImportedイベント

js
event EventImported(
    bytes32 indexed sourceBlockchainID,
    bytes32 indexed sourceBlockHash,
    address indexed loggerAddress,
    uint256 txIndex,
    uint256 logIndex
);
フィールド 意味
sourceBlockchainID ソースチェーンの識別子
sourceBlockHash 取り込んだブロックのハッシュ
loggerAddress ソース側でイベントを発行したコントラクトアドレス
txIndex ブロック内のトランザクション位置
logIndex レシート内のログ位置

このイベントにより、どのチェーン・どのブロック・どのトランザクション・どのログを取り込んだかをオンチェーンに記録できます。

抽象コントラクトの処理

抽象コントラクトは、importEventの内部で次の一連の作業を標準化します。

  1. ソースチェーンの認証済みブロックハッシュを取得し、与えられたblockHeaderと一致するかを確認します。
  2. blockHeaderからreceiptsrootを取得します。
  3. txIndexとreceiptProofを使ってMerkle検証を行い、対象トランザクションのレシートを復元します。
  4. レシートからlogIndexのLogを取り出し、メタ情報をEVMEventInfoにまとめます。
  5. 成功したらEventImportedをemitし、内部フック_onEventImportを呼び出して、継承先のビジネスロジックに委ねます。

関数とデータ構造

コントラクトが継承した際に実装すべきものは、内部フック関数の1つだけです。

js
function _onEventImport(EVMEventInfo memory eventInfo) internal virtual;

関連構造体は以下です。

js
struct EVMLog {
    address loggerAddress;
    bytes32[] topics;
    bytes data;
}

struct EVMEventInfo {
    bytes32 blockchainID;
    uint256 blockNumber;
    uint256 txIndex;
    uint256 logIndex;
    EVMLog log;
}
構造体 フィールド 意味
EVMLog loggerAddress ソース側でイベントを出したコントラクト
topics イベントシグネチャやインデックス付与された引数のハッシュ群
data 非インデックス引数が格納されたバイト列
EVMEventInfo blockchainID ソースチェーンID
blockNumber ソース側ブロック番号
txIndex ブロック内トランザクション位置
logIndex レシート内ログ位置
log 実イベントの本体

処理フロー

  1. クライアント(あるいはリレイヤ)がimportEventを呼び出し、sourceBlockchainID,blockHeader,txIndex,receiptProof,logIndexを渡します。
  2. 実装はWarpprecompileまたは外部レジストリを用いて、keccak256(blockHeader)が認証済みブロックハッシュと一致するかを確認します。
  3. 一致したらblockHeaderをデコードしてreceiptsrootを取り出します。
  4. txIndexとreceiptProofでMerkle検証を行い、エンコード済みレシートを復元してデコードします。
  5. 復元レシートからlogIndexのLogを抽出し、EVMEventInfoに整形します。
  6. EventImportedをemitし、_onEventImport(eventInfo)を呼び出し、ステートの更新やアプリケーション固有の処理を実行します。

シーケンス図

sequenceDiagram
    participant Caller as Caller/Relayer
    participant Target as TargetContract
    participant Auth as WarpPrecompile/Registry
    participant EVM as EVMReceiptTrie

    Caller->>Target: importEvent(sourceBlockchainID, blockHeader, txIndex, receiptProof, logIndex)
    Target->>Auth: 認証済みブロックハッシュの取得/照会
    Auth-->>Target: verifiedBlockHash
    Target->>Target: keccak256(blockHeader)と一致確認
    Target->>Target: blockHeaderデコード→receiptsroot取得
    Target->>EVM: Merkle検証(receiptProof, txIndex, receiptsroot)
    EVM-->>Target: 取引レシート(デコード可能)
    Target->>Target: レシートからlogs[logIndex]抽出
    Target-->>Caller: emit EventImported(...)
    Target->>Target: _onEventImport(EVMEventInfo)

リファレンス実装

js
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

abstract contract EventImporterBase {
    event EventImported(
        bytes32 indexed sourceBlockchainID,
        bytes32 indexed sourceBlockHash,
        address indexed loggerAddress,
        uint256 txIndex,
        uint256 logIndex
    );

    struct EVMLog {
        address loggerAddress;
        bytes32[] topics;
        bytes data;
    }

    struct EVMEventInfo {
        bytes32 blockchainID;
        uint256 blockNumber;
        uint256 txIndex;
        uint256 logIndex;
        EVMLog log;
    }

    // 実装依存: 認証済みブロックハッシュの取得方法を差し替え可能に
    function _getVerifiedBlockHash(bytes32 sourceBlockchainID, bytes calldata blockHeader)
        internal
        view
        virtual
        returns (bytes32)
    {
        // 例: Warpprecompileや外部Registryを参照
        // return IBlockHashRegistry(registry).get(sourceBlockchainID, keccak256(blockHeader));
        return bytes32(0);
    }

    // 実装依存: blockHeaderデコード→(blockNumber, receiptsRoot)を返す
    function _decodeBlockHeader(bytes calldata blockHeader)
        internal
        pure
        virtual
        returns (uint256 blockNumber, bytes32 receiptsRoot)
    {
        // 実際にはRLPデコード等を行う
        return (0, bytes32(0));
    }

    // 実装依存: Merkle証明の検証→エンコード済みレシート
    function _proveReceipt(
        bytes32 receiptsRoot,
        uint256 txIndex,
        bytes[] calldata receiptProof
    ) internal view virtual returns (bytes memory encodedReceipt) {
        return bytes("");
    }

    // 実装依存: レシートからlogIndexのログをデコード
    function _extractLog(bytes memory encodedReceipt, uint256 logIndex)
        internal
        pure
        virtual
        returns (EVMLog memory log)
    {
        return EVMLog({loggerAddress: address(0), topics: new bytes32, data: bytes("")});
    }

    function importEvent(
        bytes32 sourceBlockchainID,
        bytes calldata blockHeader,
        uint256 txIndex,
        bytes[] calldata receiptProof,
        uint256 logIndex
    ) external {
        bytes32 verifiedHash = _getVerifiedBlockHash(sourceBlockchainID, blockHeader);
        require(verifiedHash != bytes32(0), "Block not verified");

        bytes32 headerHash = keccak256(blockHeader);
        require(headerHash == verifiedHash, "Invalid block header");

        (uint256 blockNumber, bytes32 receiptsRoot) = _decodeBlockHeader(blockHeader);

        bytes memory encodedReceipt = _proveReceipt(receiptsRoot, txIndex, receiptProof);

        EVMLog memory evmLog = _extractLog(encodedReceipt, logIndex);

        emit EventImported(
            sourceBlockchainID,
            headerHash,
            evmLog.loggerAddress,
            txIndex,
            logIndex
        );

        _onEventImport(
            EVMEventInfo({
                blockchainID: sourceBlockchainID,
                blockNumber: blockNumber,
                txIndex: txIndex,
                logIndex: logIndex,
                log: evmLog
            })
        );
    }

    function _onEventImport(EVMEventInfo memory eventInfo) internal virtual;
}

OpenQuestionsの位置づけ

OpenQuestionsは、今後の運用や機能拡張にあたって検討すべき課題をまとめたものです。 例えば、「どこまでを信頼するか」、「処理コストをどう抑えるか」、「認証をどのように再利用するか」といった観点が含まれます。

今回の仕様はinterfaceと検証手順の標準化に焦点を当てており、オラクルの設計やレジストリの運用方法といった詳細は対象外です。 開発者はユースケースに応じて、Warp precompileを直接呼び出すか、外部レジストリを利用するかを選び、セキュリティと再利用性のバランスを考えて実装を決めることが推奨されます。

セキュリティ

Avalanche内で他チェーンのイベントをインポートする仕組みでは、「そのイベントが本当に指定されたブロックで発行されたのか」を正しく証明できることが重要です。 この正しさは、主に以下の二つの要素に依存します。

  • ブロックハッシュが本当に他チェーンで確定(finalized)したものかを認証する仕組み
  • 指定されたトランザクションレシートが、そのブロックに含まれていることを示すMerkle証明の検証ライブラリ

どちらかに不具合や脆弱性があれば、偽のイベントが「本物」として扱われてしまう可能性があります。

ブロックハッシュ認証の信頼性

あるイベントが含まれたブロックを証明するためには、まずそのブロックが本当にAvalanche内の別チェーンで最終確定(ファイナリティ)している必要があります。 これを担保するのが「ブロックハッシュの認証」です。

もし攻撃者が偽のブロックハッシュを提示できてしまうと、存在しないイベントを「存在する」と偽装できます。 したがって、この認証プロセスは信頼できる仕組みで行わなければなりません。 AvalancheではWarpMessagingを使うことで、このブロックハッシュの認証を安全に行う方法が提供されています。

Merkle証明検証の正確さ

次に重要なのがMerkle証明の検証です。 Merkle証明とは「あるデータ(ここではトランザクションレシート)がブロックに含まれていることを、短い経路情報で確認できる仕組み」です。

これを正しく検証するには、Merkle木を正しく扱うライブラリを使わなければなりません。 もしライブラリにバグがあれば、偽造された証明を「正しい」と誤って受け入れる可能性があります。

そのため、実装では「十分に監査され、広く利用されているライブラリ」を選ぶことが推奨されます。 信頼性の低い独自実装を使うと、脆弱性が入り込むリスクが高まります。

最後に

今回は「Avalanche内の別チェーンで発行されたイベントを、安全に取り込み利用するための標準的な仕組みを提案しているACP108」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!