# [EIP8016] SSZ CompatibleUnionで共通フィールドのMerkle証明を安定させる仕組みを理解しよう！

> EIP8016は、SSZにCompatibleUnion({selector: type})という型を追加します。この型は、selectorで選ばれた1つの型オプションを中に持ちます。

- URL: https://cardene.dev/articles/698ccc9793ae9f7d4437
- 公開日: 2026-06-17
- 更新日: 2026-06-17
- トピック: Blockchain
- タグ: eip, ブロックチェーン, ssz, merkleproof

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください！

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://twitter.com/cardene777

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

今回は「**EIP8016**」についてまとめていきます。

EIP8016は、SSZに`CompatibleUnion`という複合型を追加し、複数の型オプションを持つデータでも共通フィールドのMerkle証明を安定して扱えるようにする提案です。
トランザクションのように複数の形式が併存するデータで、同じ意味のフィールドを同じ位置番号で検証し続けるための仕組みです。

https://eips.ethereum.org/EIPS/eip-8016

## 概要

EIP8016は、SSZに`CompatibleUnion({selector: type})`という型を追加します。
この型は、`selector`で選ばれた1つの型オプションを中に持ちます。
ただし、通常のUnionと違い、選べる型オプションはMerkleizationの互換性を満たすものに限定されます。

SSZは、Ethereumのコンセンサスレイヤーで使われるシリアライズ形式です。
SSZではデータをMerkleツリーへ変換し、特定フィールドのMerkle証明を作れます。
この時、フィールドの位置はgeneralized index（以下、gindex）で表されます。

`CompatibleUnion`の目的は、型オプションが変わっても、共通フィールドが同じgindexに割り当てられるようにすることです。
これにより、検証者は「型1なら`amount`はこの位置、型2なら別の位置」といった個別対応表を持たずに、共通フィールドを検証できます。

`CompatibleUnion`の全体像は以下です。

![EIP8016の全体像](/images/qiita-37e67ca4-134a-4069-8d9f-cfc8a650516d.webp)

左側の型1と型2は、`amount`と`recipient`を共通フィールドとして持ちます。
中央の`CompatibleUnion`は`selector`で使う型を選びます。
右側では、共通フィールドが同じ位置番号に置かれるため、検証者は同じMerkle証明の考え方で値を確認できます。

## 動機

トランザクションのようなデータ型は、時間とともに複数の形式を持つようになります。
新しい機能を追加した形式、古い形式、特定の用途に合わせた形式が同時に存在するためです。

この時に問題になるのが、同じ意味のフィールドが型ごとに別のgindexへ割り当てられることです。
例えば、複数のトランザクション型がどれも`amount`や`recipient`を持っているのに、Merkleツリーの中で別々の位置に置かれると、検証者は型ごとの対応表を持つ必要があります。
型が増えるほど、対応表の更新漏れや検証ロジックの複雑化が起きます。

`CompatibleUnion`は、この負担を減らします。
型オプションを互換なMerkleizationを持つものだけに制限することで、共通フィールドのgindexを安定させます。
その結果、複数の型が併存しても、検証者は共通フィールドを同じ位置として扱えます。

プログラミング言語では、似た目的でタグ付きUnionが使われます。
EIP8016はSSZの世界で、タグにあたる`selector`と、Merkle証明で使う安定した位置関係を同時に扱えるようにします。

## 仕様

EIP8016の仕様は、`CompatibleUnion`型の定義、互換なMerkleization、シリアライズ、デシリアライズ、JSON形式、Merkleizationの拡張に分かれます。

### `CompatibleUnion({selector: type})`

`CompatibleUnion({selector: type})`は、指定された型オプションのうち1つを含むSSZ複合型です。
表記は以下のようになります。

```python
CompatibleUnion({1: Square, 2: Circle})
```

この例では、`selector`が`1`なら`Square`として、`2`なら`Circle`として中身を解釈します。
`CompatibleUnion`は、すべての型オプションが固定長であっても、常に可変長型として扱われます。

デフォルト値は定義されません。
つまり、`CompatibleUnion`の初期値を自動的に作ることはエラーです。

不正な型定義は以下です。

| 型定義 | 扱い |
|------|------|
| `CompatibleUnion({})` | 型オプションがないため不正です。 |
| `CompatibleUnion({selector: type})`で`selector`が`1`から`127`の範囲外 | 使えるselector範囲を外れるため不正です。 |
| 型オプション同士のMerkleizationに互換性がない | 共通フィールドの位置を安定させられないため不正です。 |

`selector`の範囲は`uint8(1)`から`uint8(127)`までです。
`0`は未初期化との衝突を避けるために予約されます。
`128`以上は最上位ビットが立つ領域なので、将来の互換拡張のために予約されます。

### 互換なMerkleization

型オプションとして使えるかどうかは、Merkleizationの互換性で決まります。
互換であるとは、Merkleツリーの構造上、同じ意味のフィールドを同じ位置で扱えることです。

互換性のルールは以下です。

| 型 | 互換条件 |
|------|------|
| 同じ型 | 自分自身と互換です。 |
| `byte`と`uint8` | 相互に互換です。 |
| `Bitlist[N]` | 容量`N`が同じなら互換です。 |
| `Bitvector[N]` | 容量`N`が同じなら互換です。 |
| `List[type, N]` | 要素型が互換で、容量`N`が同じなら互換です。 |
| `Vector[type, N]` | 要素型が互換で、長さ`N`が同じなら互換です。 |
| `ProgressiveList[type]` | 要素型が互換なら互換です。 |
| `Container` | フィールド名が同じ順序で並び、各フィールド型が互換なら互換です。 |
| `ProgressiveContainer(active_fields)` | 両方の`active_fields`で有効なフィールドが同じ名前と互換型に対応し、ほかに共有フィールド名がなければ互換です。 |
| `CompatibleUnion`同士 | 両方の型オプションを横断して、すべて互換なら互換です。 |
| そのほか | 互換ではありません。 |

`ProgressiveList`は、EIP7916で提案されている、容量上限を固定せずに将来拡張しやすいリスト型です。
`ProgressiveContainer`は、EIP7495で提案されている、有効フィールド集合を管理してコンテナを拡張しやすくする型です。

EIP8016は、この2つの拡張型も互換性判定の中に入れています。
そのため、将来フィールドやリストを伸ばす設計と、共通フィールドのMerkle証明を安定させる設計を組み合わせられます。

### シリアライズ

`CompatibleUnion`の値は、`value.selector`と`value.data`を持ちます。
`value.selector`は選ばれた型オプションを表し、`value.data`は実際の中身です。

シリアライズは、先頭1バイトに`selector`をリトルエンディアンで置き、その後ろに`data`のSSZシリアライズ結果を連結します。

```python
return value.selector.to_bytes(1, "little") + serialize(value.data)
```

`selector`は1バイトなので、バイト列の先頭を読むだけで、後続データをどの型として解釈するかが分かります。
これは、ネストした`CompatibleUnion`を扱う時にも重要です。
パーサーは早い段階で後続データの型を決められるため、全体の木構造を後から推測する必要がありません。

### デシリアライズ

デシリアライズでは、まず入力範囲の先頭1バイトを`selector`として読みます。
その後、残りのバイト列を、`selector`で選ばれた型オプションとしてデシリアライズします。

処理の流れは以下です。

![selectorを使った処理の流れ](/images/qiita-b23710e0-8947-4e44-9483-129ca3bf3528.webp)

先頭1バイトが型選択に使われ、残りのデータが選ばれた型として解釈されます。
Merkleルートを作る時は、`data`の`hash_tree_root`に`selector`を混ぜるため、同じ木構造を持つ別の型オプションでもrootを区別できます。

実装は以下の不正入力に対して堅牢である必要があります。

| 不正入力 | 扱い |
|------|------|
| 範囲外の`selector` | 型オプションを選べないため失敗します。 |
| 1バイト未満の入力 | `selector`を読めないため失敗します。 |
| 壊れたシリアライズデータ | 正しいSSZ値として読めないため失敗します。 |
| 内側の型として不正なデータ | 選ばれた型のデシリアライズ規則に従って失敗します。 |

### JSON形式

`CompatibleUnion`のJSON形式は、`selector`と`data`を持つオブジェクトです。
`data`の中身は、`selector`で選ばれた型に合わせて変わります。

```json
{
  "selector": "1",
  "data": {
    "width": "10",
    "height": "20"
  }
}
```

`selector`をJSONにも明示することで、JSONからSSZ値へ戻す時にも、どの型として`data`を読むべきかを決められます。

### Merkleization

EIP8016は、SSZのMerkleizationに`mix_in_selector`という補助関数を追加します。
`mix_in_selector`は、Merkleルートと`selector`を受け取り、それらをハッシュして`CompatibleUnion`全体のrootを作ります。
ここで渡す`selector`は、`uint8`としてシリアライズされた値です。

```python
def mix_in_selector(root, selector):
    return hash(root, selector)
```

`CompatibleUnion`の`hash_tree_root`は以下です。

```python
mix_in_selector(hash_tree_root(value.data), value.selector)
```

この処理により、`data`部分のMerkleツリーが同じ形でも、`selector`が違えば最終rootは変わります。
同じ木構造を持つ別の型オプションや、空のリストのように要素型だけではrootが区別しにくい値でも、`selector`によって型の違いをrootへ反映できます。

## 補足

#### `selector`を`1`から`127`に制限する理由

`selector`の`0`は、未初期化の値との衝突を避けるために使いません。
また、将来のEIPで`Optional`を表す値として使う余地も残されています。

`128`以上は、最上位ビットが立つ領域です。
この領域を空けておくことで、将来の後方互換な拡張に使えます。
現在想定されている用途では、`1`から`127`までの127個の型オプションで足ります。

#### フィールド集合ではなくタグを使う理由

代替案として、`active_fields`のビットベクトルをシリアライズに含める設計も検討されました。
この方式では、どのフィールドが有効かをビット列で表せます。

しかし、`ProgressiveContainer`がネストしている時に、パーサーが全体の木構造をすぐに判断できません。
さらに、ネストの各層が可変長になり、シリアライズ形式のオーバーヘッドも増えます。

`CompatibleUnion`では、先頭の`selector`が型を先に示します。
そのため、パーサーは早い段階で後続データの型を決められます。
ネストしたデータでも、各層でどの型を読むべきかが明確になります。

#### 早めに導入する必要

既存フィールドを後から`CompatibleUnion`で包む変更は、後方互換な変更ではありません。
そのため、将来の拡張が見込まれるフィールドでは、型オプションが1つだけの段階でも早めに`CompatibleUnion`を導入する必要があります。

一方で、一度`CompatibleUnion`を導入しておけば、新しい型オプションの追加や古い型オプションの削除は、検証者を壊さずに進められます。
検証者が共通フィールドのgindexを見続けられるためです。

## 互換性

`CompatibleUnion({selector: type})`は、SSZの以前のUnion提案に代わる仕組みです。
以前のUnion案は、非推奨になった仕様で使われていました。

`Portal Network`にもUnionの考え方があります。
ただし、`Portal Network`のネットワーク型では`hash_tree_root`を使っていないため、新しいネットワークバージョンを導入すれば`CompatibleUnion`へ移行できます。

重要なのは、`CompatibleUnion`で包む変更自体は後方互換ではない点です。
将来の型拡張を見込む場所では、あとから包むのではなく、早い段階で`CompatibleUnion`を使う設計が求められます。

## テストケース

EIP8016は、既存のSSZ実装とテスト資産を使って検証されます。

`ethereum/remerkleable`には、`test_impl.py`と`test_typing.py`の静的テストがあります。
また、`ethereum/consensus-specs`のリリースには、`tests/general/phase0/ssz_generic`以下のランダムテストが含まれます。
これらのランダムテストは、`tests/format/ssz_generic`で定義された形式に従って生成されます。

## 参考実装

参考実装は`ethereum/remerkleable`にあります。
`CompatibleUnion`はSSZ型システム、シリアライズ、デシリアライズ、Merkleizationのすべてに関わるため、単体のアプリケーションコードだけで扱うより、SSZライブラリ側で対応するのが自然です。

実装で特に確認するべき点は、`selector`の範囲チェック、型オプションの互換性判定、内側の型へのデシリアライズ委譲、`mix_in_selector`によるroot計算です。

## セキュリティ

`CompatibleUnion`では、`selector`をMerkleルートへ混ぜることが重要です。
これにより、複数の型オプションが同じMerkleツリー構造を持つ時でも、最終的な`hash_tree_root`を区別できます。

`selector`を混ぜない場合、同じ木構造を持つ別の型オプションが同じrootになることがあります。
また、`List[type, N]`や`ProgressiveList[type]`で要素型だけが違う空リストは、`hash_tree_root`だけでは型の違いを表しにくくなります。
その場合、アプリケーション側が署名rootに型情報を混ぜる、別フィールドで型を表す、といった追加対応を持つ必要があります。

EIP8016は、この責務をSSZのMerkleizationへ組み込みます。
`mix_in_selector(hash_tree_root(value.data), value.selector)`を使うことで、型の選択とデータ本体のrootを1つのrootに結びつけます。

デシリアライズ側では、範囲外の`selector`、1バイト未満の入力、壊れたデータ、内側の型として不正な値を確実に拒否する必要があります。
ここを曖昧にすると、実装ごとに同じバイト列の解釈がずれ、検証結果が一致しなくなります。

## 最後に

今回は「**EIP8016**」についてまとめてきました。

`CompatibleUnion`は、複数のSSZ型オプションを扱いながら、共通フィールドのMerkle証明を安定させるための提案です。
先頭1バイトの`selector`で型を選び、`data`のMerkleルートに`selector`を混ぜることで、型の違いと共通フィールドの検証しやすさを両立します。
将来拡張が見込まれるSSZデータでは、早い段階でUnion型を設計に入れておくことが重要になります。

他でも色々記事を書いているのでぜひよろしければ読んでいってください！

https://amzn.asia/d/gxvJ0Pw

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://twitter.com/cardene777

https://cardene.substack.com/
