はじめに

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

今回は、Avalancheネットワークのノード識別や通信に使われる仕組みを、従来のTLS証明書依存からより軽量で安全なEd25519鍵に移行する仕組みを提案しているACP20についてまとめていきます!

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

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

概要

ACP20は、Avalancheネットワークのp2p通信におけるTLS証明書でEd25519をサポートし、Avalanche Network Clients (ANCs) のNode IDにEd25519公開鍵をそのまま利用できるようにし、さらにProposer VMでEd25519署名を扱えるようにするものです。 Ed25519はIETF勧告(RFC8032)で標準化された署名方式で、公開鍵・秘密鍵・署名のサイズが小さく、効率的である点が特長です。

現在のAvalanche GoはRSAまたはECDSAしかサポートしておらず、TLS証明書が存在しない場合は4096 bitのRSA秘密鍵と証明書を生成してディスクに保存します。 その後、この証明書をハッシュして20バイトのNode IDを作ります。 Snowman++導入以降は、ブロックヘッダーに署名とともにTLS証明書自体を含める仕様となり、証明書への依存が強まっています。

Ed25519を導入すれば、Node IDを公開鍵(32バイト)として直接利用できるため、証明書のハッシュ依存をなくし、秘密鍵のみを管理すればよくなります。 また、Proposer VMでもEd25519署名をサポートすることで、署名の取り扱いを軽量かつ一貫性のあるものにできます。

つまり、ACP20は「Avalancheのノード同士の通信や署名の仕組みをEd25519に対応させ、証明書管理を簡単にしてデータを軽量化すること」で、ネットワークの運用をシンプルかつ効率的にする狙いがあります。

動機

現在の課題

  • RSA/ECDSAへの依存 Avalanche GoはRSAまたはECDSA以外の署名アルゴリズムを禁止しているため、TLS証明書が存在しない場合起動時に4096 bitのRSA秘密鍵を生成し、証明書を作成してディスクに保存する必要があります。

  • Node IDの生成方式 Node IDはTLS証明書のハッシュ(20バイト)によって作られており、証明書への依存が避けられません。

  • Snowman++の仕様による負担 Snowman++では、ブロックヘッダーに署名だけでなくTLS証明書全体を含める必要があり、データサイズと依存関係が増えています。

Ed25519採用による改善

Ed25519のサイズ特性は以下のとおりです。

項目 サイズ
公開鍵 32バイト
秘密鍵 64バイト
署名 64バイト

Node IDとして公開鍵を直接利用すれば、従来の20バイトから32バイトに増えるだけで、差は12バイトにとどまります。 サイズ増加はごくわずかであり、証明書をハッシュする方式よりもシンプルです。

さらに、Ed25519秘密鍵を持っていれば、起動時にメモリ上でTLS証明書を生成してp2p通信に利用できます。 これにより運用者はRSA秘密鍵と証明書を両方管理・バックアップする必要がなくなり、Ed25519秘密鍵のみを扱えばよくなります。

Proposer VMでEd25519署名をサポートすることで、ブロック提案や検証時に軽量で一貫性のある署名方式を利用でき、ネットワーク全体の効率性と整合性が高まります。

現行方式と提案方式の比較

観点 現行 (RSA/ECDSA) 提案 (Ed25519)
Node IDの生成 TLS証明書のハッシュ(20B) 公開鍵そのもの(32B)
バックアップ対象 RSA秘密鍵+TLS証明書 Ed25519秘密鍵のみ
ブロックヘッダー 証明書+署名を含める 軽量署名方式に対応
運用負担 秘密鍵と証明書を両方管理 秘密鍵のみ管理

フローの違い

現状の流れ
sequenceDiagram
  autonumber
  participant Node as ノード
  participant Disk as ディスク
  participant Net as ネットワーク

  Node->>Disk: TLS証明書の有無を確認
  alt 証明書なし
    Node->>Disk: 4096 bitのRSA秘密鍵を生成・保存
    Node->>Disk: RSAでTLS証明書を生成・保存
  end
  Node->>Node: 証明書をハッシュしてNode IDを生成(20B)
  Node-->>Net: TLSハンドシェイクで証明書を提示
  Note over Node: Snowman++ブロックヘッダーに証明書と署名を含める
提案後の流れ
sequenceDiagram
  autonumber
  participant Node as ノード
  participant Mem as メモリ
  participant Net as ネットワーク

  Node->>Node: Ed25519秘密鍵を用意
  Node->>Mem: 起動時にTLS証明書をメモリ上で生成
  Node->>Node: Node ID = 公開鍵そのもの(32B)
  Node-->>Net: TLSハンドシェイクで証明書を提示
  Note over Node: Proposer VMはEd25519署名をサポート

仕様

必要な変更点

Node IDの拡張

AvalancheのP-Chainでは、これまでNode IDは常に20バイトと決め打ちされていました。 Ed25519公開鍵をNode IDとして利用するには32バイトが必要です。 そのため、P-Chainが32バイトのNode IDを登録・扱えるように仕様を変更する必要があります。 ただし、過去のトランザクションは20バイト前提で作られているため、実装者は古い形式も正しく解釈できるように互換性を保つ必要があります。

デフォルト鍵の変更

ノード起動時に生成される鍵は、これまでRSA鍵でしたが、今後はEd25519鍵 (staker.key) をデフォルトで生成するように変更します。 これにより、運用者はEd25519秘密鍵だけを扱えばよくなり、証明書やRSA鍵を別途管理する必要がなくなります。

TLS証明書の生成

起動時に生成したEd25519秘密鍵を使って、その場でTLS証明書を作成します。 証明書はディスクに保存しておくのではなく、メモリ上で作成され、そのままp2p通信のハンドシェイクに利用されます。

Proposer VMでのEd25519サポート

Proposer VMはブロックを提案する時に署名を行います。 ここにEd25519の署名アルゴリズムを追加でサポートすることで、Ed25519鍵を使うノードが正しくブロック提案や署名検証を行えるようになります。

ブロックへの証明書埋め込みの削除

従来は、Proposer VMのブロックにTLS証明書そのものを含める仕様でした。 これは証明書に依存していたため、ブロックサイズの増加や管理の複雑さを招いていました。 Ed25519 Node IDを持つノードがブロックを提案する場合には、証明書を埋め込む必要がなくなります。

PeerListメッセージでのEd25519対応

ノード間で交換するPeerListメッセージにもEd25519対応を追加します。 これにより、ネットワーク層でEd25519 Node IDを持つピアを正しく認識・共有できるようになります。

p2pレイヤーへの影響

変更は最小限に抑えられます。 Avalancheネットワークのp2p通信はTLSハンドシェイクを使っているため、そこにEd25519を新しいサポートアルゴリズムとして追加するだけです。 基本的な通信の流れやハンドシェイクの仕組みを大きく変える必要はありません。

トランザクション仕様の拡張

Ed25519 Node IDを前提とした新しいトランザクション形式を追加する方法も検討されています。 その場合、このACPでは詳細を定義せず、別のフォローアップACPでそのトランザクション形式の具体的な仕様を示す必要があります。 これにより、古い形式と新しい形式を区別して運用できるようになります。

将来的な課題

このACPの段階ではEd25519を追加サポートする形ですが、将来的にはEd25519以外のTLS証明書を完全に廃止することが推奨されています。 そうすることで、RSAやECDSAといった他の方式に依存する必要がなくなり、Avalancheネットワーク全体のセキュリティが向上し、実装もシンプルになります。

ただし、このACP自体はその移行手順までは示していません。 Ed25519以外を廃止するまでの具体的な道筋は、今後の別途提案に委ねられます。

イメージ図

以下は、現行の流れとEd25519導入後の流れを対比したイメージです。

現行方式

sequenceDiagram
  autonumber
  participant Node as ノード
  participant Disk as ディスク
  participant Net as ネットワーク

  Node->>Disk: RSA秘密鍵を生成・保存
  Node->>Disk: RSAでTLS証明書を生成・保存
  Node->>Node: 証明書のハッシュ = Node ID(20B)
  Node-->>Net: TLSハンドシェイクで証明書を提示
  Note over Node: ブロックヘッダーに証明書と署名を含める

提案方式

sequenceDiagram
  autonumber
  participant Node as ノード
  participant Mem as メモリ
  participant Net as ネットワーク

  Node->>Node: Ed25519秘密鍵を生成(staker.key)
  Node->>Mem: 起動時にTLS証明書をメモリ上で生成
  Node->>Node: 公開鍵(32B) = Node ID
  Node-->>Net: TLSハンドシェイクで証明書を提示
  Note over Node: ブロックヘッダーでは証明書を省略可能

互換性

ACP20を実装しても、既存ノードや履歴データとの非互換は生じない設計です。 従来どおり20バイトのNode IDは「TLS証明書のハッシュ」として解釈し続けます。 一方で、ACP20を実装した環境では32バイトのNode ID(Ed25519公開鍵の長さ)も受け付けます。

注意点はP-Chainのシリアライズ(データの書き出し)です。 従来はNode IDの長さを一緒に書かず、常に20バイト前提で読み書きしていました。 そのため、新しい形式(32バイト)を導入しても、過去のトランザクションは引き続き20バイトとして正しく読み取れるようにする必要があります。 実装の現実的な方針は、以下のいずれか(または組み合わせ)です。

  • ネットワークのアップグレードフラグやトランザクション種別で「20バイトを読む区間」と「32バイトを読む区間」を明確に分ける。
  • コンテキスト(対象フィールドやバージョン)によって固定長読み出しを切り替える。曖昧な自動判別は避け、誤読を防止します。

簡潔な擬似コードの例を示します。

go
// 例: コンテキストに応じてNodeIDの長さを固定で読む
func readNodeID(r io.Reader, ctx Context) (NodeID, error) {
    switch {
    case ctx.BeforeEd25519Activation: // 互換区間
        var id20 [20]byte
        if _, err := io.ReadFull(r, id20[:]); err != nil { return NodeID{}, err }
        return NodeID{Type: LegacyHash20, Bytes: id20[:]}, nil
    case ctx.RequiresEd25519: // 新形式のみ
        var id32 [32]byte
        if _, err := io.ReadFull(r, id32[:]); err != nil { return NodeID{}, err }
        return NodeID{Type: Ed25519PubKey32, Bytes: id32[:]}, nil
    default: // 明示的に決まるまで拒否 or フォールバック
        return NodeID{}, errors.New("ambiguous NodeID length")
    }
}

この方針なら、既存の20バイトNode IDを壊さず、32バイトNode IDを段階的に有効化できます。

参考実装

TLS証明書をEd25519秘密鍵から生成するのは一般的な手順です。 Goの標準ライブラリにあるサンプル(crypto/tls/generate_cert.go)が参考になります。 Avalanche Goにも、TLS証明書から公開鍵を取り出して検証するコードがすでに存在します。

以下は、GoでEd25519鍵を生成し、その鍵で自己署名のTLS証明書を作ってtls.Configに載せる最小例です。

go
package main

import (
	"crypto/ed25519"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"math/big"
	"time"
)

func ed25519SelfSignedCert() (tls.Certificate, ed25519.PublicKey, error) {
	// 1. Ed25519鍵ペアを生成
	pub, priv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil { return tls.Certificate{}, nil, err }

	// 2. x509テンプレート(自己署名用)
	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	serial, _ := rand.Int(rand.Reader, serialNumberLimit)
	tpl := &x509.Certificate{
		SerialNumber: serial,
		Subject:      pkix.Name{CommonName: "avalanche-node"},
		NotBefore:    time.Now().Add(-5 * time.Minute),
		NotAfter:     time.Now().Add(365 * 24 * time.Hour),
		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
		BasicConstraintsValid: true,
	}

	// 3. 自己署名で証明書DERを作成
	der, err := x509.CreateCertificate(rand.Reader, tpl, tpl, pub, priv)
	if err != nil { return tls.Certificate{}, nil, err }

	// 4. tls.Certificateに詰める(Ed25519はそのまま使える)
	cert := tls.Certificate{
		Certificate: [][]byte{der},
		PrivateKey:  priv,
	}

	return cert, pub, nil
}

func main() {
	cert, pub, err := ed25519SelfSignedCert()
	if err != nil { panic(err) }

	// Node IDにEd25519公開鍵(32バイト)を使う想定
	_ = pub

	// p2p用のTLS設定例
	_ = &tls.Config{
		Certificates: []tls.Certificate{cert},
		MinVersion:   tls.VersionTLS12,
		// Ed25519対応はGoの標準TLS実装で自動ハンドリングされます
	}
}

既存のTLS証明書から公開鍵を取り出す処理(例:x509.ParseCertificateでcert.PublicKeyを得る)は標準的で、Avalanche Goも同等の検証ロジックを持っています。 Ed25519対応では、ここで得られる公開鍵がEd25519公開鍵(32バイト)であることを前提に扱います。

セキュリティ

検証基準

Ed25519はRFC8032で標準化されていますが、「どの入力を有効とみなすか」という検証の細かい基準が統一されていません。 実際には以下のような差異が実装ごとに存在します。

  • 公開鍵や署名のエンコードが正規形でない場合(non-canonical encoding)を拒否するかどうかの扱いが異なること。
  • 検証式(verification equation)について、RFC上では複数の定義が許容されており、実装によって異なる検証式を採用していること。 その結果、ある実装では署名を「有効」と判定する一方で、別の実装では「無効」と判定することがあり得ます。

ブロックチェーンのように「署名の検証結果が全ノードで一致しなければならない」環境ではこのような差異は致命的です。 そのため、ZcashはZIP215という補助仕様を導入し、Ed25519の検証基準を明示的に固定しました。 ACP20の実装者も必ずZIP215の検証基準を採用する必要があります。

Goでの実装においては、ed25519consensus ライブラリの利用が推奨されます。 これはGo標準のcrypto/ed25519パッケージを最小限フォークし、ZIP215に準拠した検証を提供するものです。

検証コードの最小例は次のとおりです。

go
import (
	"github.com/hdevalence/ed25519consensus"
)

// pub: 32バイトのEd25519公開鍵
// msg: 署名対象メッセージ
// sig: 64バイトのEd25519署名
func verifySIG(pub, msg, sig []byte) bool {
	return ed25519consensus.Verify(pub, msg, sig) // ZIP-215準拠
}

このように検証基準を固定することで、「ある実装では署名が有効と判定されるのに、別の実装では無効と判定される」といった事態を避けられます。 つまり、ネットワーク全体で検証結果が一致しなくなることでコンセンサス(合意形成)が崩れてしまうリスクを抑えられます。

設計上の問いと回答

Ed25519鍵は他の通信プロトコルでも使えますか?

使えます。 例えば、QUIC(次世代の汎用トランスポート)やNoise(安全なハンドシェイク・暗号セッションを定義するフレームワーク)など、TLS以外のプロトコルでもEd25519は広く利用可能です。 ACP20は「Node IDをTLS証明書のハッシュ」から「Ed25519公開鍵」へと置き換えます。 このため、TLSへの依存が薄まり、Avalancheのような高スループットチェーンに適した通信プロトコルの実験・移行がやりやすくなります。

Ed25519鍵でVRF(Verifiable Random Function)は使えますか?

使えます。 RFC 9381は楕円曲線ベースのVRF(ECVRF)を規定しており、Ed25519のテストベクタも含まれています。 暗号学的ランダムオラクルモデルで安全な楕円曲線を用いることで、検証可能な乱数を生成できます。 これにより、バリデータはブロックごとにEd25519鍵でVRF値を作る、といった設計(サブネットを含む)も実装上の選択肢になります。

最後に

今回は「Avalancheネットワークのノード識別や通信に使われる仕組みを、従来のTLS証明書依存からより軽量で安全なEd25519鍵に移行する仕組みを提案しているACP20」についてまとめてきました! いかがだったでしょうか?

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

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