# [ACP77] バリデータ管理をL1に移す仕組みを理解しよう！

> ACP77は、Subnetsの作成と運用のしやすさ・柔軟性を高めるため、以下の3つを提案しています。

- URL: https://cardene.dev/articles/3c28cdfe275253b75da5
- 公開日: 2025-09-03
- 更新日: 2025-09-03
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、AvalancheのSubnetを拡張し、PrimaryNetworkの制約に縛られず独立したLayer1として運用できる仕組みを提案している**ACP77**についてまとめていきます！

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

https://github.com/avalanche-foundation/ACPs/blob/main/ACPs/77-reinventing-subnets/README.md

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

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

## 概要

**ACP77**は、Subnetsの作成と運用のしやすさ・柔軟性を高めるため、以下の3つを提案しています。

#### SubnetバリデータをPrimary Networkバリデータから分離

現状は、Subnet のバリデータになる前に、まずPrimary Networkのバリデータ（最低 2000 AVAX のステーク）になる必要があり、さらにX/P/C-Chain 全体を同期し、コンセンサスに参加する必要があります。
提案はこの前提条件を取り払い、SubnetバリデータがPrimary Networkにフル同期して検証に参加する必要をなくします。
これにより『**2000 AVAX 必須**』の条件が撤廃され、Primary Network の同期も必要最小限（Partial Sync）で済む設計を目指します。

#### バリデータセット管理の主体をP-ChainからSubnetへ移譲

これまでP-Chainが担っていた「**バリデータセットの登録・更新・報酬設計**」などの権限を、各Subnet側へ移します。
その結果、**ERC20** / **ERC721** / 任意資産によるステーキング（Stake の担保資産設計）や、報酬管理（報酬計算・配布）をSubnetのルールで自由に決められます。

#### Subnetバリデータ向けのP-Chain連続手数料（Continuous Subnet Staking）の導入

Subnetを運用するためにP-Chainへ支払う費用（これまでは大きな初期コスト側に偏りがち）を、時間に沿って継続的に支払うメカニズムへ設計転換します。
活動量や期間に応じた持続的・可変的な費用構造を整えることで、初期負担を抑えつつ、利用の伸びに合わせた費用負担へシフトできます。

さらに、この新しいネットワーク作成フローで生まれるネットワークは、従来のSubnetよりも機能面・主権（sovereignty）が強化され、Primary Networkとバリデータの重なりを必要としなくなります。
このような新しいタイプのネットワークをコミュニティでは「**Avalanche Layer 1（L1）**」と呼び、従来のフローで作られたものを引き続き「**Subnet**」と呼び分けます。
なお、この提案は過去の提案である**ACP13**を置き換えるものであり、その内容の一部を引き継いでいます。

:::message
**ACP13**については以下の記事を参考にしてください。
:::

[\[ACP13\] Subnet専用Validatorの仕組みを理解しよう！](/articles/fb5625058f8ff4bc9bc0)

## 動機

#### 高額な初期ステーク要件とハードウェア要件が参入障壁になっている

現在、Subnetの検証者になるには、その前段としてPrimary Networkの検証者である必要があります。
各ノード運営者は最低 **2000 AVAX**（本文記載時点で約 **\$70,000**）をステークする必要があります。
多くのSubnetは少なくとも8バリデータでの立ち上げを目指すため、合計では **16,000 AVAX**（同 **\$560,000**）という高い初期資本が必要です。
加えて、SubnetsのバリデータはX-Chain / P-Chain / C-Chainをすべて同期し、そのコンセンサスに参加するための十分なリソースを用意する必要があります（例：**8 vCPU / 16 GB RAM / 1 TB ストレージ**）。
これはSubnet側のリソース要求に上乗せされるため、初期投資だけでなく運用コストも増やします。

| 現状の前提 | 具体例 |
| -------- | ----- |
| 初期ステーク | `2000` AVAX/バリデータ（Subnet立ち上げ8バリデータなら合計 16,000 AVAX） |
| Primary Network要件 | X/P/C-Chainの全面同期と合意参加 |
| ハードウェア要件（例 | 8 vCPU / 16 GB RAM / 1 TB ストレージ（Primary 側だけで） |
| 影響 | 初期コストが大きく、運用も重い（Subnet 用リソースに加算） |

提案は、固定的な初期コストを軽減してSubnetsを立ち上げやすくし、需要に応じて柔軟に拡張できるようにすることを目指しています。

#### 規制制約によりSubnetを作れない主体が存在する

パーミッションレスでスマートコントラクト可能なブロックチェーン（C-Chainなど）の検証に関与することを規制上禁じられている主体（例：一部の規制下の金融機関）は、現在のモデルではPrimary Networkの検証を回避できないため、Subnet自体を立ち上げられません。
これは、RWA（現実世界の資産）発行体がSubnetを通じてAvalancheエコシステムに参加し、C-ChainとSubnet間をAWM/Teleporterで連携させるようなユースケースを阻害しています。
提案は、Primary Network検証への必須参加を外す方向を示し、規制要件のある主体でもSubnetを構築・運用しやすくします。

#### SubnetとPrimary Networkの相互負荷が安定性を損る可能性

多数のバリデータを抱えるSubnetが、十分なメトリクス監視やリソース見積もりなしに急に負荷増した場合、Primary Networkの安定性に悪影響を与えるおそれがあります。
特に、リソース不足（Underprovisioned）なPrimary Networkのバリデータが同時にトランザクション数が多いSubnetを抱えると、OOM（メモリ不足によるクラッシュ）やディスク性能劣化、P/X/C-Chainの検証に必要なCPU時間確保の失敗といった問題が起こりえます。
逆に、Primary Network側の不具合や想定外の挙動がSubnetの稼働を巻き込んで停止させる可能性もあります。
提案が目指す検証者の分離と費用の継続課金化は、この相互依存リスクを緩和してネットワークごとの独立性と安定性を高める方向です。

#### コスト構造が「初期固定費に偏っている」ことが新規参入を阻む

現行では、Subnetを運用するためにPrimary Networkへ支払う費用はトラフィック連動で増えないのに対し、Subnetバリデータとしての初期セットアップ費用（前述の **2000 AVAX** ステークやハードウェア負担）が大きな足かせになっています。
需要がまだ読めない段階では、小さく始めて、伸びたら増やすという段階的投資を行いたいプロジェクトが多く、この点が採用のハードルになっていました。
対照的に一部のL2は、外部チェーンに対するデータ可用性（Data Availability）やセキュリティのコストが活動量に応じて増える構造をとることが一般的です。Subnets はセキュリティとデータ可用性（DA）を自前で確保する設計のため、トランザクション処理やネットワーク利用が増えた場合のコストは、主に CPU・メモリ・ストレージといったハードウェアリソースの追加として必要になります。
**ACP77**の継続的な手数料（Continuous Subnet Staking）は、利用度合いに応じた支払いを可能にし、初期費用に集中していた負担を和らげます。

#### Elastic Subnets（Banff）の限界と実利用の停滞

Banffで導入された**Elastic Subnets**は、Subnet独自トークンでの**Proof-of-Stake（PoS）**と**稼働時間ベースの報酬**を可能にしましたが、トークンが `ANT`（X-Chain で発行）であることとP-Chainにロックすることが前提でした。
さらに、報酬配布はP-Chain上で行われ、`TransformSubnetTx` で定義した報酬カーブは後から変更できないという制約がありました。
その結果、メインネット稼働の**Elastic Subnet**は現時点で存在しないという事実が示すように、**Permissionless Subnets**の現行デザインには改善余地があることが明らかです。
**ACP77**は、バリデータセット管理と報酬管理をSubnet側に移すことで、担保資産の自由度（**ERC20**/**ERC721**/任意資産）や報酬設計の可変性を高めて実用に耐える柔軟性を確保します。

:::message
「**Banff（バンフ）**」はAvalancheネットワークにおけるネットワークアップグレード（ハードフォーク）のひとつです。
Avalancheでは都市名をコードネームとして各アップグレードに付けており、その中でBanffアップグレードでは「**Elastic Subnets**」という仕組みが導入されました。
**Elastic Subnets**により、各Subnetが独自のProof-of-Stake（PoS）トークンを利用し、バリデータへの報酬を自前のルールで分配できるようになったのが大きな特徴です。
:::

:::message
`TransformSubnetTx`（Transform Subnet Transaction） は、P-Chain上でSubnetの設定を変更するためのトランザクションのひとつです。
Elastic Subnetsを有効化する時に用いられ、具体的には以下の役割を持ちます。

- Subnetを「**Elastic Subnet**」へ変換する処理を担うトランザクション。
- 報酬配布のルール（報酬カーブ）を定義する。
    報酬カーブとは、ステーク量や稼働時間に応じてどのように報酬が増減するかを決める関数的なルールです
- 一度設定した報酬カーブは変更できない制約がある。
:::

### 命名について

Avalancheの「**Subnet**」は、Primary Networkバリデータ集合の部分集合によって検証されるネットワークを意味してきました。
**ACP77**の新しい作成フローでは、新ネットワークのバリデータ集合とPrimary Networkバリデータ集合の重なりを必要としません。
加えて、機能面と主権性（sovereignty）がより強化された設計が可能になります。
この違いを明確にするため、コミュニティでは新フローで作られるネットワークを**Avalanche Layer 1（L1）**と呼び、従来フローのものを引き続き**Subnet**と呼びます。
用語を分けることで、設計・運用・規制面の議論を整理しやすくします。

### 旧提案（ACP13）との関係

**ACP77**は **ACP13**を置き換え（supersede）て一部を継承しています。
ただし、バリデータの分離、管理権限の移譲、継続課金の導入という点で、設計思想と到達点がより包括的になっています。

### 図解

#### ACP13フロー

```mermaid
flowchart TB
  subgraph Old[ACP13フロー]
    A["Primary Network\n(X/P/C を全同期し合意参加)"] -->|必須前提| B["Subnet バリデータ"]
    A -->|P-Chain が管理| C["バリデータセット"]
    C -->|ANT ロック &\nTransformSubnetTx| D["報酬: P-Chain で分配"]
    B -->|初期要件| E["2000 AVAX ステーク"]
  end
```

#### ACP77のフロー

```mermaid
flowchart TB
  subgraph New[ACP77フロー]
    F["Subnet バリデータ"] -. 分離 .-> G["Primary Network\nPartial Sync（必要最小限）"]
    F -->|ERC-20 / ERC-721 / 任意資産| H["ステーキング & 報酬管理\n（Subnet 側で実装・運用）"]
    G -->|継続的な手数料| I["P-Chain 手数料\n（Continuous Subnet Staking）"]
  end
```

## 仕様

### 高レベル設計

L1はvalidator manager（管理者）を指定します。
指定はblockchain ID（どのチェーン上にあるか）とそのチェーン上のアドレスの二つの値で行います。
以後、L1のバリデータを追加・削除・重み（weight）変更したいときは、このvalidator managerがWarp メッセージをP-Chainに送ります。

P-Chainは `PlatformVM` でWarpを検証します。
送信元チェーン（`sourceChainID`）のバリデータ重みの67%以上がBLS集約署名に参加していれば有効とみなし、L1のバリデータ集合を更新します（この67%はC-Chainと同じ基準です）。
将来はL1ごとにこの閾値を調整できるようにする提案も想定されています。

更新結果を第三者が確かめられるように、P-Chainは確認用のWarpメッセージも発行できます。
また、L1のバリデータはPrimary Networkのフル同期やコンセンサス参加は不要で、P-Chainだけを同期すれば十分です。
アクティブ状態を維持するために、$AVAX 建ての継続的な手数料（Continuous Fee）を支払います。

### Warpの検証要件とペイロード

#### 検証要件（閾値署名）

P-ChainがWarpを有効とみなすには、`sourceChainID` の重み67%以上がBLS集約署名に参加している必要があります。
これはC-Chainと同じ閾値です。
将来、L1ごとの閾値変更に関する提案が想定されています。

#### ノードID表現

メッセージ内のnodeIDは可変長バイト列で表現します。
将来のノードID形式追加に備えるためです。

#### ペイロードの種類と役割

仕様に追加されるWarpペイロードは以下の4種です。
署名収集方式は固定せず、実装案として**ACP118**の `SignatureRequest` が挙げられています。

| ペイロード名 | 方向 | 役割 |
| ---------- | --- | ---- |
| `SubnetToL1ConversionMessage`    | P-Chain → validator manager | Subnet から L1 への変換後、**初期バリデータ集合**を通知       |
| `RegisterL1ValidatorMessage`     | L1 → P-Chain                | **新規バリデータ登録**の要求                          |
| `L1ValidatorRegistrationMessage` | P-Chain → 消費者               | ある登録が **有効か／今後も無効か** を証明                  |
| `L1ValidatorWeightMessage`       | 双方向                         | 既存バリデータの **weight 更新／削除（weight=0）** と結果確認 |

### 初期バリデータ集合の通知

SubnetからL1への変換が受理された後に、L1の初期バリデータ集合を第三者が検証可能な形で通知します。
通知に使うWarpメッセージ名は`SubnetToL1ConversionMessage`です。`sourceChainID`はP-ChainID、`sourceAddress`は空です。
識別子`conversionID`は後述のConversionData全体のSHA256です。

手順は以下です。
1.P-Chainが`ConvertSubnetToL1Tx`を検証し承認する。
2.P-Chainが初期集合を`SubnetToL1ConversionMessage`として発行する。
3.L1のvalidatormanagerやL1ノードがこのWarpメッセージを取得し、初期集合の正当性を検証して状態に反映する。

#### `ValidatorData`

|  Field | Type | Size |
| ------ | ---- | ---- |
|  `nodeID` |   `[]byte` |  `4 + len(nodeID)` |
| `blsPublicKey` | `[48]byte` |     `48` |
| `weight` |   `uint64` |     `8` |
| 合計 |    | `60 + len(nodeID)` |

#### `ConversionData`

| Field | Type | Size |
| ---- | --- | ------ |
|  `codecID` | `uint16` | `2` |
| `subnetID` | `[32]byte` | `32` |
| `managerChainID` | `[32]byte` | `32` |
| `managerAddress` | `[]byte` |  `4 + len(managerAddress)` |
| `validators` | `[]ValidatorData` | `4 + sum(validatorLengths)` |
| 合計 |  | `74 + len(managerAddress) + sum(validatorLengths)` |

`codecID=0x0000`は固定されています。
`managerChainID`と`managerAddress`でvalidatormanagerを特定します。
`validators`は継続してあ手数料の課金対象として初期登録されるL1バリデータ集合です。

#### `AddressedCall`（通知本体）

|          Field |       Type |              Size |
| ------------- | --------- | ---------------- |
|      `codecID` |   `uint16` |               `2` |
|       `typeID` |   `uint32` | `4`（`0x00000000`） |
| `conversionID` | `[32]byte` |              `32` |
|             合計 |            |              `38` |

### 新規バリデータの登録要求

新規バリデータの追加には、`validatormanager`は`RegisterL1ValidatorMessage`を含む`RegisterL1ValidatorTx`を作成し、P-Chainでの処理を要求します。
メッセージには`nodeID`、`blsPublicKey`、`weight`、未使用残高の払い戻し先（`remainingBalanceOwner`）、無効化の権限者（`disableOwner`）、有効期限（`expiry`）を含めます。

#### 手順

1. バリデータとして参加したいノード運営者が、L1側で定められた手順に従って情報を登録します（例: EVM互換L1ならL1コントラクトに情報を記録 → Warpメッセージ生成）。
2. `validatormanager`が`RegisterL1ValidatorMessage`を含む`RegisterL1ValidatorTx`を作成し、P-Chainでの処理を要求します。
3. P-Chainが署名や内容を検証し、問題がなければL1のバリデータ集合を更新します。
4. 処理後、P-Chainは登録済みであることを示す確認用Warp（`L1ValidatorRegistrationMessage`で`registered=true`）を発行できます。

#### リプレイ保護と期限

`validationID`は`AddressedCall.Payload`のSHA256で一意化します。
使用済みの`validationID`は無効です。
`expiry`はこのトランザクションがP-Chainで承認される時刻を基準に24時間以内に設定し、期限切れの`validationID`はP-Chainのステートから破棄できます。

#### `PChainOwner`

以下の表はPChainOwnerの構造です。`remainingBalanceOwner`や`disableOwner`で使います。

|       Field |         Type |                      Size |
| ---------- | ----------- | ------------------------ |
| `threshold` |     `uint32` |                       `4` |
| `addresses` | `[][20]byte` | `4 + 20 * len(addresses)` |
|          合計 |              | `8 + 20 * len(addresses)` |

以下は、P-Chainが有効として処理するために必ず満たす条件です。

1. `threshold==0`のときは`addresses`を空にすること。
    しきい値が0なら署名者はいないはずなので、アドレスを並べる必要がありません。
2. `threshold<=len(addresses)`であること。
    要求する署名数が登録された署名者数を超えてはいけません。
3. `addresses`は重複を含まず、昇順で並べること。
    重複は同一人物の多重カウントを招きます。
    昇順は検証を一意にし、実装簡素化のためです。
    ここでの昇順は、各20バイト値の先頭バイトから順に0x00〜0xFFで比較し、途中で差が出た位置で数値が小さい方を前にする順序とします。
    全バイトが同一なら同一とみなします。

上記のいずれかに違反した場合、そのPChainOwnerを含むメッセージは無効として扱われます。

#### `AddressedCall`（登録要求本体）

| Field | Type | Size |
| ---------------------- | ------------ | ---------- |
|    `codecID` |      `uint16` | `2` |
|   `typeID` |      `uint32` |   `4`（`0x00000001`） |
| `subnetID` |    `[32]byte` |    `32` |
|  `nodeID` |      `[]byte` |   `4 + len(nodeID)` |
|  `blsPublicKey` |    `[48]byte` |     `48` |
| `expiry` |      `uint64` |  `8` |
| `remainingBalanceOwner` | `PChainOwner` |  `8 + 20 * len(addresses1)` |
|   `disableOwner` | `PChainOwner` | `8 + 20 * len(addresses2)` |
|  `weight` |      `uint64` |    `8` |
|  合計 |  | `122 + len(nodeID) + 20 * (len(addresses1)+len(addresses2))` |

### 登録状態の証明

P-Chainは確認用Warpメッセージ`L1ValidatorRegistrationMessage`を発行します。
これは`validationID`が現在の集合に存在する場合は`registered=true`、集合に存在せず今後も登録されない場合は`registered=false`を示します。
`expiry`が未来で、かつactiveではない`validationID`については、P-Chainは`registered=false`を返しません。

|          Field |       Type |              Size |
| ------------- | --------- | ---------------- |
|      `codecID` |   `uint16` |               `2` |
|       `typeID` |   `uint32` | `4`（`0x00000002`） |
| `validationID` | `[32]byte` |              `32` |
|   `registered` |     `bool` |               `1` |
|             合計 |            |              `39` |

### weight更新・削除の指示

既存バリデータの`weight`を変更する、または`weight=0`で削除する場合は、Warpメッセージ`L1ValidatorWeightMessage`を`SetL1ValidatorWeightTx`でP-Chainに処理させます。
P-Chainは指示を取り込み、結果確認用の同型メッセージも発行できます。
想定される利用は、委任や報酬で実効ステークが増えた時の増加、不正行為に対する減少、inactive整理の削除などです。

#### 条件

1. `nonce>=minNonce`であること（連番である必要はありませんが小さくしてはいけません）。
2. `minNonce==MaxUint64`の場合は`nonce==MaxUint64`かつ`weight==0`にすること（将来の削除不能を避ける安全装置）。
3. `weight==0`による削除は集合の最後の1名には適用しないこと（全削除による復旧不能を防ぐガード）。

#### ステート遷移

* `weight!=0`が承認されると`weight`を更新し、`minNonce=nonce+1`に進めます。
* `weight==0`が承認されると当該`validationID`と`minNonce`をP-Chainのステートから削除し、未使用の`Balance`を`RemainingBalanceOwner`へ単一UTXOで払い戻します。

|          Field |       Type |              Size |
| ------------- | --------- | ---------------- |
|      `codecID` |   `uint16` |               `2` |
|       `typeID` |   `uint32` | `4`（`0x00000003`） |
| `validationID` | `[32]byte` |              `32` |
|        `nonce` |   `uint64` |               `8` |
|       `weight` |   `uint64` |               `8` |
|             合計 |            |              `54` |

### 新規P-Chainトランザクション

PermissionlessなL1として扱うには2つの条件があります。
1つ目は、`CreateSubnetTx`で定義された`Owner`キーがバリデータ集合を直接変更できないこと。
2つ目は、集合の更新をWarpメッセージ経由で行えることです。

これを実現するため、P-Chainには以下の5種類の新しいトランザクションが導入されます。

- `ConvertSubnetToL1Tx`はSubnetをL1へ変換する。
- `RegisterL1ValidatorTx`は新しいバリデータを追加する。
- `SetL1ValidatorWeightTx`は既存バリデータのweightを変更または削除する。
- `DisableL1ValidatorTx`は一時的に無効化する。
- `IncreaseL1ValidatorBalanceTx`は残高を補充する。

これらによって、SubnetからL1に移行した後も、バリデータ集合の管理はWarpメッセージを通じて一貫して行えるようになります。

#### `ConvertSubnetToL1Tx`（Subnet→L1）

SubnetをPermissionlessなL1へ切り替えるときに使うトランザクションです。
ここで指定する**validator manager**（L1のバリデータ集合を管理する主体）は、`blockchainID`と`address`の組み合わせで識別します。
この設定を行うには、もともと`CreateSubnetTx`で定義された`Owner`の署名が必要です。

この手続きが承認されると、そのSubnetでは`CreateChainTx`と`AddSubnetValidatorTx`が使えなくなります。
`Owner`に残る権限は、過去に`AddSubnetValidatorTx`で追加されたバリデータを`RemoveSubnetValidatorTx`で外すことだけです。
既存のバリデータは指定された`End`時刻まで残留しますが、全員が退場すると`Owner`の権限は完全に失われます。
その後は、`RegisterL1ValidatorTx`や`SetL1ValidatorWeightTx`によって集合を管理していく仕組みに移行します。

このとき、各バリデータには`validationID`が割り当てられます。
定義は`SHA256(subnetID(32B) || validatorIndex(4B))`です。
さらにP-Chainは`SubnetToL1ConversionMessage`を生成し、`conversionID`（ConversionDataのSHA256）を付けて署名します。
これにより第三者は初期バリデータ集合を正しく追跡できます。

実装の型は以下です。

```golang
type PChainOwner struct {
    Threshold uint32
    Addresses []ids.ShortID // 20B。重複なし・昇順
}

type L1Validator struct {
    NodeID []byte
    Weight uint64
    Balance uint64
    Signer signer.ProofOfPossession // BLS公開鍵 + PoP
    RemainingBalanceOwner PChainOwner
    DisableOwner PChainOwner
}

type ConvertSubnetToL1Tx struct {
    BaseTx
    Subnet  ids.ID
    ChainID ids.ID
    Address []byte
    Validators []L1Validator
    SubnetAuth verify.Verifiable
}
```

#### `RegisterL1ValidatorTx`（新規バリデータの追加）

L1に新しいバリデータを加えるときに利用するトランザクションです。

このトランザクションには、次の3つの要素が含まれます。

* `Balance`
    初期残高。
    トランザクションの入力額から出力額と手数料を引いた範囲で指定します。
* `Message`
    Warpメッセージである`RegisterL1ValidatorMessage`。
    登録対象のノード情報を含みます。
* `Signer`
    96バイトのProof of Possession。
    提出者が`blsPublicKey`の正当な所有者であることを証明します。

```golang
type RegisterL1ValidatorTx struct {
    BaseTx
    Balance uint64 // <= sum(inputs) - sum(outputs) - TxFee
    Signer  [96]byte
    Message warp.Message // RegisterL1ValidatorMessage
}
```

##### 承認後の処理

* 新規バリデータには`validationID = SHA256(AddressedCall.Payload)`が割り当てられます。
* P-Chainは承認時に`minNonce=0`を記録し、以降の`SetL1ValidatorWeightTx`で検証する時に利用します。
* リプレイ防止のため、同じ`validationID`は再利用できません。
* `expiry`は24時間以内に制限され、この期限を過ぎるとIDは破棄されます。

##### 登録確認

登録が有効になると、P-Chainは`registered=true`の`L1ValidatorRegistrationMessage`を返します。
逆に、期限切れや削除済みの場合は`registered=false`を返します。
ただし、`expiry`が未来で、まだアクティブ化されていないバリデータについては`false`は返されません。

##### 想定される利用例

EVM互換のL1では、以下のような設計が考えられます。
ノード運営者がスマートコントラクトに自身の情報を登録し、そこにステークしたトークン量を`weight`として採用します。
その結果をもとにWarpメッセージを生成し、P-Chainに提出することで新規バリデータとして登録されます。

#### `SetL1ValidatorWeightTx`（weight更新／削除）

既存バリデータの`weight`を変更したり、`weight=0`を指定して削除したりするときに利用するトランザクションです。

```golang
type SetL1ValidatorWeightTx struct {
    BaseTx
    Message warp.Message // L1ValidatorWeightMessage
}
```

##### 利用場面

* 委任や報酬によってステークが増えた場合に`weight`を増加させる
* 不正行為を検知した場合に`weight`を減少させる
* 長期間inactiveなバリデータを整理する際に`weight=0`を指定して削除する

##### 削除時の扱い

`weight=0`による削除では、未使用の残高を払い戻したうえで、対応するバリデータのエントリをP-Chainのステートから削除します。
明示的な`EndTime`は存在せず、バリデータを外す方法はこの削除処理のみです。

##### 検証条件

1. `nonce`は常に`minNonce`以上でなければならない
2. `minNonce==MaxUint64`のときは、`nonce==MaxUint64`かつ`weight==0`でなければならない（削除不能になることを防ぐための仕組み）
3. `weight=0`による削除は、集合に残る最後の1名には適用できない（完全消滅を防ぐためのガード）

##### ステートの更新

* `weight!=0`が承認された場合
    対象バリデータの`weight`を更新し、`minNonce`を`nonce+1`に進めます。
* `weight==0`が承認された場合
    `validationID`と`minNonce`をP-Chainのステートから削除し、未使用残高を`RemainingBalanceOwner`に単一UTXOで払い戻します。

#### `DisableL1ValidatorTx`（一時無効化）

このトランザクションは、バリデータを集合から完全に削除せず、一時的に**inactive**にするために利用します。

```golang
type DisableL1ValidatorTx struct {
    BaseTx
    ValidationID ids.ID
    DisableAuth verify.Verifiable // DisableOwner が署名
}
```

実行には`DisableOwner`の署名が必要です。
処理が成立すると未使用の残高は`RemainingBalanceOwner`に払い戻されます。
ただし、これは一時的な無効化にとどまるため、集合から完全に削除する場合には`SetL1ValidatorWeightTx(weight=0)`を使う必要があります。
その時はvalidator managerからのWarpを経由して指示を行います。

P-Chainは`MinStakeDuration`を強制しないため、もしロック期間が必要であればL1側のスマートコントラクトで定義する必要があります。
無効化されたバリデータは、`IncreaseL1ValidatorBalanceTx`で残高を追加することで再びアクティブに復帰できます。

#### `IncreaseL1ValidatorBalanceTx`（残高追加）

指定した`validationID`に対して誰でも\$AVAXを追加入金できます。
inactiveのバリデータも、入金によりアクティブへ戻ります。

```golang
type IncreaseL1ValidatorBalanceTx struct {
    BaseTx
    ValidationID ids.ID
    Balance uint64 // <= sum(inputs) - sum(outputs) - TxFee
}
```

追加入金された分は、`DisableL1ValidatorTx`でいつでも払い戻し可能です。

### L1ノードのブートストラップ

新しいノードやバリデータを立ち上げるには、ネットワークの正しい最新ステートをローカルで再構築する初期同期が必要です。
PrimaryNetworkでは、信頼済みブートストラッパへ接続してピア探索を開始します。
一方でL1のバリデータはPrimaryNetworkのバリデータである必要がないため、PrimaryNetworkに接続するだけではL1のピアを見つけられません。
そこでPrimaryNetworkがL1バリデータのIPアドレスを追跡し、それをネットワークに伝播させます。
この仕組みによって、L1は独自のブートストラッパを運用しなくても効率よくピアを発見できます。

#### L1の主権（Sovereignty）とP-Chainの境界

**ACP77**が有効化されると、P-ChainはPrimaryNetworkにおける\$AVAX以外の資産をステーキング対象として扱いません。
L1のステーキング資産、ロック、報酬配布、スラッシングなどはL1のvalidatormanagerが管理します。
P-Chainに求められるのは、バリデータごとの継続手数料の支払いだけです。
L1が望めば、P-Chain上に全バリデータ分の残高をまとめて費用を肩代わりする設計も可能です。
バリデータ集合の所有権はL1にあり、P-Chainは所有しません。
参加条件にも制約もなく、ステークはP-Chainにロックされません。`SetL1ValidatorWeightTx(weight=0)`で外された場合でも、ステークはP-Chainの外でロックされたままです。
P-ChainはWarp検証・反映に加えて中立的な裁定役も担えます。
例えば、不正なバリデータに対して、L1側でBLSマルチシグを生成してweightを減らす決定をし、PrimaryNetworkのセキュリティに基づいて実行できます。
将来的には、現在固定されている67%の閾値を、L1ごとに51%などへ調整できるようにする拡張も想定されています。

### 継続手数料メカニズム

アクティブなL1バリデータは、BLS鍵やweight、nonceの維持、さらにIPアドレスの追跡といった処理を継続的に行います。
これらは時間に比例してP-Chainに負荷を与えるため、その分を料金として反映させる仕組みが必要になります。
そのため各バリデータには`Balance`（残高）が設けられ、稼働時間に応じて動的に手数料が差し引かれるよう設計されています。

`Balance`は`RegisterL1ValidatorTx`で初期化され、必要に応じて`IncreaseL1ValidatorBalanceTx`で追加できます。
残高がゼロになるとバリデータはinactive扱いとなり、メモリ上のプロパティが削除され、そのノードからのメッセージは無効化されます。
ただし、残高を補充すれば再びアクティブに戻ることができます。
inactiveが増えた場合には、`SetL1ValidatorWeightTx(weight=0)`を用いて集合から完全に除外できます。

#### 償却処理の方法

料金の差し引きは、累積器`acc`を用いることで効率的に管理できます。
`acc`は「**そのバリデータが開始時点から常にアクティブであった場合に発生していた累積料金**」を表します。
各バリデータの`balance`と比較することで、どの順番で残高が枯渇するかを効率的に判断できます。

以下は擬似コードの例です。

```python
class ValidatorQueue:
    def __init__(self, fee_getter):
        self.acc = 0
        self.queue = PriorityQueue()
        self.fee_getter = fee_getter

    # 時間経過に応じて累積器を進め、残高不足のバリデータを外す
    def time_elapse(self, t):
        self.acc = self.acc + self.fee_getter(t)
        while True:
            vdr = self.queue.peek()
            if vdr.balance < self.acc:
                self.queue.pop()
                continue
            return

    # バリデータの参加時に累積器を反映して登録
    def validator_enter(self, vdr):
        vdr.balance = vdr.balance + self.acc
        self.queue.add(vdr)

    # バリデータ削除時に残高精算と払い戻しを行う
    def validator_remove(self, vdrNodeID):
        vdr = find_and_remove(self.queue, vdrNodeID)
        vdr.balance = vdr.balance - self.acc
        vdr.refund() # RemainingBalanceOwnerに払い戻し
        self.queue.remove()

    # 残高を追加してアクティブ化
    def validator_increase(self, vdrNodeID, balance):
        vdr = find_and_remove(self.queue, vdrNodeID)
        vdr.balance = vdr.balance + balance
        self.queue.add(vdr)
```

#### 単価調整の仕組み

単価は**ACP103**で導入されたダイナミックベースフィーの仕組みを応用して計算されます。
目標とするアクティブ数を`T`とし、実際のアクティブ数を`V`としたとき、その差分によって超過アクティブ数`x`が決まり、これに応じて単価が変化します。

初期状態では`x=0`。毎秒の更新ルールは以下のとおりです。

* 単価

$$
\text{feePerSecond} = M \cdot \exp\left(\frac{x}{K}\right)
$$

    （`M`は最小単価、`K`は変化率を制御する定数）

* 更新式

$$
x = \max(x + (V - T), 0)
$$

    この設計により、アクティブ数が目標を超えると`x`が増え、それに伴い料金は指数関数的に上昇します。
    `x`が`K`増えるたびに単価はおよそe倍（約2.7倍）になります。

#### 実装例（近似と期間計算）

指数計算は近似式を使って計算され、一定時間 $Δt$ にかかる総費用は以下のように表せます。

```python
# 指数関数の近似計算
def fake_exponential(factor: int, numerator: int, denominator: int) -> int:
    i = 1
    output = 0
    numerator_accum = factor * denominator
    while numerator_accum > 0:
        output += numerator_accum
        numerator_accum = (numerator_accum * numerator) // (denominator * i)
        i += 1
    return output // denominator

# Δt秒の総費用を計算
def cost_over_time(V:int, T:int, x:int, Δt:int) -> int:
    cost = 0
    for _ in range(Δt):
        x = max(x + V - T, 0)
        cost += fake_exponential(M, x, K)
    return cost
```

このように、バリデータのアクティブ維持に必要な負荷を残高と時間に基づいて料金化することで、P-Chainは公平かつ効率的に資源使用を調整できます。

### ブロック処理

ブロックを処理する時には、まず残高が不足しているバリデータをinactiveにし、その後にブロック内のトランザクションを適用します。
処理が完了した段階で、次の1秒間をカバーできる残高を持たないバリデータもinactiveに移されます。

タイムスタンプの扱いは厳格です。
もしチェーンの時刻を進めた結果、残高がマイナスになるバリデータが出てしまう場合、そのブロックは無効とされます。
ブロックを組み立てる際には、次のルールが適用されます。

* 現在の実時間を用いても、すべてのバリデータが残高不足にならない場合は、その実時間をタイムスタンプとして使います。
* 実時間をそのまま使うと残高が尽きるバリデータが出る場合は、最初に残高が尽きる正確な時刻をタイムスタンプに設定します。

このようにすることで、ブロックの間でアクティブなバリデータの数が急に変動しないように設計されています。

### パラメータ（起動時の既定値）

| パラメータ | 意味              | 値                |
| ----- | --------------- | ---------------- |
| T     | 目標アクティブ数        | 10,000           |
| C     | 容量（上限目安）        | 20,000           |
| M     | 最小単価（1バリデータ・1秒） | 512 nAVAX/s      |
| K     | 変化率調整の定数        | 1\_246\_488\_515 |

- `M=512 nAVAX/s`は、アクティブなバリデータ1体あたり月約1.33 AVAXに相当します（全体のアクティブ数がT以下の場合）。
- `K`は最悪（Vが長期間Cに近い）でも単価の倍化が約24時間に留まるように選定されています。
- VがT+1程度の軽微な超過なら、倍化まで約27年かかる設計です。
- 将来のACPでT・M・Kは変更可能です。

### バリデータのUX

L1バリデータは稼働している間、継続手数料によって残高が徐々に減っていきます。
そのため、ノードクライアントが残高を確認できるAPIを提供することが推奨されます。
運用者は残高の減少に応じて`IncreaseL1ValidatorBalanceTx`を使い補充でき、逆に多く入れすぎた分は`DisableL1ValidatorTx`で払い戻すことが可能です。

実際には、多くのバリデータ運用者はノードを直接操作せず、ウォレットやL1-as-a-Serviceといった仕組みを利用します。
これらのサービスはチャージや残高監視を代替し、必要であれば残高を自動的に補充する運用体験を提供できます。

### 参考図

```mermaid
flowchart TB
  subgraph Old[旧来フロー]
    A["Primary Network\n(X/P/C をフル同期しコンセンサス参加)"] -->|必須前提| B["Subnet バリデータ"]
    A -->|P-Chain が管理| C["バリデータセット"]
    C -->|ANT ロック &\nTransformSubnetTx| D["報酬: P-Chain で配布"]
    B -->|初期要件| E["2000 AVAX ステーク"]
  end
```

```mermaid
flowchart TB

  subgraph New[提案後フロー]
    F["Subnet/L1 バリデータ"] -. 分離 .-> G["Primary Network\nPartial Sync（必要最小限）"]
    F -->|ERC-20 / ERC-721 / 任意資産| H["ステーキング & 報酬管理\n（L1 側で実装・運用）"]
    G -->|継続手数料| I["P-Chain への支払い\n（Continuous Fee）"]
  end
```

## 互換性

**ACP77**は、SubnetとL1に関する仕組みを大幅に見直すものです。
そのため、既存のSubnetが稼働している環境に影響を与えないよう、導入は「前方互換的（going-forward）」に行うことが求められます。
つまり、すでに稼働しているSubnetやバリデータには停止や中断を発生させず、新しいルールを順次適用していく形で進められます。

すでにSubnetsで稼働しているバリデータは、これまで通り**PrimaryNetwork**と自分が担当するSubnetの両方を検証し続けることができます。
新しい仕組みが導入されたからといって、現行のSubnetの運用が直ちに変わるわけではありません。

ただし、**ACP77**には「**実行ルールの変更（state execution changes）**」が含まれるため、強制的なアップグレードが必須となります。
アップグレードの前後では次のような注意が必要です。

* アップグレードが有効になるまでは、従来のルールセットを検証し続ける必要があります。
* アップグレードが有効になった時点で、新しいルールセットに基づいて検証を行わなければなりません。
* 特に重要なのは、アップグレード前は「**2000AVAXのステークを持っているかどうか**」の条件を引き続き検証し続ける点です。
    アップグレード後に初めて、その条件が撤廃されます。

## 無効化されるトランザクション

**ACP77**が有効化されると、P-Chainで使われていた一部のトランザクションが廃止されます。
代表的なのは`TransformSubnetTx`です。

ElasticSubnetsはBanffで導入された仕組みで、Subnet独自トークンを使ったProof-of-Stakeと稼働時間に基づく報酬分配を可能にするものでした。
しかし、今回の提案が有効化されると、ElasticSubnetsは無効化され、`TransformSubnetTx`も受け付けられなくなります。

ここで重要なのは、現在Mainnet上にはElasticSubnetsが存在しないという点です。
そのため、この無効化による実運用上の影響はありません。

## 新しく導入されるトランザクション

**ACP77**によってP-Chainに5種類の新しいトランザクションが追加されます。
これらは、SubnetをL1へ転換した後に必要となるバリデータ管理や手数料処理を支えるためのものです。

| トランザクション名 | 概要 |
| ---------- | --------------------- |
| `ConvertSubnetToL1Tx`          | 既存のSubnetをL1へ変換するための取引。変換時にvalidator manager（集合管理者）を設定する。 |
| `RegisterL1ValidatorTx`        | 新しいバリデータをL1に追加登録するための取引。                                  |
| `SetL1ValidatorWeightTx`       | 既存バリデータのweight（ステーク比重）の変更や削除に使う取引。                        |
| `DisableL1ValidatorTx`         | バリデータを集合から完全に外さず、一時的にinactive状態にする取引。                     |
| `IncreaseL1ValidatorBalanceTx` | 既存バリデータに対して残高を追加する取引。これによりinactiveからの復帰も可能。               |

## 参考実装

**ACP77**の実装はすでに進められており、AvalancheGoの`Etna`アップグレードフラグの裏側で統合される予定です。
実際のコードや進捗は、GitHub上で`acp77`ラベルの付いたイシューから確認できます。

ただし、Etnaはまだアクティベートされていないため、**ACP77**で新しく導入されるトランザクションは現時点のAvalancheGoではすべて拒否されます。
これは、既存の運用環境に影響を与えないための措置です。

また、ACPプロセスの中で提案に修正が加えられた場合、その内容は実装にも反映される必要があります。
つまり、正式にEtnaが有効化される前に実装の更新を行い、最終仕様と実装が完全に一致していなければならないということです。

## セキュリティ

**ACP77**で導入されるAvalanche Layer1（L1）は、従来のSubnetに比べて大幅に低コストでネットワークを立ち上げられる仕組みです。
そのため、多数のL1が作成され、結果として大量のバリデータが増える可能性があります。
バリデータが増えると、それぞれがP-Chainに対して一定のRAM使用を発生させますが、この問題については、すでに設計された「**継続手数料メカニズム（Continuous Fee Mechanism）**」によって適切に抑制される想定です。

一方で、L1はP-Chainから独立した主権を持つため、L1のステーキング資産はP-Chainにロックされません。
この点が新たなリスクを生みます。
悪意あるL1チェーンが存在した場合、そのチェーンはバリデータを自由に削除でき、さらにバリデータがL1にロックしていた資産を奪うことも可能です。
P-Chainが保証できるのは、`DisableL1ValidatorTx`を通じてL1バリデータが自分の残りの\$AVAX残高を回収できる点のみです。
L1上で管理される資産については完全にL1の裁量に委ねられるため、L1バリデータ自身が参加するL1のセキュリティ設計を十分に吟味する必要があります。

さらに、`RegisterL1ValidatorTx`で使われるWarpメッセージには24時間という長めの有効期限が設けられています。
この仕様によって、悪意のあるユーザーが大量に登録申請を送り続けると、P-Chainに過剰なメモリ負荷がかかる可能性があります。
これが現実に問題となった場合、将来のACPによって有効期限を短縮することが検討されます。

もう1つの重要な点は、NodeIDがL1のバリデータ集合に意図せず追加される可能性があることです。
ただし、この場合でもステークや報酬が失われるリスクはありません。
NodeIDはステークを伴わずに生成可能であるため、勝手に追加された運営者は新しい鍵を作り直してNodeIDをローテーションすれば対処できます。
この設計は、オンボーディングを容易にする代わりに、NodeIDが不本意に追加される可能性を許容するものです。
これは、PrimaryNetworkのバリデータに対して「**ステークや報酬が危険にさらされない**」という保証と同じ思想に基づいています。

最後に、継続手数料メカニズムは「**アクティブなバリデータ**」にしか適用されません。
inactive状態のバリデータはメモリには保持されませんが、ディスク上には記録が残ります。
そのため、inactiveバリデータが増え続けるとP-Chainのステートが肥大化し続ける可能性があります。
この点についても、将来のACPでステートの成長を抑制する仕組みやステートを期限付きで削除する仕組みが追加される可能性が指摘されています。

## 最後に

今回は「AvalancheのSubnetを拡張し、PrimaryNetworkの制約に縛られず独立したLayer1として運用できる仕組みを提案している**ACP77**」についてまとめてきました！
いかがだったでしょうか？

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

[Twitter @cardene777](https://twitter.com/cardene777)

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

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
