はじめに

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

今回は、Warpメッセージの署名リクエストをAvalancheGoで標準化し、どのVMでも共通の方法で署名を取得できるようにする仕組みを提案しているACP118についてまとめていきます!

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

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

概要

ACP118は、AvalancheネットワークにおいてWarpメッセージの署名を取得する時に利用するAppRequestペイロード形式を標準化しようというものです。 現在、Warp署名のリクエストは各VM(仮想マシン)ごとに独自実装されています。 そのため、SubnetEVMやCorethのようにVMごとに異なるコーデック(データのエンコード/デコード方式)を持っており、リレーサービスやアプリケーションが複数のVMにまたがって動作する場合に大きな障害となっています。

ACP118では、AppRequestペイロードの標準的な型をAvalancheGoに定義し、VMはそれを直接インポートできるようにすることが目標です。 これにより、署名集約のための実装はVMごとに依存せず、AvalancheGoに依存するだけで済みます。 結果として、Warpメッセージの署名集約処理が簡素化され、クロスVM環境における開発負担が大幅に減少します。

動機

Warpメッセージは、Subnetのバリデータが生成する個別の署名をBLS署名という仕組みで集約した「集約署名」に基づいて検証されます。 そのため、集約署名を構築するには、まず各バリデータの個別署名を取得する必要があります。 しかし現状では、この署名取得の仕組みが各VMごとに異なっており、以下のような問題を引き起こしています。

  • VMごとの独自実装の存在 SubnetEVMやCorethなどは、それぞれが異なる署名リクエスト形式を実装しています。 アプリケーション側は対象VMに応じて異なる処理を記述する必要があり、開発が複雑になります。

  • クロスVMアプリケーションの摩擦 代表的な例としてWarpメッセージリレーサービス(awm-relayer)があります。 このリレーサービスは、バリデータから署名を集めて集約し、最終的にWarpメッセージを送信先チェーンで検証させます。 しかし、VMごとに異なるコーデックを利用しているため、コード内で明示的に分岐処理を入れる必要があり、拡張性や保守性を損なっています。

  • 他の提案(ACP75)の実装を妨げている ACP75はWarpを利用した「受理証明(acceptance proof)」の仕組みを導入しようとしていますが、署名集約の標準仕様が存在しないことが、実装可能と判断されるための大きな障害になっています。

このように、Warp Signature RequestをAvalancheGoで標準化することは次の利点をもたらします。

  • 各VMごとに独自実装を持つ必要がなくなる
  • Warpリレーや署名集約サービスが単一の標準インターフェースに依存できる
  • ACP75をはじめとするWarp関連提案の実装を容易にする

結果として、Avalanche全体におけるクロスチェーン通信やWarpメッセージの利用がシンプルかつ効率的に進められるようになります。

仕様

仕様の狙いと前提

ACP118は、AvalancheのP2PメッセージAppRequest/AppResponseのapp_bytesに格納する共通ペイロード型を定義し、Warpメッセージに対する署名要求をVM非依存で行えるようにする提案です。 型はProtobufで定義し、AvalancheGoに実装してVMがソースコードレベルでインポートできる想定です。 AvalancheGo本体に標準型を置くことで、署名アグリゲータやリレー実装は各VMごとのコーデックに依存せず、AvalancheGoだけに依存してメッセージ構築・デコードができます。 なお、この「app_bytesにProtobufで型を載せる方式」は、ゴシップ用AppRequestで既に採用されているやり方と同じ考え方です。

ここでいうWarpメッセージは「未署名のWarpメッセージ(unsignedWarpメッセージ)」のバイト列を対象にします。 検証時に使うBLS集約署名は、各バリデータの個別署名を集めて合成したものなので、まず個別署名を取得するための標準的なリクエスト形式を持つのが目的です。

Protobuf型定義

SignatureRequest

未署名のWarpメッセージに対する署名を要求するための型です。 messageには署名対象となる未署名Warpメッセージのシリアライズ済みバイト列を入れます。 justificationには、当該ノードが署名可否を判断するための任意データを入れます。 justificationはVM実装によっては不要ですが、messageには常に署名対象のバイト列を入れます。 messageとjustificationの妥当性要件はVMが定義します。

protobuf
message SignatureRequest {
    bytes message = 1;
    bytes justification = 2;
}

SignatureResponse

対応するAppResponse側の型です。 要求に対する署名のバイト列を返します。

protobuf
message SignatureResponse {
    bytes signature = 1;
}

フィールド一覧

型名 フィールド 型 役割
SignatureRequest message bytes 署名対象の未署名Warpメッセージ(シリアライズ済み)
justification bytes ノードが署名可否判断に用いる任意データ(不要なVMもある)
SignatureResponse signature bytes 返却される個別署名のバイト列

ハンドラー実装と登録

各VMは上記型に対応するAppRequest/AppResponseハンドラーを実装します。 AppRequestハンドラーはAvalancheGoのP2Pネットワークに「標準のハンドラーID=2」で登録します。 これにより、送信側は「ID=2のAppRequestにSignatureRequestをapp_bytesで載せる→受信側がSignatureRequestとしてデコード→署名を生成→SignatureResponseをapp_bytesに載せて返信」という流れをVM非依存に実現できます。

登録と処理のイメージ(Go擬似例)

go
// 受信側(署名提供ノード)
network.RegisterAppRequestHandler(2, func(ctx context.Context, nodeID ids.NodeID, reqBytes []byte) ([]byte, error) {
    var req SignatureRequest
    if err := proto.Unmarshal(reqBytes, &req); err != nil {
        return nil, err
    }

    // req.message: 未署名Warpメッセージ
    // req.justification: 署名可否判断のための任意データ(VMルールで評価)
    if err := vmValidateMessageAndJustification(req.Message, req.Justification); err != nil {
        return nil, err
    }

    sig, err := signWarpMessage(req.Message) // 個別署名を生成
    if err != nil {
        return nil, err
    }

    resp := &SignatureResponse{Signature: sig}
    return proto.Marshal(resp)
})

エンコード/デコードの流れ

  1. 送信側が未署名WarpメッセージをシリアライズしてSignatureRequest.messageに入れる。 必要に応じてjustificationも設定する。
  2. SignatureRequestをProtobufエンコードし、AppRequest(app_bytes=...)としてP2Pで送信する。 ハンドラーIDは2を指定する。
  3. 受信側はapp_bytesをSignatureRequestとしてProtobufデコードし、VMルールに従ってmessageとjustificationを検証する。
  4. 受信側が署名を生成し、SignatureResponse.signatureに入れてProtobufエンコードし、AppResponse(app_bytes=...)で返信する。

メッセージ交換のシーケンス図

sequenceDiagram
    participant R as 署名要求側
    participant V as 署名提供ノード(Validator)

    R->>V: AppRequest(ID=2, app_bytes=SignatureRequest)
    V->>V: Protobufでデコード→VMルールで検証
    V->>V: 未署名Warpメッセージに署名(個別署名生成)
    V-->>R: AppResponse(app_bytes=SignatureResponse(signature))
    R->>R: 署名収集・後段でBLS集約に利用

代表的な使い方

SignatureRequestは、基本的に「未署名のWarpメッセージをmessageに入れて署名を依頼する」ために使います。 justificationは、その依頼に付随する根拠や補足を渡したい場合にのみ使います。

1. 既知のWarpメッセージに署名してもらう

SubnetEVM/Corethは、オンチェーンのWarpPrecompile経由で送信されたメッセージやオフチェーンWarpメッセージをデータベースに保存しているため、要求されたメッセージに対して署名を返せます。 この場合は、messageに該当Warpメッセージ(未署名)のシリアライズをそのまま入れます。 対象ノードは自分のDBから同一メッセージを見つけ、対応する署名を返せます。justificationは不要です。

手順の要点

  1. 未署名Warpメッセージをシリアライズしてmessageに設定する。
  2. AppRequest(ID=2)で送信する。
  3. 受信ノードはDBを照合して署名を返す。

2. オンチェーンイベント(例:BlockID)へのアテステーション

SubnetEVM/Corethでは、BlockSignatureRequest{BlockID ids.ID}のような型でブロックハッシュに対する署名提供もサポートしています。 SignatureRequestでも同様のことができます。 未署名WarpメッセージのペイロードとしてBlockIDを埋め込んだWarpメッセージを組み立て、それをシリアライズしてmessageに入れます。 必要であればjustificationに補足(例:そのBlockIDに対応するBlockHeightなど)を入れます。

手順の要点

  1. ペイロードにBlockIDを含む未署名Warpメッセージを作る。
  2. シリアライズしてmessageに入れる(justificationは任意)。
  3. 署名を受け取り、後段の検証・集約に用いる。

3. ある出来事が起きなかったことの確認(失効の証明)

ACP77によりSubnetは独自のバリデータセット管理が可能になります。 RegisterSubnetValidatorTxに含まれるWarpメッセージペイロードはexpiryを持ち、期限が過ぎると該当のvalidationID(SubnetID・NodeID・StakeWeight・expiryのハッシュで一意)が無効化されます。 SubnetはこのvalidationIDの失効を認識し、P-Chainと整合するようにローカルのバリデータセットを更新する必要があります。 P-Chain側は失効済み/無効なvalidationIDを永続保存しない想定です。

ACP77については以下の記事を参考にしてください。

このケースでは、SignatureRequestでvalidationIDの失効を署名で証明するWarpメッセージを構築します。 未署名Warpメッセージのペイロードに該当validationIDを入れてシリアライズしmessageに設定します。 さらにP-Chainが失効したvalidationIDを再構成できるよう、そのハッシュのプレイメージをjustificationに含めます。

手順の要点

  1. 対象のvalidationIDをペイロードに入れた未署名Warpメッセージを作る。
  2. シリアライズしてmessageに入れる。
  3. justificationにvalidationIDハッシュのプレイメージを入れる。
  4. 返却署名を集め、Warp検証側で失効アテステーションとして利用する。

セキュリティ

SignatureRequestハンドラーの実装は各VMに任されています。 そのため、VM開発者はどのようなmessageペイロードに署名するかを慎重に決めなければなりません。 単に署名するだけでは、意図しないメッセージにも署名してしまうリスクがあるためです。具体的には以下のような観点が重要です。

入力の検証

ハンドラーはmessageやjustificationを受け取った際に、必ず形式と内容を検証する必要があります。 デコードした結果が正しく期待通りの型であるか、余分な情報や不正なデータが含まれていないかを確認しなければなりません。 これを怠ると、意図しないデータに署名してしまう可能性があります。

署名を悪用したDoS攻撃への注意

AvalancheGoのP2Pネットワークには、一定のレート制限が備わっており、大量のリクエストによるDoS(サービス妨害)攻撃への耐性があります。 しかしVM側でも、ペイロードの解析や署名処理に過度な計算コストをかけすぎないようにする必要があります。 攻撃者がリソースを消費させる目的で大量の署名リクエストを送ってくることを想定し、署名要求を扱う処理の負荷を最小限に抑える工夫が求められます。

ペイロードの衝突リスク

messageペイロードの設計では、異なる用途のデータが同じ形式に見えてしまう「衝突」を避ける必要があります。 例えば32バイトのハッシュを受け取ったとき、それがトランザクションハッシュなのか、ブロックハッシュなのか、あるいはブロックチェーンIDなのかが区別できなければ、署名対象を誤解するリスクがあります。 そのため、VMごとに署名対象として扱うペイロードの種類は明確に分離し、重複や混同が生じないように設計しなければなりません。

これらの点を考慮することで、署名処理の安全性を高め、Warpメッセージを悪用した攻撃を防止できます。

互換性

この変更はVMに対して後方互換性があります。 つまり、古いバージョンのノードは新しいメッセージ型に対応していないため、それらのメッセージを単純に無視するだけです。 結果として、古いノードがネットワーク全体の動作に悪影響を与えることはありません。 新しい機能を利用できるのは最新の実装を持つノードだけですが、既存ネットワークとの衝突や動作停止は起こりません。

参照実装

この仕様に基づく参照実装はAvalancheGoに追加されています。 Protobufで定義された型と標準ハンドラーIDが含まれており、具体的なコードは以下のPull Requestで確認できます。

これにより、開発者は提案されているSignatureRequestとSignatureResponseの具体的な実装例を確認でき、自身のVMや関連サービスに取り込む時の参考とすることができます。

最後に

今回は「Warpメッセージの署名リクエストをAvalancheGoで標準化し、どのVMでも共通の方法で署名を取得できるようにする仕組みを提案しているACP118」についてまとめてきました! いかがだったでしょうか?

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

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