はじめに

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

今回は、AvalancheのEVMチェーンでL1のバリデータ集合(追加・削除・重み変更)を管理するためのコントラクトを提案しているACP99についてまとめていきます!

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

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

概要

ACP99Managerは、Avalanche上の任意のEVMチェーンにデプロイできるValidatorManager(バリデータを管理するコントラクト)の標準仕様を定める提案です。 これはACP-77 (Reinventing Subnets)で示された仕組みに依存しており、その仕様が「実装可能(Implementable)」とされることが前提になります。

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

ACP77では、P-Chainに保存されているL1のValidatorSetをAvalancheネットワーク内の任意のチェーンから管理できる提案がされています。 SubnetをConvertSubnetToL1TxによってL1に変換するとき、(blockchainID, address)の組み合わせで「ValidatorManager」を指定でき、そのアドレスがRegisterL1ValidatorTxやSetL1ValidatorWeightTxといったICMメッセージをP-Chainへ送る役割を担います。

また、ValidatorSetの変更がされるたびに、P-ChainはAddressedCallに署名し、その情報を追跡しているコントラクトへ通知できます。 受け取った側はこのメッセージを解釈し、必要な処理を実行する必要があります。 ACP77で定義されているAddressedCallは、L1ValidatorRegistrationMessageとL1ValidatorWeightMessageの2種類です。

この前提とAvalanche Mainnetで多くのチェーンがEVMベースである事実を踏まえ、ACP99Managerは以下の役割を果たす標準Solidityコントラクト仕様として提案されます。

役割 説明
ValidatorSetの情報保持 現在のL1ValidatorSetに関する情報をコントラクトに保持する
P-Chainへの更新送信 ACP-77で定義されたAddressedCallを生成し、P-Chainに更新を伝える
通知の解釈と反映 P-Chainからの通知を正しく処理し、ValidatorSetを更新する
他モデルとの統合 Proof-of-Stakeなどのセキュリティモデルに簡単に統合できる

監査済みかつオープンソースの参照実装を利用可能にすることで、AvalancheでL1を立ち上げるコストを下げる狙いがあります。 デプロイされたACP99Managerは、ConvertSubnetToL1Txで指定するAddressとしてそのまま利用できます。

動機

ACP77 (Reinventing Subnets)では、P-Chainに保存されているL1のValidatorSetを、Avalancheネットワーク上の任意のチェーンから管理できる仕組みが提案されています。 SubnetをConvertSubnetToL1TxでL1に変換するとき、(blockchainID, address)の形で「ValidatorManager」を指定でき、そのアドレスがRegisterL1ValidatorTxやSetL1ValidatorWeightTxに対応するICMメッセージをP-Chainに送ることで、Validatorの追加や重みの変更、削除を行うことができます。

また、ValidatorSetの変更がされるたびに、P-ChainはAddressedCallに署名し、その情報を追跡しているコントラクトへ通知します。 受け取る側はこのメッセージを正しく処理し、必要な更新を反映する必要があります。ここで利用されるAddressedCallはACP77で定義されており、種類はL1ValidatorRegistrationMessageとL1ValidatorWeightMessageです。

Avalanche Mainnetで多くのチェーンがEVMベースで動いていることから、共通して利用できるSolidity実装が必要になります。 ACP99Managerはその標準仕様として提案されており、以下の利点があります。

  • ValidatorSetの管理機能を1つの仕様にまとめることで、実装コストを削減できる
  • Proof-of-Stakeなど多様なセキュリティモデルに統合しやすくなる
  • 監査済みかつオープンソースの参照実装を提供することで、L1立ち上げのハードルを下げられる

最終的には、ACP99Managerをデプロイし、それをConvertSubnetToL1TxのAddressとして指定するだけで、P-Chainとの連携によるValidatorSet管理が可能になります。

仕様

型定義

ConversionData

javascript
struct ConversionData {
    bytes32 subnetID;
    bytes32 validatorManagerBlockchainID;
    address validatorManagerAddress;
    InitialValidator[] initialValidators;
}

SubnetをP-Chain上でL1に変換するために使われるデータを表す構造体。

このデータは、P-Chainによって認証されるハッシュの元データとなります。ValidatorManagerがこれを検証し、初期バリデータ情報を含めてL1への変換を行う際に利用されます。

パラメータ

  • subnetID
    • Subnetを識別する32バイトのID。
  • validatorManagerBlockchainID
    • ValidatorManagerが動作するブロックチェーンのID。
  • validatorManagerAddress
    • ValidatorManagerコントラクトのアドレス。
  • initialValidators
    • 初期バリデータのリスト。

InitialValidator

javascript
struct InitialValidator {
    bytes nodeID;
    bytes blsPublicKey;
    uint64 weight;
}

初期バリデータを指定するための構造体。 SubnetをL1へ変換する時に、最初に登録されるValidatorの情報を保持します。

パラメータ

  • nodeID
    • ノードを識別するID。
  • blsPublicKey
    • ValidatorのBLS公開鍵。
  • weight
    • Validatorの重み。

ValidatorStatus

javascript
enum ValidatorStatus {
    Unknown,
    PendingAdded,
    Active,
    PendingRemoved,
    Completed,
    Invalidated
}

Validatorのステートを表す列挙型。 バリデータが登録前・稼働中・削除予定など、どの段階にあるかを示すステータスを管理します。

パラメータ

  • Unknown
    • 不明な状態。
  • PendingAdded
    • 追加待ちの状態。
  • Active
    • 有効な状態。
  • PendingRemoved
    • 削除待ちの状態。
  • Completed
    • 処理が完了した状態。
  • Invalidated
    • 無効化された状態。

PChainOwner

javascript
struct PChainOwner {
    uint32 threshold;
    address[] addresses;
}

Validatorの残高所有者または無効化権限所有者を表す構造体。 P-Chainアドレスは20バイトのため、Ethereumアドレスと同じaddress型で表現しています。

パラメータ

  • threshold
    • 署名を有効とするために必要な最小署名数。
  • addresses
    • 関連付けられたP-Chainアドレスの配列。

Validator

javascript
struct Validator {
    ValidatorStatus status;
    bytes nodeID;
    uint64 startingWeight;
    uint64 sentNonce;
    uint64 receivedNonce;
    uint64 weight;
    uint64 startTime;
    uint64 endTime;
}

Validatorの稼働状態を保持する構造体。 バリデータが登録されてから削除されるまでの期間を通じて、ステータスや重み、開始・終了時間などを追跡します。

パラメータ

  • status
    • Validatorの状態。
  • nodeID
    • ノードID。
  • startingWeight
    • 登録時の重み。
  • sentNonce
    • Managerから送信された最新のNonce。
  • receivedNonce
    • P-Chainから受信した最新のNonce。
  • weight
    • 現在の重み。
  • startTime
    • 稼働開始時間。
  • endTime
    • 稼働終了時間。

イベント

RegisteredInitialValidator

javascript
event RegisteredInitialValidator(
    bytes32 indexed validationID,
    bytes20 indexed nodeID,
    bytes32 indexed subnetID,
    uint64 weight,
    uint32 index
);

初期バリデータが登録された時に発行されるイベント。 変換データに含まれる初期Validatorが登録された時に発行されます。 subnetIDとindexは、将来Validator削除時に署名リクエストの根拠として使われます。

パラメータ

  • validationID
    • Validatorを識別するID。
  • nodeID
    • ノードID。
  • subnetID
    • SubnetのID。
  • weight
    • Validatorの重み。
  • index
    • 初期バリデータのインデックス。

InitiatedValidatorRegistration

javascript
event InitiatedValidatorRegistration(
    bytes32 indexed validationID,
    bytes20 indexed nodeID,
    bytes32 registrationMessageID,
    uint64 registrationExpiry,
    uint64 weight
);

Validatorの登録が開始された時に発行されるイベント。 新しいValidatorの登録を試みる際に発行されます。 登録完了前はまだ有効なValidatorとはみなされません。

パラメータ

  • validationID
    • Validatorを識別するID。
  • nodeID
    • ノードID。
  • registrationMessageID
    • 登録メッセージのID。
  • registrationExpiry
    • 登録の有効期限。
  • weight
    • Validatorの重み。

CompletedValidatorRegistration

javascript
event CompletedValidatorRegistration(bytes32 indexed validationID, uint64 weight);

Validatorの登録が完了した時に発行されるイベント。 P-Chainによる承認を受けてValidatorが正式に登録されたことを示します。

パラメータ

  • validationID
    • Validatorを識別するID。
  • weight
    • Validatorの重み。

InitiatedValidatorRemoval

javascript
event InitiatedValidatorRemoval(
    bytes32 indexed validationID,
    bytes32 validatorWeightMessageID,
    uint64 weight,
    uint64 endTime
);

Validatorの削除が開始された時に発行されるイベント。 Validatorを削除する処理が始まった時に発行されます。 まだ削除は完了していません。

パラメータ

  • validationID
    • 削除対象のValidatorID。
  • validatorWeightMessageID
    • 重み変更メッセージID。
  • weight
    • Validatorの重み。
  • endTime
    • 終了時間。

CompletedValidatorRemoval

javascript
event CompletedValidatorRemoval(bytes32 indexed validationID);

Validatorの削除が完了した時に発行されるイベント。 P-Chainで削除が承認され、Validatorが取り除かれたことを示します。

パラメータ

  • validationID
    • 削除完了したValidatorID。

InitiatedValidatorWeightUpdate

javascript
event InitiatedValidatorWeightUpdate(
    bytes32 indexed validationID, uint64 nonce, bytes32 weightUpdateMessageID, uint64 weight
);

Validatorの重み更新が開始された時に発行されるイベント。

パラメータ

  • validationID
    • ValidatorのID。
  • nonce
    • 更新Nonce。
  • weightUpdateMessageID
    • 重み更新メッセージID。
  • weight
    • 新しい重み。

CompletedValidatorWeightUpdate

javascript
event CompletedValidatorWeightUpdate(bytes32 indexed validationID, uint64 nonce, uint64 weight);

Validatorの重み更新が完了した時に発行されるイベント。 P-Chainによる承認を受け、Validatorの重みが正式に更新されたことを示します。

パラメータ

  • validationID
    • ValidatorのID。
  • nonce
    • 更新Nonce。
  • weight
    • 新しい重み。

関数

subnetID

javascript
function subnetID() public view returns (bytes32 id);

このManagerが管理するL1のSubnetIDを返す関数。 コントラクトに紐づくSubnetのIDを取得します。

戻り値

  • id
    • SubnetのID。

getValidator

javascript
function getValidator(bytes32 validationID)
    public
    view
    returns (Validator memory validator);

指定したValidatorIDに対応するValidatorの情報を返す関数。

引数

  • validationID
    • Validatorを識別するID。

戻り値

  • validator
    • Validator構造体。

l1TotalWeight

javascript
function l1TotalWeight() public view returns (uint64 weight);

現在のL1ValidatorSet全体の合計重みを返す関数。 全てのアクティブなValidatorの重みを合算した結果を返します。

戻り値

  • weight
    • 合計の重み。

initializeValidatorSet

javascript
function initializeValidatorSet(
    ConversionData calldata conversionData,
    uint32 messsageIndex
) public;

初期のValidatorSetを設定する関数。 P-Chainから送られるSubnetToL1ConversionMessageを取り込み、L1の初期ValidatorSetを確立します。 与えられたconversionDataを基に検証を行い、各初期ValidatorについてRegisteredInitialValidatorイベントを発行します。

引数

  • conversionData
    • Subnet変換時に使用するデータで、P-Chain上のConversionIDと一致するか検証されます。
  • messsageIndex
    • ICMメッセージのインデックス。
    • ここに含まれるSubnetToL1ConversionMessageのConversionIDがconversionDataと一致する必要があります。

completeValidatorRegistration

javascript
function completeValidatorRegistration(uint32 messageIndex)
    public
    returns (bytes32 validationID);

Validatorの登録を完了させる関数。 P-ChainからのL1ValidatorRegistrationMessageを受信して処理し、Validatorの登録を正式に完了させます。 この関数の実行が成功して初めて、Validatorはアクティブな状態とみなされます。 成功時にはCompletedValidatorRegistrationイベントが発行されます。

引数

  • messageIndex
    • L1ValidatorRegistrationMessageのインデックス。

戻り値

  • validationID
    • 登録が完了したValidatorのID。

completeValidatorRemoval

javascript
function completeValidatorRemoval(uint32 messageIndex)
    public
    returns (bytes32 validationID);

Validatorの削除を完了させる関数。 P-Chainから送られるRegisterL1ValidatorMessageを受信し、対象のValidatorが正式に削除されたことを確認します。 成功時にはCompletedValidatorRemovalイベントが発行されます。

引数

  • messageIndex
    • 削除確認を行うRegisterL1ValidatorMessageのインデックス。

戻り値

  • validationID
    • 削除が完了したValidatorのID。

completeValidatorWeightUpdate

javascript
function completeValidatorWeightUpdate(uint32 messageIndex)
    public
    returns (bytes32 validationID, uint64 nonce);

Validatorの重み更新を完了させる関数。 P-Chainから送られるL1ValidatorWeightMessageを受信して処理し、Validatorの重み変更を正式に反映します。 この関数が実行されるまで、重みの変更は有効になりません。 成功時にはCompletedValidatorWeightUpdateイベントが発行されます。

引数

  • messageIndex
    • L1ValidatorWeightMessageのインデックス。

戻り値

  • validationID
    • 更新されたValidatorのID。
  • nonce
    • 更新に対応するNonce値。

内部関数

_initiateValidatorRegistration

javascript
function _initiateValidatorRegistration(
    bytes memory nodeID,
    bytes memory blsPublicKey,
    PChainOwner memory remainingBalanceOwner,
    PChainOwner memory disableOwner,
    uint64 weight
) internal returns (bytes32 validationID);

Validator登録を開始する内部関数。 新しいValidatorを追加するためのRegisterL1ValidatorMessageを発行します。 この関数を呼び出しただけではValidatorはまだアクティブではなく、後でcompleteValidatorRegistrationが呼ばれる必要があります。 成功するとInitiatedValidatorRegistrationイベントが発行されます。

引数

  • nodeID
    • 登録するノードのID。
  • blsPublicKey
    • ValidatorのBLS公開鍵。
  • remainingBalanceOwner
    • 残高所有者情報。
  • disableOwner
    • 無効化権限所有者情報。
  • weight
    • ノードの重み。

戻り値

  • validationID
    • 登録されたValidatorのID。

_initiateValidatorRemoval

javascript
function _initiateValidatorRemoval(bytes32 validationID) internal;

Validator削除を開始する内部関数。 対象のValidatorについて、重みを0にするL1ValidatorWeightMessageを発行し削除プロセスを開始します。 この呼び出し直後に対象のValidatorは非アクティブとみなされます。 成功時にはInitiatedValidatorRemovalイベントが発行されます。

引数

  • validationID
    • 削除対象のValidatorID。

_initiateValidatorWeightUpdate

javascript
function _initiateValidatorWeightUpdate(
    bytes32 validationID,
    uint64 weight
) internal returns (uint64 nonce, bytes32 messageID);

Validatorの重み更新を開始する内部関数。 指定したValidatorについて新しい重みを設定するL1ValidatorWeightMessageを発行します。 更新はこの関数実行直後にはまだ有効にならず、completeValidatorWeightUpdateの呼び出しによって正式に反映されます。 成功時にはInitiatedValidatorWeightUpdateイベントが発行されます。

引数

  • validationID
    • 重みを変更するValidatorのID。
  • weight
    • 新しい重み。

戻り値

  • nonce
    • この更新に対応するNonce。
  • messageID
    • 重み更新メッセージのID。

DisableL1ValidatorTx について

Validatorは、上記の削除プロセスとは別に、P-ChainでDisableL1ValidatorTxを発行して直接無効化することができます。

これは、ValidatorのDisableOwnerが実行できる特権トランザクションであり、失敗したL1からValidatorに紐づく残高を回収するために利用されます。 そのため、通常のACP99Managerコントラクトから呼び出すものではありません。

互換性

ACP99ManagerはAvalancheプロトコルの現在の挙動には影響しません。 既存ノードや既存チェーンの設定変更やネットワークアップグレードを前提とせず、実装者がこの仕様に沿ったACP99Managerコントラクトを採用するかどうかは各プロジェクトの判断に委ねられます。

参考実装

AvaLabsのICMContractsリポジトリで参照実装が提供されます。 この参照実装は、ACP99Manager仕様に整合するように更新される予定であり、その更新が完了してはじめて本ACPをImplementable(実装可能)とマークできます。

例:統合アーキテクチャ

ACP99Managerはどのようなアーキテクチャにも組み込みやすい設計です。 ACP99では、異なるアーキテクチャ方針を示す2つの例を提示しています。

マルチコントラクト設計

この設計では、ACP99Managerを実装するコントラクトと、PoSやPoAなどのセキュリティモデルを担う「セキュリティモジュール」コントラクトを分離します。 各ACP99Manager実装は、1つ以上のセキュリティモジュールと関連づけられ、ValidatorSet変更を開始する関数(initiateValidatorRegistration、initiateValidatorWeightUpdate)を呼べるのはセキュリティモジュールのみとします。 Validatorの追加・削除・重み変更が開始されるたびに、ACP99Manager実装は対応するセキュリティモジュールの関数(handleValidatorRegistrationまたはhandleValidatorWeightChange)を呼び出します。 安全性の観点から、セキュリティモジュールは不変のACP99Managerアドレスを参照することが推奨されます。

セキュリティモジュールが、P-Chainで確定した「追加/削除/重み変更」を受けてどのようなアクションを取るかはモジュール側の設計に委ねられます(例:PoSL1における出金待機期間の開始、報酬の割当など)。

---
title: ACP-99 Multi-contract Architecture with a PoA Security Module
---
graph LR
  Safe(Safe multisig)
  SecurityModule(ACP99PoAModule)
  Manager(ACP99Manager)
  P(P-Chain)

  subgraph "Manager chain"
    Safe
    SecurityModule
    Manager
  end
  Safe -.->|Own| SecurityModule
  Safe -.->|Own| Manager
  SecurityModule <-.->|Reference| Manager
  Safe -->|addValidator| SecurityModule
  SecurityModule -->|initiateValidatorRegistration| Manager
  Manager -->|sendWarpMessage| P
  P -->|completeValidatorRegistration| Manager
  Manager -->|handleValidatorRegistration| SecurityModule

この設計を採る「セキュリティモジュール」は、PoS、LiquidPoSなど様々な方式を実装可能です(これらモジュールの仕様は本ACPの範囲外です)。 進行中の実装はSuzakuContractsLibraryにあり、議論の結果に基づきImplementableと見なされるまで更新されます。 AvaLabsのV2ValidatorManagerもこの構成でPoSモジュールを実装しており、ICMContractsRepositoryで公開されています。

シングルコントラクト設計

この設計では、基底クラスがACP99Managerを実装し、その子クラスで特定のセキュリティモデル(例:PoA)を実装します。 下図ではPoAValidatorManagerが子クラス例ですが、PoSなど別モデルを実装する子クラスに差し替えることもできます。

---
title: ACP-99 Single-Contract Architecture implementing PoA
---
  classDiagram
  class ACP99Manager {
    initializeValidatorSet
    initiateValidatorRegistration
    completeValidatorRegistration
    initiateValidatorWeightUpdate
    completeValidatorWeightUpdate
  }
  <<abstract>> ACP99Manager

  class ValidatorManager {
    completeValidatorRegistration
  }
  <<abstract>> ValidatorManager
  class PoAValidatorManager {
    initiateValidatorRegistration
    initiateEndValidation
    completeEndValidation
  }

  ACP99Manager <|--ValidatorManager
  ValidatorManager <|-- PoAValidatorManager

この構成に特化した参照実装は提供されていませんが、AvaLabsのV1ValidatorManagerは、ACP99で説明される機能の多くを実装しています。 ただし本仕様の策定前に作られたため相違点があり、「標準の概念実装モデル」として参照するにとどめるべきとされています。

セキュリティ

ACP99Managerおよび参照実装の監査は、Avalancheエコシステムの将来にとって重要です。 多くのL1がこれに依存して自身の安全性を確保することになるため、仕様適合性と実装の健全性の確認は最優先で実施されるべきです。 設計・実装・運用の各段階でのレビューと検証を通じて、想定外の挙動や脆弱性の混入を防ぐことが求められます。

Open Questions

マネージャーチェーン上でバリデータ集合の履歴情報を保持すべきか

getValidator関数で過去のバリデータ情報を返すべきかどうかは、実装者の判断に委ねられています。

過去のバリデータの稼働実績や重みの推移などの情報が有益である場合もありますが、すべてのアプリケーションに必要とは限りません。 例えば、PoA(Proof-of-Authority)モデルでは過去のバリデータ稼働率を参照する必要がなく、履歴保持の意義は薄いと考えられます。

履歴情報そのものは、アーカイブノードやエクスプローラーなどのオフチェーンツールで取得可能です。 そのため、コントラクトレベルで必ず保持する必要はなく、必要に応じて実装されるべき性質のものです。

ACP99Managerに「churn control(入れ替え制御)」機能を含めるべきか

AvaLabsが提供するValidatorManager実装には「churn control」機能が含まれています。

これは、短時間に大量の重みが追加・削除されることを防止する仕組みです。バリデータの入れ替えや重み変更が急激に行われると、コンセンサスに失敗するリスクが高まります。 そのため、一定の範囲内でしか変更を許さないように制御することは、安定性確保の観点で有効と考えられています。

一方で、すべての実装やユースケースに必須とは限りません。 PoAのようなモデルでは大規模な入れ替え制御が不要な場合もあり、設計の柔軟性を残すべきとの考え方もあります。 したがって、この機能を必須とするかどうかは依然として議論中です。

最後に

今回は「AvalancheのEVMチェーンでL1のバリデータ集合(追加・削除・重み変更)を管理するためのコントラクトを提案しているACP99」についてまとめてきました! いかがだったでしょうか?

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

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