概要
クラスで急ぎの連絡を回す場面を想像してみてください。
たとえば、明日の集合時間が変わったとします。
代表者が1人いて、その人が全員に連絡する形なら、とても分かりやすいです。誰に聞けばよいかも明確ですし、連絡内容も一か所で管理しやすくなります。
ただし、その代表者がスマホを見ていなかったり、通信環境が悪かったり、体調不良で連絡できなかったりしたらどうでしょうか。
連絡の入口が1か所に集中していると、そこが止まっただけで、後ろの人に情報が届かなくなるかもしれません。
一方で、近くの人同士が「この連絡、見た?」と伝え合う形なら、一部の人が気づかなくても、別の経路から情報が広がる可能性があります。
上の図のように、代表者から全員へ一斉に連絡する形は分かりやすいです。
ただし、代表者のところで連絡が止まると、全体に影響が出やすくなります。
今度は、近くの人同士で連絡を伝え合う形を考えてみます。
こちらは、誰か1人だけが全員を管理する形ではありません。
それぞれの参加者が、知っている相手へ情報を伝えます。一部の経路が止まっても、別の経路から情報が届く場合があります。
もちろん、この形にも注意点があります。
伝言ゲームのように内容が変わってしまったり、間違った情報が広がったりするかもしれません。さらに、誰が誰とつながっているのか、どうやって最初の相手を見つけるのかも考える必要があります。
ブロックチェーンで使われる P2Pネットワーク も、まずはこのような 中央の1か所だけに頼らず、参加者同士で情報を伝え合う通信の考え方 として見ると分かりやすいです。
Bitcoin Developer Guideでは、Bitcoinのネットワークプロトコルにより、フルノードがブロックや取引を交換するためのP2Pネットワークを共同で維持すると説明されています。
また、Ethereum公式ドキュメントでは、Ethereumは多数のノードが標準化されたプロトコルで通信するP2Pネットワークであり、gossipやリクエスト/レスポンスによって情報を交換すると説明されています。
参考: Bitcoin Developer Guide: P2P Network
参考: Ethereum Documentation: Networking layer
ただし、ここで大切なのは、P2Pネットワークを万能な仕組みとして扱わないことです。
P2Pネットワークは、ブロックチェーンにおいて取引やブロックを広げるための重要な土台です。
しかし、「届いた取引が本当に正しいか」「届いたブロックを受け入れてよいか」は、P2Pネットワークだけでは決まりません。
各ノードが取引やブロックを検証し、さらにブロックチェーンごとのコンセンサスルールに従って、どの履歴を正しいものとして扱うかを判断します。
ここは、先ほどの連絡網の例でも同じです。
「集合時間が変わった」という連絡が自分のところに届いたとしても、それが本当に先生や代表者からの連絡なのか、古い情報ではないのかを確認する必要があります。
つまり、情報が届くこと と 情報が正しいこと は別の問題です。
| 観点 | 身近な連絡の例 | ブロックチェーンでのイメージ |
|---|---|---|
| 情報を広げる | 近くの人に連絡を回す | 取引やブロックをノード間へ伝える |
| 相手を見つける | 誰に連絡すればよいか名簿で探す | 接続するピアを見つける |
| 内容を確認する | その連絡が本当に正しいか確認する | 取引やブロックを検証する |
| 正式な記録にする | 決定事項として共有メモに残す | ブロックに含め、合意形成のルールに従って台帳へ反映する |
NISTIR 8202では、ブロックチェーンを中央の保管場所や中央権限なしに分散的に実装される、改ざんを検知しやすく耐性を持つデジタル台帳として説明しています。
このような分散的な台帳を成り立たせるには、記録を複数の参加者へ共有する通信の土台が必要になります。
参考: NISTIR 8202: Blockchain Technology Overview
この記事では、P2Pネットワークをいきなり難しい仕様から説明するのではなく、まずは身近な情報共有の例から整理します。
そのうえで、ノード、ピア、ブロードキャスト、gossip、peer discoveryといった用語を、できるだけ具体例を挟みながら見ていきます。
そして後半では、BitcoinやEthereumではP2Pネットワークがどのように使われているのか、P2Pにはどのような強みと注意点があるのかを整理します。
💡 豆知識
P2PはPeer-to-Peerの略です。
peerは「同じ立場の相手」「仲間」のような意味を持つ言葉です。
ブロックチェーンの文脈では、自分のノードから見た接続先のノードをpeerと呼ぶことがあります。
ここで、中央サーバー型とP2P型のイメージをざっくり比較しておきます。
| 観点 | 中央サーバー型のイメージ | P2P型のイメージ |
|---|---|---|
| 情報の通り道 | 中央のサーバーを経由する | 参加者同士が直接または間接的にやり取りする |
| 管理のしやすさ | 中央で管理しやすい | 参加者が増えるほど複雑になりやすい |
| 障害への影響 | 中央が止まると影響が大きい場合がある | 一部が止まっても別経路で伝わる場合がある |
| 情報の正しさ | 中央が確認しやすい | 各ノードの検証ルールが重要になる |
| ブロックチェーンでの役割 | RPCサービスや取引所など周辺で使われることがある | 取引やブロックを広げる土台になる |
もちろん、現実のシステムはこの表ほど単純ではありません。
ブロックチェーン関連のサービスでも、ウォレット、取引所、ブロックエクスプローラー、RPCプロバイダなど、中央集権的な構成を含む部分は多くあります。
そのため、本記事ではP2Pを「中央が完全に存在しない理想世界」としてではなく、中央の1か所だけに依存しないように情報を共有するための設計思想として扱います。
この記事の立ち位置
本記事は、以前作成した 「身近な記録から理解するブロックチェーンの全体像」 の派生記事です。
派生元の記事では、ブロックチェーンを「複数の参加者が同じ記録を共有し、その記録があとからこっそり書き換えられていないかを確認しやすくする仕組み」として整理しました。
そこでは、ブロック、チェーン、台帳、ハッシュ、デジタル署名、P2Pネットワーク、コンセンサス、スマートコントラクトなどを広く扱いました。
本記事では、その中でも特に P2Pネットワーク に絞って深掘りします。
ブロックチェーンの説明では、「分散している」「中央管理者がいない」「ノード同士が通信している」といった表現がよく出てきます。
しかし、初学者にとっては、次のような疑問が残りやすいと思います。
- そもそもノード同士はどうやってつながるのか
- 取引やブロックはどのように広がるのか
- P2Pならサーバーは一切いらないのか
- 情報が広がることと、正しい履歴が決まることは同じなのか
- P2Pにはどのような攻撃や限界があるのか
この記事では、これらの疑問に答えるために、P2Pネットワークを「ブロックチェーンの情報配送網」として整理します。
ただし、P2Pネットワークだけでブロックチェーンの安全性がすべて決まるわけではありません。
たとえば、Bitcoinでは取引やブロックの検証、PoW、各ノードのルールが関係します。Ethereumでは、実行クライアントとコンセンサスクライアントが連携し、取引のgossipやブロックのgossip、検証、PoSの合意形成が関係します。
参考: Bitcoin: A Peer-to-Peer Electronic Cash System
参考: Ethereum Documentation: Nodes and clients
参考: Ethereum Documentation: Networking layer
派生元記事との関係を整理すると、次のようになります。
| 派生元記事 | 本記事 |
|---|---|
| ブロックチェーン全体の地図を整理する | P2Pネットワークに絞って整理する |
| ブロック、台帳、ハッシュ、署名、P2P、コンセンサスを広く扱う | 取引やブロックがノード間に広がる仕組みを深掘りする |
| 情報セキュリティ上の強みと限界を広く整理する | P2P特有の強み、限界、攻撃面を整理する |
| 詳細記事へつなげる入口にする | Bitcoin・EthereumのP2Pを具体例として見る |
関連記事: 身近な記録から理解するブロックチェーンの全体像
Qiitaに投稿する際は、上記の ここに派生元記事のURLを挿入 の部分を、実際に投稿済みの派生元記事URLへ置き換えてください。
この記事で分かること
この記事で分かることは、次のとおりです。
- P2Pネットワークを身近な情報共有から理解する考え方
- 中央サーバー型とP2P型のざっくりした違い
- ノード、ピア、ブロードキャスト、gossip、peer discoveryなどの基本用語
- ブロックチェーンでP2Pネットワークが必要になる理由
- 取引やブロックがノード間へ広がる大まかな流れ
- BitcoinにおけるP2Pネットワークの位置づけ
- Ethereumにおける実行層・コンセンサス層とP2Pネットワークの関係
- P2Pネットワークの強み
- Sybil攻撃、Eclipse攻撃、ネットワーク分断などの注意点
- P2Pネットワークについてよくある誤解
特にこの記事では、次の2つを混同しないことを重視します。
| 混同しやすいこと | 分けて考えたいこと |
|---|---|
| P2P = サーバーが一切ない | P2Pは中央の1か所だけに依存しない考え方。ただし、初回接続の入口や補助的なサービスが使われる場合がある |
| P2P = 取引やブロックの正しさを決める仕組み | P2Pは情報を広げる土台。正しさは各ノードの検証やコンセンサスのルールと組み合わせて判断される |
| ノードが多い = 必ず安全 | ノード数だけでなく、接続関係、実装の多様性、地理的分散、運用主体の偏りなども重要になる |
| 情報が速く届く = 正しい | 速く届いた情報でも、検証して無効なら受け入れない |
| P2Pなら絶対に止まらない | 一部障害に強くなりやすいが、ネットワーク分断や攻撃、実装バグなどのリスクは残る |
P2Pネットワークは、ブロックチェーンを理解するときにとても重要な土台です。
しかし、P2Pだけを見てもブロックチェーン全体は理解できません。
本記事では、P2Pを「情報を届ける仕組み」として押さえたうえで、その上にある検証やコンセンサスとの違いが見えるように整理します。
対象読者
この記事は、次のような人を想定しています。
- ブロックチェーンを学び始めた人
- BitcoinやEthereumの名前は聞いたことがあるが、ノード間通信はまだ曖昧な人
- P2P、ノード、ピア、gossipなどの用語を整理したい人
- 「分散している」という説明を、もう少し具体的に理解したい人
- 情報セキュリティの観点からブロックチェーンを理解したい人
- いきなり仕様書や論文を読む前に、まず全体像をつかみたい人
前提知識としては、次の程度を想定しています。
| 前提知識 | 必要度 |
|---|---|
| ブロックチェーンが取引や状態を記録する仕組みであること | あると読みやすい |
| BitcoinやEthereumの名前を聞いたことがあること | あると読みやすい |
| ハッシュやデジタル署名という言葉を聞いたことがあること | あると読みやすい |
| ネットワークプロトコルの専門知識 | なくてもよい |
| Bitcoin CoreやEthereumクライアントの運用経験 | なくてもよい |
| 分散システムの厳密な理論 | なくてもよい |
途中で、ノード、ピア、ブロードキャスト、gossip、peer discovery、bootnode、mempool、Sybil攻撃、Eclipse攻撃といった言葉が出てきます。
ただし、最初から全部を知っている必要はありません。
それぞれの用語は、できるだけクラスの連絡網や共同編集メモのような身近な例と一緒に説明します。
たとえば、peer discoveryは「誰に連絡すればよいかを見つけること」、gossipは「うわさ話のように接続先へ少しずつ情報を広げること」、mempoolは「まだ正式なブロックに入っていない取引を一時的に扱う場所」のように、まずはざっくりしたイメージから入ります。
本記事で扱わないこと
本記事は、P2Pネットワークの考え方を初学者向けに整理する記事です。
そのため、以下は深く扱いません。
- Bitcoin CoreのP2P実装の詳細
- EthereumのDevP2P、RLPx、Discv4、Discv5、libp2p、gossipsubなどの詳細仕様
- 実際のノード構築・運用手順
- 実際の攻撃手法の再現手順
- ネットワーク測定やシミュレーションの厳密な研究
- 特定クライアントの設定比較
- 特定チェーンの優劣評価
- 投資判断、暗号資産価格、マイニング収益、ステーキング利回りの推奨
P2Pネットワークは、実際にはかなり奥が深いテーマです。
接続相手の選び方、通信の暗号化、メッセージ形式、ノード発見、トランザクション伝播、ブロック伝播、DoS対策、Eclipse攻撃対策など、深掘りすると多くの論点があります。
しかし、最初から仕様を細かく追いかけると、全体像が見えにくくなります。
そこで本記事では、まず次の観点に絞って扱います。
| 扱う観点 | 扱わない観点 |
|---|---|
| P2Pネットワークの基本的な考え方 | 各プロトコルの厳密な仕様 |
| 中央サーバー型との違い | 特定クライアントの設定手順 |
| 取引やブロックが広がる流れ | 実ネットワークの測定結果の詳細分析 |
| BitcoinとEthereumを代表例にした説明 | すべてのチェーンの網羅的比較 |
| セキュリティ上の強みと注意点 | 攻撃再現や攻撃手順の解説 |
また、P2Pネットワークの説明では、実装やプロトコルの変更が起きる可能性があります。
そのため、BitcoinやEthereumに関する具体的な記述は、できるだけ公式資料や仕様を確認し、根拠が不明な内容は断定しないようにします。
コードを試す場合の前提
本記事では、後半の章で、P2Pネットワークの考え方をイメージするためにPythonの小さなコード例を使う可能性があります。
ただし、コードはすべて 考え方を理解するための学習用 です。
実際のBitcoin Core、Ethereumクライアント、P2Pプロトコル、ノード運用、攻撃再現へそのまま使うことは想定していません。
| 項目 | 内容 |
|---|---|
| 目的 | P2Pの情報伝播や接続関係を理解するための学習用 |
| 想定環境 | Python 3.x系 |
| 主に使う標準ライブラリ |
collections, random, dataclasses など |
| 外部ライブラリ | 原則なし。必要になった場合はその章で説明する |
| 注意点 | 実際のBitcoin Core、Ethereumクライアント、ノード運用、攻撃再現には使わない |
たとえば、次のような概念コードを扱う可能性があります。
- 中央サーバー型とP2P型の情報共有の違い
- gossip風に情報が少しずつ広がる様子
- 接続先が偏ると情報が届きにくくなる様子
- Sybil攻撃やEclipse攻撃を概念的に理解する簡易モデル
ここで扱うコードは、実際のBitcoinやEthereumの通信仕様を再現するものではありません。
あくまで、「なぜP2Pでは接続関係が大切なのか」「なぜ一部のノードに偏ると危ないのか」を、手元で小さく動かしながらイメージするためのものです。
この区別を忘れないようにしながら、必要な章で小さなコード例を使っていきます。
全体の流れ
この記事では、次の順番で話を進めます。
最初は、クラスLINE、回覧板、災害時の連絡網のような身近な情報共有から入ります。
そこで、中央の1か所に頼る形は分かりやすい一方で、その場所が止まると全体に影響する場合があることを確認します。
次に、P2Pネットワークを「中央サーバーだけに頼らず、参加者同士が情報をやり取りする通信の考え方」として整理します。
ここでは、peer、node、broadcast、gossip、peer discovery といった用語を、連絡網の例に置き換えながら見ていきます。
そのうえで、ブロックチェーンにおけるP2Pネットワークの役割を確認します。
利用者が作った取引がノードへ届き、ノードが検証し、他のピアへ中継し、ブロック作成者が取引をブロックに含め、新しいブロックがまたネットワークへ広がる、という流れです。
ここで大切なのは、P2Pネットワークが担当するのは主に 情報を広げる部分 だということです。
情報を受け取ったノードは、その情報をそのまま信じるのではなく、取引やブロックがルールに合っているかを検証します。
Bitcoinのホワイトペーパーでも、新しい取引は全ノードへブロードキャストされ、各ノードが取引をブロックへ集め、PoWを見つけたブロックを全ノードへブロードキャストし、ノードは取引が有効な場合にのみブロックを受け入れる流れが説明されています。
参考: Bitcoin: A Peer-to-Peer Electronic Cash System
また、Ethereum公式ドキュメントでは、実行クライアントが実行層のP2Pネットワークで取引をgossipし、コンセンサスクライアントがコンセンサス層のP2PネットワークでBeacon blockをgossipすると説明されています。
現在のEthereumでは、実行クライアントとコンセンサスクライアントが連携して動く点も重要です。
参考: Ethereum Documentation: Networking layer
参考: Ethereum Documentation: Nodes and clients
後半では、BitcoinとEthereumを代表例として、P2Pネットワークがどのように使われているのかを見ます。
その後、P2Pネットワークの強みとして、単一障害点への依存を減らしやすいこと、参加者同士で情報を広げられること、検閲耐性や耐障害性につながる場合があることを整理します。
一方で、P2Pにも限界があります。
たとえば、偽の参加者を大量に作るSybil攻撃、特定ノードの接続先を攻撃者で囲い込むEclipse攻撃、ネットワーク分断、接続先の偏り、クライアント実装やクラウド基盤への依存などです。
そのため、本記事ではP2Pを「すごい仕組み」として持ち上げるのではなく、次のような見方で整理します。
P2Pネットワークは、ブロックチェーンにおける情報共有の土台です。
ただし、P2Pだけで正しさや安全性が決まるわけではありません。
取引・ブロックの検証、コンセンサス、実装、運用、ネットワーク構成と組み合わせて見ることが大切です。
次の章では、まず 中央の1か所だけに頼ると何が困るのか を整理します。
中央サーバー型が悪いという話ではなく、どのような場面で便利で、どのような場面で弱点になり得るのかを確認します。
ここを押さえておくと、P2Pネットワークがなぜブロックチェーンの土台として重要なのかが見えやすくなります。
1. 中央の1か所だけに頼ると何が困るのか
この章では、P2Pネットワークそのものの説明に入る前に、まず 中央の1か所だけに頼る情報共有 では何が便利で、どこに弱さが出やすいのかを整理します。
ポイントは、中央サーバー型が悪いという話ではなく、便利さと引き換えに、中央に負荷や信頼が集中しやすい という点です。
概要では、クラスLINEや研究室の連絡共有を例にして、「中央の1か所だけに頼らない通信」のイメージを見ました。
ここからは、もう少し丁寧に考えていきます。
まだこの章では、BitcoinやEthereumの細かいP2Pプロトコルには入りません。
まずは、日常生活でよくある 代表者を通した連絡 から考えてみます。
たとえば、サークルの練習場所が急に変更になったとします。
代表者が1人いて、その人が全員に連絡する形なら、流れはとても分かりやすいです。
1. 先生や施設担当者から代表者へ連絡が届く
2. 代表者が内容を確認する
3. 代表者が全員へ連絡する
4. メンバーは代表者からの連絡を見て行動する
この形は、現実の多くのシステムにも近いです。
Webサービス、SNS、オンラインゲーム、クラウドストレージなど、多くのサービスでは、中心にサーバーがあり、利用者はそのサーバーへアクセスします。
このような構成は、管理しやすく、分かりやすく、実用上もとても強力です。
| よい点 | 身近な例 | システムでのイメージ |
|---|---|---|
| 情報を一元管理しやすい | 代表者が最新情報をまとめる | サーバーが最新データを管理する |
| 利用者が迷いにくい | 困ったら代表者に聞けばよい | クライアントはサーバーへ問い合わせる |
| ルールを統一しやすい | 代表者が連絡文を整える | サーバー側で認証・権限・整合性を管理する |
| 運用しやすい | 担当者を決めれば済む | 障害対応やログ確認を中央で行いやすい |
そのため、中央サーバー型は「古い」「悪い」仕組みではありません。
むしろ、多くの場面では自然で使いやすい設計です。
ただし、中央の1か所に多くの役割が集まると、そこが止まったときの影響も大きくなります。
NISTIR 8202の用語集では、中央集権型ネットワークを、参加者が中央の権限を通じて互いに通信する構成として説明し、その中央が失われると参加者間の通信ができなくなると整理しています。
参考: NISTIR 8202: Blockchain Technology Overview
この章では、この「中央に集まる便利さ」と「中央に集まりすぎる弱さ」を、身近な例と小さなコードで見ていきます。
1.1 中央サーバー型は分かりやすい
中央サーバー型は、まず構造がとても分かりやすいです。
クラスの連絡で考えると、先生が全員に直接連絡するのではなく、代表者に伝え、代表者が全員へ共有する形です。
メンバーから見ると、「最新情報は代表者の連絡を見ればよい」と判断できます。
Webサービスでも似ています。
利用者のスマホやPC → サービスのサーバー → 必要な情報を返す
たとえば、天気アプリを開くとき、私たちは世界中の端末同士で天気情報を交換しているわけではありません。
多くの場合、アプリがサーバーへ問い合わせ、サーバーが整理済みの情報を返してくれます。
この形には、かなり大きなメリットがあります。
| 観点 | 中央サーバー型で便利な理由 |
|---|---|
| 管理 | どこにデータがあるか分かりやすい |
| 更新 | サーバー側を直せば、全体へ反映しやすい |
| 認証 | ログインや権限管理を中央で行いやすい |
| 監査 | ログや履歴を一か所で確認しやすい |
| 利用者体験 | 利用者は複雑な接続先を意識しなくてよい |
このように見ると、中央サーバー型はとても合理的です。
ブロックチェーンを学んでいると、「中央集権は悪」「分散は善」のように見えてしまうことがあります。
しかし、それは少し単純化しすぎです。
銀行、大学の学務システム、ECサイト、動画配信サービス、チャットアプリなど、中央で管理することで便利に動いている仕組みはたくさんあります。
問題は、中央サーバー型そのものではありません。
中央の1か所に依存しすぎると、その1か所が弱点になりやすい ことです。
💡 豆知識
情報システムの話では、よくsingle point of failureという言葉が出てきます。
日本語では「単一障害点」と訳されることが多いです。
ざっくり言うと、そこが止まると全体が止まりやすい場所のことです。
クラス連絡でいえば、代表者1人だけが全員への連絡手段を持っている状態に近いです。
1.2 中央が止まると、全体に影響しやすい
では、中央の1か所に頼りすぎると、どのようなことが起きるのでしょうか。
もう一度、代表者を通した連絡で考えます。
代表者が元気で、スマホも使えて、通信環境もよいなら問題ありません。
しかし、次のようなことが起きるかもしれません。
- 代表者が連絡に気づかない
- 代表者のスマホの電池が切れる
- 代表者のアカウントが使えなくなる
- 代表者が誤った内容を送ってしまう
- 代表者に連絡が集中し、対応しきれなくなる
この場合、代表者を通る連絡網は一気に弱くなります。
システムでも似たことが起きます。
| 中央に起きる問題 | 起き得る影響 |
|---|---|
| サーバー障害 | 利用者がサービスにアクセスできなくなる |
| ネットワーク障害 | サーバーまで通信できなくなる |
| 管理者の設定ミス | 多くの利用者に同じ誤設定が影響する |
| 攻撃の集中 | 重要なサーバーが攻撃対象になりやすい |
| 検閲や制限 | 中央の判断で特定の情報や操作が止められる可能性がある |
もちろん、現実のサービスでは、冗長化、バックアップ、ロードバランサ、複数データセンター、CDNなどを使って、この弱点を小さくしようとします。
そのため、「中央サーバー型 = すぐ止まる」と考えるのも正確ではありません。
ただし、構造としては、中心に重要な役割が集まりやすいという特徴があります。
この特徴を理解しておくと、P2Pネットワークがなぜ出てくるのかが見えやすくなります。
1.3 中央サーバー型の失敗を小さなコードで見る
ここで、中央サーバー型の情報共有を、かなり単純化したPythonコードで見てみます。
このコードは、実際のWebサービスやブロックチェーンの通信を再現するものではありません。
目的は、中央にいるサーバーが止まると、利用者へ情報が届かなくなる という構造を確認することです。
# 中央サーバー型の情報共有を理解するための学習用コードです。
# 実際のWebサービスやブロックチェーンの通信を再現するものではありません。
from dataclasses import dataclass
from typing import List
@dataclass
class CentralServer:
"""中央サーバーを表す学習用クラスです。"""
name: str
is_online: bool = True # サーバーが動いているかどうかを表します。
def broadcast(self, message: str, users: List[str]) -> dict[str, str]:
"""
サーバーから全利用者へメッセージを送ります。
サーバーがオンラインなら全員に届いたことにします。
サーバーがオフラインなら誰にも届かないことにします。
"""
delivery_result = {}
for user in users:
if self.is_online:
delivery_result[user] = message
else:
delivery_result[user] = "未受信"
return delivery_result
# 利用者の一覧です。
users = ["学生A", "学生B", "学生C", "学生D"]
# 中央サーバーが動いている場合です。
server = CentralServer(name="代表者", is_online=True)
result_online = server.broadcast("集合時間は10時に変更です", users)
print("サーバーが動いている場合")
for user, received in result_online.items():
print(f"{user}: {received}")
print()
# 中央サーバーが止まっている場合です。
server.is_online = False
result_offline = server.broadcast("集合時間は10時に変更です", users)
print("サーバーが止まっている場合")
for user, received in result_offline.items():
print(f"{user}: {received}")
このコードでは、CentralServer が代表者や中央サーバーの役割を持っています。
is_online が True のときは全員に連絡できますが、False になると誰にも届きません。
出力のイメージは次のようになります。
サーバーが動いている場合
学生A: 集合時間は10時に変更です
学生B: 集合時間は10時に変更です
学生C: 集合時間は10時に変更です
学生D: 集合時間は10時に変更です
サーバーが止まっている場合
学生A: 未受信
学生B: 未受信
学生C: 未受信
学生D: 未受信
この例はかなり極端です。
実際のサービスでは、中央サーバーが1台だけということは少なく、複数台で冗長化されている場合も多いです。
ただし、ここで見たいのは、中央の役割が強い構成では、その中央に問題が起きたとき、全体に影響が広がりやすい ということです。
1.4 P2P型では、参加者同士が情報を渡し合う
次に、P2P型に近い情報共有を考えてみます。
P2P型では、中央の1か所から全員へ一斉送信するのではなく、参加者同士が接続し、知っている相手へ情報を渡します。
たとえば、学生Aが新しい連絡を受け取ったあと、学生Bと学生Cへ伝える。
学生Bは学生Dへ伝え、学生Cは学生Eへ伝える。
このように、情報が少しずつ広がっていくイメージです。
この形では、学生Aが全員を直接知っている必要はありません。
また、途中の一部の経路が止まっても、別の経路から情報が届く場合があります。
ただし、P2P型にも難しさがあります。
- 誰と誰がつながっているかによって、情報の届きやすさが変わる
- 接続先が少なすぎると、情報が一部に届かないことがある
- 同じ情報が何度も届くことがある
- 間違った情報が広がる可能性もある
- 受け取った情報を検証するルールが必要になる
つまり、P2P型は中央サーバー型の弱点を減らせる場合がありますが、代わりに 接続関係の管理 や 情報の検証 が大切になります。
Bitcoin Developer Guideでは、Bitcoinのネットワークプロトコルにより、フルノードがブロックや取引を交換するためのP2Pネットワークを共同で維持すると説明されています。
また、フルノードはブロックや取引を他のノードへ中継する前に検証すると説明されています。
参考: Bitcoin Developer Guide: P2P Network
この説明からも、P2Pネットワークでは「情報を渡すこと」と「情報を確認すること」がセットで重要になると分かります。
1.5 P2P型の情報伝播を小さなコードで見る
次に、P2P型の情報共有を、簡単なコードで見てみます。
ここでは、参加者をノード、接続関係を友人関係のように表します。
最初に情報を知っているノードから、接続している相手へ情報が広がっていく様子をシミュレーションします。
# P2P型の情報伝播を理解するための学習用コードです。
# 実際のBitcoinやEthereumのP2Pプロトコルを再現するものではありません。
from collections import deque
# ノード同士の接続関係を表します。
# たとえば "学生A" は "学生B" と "学生C" に連絡できる、という意味です。
network = {
"学生A": ["学生B", "学生C"],
"学生B": ["学生A", "学生D"],
"学生C": ["学生A", "学生E"],
"学生D": ["学生B", "学生F"],
"学生E": ["学生C", "学生F"],
"学生F": ["学生D", "学生E"],
}
def spread_message(network: dict[str, list[str]], start_node: str) -> list[str]:
"""
start_node から情報が広がる順番を調べます。
ここでは、キューを使って「先に情報を受け取った人から順に、
まだ知らない接続先へ伝える」という流れを表しています。
"""
received = set() # すでに情報を受け取ったノードを記録します。
order = [] # 情報を受け取った順番を記録します。
queue = deque([start_node])
while queue:
current = queue.popleft()
# すでに情報を受け取っている場合は、重複して処理しません。
if current in received:
continue
received.add(current)
order.append(current)
# 接続している相手のうち、まだ情報を受け取っていない人へ伝えます。
for neighbor in network[current]:
if neighbor not in received:
queue.append(neighbor)
return order
order = spread_message(network, start_node="学生A")
print("情報を受け取った順番:")
for index, node in enumerate(order, start=1):
print(f"{index}. {node}")
このコードでは、学生A から始まった情報が、接続関係をたどって他の学生へ広がります。
出力のイメージは次のようになります。
情報を受け取った順番:
1. 学生A
2. 学生B
3. 学生C
4. 学生D
5. 学生E
6. 学生F
ここで使っている考え方は、グラフ探索の一種です。
ノードを点、接続関係を線として見れば、「情報がどの点まで届くか」を調べられます。
この例では、学生Aが全員を直接知っていなくても、接続関係を通じて情報が広がりました。
ただし、これは「ネットワークがうまくつながっている場合」です。
接続が少なかったり、一部のノードが孤立していたりすると、情報が届かない人も出てきます。
1.6 一部の経路が止まっても、別経路があれば届くことがある
P2P型の強みの一つは、一部の経路が止まっても、別の経路から情報が届く場合があることです。
たとえば、学生Bがスマホを見ていなくても、学生Aから学生C、学生E、学生Fを通って情報が届くかもしれません。
これも、簡単なコードで見てみます。
# 一部のノードが使えない場合でも、別経路で情報が届くかを確認する学習用コードです。
# 実際のP2Pネットワークでは、遅延、再送、ピア選択、帯域制限なども関係します。
from collections import deque
network = {
"学生A": ["学生B", "学生C"],
"学生B": ["学生A", "学生D"],
"学生C": ["学生A", "学生E"],
"学生D": ["学生B", "学生F"],
"学生E": ["学生C", "学生F"],
"学生F": ["学生D", "学生E"],
}
def spread_message_with_offline_nodes(
network: dict[str, list[str]],
start_node: str,
offline_nodes: set[str],
) -> set[str]:
"""
一部のノードがオフラインの状態で、情報がどこまで届くかを調べます。
offline_nodes に含まれるノードは、情報を受け取ることも、
他のノードへ伝えることもできないものとして扱います。
"""
received = set()
queue = deque([start_node])
while queue:
current = queue.popleft()
# オフラインのノードは処理しません。
if current in offline_nodes:
continue
if current in received:
continue
received.add(current)
# 現在のノードから、接続先へ情報を広げます。
for neighbor in network[current]:
if neighbor not in received and neighbor not in offline_nodes:
queue.append(neighbor)
return received
# 学生Bがオフラインでも、別経路があれば学生Fまで届く可能性があります。
offline_nodes = {"学生B"}
received = spread_message_with_offline_nodes(network, "学生A", offline_nodes)
print("オフラインのノード:", offline_nodes)
print("情報を受け取れたノード:", sorted(received))
この例では、学生B がオフラインでも、学生A → 学生C → 学生E → 学生F という別経路があります。
そのため、ネットワーク全体がすぐに止まるわけではありません。
このような性質は、ブロックチェーンのP2Pネットワークを考えるときにも重要です。
一部のノードが落ちたり、一部の通信経路が不安定になったりしても、他のノードや経路を通じて取引やブロックが広がる可能性があります。
ただし、ここでも注意が必要です。
P2Pだからといって、必ず全員に届くわけではありません。
ネットワークが分断されていたり、接続先が偏っていたり、攻撃者に接続先を囲い込まれていたりすると、情報が届きにくくなる可能性があります。
この話は、後半の「P2Pネットワークの限界とセキュリティ上の注意点」で、Sybil攻撃やEclipse攻撃と一緒に扱います。
1.7 ただし、P2Pは「正しさ」を自動で保証しない
ここまで見ると、P2P型はとても便利に見えるかもしれません。
中央の1か所に頼らず、参加者同士で情報を広げられる。
一部の経路が止まっても、別経路から届くことがある。
これは確かに大きな強みです。
しかし、P2Pには重要な注意点があります。
情報が届くことと、その情報が正しいことは別です。
たとえば、連絡網で次のようなメッセージが回ってきたとします。
明日の集合時間は10時に変更です。
このメッセージが多くの人から届いたとしても、本当に正しいとは限りません。
誰かが古い情報を送っているかもしれませんし、勘違いしているかもしれません。悪意を持って嘘を流している可能性もあります。
ブロックチェーンでも同じです。
ノードが取引やブロックを受け取ったとしても、それをそのまま信じるわけではありません。
各ノードは、取引やブロックがルールに合っているかを検証します。
Bitcoin Developer Guideでは、フルノードがすべてのブロックと取引をダウンロードし、他ノードへ中継する前に検証すると説明されています。
参考: Bitcoin Developer Guide: P2P Network
つまり、P2Pネットワークは「情報の通り道」です。
その情報を正式に受け入れるかどうかは、検証ルールやコンセンサスの仕組みと組み合わせて判断されます。
| 役割 | ざっくりした意味 | ブロックチェーンでの例 |
|---|---|---|
| P2Pネットワーク | 情報を他のノードへ届ける | 取引やブロックをピアへ伝える |
| 検証ルール | 情報が形式やルールに合っているか確認する | 署名、残高、ブロック形式などを確認する |
| コンセンサス | どの履歴を正しいものとして扱うか決める | PoW、PoS、フォーク選択などが関係する |
この区別は、この記事全体で何度も出てきます。
💡 豆知識
P2Pネットワークでよく出てくるbroadcastは「広く知らせる」という意味です。
ただし、ブロックチェーンの世界では、情報をbroadcastしただけで正式な記録になるわけではありません。
取引やブロックは、受け取ったノードによって検証され、ブロックチェーンごとのルールに従って扱われます。
1.8 中央サーバー型とP2P型を比べる
ここまでの内容を、中央サーバー型とP2P型で比べてみます。
| 観点 | 中央サーバー型 | P2P型 |
|---|---|---|
| 情報の流れ | 中央を通して配る | 参加者同士で広げる |
| 分かりやすさ | 構造が分かりやすい | 接続関係が複雑になりやすい |
| 管理 | 中央で管理しやすい | 各ノードの振る舞いが重要になる |
| 障害の影響 | 中央に問題があると全体へ影響しやすい | 一部が止まっても別経路で届く場合がある |
| 正しさの確認 | 中央が確認しやすい | 各ノードの検証ルールが重要になる |
| 悪意ある参加者 | 中央で制限しやすい場合がある | Sybil攻撃やEclipse攻撃などに注意が必要 |
| ブロックチェーンとの関係 | RPCサービスや取引所などで使われることがある | 取引やブロックを広げる土台になる |
この表から分かるように、中央サーバー型とP2P型は、どちらかが常に正解というものではありません。
中央サーバー型は、管理しやすく、利用者にとって分かりやすいです。
P2P型は、中央の1か所だけに頼らずに情報を広げやすくなります。
どちらにも強みがあり、どちらにも注意点があります。
ブロックチェーンでは、特定の管理者だけに頼らず、多くの参加者が同じ記録を共有するために、P2Pネットワークが重要な土台になります。
ただし、P2Pだけでは十分ではありません。
P2Pネットワークで情報を広げ、各ノードが検証し、コンセンサスのルールに従って履歴を選ぶ。
この組み合わせによって、ブロックチェーンは動いています。
NISTIR 8202でも、ブロックチェーンは中央の保管場所や中央権限なしに分散的に実装される台帳として説明され、取引は検証され、コンセンサス判断を経てブロックが追加されると整理されています。
参考: NISTIR 8202: Blockchain Technology Overview
1.9 この章のまとめ
この章では、P2Pネットワークそのものに入る前に、中央の1か所だけに頼る情報共有では何が便利で、どこに弱さが出やすいのかを整理しました。
中央サーバー型は、管理しやすく、構造も分かりやすいです。
多くのサービスでは、中央でデータや認証を管理することで、利用者にとって使いやすい仕組みを実現しています。
一方で、中央に役割が集まりすぎると、その中央が止まったときに全体へ影響が広がりやすくなります。
また、中央が情報を止めたり、誤った情報を配ったりすると、多くの参加者に影響する可能性もあります。
この章で押さえたいポイントは、次の通りです。
| ポイント | 説明 |
|---|---|
| 中央サーバー型は便利 | 管理しやすく、利用者も迷いにくい |
| 中央に依存しすぎると弱点になる | 中央が止まると全体に影響する可能性がある |
| P2P型は参加者同士で情報を広げる | 一部の経路が止まっても、別経路で届く場合がある |
| P2Pにも難しさがある | 接続関係、情報の重複、誤情報、攻撃への対策が必要になる |
| 情報が届くことと正しいことは別 | 受け取った取引やブロックは検証する必要がある |
次の章では、この流れを受けて、いよいよ P2Pネットワークを一言でいうと何なのか を整理します。
ただし、次章でもいきなり難しい仕様には入りません。
この章で見た「代表者に頼る連絡」と「参加者同士で伝え合う連絡」をもとに、ノード、ピア、接続、情報伝播という基本的な考え方をやわらかく整理していきます。
2. P2Pネットワークを一言でいうと
この章では、1章で見た「中央の1か所だけに頼ると何が困るのか」を受けて、P2Pネットワークの考え方を整理します。
ポイントは、P2Pを サーバーが一切ない仕組み と決めつけず、中央の1か所だけに頼らず、参加者同士が情報をやり取りする通信の考え方 として見ることです。
前の章では、中央サーバー型とP2P型の違いを、代表者を通した連絡と、参加者同士で伝え合う連絡の例で見ました。
中央サーバー型は、管理しやすく、利用者にとっても分かりやすい構成です。
一方で、中央の1か所に多くの役割が集まるため、そこが止まったり、攻撃されたり、誤った情報を配ったりすると、全体へ影響が広がりやすくなります。
そこで出てくる考え方が、P2Pネットワークです。
P2Pネットワークは、ざっくり言えば 参加者同士が互いに接続し、情報をやり取りするネットワーク です。
ブロックチェーンの文脈では、取引やブロックをノード間へ広げるための土台として使われます。
Bitcoin Developer Guideでは、Bitcoinのネットワークプロトコルによって、フルノードがブロックや取引を交換するためのP2Pネットワークを共同で維持すると説明されています。
参考: Bitcoin Developer Guide: P2P Network
また、Ethereum公式ドキュメントでは、Ethereumは多数のノードが標準化されたプロトコルで通信するP2Pネットワークであり、gossipによる一対多の情報共有や、特定ノード間のリクエスト/レスポンスによって情報を交換すると説明されています。
参考: Ethereum Documentation: Networking layer
ここで大切なのは、P2Pを「全員が全員と直接つながる仕組み」と考えないことです。
実際には、各ノードは一部の接続先とつながり、そこから情報が少しずつ広がっていきます。
身近な例で言えば、クラス全員と直接連絡先を交換している状態ではなく、何人かの友人やグループを通じて連絡が広がっていく状態に近いです。
この図では、ノードAはノードBとノードCには直接つながっています。
しかし、ノードD、ノードE、ノードFとは直接つながっていません。
それでも、ノードBやノードCを通じて情報が広がれば、最終的に他のノードへも届く可能性があります。
この章では、P2Pネットワークを理解するために、peer、node、接続、情報伝播という基本的な見方を整理します。
2.1 peerは「対等な通信相手」というイメージ
P2Pは、Peer-to-Peer の略です。
peer は、「同じ立場の相手」「仲間」「対等な相手」のような意味を持ちます。
ネットワークの文脈では、自分が通信している相手のノードを peer と呼ぶことがあります。
ここで、node と peer という言葉が少し紛らわしいかもしれません。
ざっくり分けると、次のように考えると読みやすいです。
| 用語 | ざっくりした意味 | 身近な例 |
|---|---|---|
| node | ネットワークに参加している1つの端末やソフトウェア | 連絡網に参加している1人 |
| peer | 自分のノードから見た接続相手 | 自分が直接連絡できる相手 |
| connection | ノード同士のつながり | 連絡先を知っていて、やり取りできる状態 |
| message | ノード間で送る情報 | 「集合時間が変わった」という連絡 |
たとえば、ノードAから見ると、直接つながっているノードBやノードCはpeerです。
このとき、ノードDはネットワーク全体ではnodeですが、ノードAから見ると直接のpeerではありません。
ノードAは、ノードBを通じて間接的にノードDへ情報を届ける可能性があります。
ここでいう「対等」は、すべての参加者が完全に同じ性能や同じ役割を持つという意味ではありません。
現実には、回線速度、保存しているデータ量、オンライン時間、ノードの種類、ソフトウェア実装などに違いがあります。
それでもP2Pでは、基本的な考え方として、中央の1台だけが親になり、他がすべて子になる形ではなく、参加者同士が互いに情報をやり取りします。
💡 豆知識
peerは「同僚」「同級生」「同じ立場の人」のような意味でも使われます。
P2Pネットワークでは、中央の先生や代表者だけが全員へ連絡するのではなく、同じ立場の参加者同士が情報を渡し合うイメージで考えると分かりやすいです。
2.2 P2Pは「全員と直接つながる」わけではない
P2Pネットワークを初めて聞くと、すべてのノードが互いに直接つながっている状態を想像するかもしれません。
たとえば、6人全員が全員と直接連絡できる状態です。
このような構成は、人数が少ないうちは分かりやすいです。
しかし、参加者が増えると、全員が全員と直接つながるのは現実的ではありません。
たとえば、1000人全員が全員と直接つながると、接続数は非常に多くなります。
管理も通信量も大きくなり、現実のネットワークでは負担が大きくなります。
そのため、P2Pネットワークでは、多くの場合、各ノードは一部のpeerと接続します。
そして、情報はpeerからpeerへ少しずつ広がっていきます。
身近な連絡網でも、全員が全員に電話する必要はありません。
何人かに連絡し、その人たちがさらに別の人へ連絡すれば、情報は広がります。
この形のよいところは、ノードAが全員の連絡先を知らなくてもよいことです。
一方で、どのノードとつながるかによって、情報の届き方や速さが変わります。
ここから、P2Pネットワークでは 接続先の選び方 が大切になります。
2.3 「サーバーがない」ではなく「中央だけに頼らない」
P2Pについて説明するときに、よくある表現が「サーバーがないネットワーク」です。
入口としては分かりやすい表現ですが、ブロックチェーンの記事としては少し注意が必要です。
より安全には、次のように表現した方がよいです。
P2Pネットワークは、サーバーが一切存在しない仕組みというより、中央サーバーだけに頼らず、参加者同士が情報をやり取りできる通信の考え方です。
なぜなら、実際のP2Pネットワークでも、最初に接続先を見つけるための入口や補助的な仕組みが使われることがあるからです。
たとえばBitcoinでは、初回起動時にまだアクティブなフルノードのIPアドレスを知らないため、DNS seedと呼ばれる仕組みを使って接続先候補を取得することがあります。Bitcoin Developer Guideでは、DNS seedだけに頼るべきではなく、接続後はpeerから addr メッセージを受け取って、他のpeer情報を得られることも説明されています。
参考: Bitcoin Developer Guide: Peer Discovery
Ethereumでも、初回接続の入口としてbootnodeが使われます。Ethereum公式ドキュメントでは、bootnodeは新しいノードにpeer集合を紹介するために使われ、通常のクライアントタスクには参加しないと説明されています。
参考: Ethereum Documentation: Networking layer - Discovery
つまり、P2Pネットワークでも「最初に誰を見つけるか」は重要です。
身近な例で言えば、新しくサークルに入った人が、最初から全員の連絡先を知っているわけではありません。
まず代表者や知り合いに何人か紹介してもらい、そこから少しずつ連絡網に入っていくイメージです。
この図の入口になるノードは、情報をすべて管理する中央サーバーとは少し役割が違います。
あくまで、新しく参加するノードに「まずはこのあたりの相手に話しかけてみるとよい」と紹介する役割です。
そのため、記事内ではP2Pを次のように扱います。
| 雑に言い切る表現 | 本記事での表現 |
|---|---|
| P2Pにはサーバーがない | 中央サーバーだけに頼らない |
| すべてのノードが完全に対等 | 参加者同士が直接または間接的に通信する。ただし役割や性能差はある |
| 誰の助けもなく接続できる | 初回接続の入口としてDNS seedやbootnodeなどが使われる場合がある |
| P2Pなら完全分散 | 分散性を高めやすいが、接続先や運用主体の偏りはあり得る |
このように表現しておくと、P2Pのイメージを保ちながら、実際のブロックチェーンネットワークに近い説明になります。
2.4 P2Pは「情報を届ける仕組み」であり、「正しさを決める仕組み」ではない
P2Pネットワークを理解するときに、もう一つ大切な区別があります。
それは、情報を届けること と 情報が正しいと判断すること は別だという点です。
たとえば、友人から次のような連絡が届いたとします。
明日の集合場所は第2体育館に変更です。
この連絡は、たしかに自分のところに届きました。
しかし、それだけで本当に正しいとは限りません。
- 古い情報かもしれない
- 誰かが勘違いしているかもしれない
- 別のイベントの連絡と混ざっているかもしれない
- 悪意ある人がわざと嘘を流しているかもしれない
そのため、必要なら公式の連絡元や、複数の信頼できる人に確認します。
ブロックチェーンでも同じです。
P2Pネットワークで取引やブロックが届いたとしても、ノードはそれをそのまま信じるわけではありません。
Bitcoin Developer Guideでは、フルノードはすべてのブロックと取引をダウンロードし、他のノードへ中継する前に検証すると説明されています。
参考: Bitcoin Developer Guide: P2P Network
Ethereum公式ドキュメントでも、Ethereumの各ノードは正しい情報を送受信するために特定のネットワークルールに従う必要があると説明されています。さらに、実行クライアントが取引をgossipし、コンセンサスクライアントがBeacon blockをgossipするなど、情報伝播と検証・合意形成が組み合わさって動きます。
参考: Ethereum Documentation: Networking layer
ここで、P2Pの役割を表にすると次のようになります。
| 役割 | P2Pネットワークが担当すること | P2Pだけでは決まらないこと |
|---|---|---|
| 取引の共有 | 取引を他のノードへ広げる | その取引が有効かどうか |
| ブロックの共有 | 新しいブロックを他のノードへ広げる | そのブロックを受け入れるかどうか |
| peerとの通信 | 接続先とメッセージを交換する | どの履歴を最終的に正しいと見るか |
| 情報の伝播 | 情報をネットワークへ広げる | 署名、残高、フォーク選択、コンセンサス |
P2Pは、ブロックチェーンにおける「情報の配送網」です。
配送網があるからこそ、取引やブロックは多くのノードへ届きます。
ただし、届いた荷物の中身を確認する作業は別です。
ブロックチェーンでは、それが取引検証やブロック検証、コンセンサスのルールに当たります。
2.5 小さなコードで見る:直接のpeerにだけ送る
ここで、P2Pネットワークの基本を小さなコードで見てみます。
まずは、ノードAが直接つながっているpeerにだけメッセージを送る例です。
このコードは、実際のBitcoinやEthereumの通信仕様ではありません。
「自分の直接のpeerにだけ送ると、まだ全体には届かない」という感覚をつかむための学習用コードです。
# P2Pネットワークで「直接つながっているpeerにだけ送る」様子を理解するための学習用コードです。
# 実際のBitcoinやEthereumのP2Pプロトコルを再現するものではありません。
network = {
"ノードA": ["ノードB", "ノードC"],
"ノードB": ["ノードA", "ノードD"],
"ノードC": ["ノードA", "ノードE"],
"ノードD": ["ノードB", "ノードF"],
"ノードE": ["ノードC", "ノードF"],
"ノードF": ["ノードD", "ノードE"],
}
def send_to_direct_peers(
network: dict[str, list[str]],
sender: str,
message: str,
) -> dict[str, str]:
"""
sender が直接つながっているpeerへだけメッセージを送ります。
この関数では、senderの直接の接続先だけを受信済みにします。
つまり、peerのpeerにはまだ届きません。
"""
delivery_result = {}
# まず、全ノードを「未受信」にしておきます。
for node in network:
delivery_result[node] = "未受信"
# sender自身はメッセージを知っている状態にします。
delivery_result[sender] = message
# senderが直接つながっているpeerへメッセージを送ります。
for peer in network[sender]:
delivery_result[peer] = message
return delivery_result
result = send_to_direct_peers(
network=network,
sender="ノードA",
message="新しい取引 tx_001",
)
for node, received_message in result.items():
print(f"{node}: {received_message}")
このコードを実行すると、ノードA、ノードB、ノードCはメッセージを受け取ります。
しかし、ノードD、ノードE、ノードFにはまだ届きません。
ノードA: 新しい取引 tx_001
ノードB: 新しい取引 tx_001
ノードC: 新しい取引 tx_001
ノードD: 未受信
ノードE: 未受信
ノードF: 未受信
この結果から分かるように、1回送っただけではネットワーク全体へ届きません。
P2Pネットワークでは、受け取ったpeerがさらに自分のpeerへ伝えることで、情報が少しずつ広がっていきます。
2.6 小さなコードで見る:gossip風に情報を広げる
次に、受け取ったノードがさらに接続先へ伝える様子を見てみます。
このように、情報が隣のノードへ広がり、そのノードがまた隣へ広げていく流れは、gossip という言葉で説明されることがあります。
gossipは「うわさ話」という意味で、情報が人づてに広がっていくイメージです。
Ethereum公式ドキュメントでも、ネットワーキング層にはgossipによる一対多通信と、特定ノード間のリクエスト/レスポンスが含まれると説明されています。
また、コンセンサス層ではBeacon block、attestation、slashing関連情報などがgossip domainで扱われると説明されています。
参考: Ethereum Documentation: Networking layer
ここでは、gossip風に情報が広がる様子を、かなり単純化したコードで見てみます。
# gossip風に情報が広がる様子を理解するための学習用コードです。
# 実際のEthereumのgossipsubやBitcoinのメッセージ伝播を再現するものではありません。
from collections import deque
network = {
"ノードA": ["ノードB", "ノードC"],
"ノードB": ["ノードA", "ノードD"],
"ノードC": ["ノードA", "ノードE"],
"ノードD": ["ノードB", "ノードF"],
"ノードE": ["ノードC", "ノードF"],
"ノードF": ["ノードD", "ノードE"],
}
def gossip_message(
network: dict[str, list[str]],
start_node: str,
message_id: str,
max_hops: int = 10,
) -> list[tuple[str, int]]:
"""
start_node から情報が広がる順番を調べます。
message_id:
メッセージを識別するためのIDです。
実際のP2Pネットワークでは、同じ情報を何度も処理しない工夫が重要になります。
max_hops:
情報を何段階先まで広げるかを表します。
無制限に広げると、ネットワーク上をメッセージが増えすぎる可能性があるため、
ここでは説明用に上限を付けています。
"""
received = set() # すでにメッセージを受け取ったノードです。
received_order = [] # 受け取った順番と、start_nodeからの距離を記録します。
queue = deque([(start_node, 0)])
while queue:
current, hop = queue.popleft()
# すでに受け取ったノードは、重複して処理しません。
if current in received:
continue
received.add(current)
received_order.append((current, hop))
# hop数が上限に達したら、それ以上は広げません。
if hop >= max_hops:
continue
# 現在のノードが知っているpeerへメッセージを広げます。
for peer in network[current]:
if peer not in received:
queue.append((peer, hop + 1))
return received_order
order = gossip_message(
network=network,
start_node="ノードA",
message_id="tx_001",
max_hops=10,
)
print("メッセージを受け取った順番:")
for node, hop in order:
print(f"{node}: {hop} hop")
このコードでは、ノードAから始まったメッセージが、peerをたどって少しずつ広がります。
出力のイメージは次のようになります。
メッセージを受け取った順番:
ノードA: 0 hop
ノードB: 1 hop
ノードC: 1 hop
ノードD: 2 hop
ノードE: 2 hop
ノードF: 3 hop
hop は、何段階の接続をたどって届いたかを表します。
ノードBとノードCはノードAから直接届くので 1 hop です。
ノードFは、ノードAから見て複数のpeerを経由して届くので 3 hop になっています。
このように、P2Pネットワークでは、情報が一気に全員へ届くというより、接続関係をたどって広がります。
2.7 重複を避けることも大切
P2Pネットワークでは、同じ情報が複数の経路から届くことがあります。
たとえば、ノードFは、次の2つの経路から同じメッセージを受け取るかもしれません。
ノードA → ノードB → ノードD → ノードF
ノードA → ノードC → ノードE → ノードF
このような重複は、P2Pネットワークでは自然に起こり得ます。
同じ情報が複数経路から届くことは、情報が届きやすくなるという意味では利点にもなります。
一方で、同じ情報を何度も処理し続けると、無駄な通信や処理が増えてしまいます。
そのため、実際のP2Pネットワークでは、すでに見たメッセージを識別し、重複処理を避ける工夫が重要になります。
ここでも、かなり単純化したコードで見てみます。
# 同じメッセージを何度も処理しないための考え方を理解する学習用コードです。
# 実際のBitcoinやEthereumの重複排除処理を再現するものではありません。
from collections import deque
network = {
"ノードA": ["ノードB", "ノードC"],
"ノードB": ["ノードA", "ノードD"],
"ノードC": ["ノードA", "ノードE"],
"ノードD": ["ノードB", "ノードF"],
"ノードE": ["ノードC", "ノードF"],
"ノードF": ["ノードD", "ノードE"],
}
def gossip_with_seen_cache(
network: dict[str, list[str]],
start_node: str,
message_id: str,
) -> dict[str, set[str]]:
"""
各ノードが「すでに見たメッセージID」を記録しながら、
gossip風にメッセージを広げます。
seen_messages:
ノードごとに、処理済みのmessage_idを保存します。
これにより、同じメッセージを何度も処理しにくくします。
"""
seen_messages = {node: set() for node in network}
queue = deque([start_node])
while queue:
current = queue.popleft()
# そのノードがすでに同じメッセージを見ていれば、処理をスキップします。
if message_id in seen_messages[current]:
continue
# 初めて見たメッセージなので、処理済みとして記録します。
seen_messages[current].add(message_id)
# まだそのメッセージを見ていないpeerへ広げます。
for peer in network[current]:
if message_id not in seen_messages[peer]:
queue.append(peer)
return seen_messages
seen = gossip_with_seen_cache(
network=network,
start_node="ノードA",
message_id="tx_001",
)
for node, messages in seen.items():
print(f"{node}: {messages}")
このコードでは、各ノードが seen_messages という記録を持っています。
すでに tx_001 を見たノードは、同じメッセージをもう一度処理しません。
実際のブロックチェーンネットワークでは、メッセージの識別、再送、帯域制限、DoS対策、peer管理など、もっと多くの工夫が必要になります。
ここでは、P2Pの基本として「情報は複数経路から届くことがあり、重複を避ける仕組みが重要になる」と押さえておきます。
💡 豆知識
P2Pネットワークでは、情報が複数経路から届くことがあります。
これは一見むだに見えますが、別経路から届くことで一部の経路障害に強くなる場合もあります。
一方で、同じ情報を無限に回し続けないように、メッセージIDや既読管理のような考え方が重要になります。
2.8 クライアントサーバー型との違いをもう一度整理する
ここまで見た内容を、クライアントサーバー型とP2P型で整理します。
| 観点 | クライアントサーバー型 | P2P型 |
|---|---|---|
| 基本構造 | 利用者が中央サーバーへ問い合わせる | ノード同士がpeerとして接続する |
| 情報の広がり方 | サーバーから利用者へ配る | peerからpeerへ少しずつ広がる |
| 接続先 | 利用者は主にサーバーへ接続する | ノードは複数のpeerへ接続する |
| 管理のしやすさ | 中央で管理しやすい | 分散しているため管理が複雑になりやすい |
| 障害時の特徴 | 中央に問題があると影響が集中しやすい | 一部が止まっても別経路で届く場合がある |
| 情報の確認 | サーバー側で統一しやすい | 各ノードの検証ルールが重要になる |
| ブロックチェーンでの役割 | ウォレットのRPC接続、取引所、ブロックエクスプローラーなどで使われることがある | 取引やブロックをノード間で広げる土台になる |
この表で大切なのは、クライアントサーバー型とP2P型を単純な優劣で見ないことです。
クライアントサーバー型は、管理しやすく、利用者体験を作りやすいです。
P2P型は、中央の1か所だけに頼らず、参加者同士で情報を広げやすくなります。
ブロックチェーンでは、P2Pネットワークが重要な土台になります。
しかし、ブロックチェーンを使うアプリやサービスの周辺には、中央集権的な構成も多く存在します。
たとえば、ウォレットが特定のRPCプロバイダへ問い合わせたり、利用者が取引所のWebサービスを使ったり、ブロックエクスプローラーで取引を検索したりする場面です。
これらはブロックチェーンそのもののP2Pネットワークとは別に、利用者に分かりやすい入口を提供しているとも言えます。
そのため、P2Pを理解するときは、次のように分けて考えると混乱しにくくなります。
| 見る対象 | 例 |
|---|---|
| ブロックチェーンの基盤ネットワーク | ノード同士が取引やブロックを共有するP2Pネットワーク |
| 利用者向けサービス | ウォレット、取引所、ブロックエクスプローラー、RPCプロバイダ |
| 正しさの判断 | 取引検証、ブロック検証、コンセンサス、フォーク選択 |
| 使いやすさの提供 | UI、検索、API、通知、管理画面 |
この切り分けをしておくと、「ブロックチェーンはP2Pなのに、なぜウォレットは特定のサーバーへ接続しているのか」といった疑問も整理しやすくなります。
2.9 この章のまとめ
この章では、P2Pネットワークを一言でどう捉えるかを整理しました。
P2Pネットワークは、中央の1か所だけに頼らず、参加者同士が情報をやり取りする通信の考え方です。
ただし、サーバーや入口が一切存在しないという意味ではありません。BitcoinではDNS seed、Ethereumではbootnodeのように、最初の接続先を見つけるための補助的な仕組みが使われる場合があります。
また、P2Pネットワークは情報を広げるための土台ですが、情報の正しさを単独で決める仕組みではありません。
届いた取引やブロックは、各ノードが検証し、ブロックチェーンごとのコンセンサスルールと組み合わせて扱われます。
この章で押さえたいポイントは、次の通りです。
| ポイント | 説明 |
|---|---|
| P2PはPeer-to-Peerの略 | peerは、自分のノードから見た通信相手のような意味 |
| 全員が全員と直接つながるわけではない | 多くの場合、各ノードは一部のpeerと接続し、情報はそこから広がる |
| サーバーが一切ないとは言い切らない | 初回接続の入口や補助的な仕組みが使われる場合がある |
| 情報が届くことと正しいことは別 | 取引やブロックは受け取ったあとに検証される |
| gossipでは情報が少しずつ広がる | うわさ話のように、peerからpeerへ情報が伝わる |
| 重複処理を避ける工夫も必要 | 同じ情報が複数経路から届くため、既に見た情報を管理する必要がある |
次の章では、P2Pネットワークを理解するための基本用語をもう少し整理します。
この章でもnodeやpeerという言葉を使いましたが、次章では、ノード、ピア、ブロードキャスト、gossip、peer discovery、bootnodeなどを、ブロックチェーンの文脈に合わせて一つずつ見ていきます。
3. P2Pネットワークの基本用語
この章では、P2Pネットワークを読むうえで出てくる基本用語を整理します。
ポイントは、用語を暗記することではなく、「誰が参加者で、誰とつながり、どのように情報を広げるのか」 をイメージできるようにすることです。
前の章では、P2Pネットワークを 中央の1か所だけに頼らず、参加者同士が情報をやり取りする通信の考え方 として整理しました。
ただ、この説明だけだと、まだ少しふわっとしています。
実際にブロックチェーンの記事や公式ドキュメントを読むと、次のような言葉が出てきます。
- ノード
- ピア
- ブロードキャスト
- gossip
- peer discovery
- bootnode
- DNS seed
- mempool
初めて見ると、いかにもネットワークの専門用語という感じがします。
しかし、一つずつ見ると、身近な連絡網や共同編集メモに近い考え方も多いです。
たとえば、クラスの連絡網で考えると、連絡網に参加している1人ひとりが「ノード」、自分が直接連絡できる相手が「ピア」、連絡を周りに広げることが「ブロードキャスト」や「gossip」に近いです。
| P2Pの用語 | 身近なイメージ | ブロックチェーンでのイメージ |
|---|---|---|
| ノード | 連絡網に参加している人 | ネットワークに参加するコンピュータやソフトウェア |
| ピア | 自分が直接連絡できる相手 | 自分のノードが直接接続している相手ノード |
| ブロードキャスト | 連絡を周りへ知らせる | 取引やブロックをネットワークへ広げる |
| gossip | うわさ話のように少しずつ広がる | peerからpeerへ情報が伝わる通信方式のイメージ |
| peer discovery | 誰に連絡すればよいか探す | 接続できるpeerを見つける |
| mempool | まだ正式版に入っていないメモの一時置き場 | まだブロックに入っていない取引を一時的に扱う場所 |
この章では、これらの言葉を、P2Pネットワークとブロックチェーンの文脈に合わせて整理していきます。
Bitcoin Developer Guideでは、Bitcoinのネットワークプロトコルが、フルノード同士でブロックや取引を交換するP2Pネットワークを維持すると説明されています。
また、Ethereum公式ドキュメントでは、Ethereumのnetworking layerは、ノード同士が互いを見つけ、情報を交換するためのプロトコル群だと説明されています。
参考: Bitcoin Developer Guide: P2P Network
参考: Ethereum Documentation: Networking layer
3.1 ノード:ネットワークに参加する「1つの点」
まずは ノード です。
ノードは、P2Pネットワークに参加している1つのコンピュータ、またはその上で動いているソフトウェアを指す言葉として使われます。
身近な例で言えば、クラスの連絡網に参加している1人ひとりです。
Aさん、Bさん、Cさんがそれぞれ連絡を受け取り、必要に応じて他の人へ伝えるなら、それぞれが連絡網の参加者です。
ブロックチェーンでは、ノードは取引やブロックを受け取り、必要に応じて検証し、他のノードへ中継します。
Bitcoin Developer Guideでは、フルノードはすべてのブロックと取引をダウンロードし、他ノードへ中継する前に検証すると説明されています。
参考: Bitcoin Developer Guide: P2P Network
ここで注意したいのは、ノード = マイナー ではないことです。
Bitcoinの文脈では、マイナーはPoWによってブロック作成に参加する主体です。
一方で、フルノードはブロックや取引を検証する役割を持ちます。もちろん、同じ機械や運用主体がマイニングとノード運用の両方を行う場合もありますが、用語としては分けて考える方が安全です。
Ethereumでも同じように、ノード、バリデータ、クライアントは分けて理解した方が読みやすくなります。
| 用語 | ざっくりした意味 | 混同しやすい点 |
|---|---|---|
| ノード | ネットワークに参加するコンピュータやソフトウェア | すべてのノードがブロック作成者とは限らない |
| マイナー | PoWでブロック作成に参加する主体 | Bitcoinの文脈でよく出てくる |
| バリデータ | PoSでブロック提案や検証に参加する主体 | EthereumのPoSで重要になる |
| クライアント | ノードを動かすためのソフトウェア | Ethereumでは実行クライアントとコンセンサスクライアントがある |
💡 豆知識
nodeは、ネットワーク図で見ると「点」のように描かれます。
その点同士を線でつなぐと、ネットワークの形が見えてきます。
P2Pネットワークを図にすると、サーバーを中心にした星形ではなく、点同士がいくつもつながる網のような形になりやすいです。
3.2 ピア:自分のノードから見た「直接つながっている相手」
次に ピア です。
ピアは、自分のノードが直接接続している相手ノードを指す言葉として使われます。
ここで大切なのは、ピアは「ネットワーク全体の参加者全員」ではないことです。
自分が直接やり取りしている相手が、自分から見たピアです。
身近な例で考えると、クラス全員が連絡網に参加していても、自分が直接LINEで連絡する相手は数人かもしれません。
その数人が、自分から見た「直接の連絡相手」です。
P2Pネットワークでも、多くの場合、1つのノードがネットワーク上の全ノードと直接つながるわけではありません。
いくつかのピアと接続し、そのピアを通じて情報がさらに広がります。
上の図では、自分のノードから見た直接のピアは peer B、peer C、peer D です。
その先にも別のノードがありますが、自分が直接接続しているとは限りません。
この違いを小さなコードで確認してみます。
# ノードとpeerの関係を理解するための学習用コードです。
# 実際のBitcoinやEthereumの接続管理を再現するものではありません。
from dataclasses import dataclass, field
from typing import Dict, List
@dataclass
class Node:
"""P2Pネットワークに参加する1台分のノードを表します。"""
name: str
peers: List[str] = field(default_factory=list)
def show_peers(network: Dict[str, Node]) -> None:
"""各ノードが、どのpeerと接続しているかを表示します。"""
for node in network.values():
print(f"{node.name} のpeer: {', '.join(node.peers)}")
# 学習用の小さなP2Pネットワークです。
# AはBとCに直接つながっていますが、Dとは直接つながっていません。
network = {
"A": Node(name="ノードA", peers=["ノードB", "ノードC"]),
"B": Node(name="ノードB", peers=["ノードA", "ノードD"]),
"C": Node(name="ノードC", peers=["ノードA", "ノードD"]),
"D": Node(name="ノードD", peers=["ノードB", "ノードC"]),
}
show_peers(network)
このコードでは、ノードA は ノードB と ノードC をpeerとして持っています。
しかし、ノードD とは直接つながっていません。
それでも、ノードA の情報は ノードB や ノードC を通じて ノードD へ届く可能性があります。
ここで押さえたいのは、P2Pネットワークでは「直接つながっている相手」と「ネットワーク全体の参加者」を分けて考えることです。
3.3 ブロードキャスト:情報をネットワークへ知らせること
ブロードキャスト は、情報をネットワーク上の他の参加者へ広げることです。
ブロックチェーンでは、新しい取引やブロックを他のノードへ伝えるときに、この考え方が出てきます。
たとえば、あるユーザーが新しい取引を作ったとします。
その取引は、まずどこかのノードへ送られます。ノードがその取引を有効だと判断すれば、接続しているpeerへ知らせます。peerも同じように、自分のpeerへ伝えていきます。
Bitcoin Developer Guideでは、取引をpeerへ送るために inv メッセージで通知し、相手から getdata が来た場合に tx メッセージで取引を送る流れが説明されています。受け取ったpeerは、その取引が有効なら同じように転送します。
参考: Bitcoin Developer Guide: Transaction Broadcasting
ここで大切なのは、「全員へ一気に直接送る」というより、直接つながっているpeerへ送り、そこからさらに広がる という点です。
このように、情報は段階的に広がります。
ただし、ブロードキャストされた情報が必ず正式な記録になるわけではありません。
届いた取引やブロックは、各ノードによって検証されます。無効なものは受け入れられません。
この点は、連絡網の例でも同じです。
「明日は10時集合らしい」という連絡が届いたとしても、それが本当に正式な連絡かどうかは確認する必要があります。
情報が届くことと、情報が正しいことは別です。
3.4 gossip:うわさ話のように情報が広がるイメージ
P2Pネットワークでは、gossip という言葉もよく出てきます。
gossipは、英語では「うわさ話」という意味です。
ネットワークの文脈では、あるノードが知った情報を、接続しているpeerへ伝え、そのpeerがさらに別のpeerへ伝えるような広がり方を指すイメージで使われます。
Ethereum公式ドキュメントでは、Ethereumのnetworking layerは、ノードが互いを見つけ、情報を交換するためのプロトコル群であり、gossipingはネットワーク上のone-to-many communicationとして説明されています。
また、実行クライアントは取引を実行層のP2Pネットワークでgossipし、コンセンサスクライアントはBeacon blockをコンセンサス層のP2Pネットワークでgossipすると説明されています。
参考: Ethereum Documentation: Networking layer
gossipのイメージを、もう少しコードで見てみます。
# gossip風にメッセージが広がる様子を理解するための学習用コードです。
# ttlは、メッセージが無限に広がり続けないようにするための説明用の値です。
# 実際のP2Pネットワークでは、メッセージID、検証、帯域制限、peer管理などが関係します。
from collections import deque
from typing import Dict, List, Set
def broadcast_with_ttl(
graph: Dict[str, List[str]],
start_node: str,
message_id: str,
ttl: int,
) -> Dict[str, Set[str]]:
"""
message_id を start_node からpeerへ広げます。
ttl は「あと何回まで中継してよいか」を表す学習用の値です。
"""
seen_messages: Dict[str, Set[str]] = {node: set() for node in graph}
# queueには「現在のノード」「メッセージID」「残りの中継回数」を入れます。
queue = deque([(start_node, message_id, ttl)])
while queue:
current_node, current_message_id, remaining_ttl = queue.popleft()
# すでに同じメッセージを見たノードでは、重複処理を避けます。
if current_message_id in seen_messages[current_node]:
continue
seen_messages[current_node].add(current_message_id)
print(f"{current_node} が {current_message_id} を受信しました。ttl={remaining_ttl}")
# ttlが0になったら、それ以上は中継しません。
if remaining_ttl <= 0:
continue
# 直接つながっているpeerへ、残りttlを1つ減らして渡します。
for peer in graph[current_node]:
queue.append((peer, current_message_id, remaining_ttl - 1))
return seen_messages
# 学習用の接続関係です。
# 実際のP2Pネットワークでは、接続先はもっと多く、動的に変化します。
graph = {
"ノードA": ["ノードB", "ノードC"],
"ノードB": ["ノードA", "ノードD"],
"ノードC": ["ノードA", "ノードD", "ノードE"],
"ノードD": ["ノードB", "ノードC"],
"ノードE": ["ノードC"],
}
seen = broadcast_with_ttl(
graph=graph,
start_node="ノードA",
message_id="tx_001",
ttl=2,
)
print("\n各ノードが受信済みのメッセージ:")
for node, messages in seen.items():
print(f"{node}: {messages}")
このコードでは、ノードA が受け取った tx_001 というメッセージが、peerを通じて広がります。
ttl は、説明用に入れている「あと何回まで中継するか」という値です。
実際のBitcoinやEthereumの仕組みをそのまま表しているわけではありませんが、メッセージが無限に回り続けないようにする工夫が必要だという感覚をつかみやすくなります。
また、各ノードは seen_messages を持っています。
これは「すでに見たメッセージをもう一度処理しない」ための記録です。
P2Pネットワークでは、同じ情報が複数の経路から届くことがあります。
そのため、重複を避ける考え方はとても大切です。
3.5 peer discovery:最初に誰とつながるかを探す
P2Pネットワークでは、最初から接続相手を知っているとは限りません。
新しく参加したノードは、まず「誰とつながればよいのか」を知る必要があります。
この接続相手を見つける処理が peer discovery です。
身近な例で言えば、新しいクラスに入ったばかりの人が、まず連絡網の入口になる人を教えてもらうようなものです。
最初に何人かとつながれば、そこからさらに別の人を紹介してもらえます。
Bitcoin Developer Guideでは、初回起動時のプログラムはアクティブなフルノードのIPアドレスを知らないため、Bitcoin CoreなどにハードコードされたDNS seedを問い合わせて、接続を受け付ける可能性があるフルノードのIPアドレスを取得すると説明されています。
参考: Bitcoin Developer Guide: Peer Discovery
Ethereum公式ドキュメントでは、discoveryはネットワーク内の他ノードを見つけるプロセスであり、少数のbootnodeによってbootstrapされると説明されています。bootnodeの目的は、新しいノードをpeer集合へ紹介することであり、通常の同期処理などには参加しないと説明されています。
参考: Ethereum Documentation: Networking layer - Discovery
libp2pのドキュメントでも、peer discoveryはP2Pネットワーク内の利用可能なpeerを見つけたり、サービスを知らせたりする処理として説明されています。
参考: libp2p Documentation: What is Discovery & Routing
このように、P2Pネットワークは「中央サーバーが一切ない」というより、接続の入口や紹介役を使いながら、最終的にはpeer同士で情報をやり取りする仕組み と捉える方が正確です。
peer discoveryの考え方を、かなり単純化したコードで見てみます。
# peer discoveryの雰囲気を理解するための学習用コードです。
# 実際のBitcoinのDNS seedやEthereumのbootnodeを再現するものではありません。
import random
from typing import List, Set
def discover_peers(
known_peers: List[str],
already_connected: Set[str],
max_new_peers: int,
) -> List[str]:
"""
既に知っている接続候補から、まだ接続していないpeerを選びます。
実際のBitcoinやEthereumでは、DNS seed、bootnode、routing table、
接続数の上限、IPアドレスの偏り対策など、より多くの条件が関係します。
"""
candidates = [peer for peer in known_peers if peer not in already_connected]
# 説明用に、候補の順番をランダムに並べ替えます。
random.shuffle(candidates)
# 接続数が増えすぎないように、必要な数だけ選びます。
return candidates[:max_new_peers]
# 再現しやすいように乱数の種を固定します。
random.seed(42)
# 最初に知っているpeer候補です。
# 実際にはDNS seedやbootnodeなどを通じて候補を得る場合があります。
known_peers = [
"ノードB",
"ノードC",
"ノードD",
"ノードE",
"ノードF",
]
# すでにノードBとは接続済みだとします。
already_connected = {"ノードB"}
new_peers = discover_peers(
known_peers=known_peers,
already_connected=already_connected,
max_new_peers=2,
)
print("新しく接続するpeer候補:")
for peer in new_peers:
print(peer)
このコードでは、すでに接続しているpeerを除外し、まだ接続していない候補から新しいpeerを選んでいます。
実際のP2Pネットワークでは、もっと多くの条件があります。
たとえば、接続数が偏りすぎないようにしたり、同じIP範囲に偏らないようにしたり、応答しないpeerを避けたりする必要があります。
ただ、入口としては次のように考えると分かりやすいです。
peer discoveryは、P2Pネットワークに参加するための「最初の連絡先探し」です。
最初に何人かのpeerを見つけ、そこからさらにネットワークへ入っていきます。
💡 豆知識
BitcoinのDNS seedやEthereumのbootnodeは、「中央管理者として全取引を処理するサーバー」ではありません。
どちらかというと、新しく来た人に「まずこのあたりの人に聞いてみるとよいですよ」と教える案内係に近いです。
そのため、P2Pを説明するときは「サーバーが一切ない」よりも「中央の1か所だけに頼らない」と表現する方が誤解が少なくなります。
3.6 mempool:まだブロックに入っていない取引の一時置き場
ブロックチェーンのP2Pネットワークを理解するとき、mempool という言葉もよく出てきます。
mempoolは、ざっくり言えば、まだブロックに入っていない取引を一時的に扱う場所です。
身近な例で言えば、共同編集メモに正式に反映する前の「未処理メモ置き場」に近いです。
誰かが新しい支出メモを出したけれど、まだ正式な会計ノートには書き込まれていない状態を想像すると分かりやすいです。
Bitcoin Developer Guideでは、フルピアは次のブロックに含められる可能性のある未承認取引を追跡する場合があり、Bitcoin Coreはこれをmemory pool、つまりmempoolとして非永続のメモリに保存すると説明されています。
また、未承認取引は永久的な状態を持たないため、ノードが再起動したり、メモリ確保のために削除したりすると、ネットワークからゆっくり消えていく場合があると説明されています。
参考: Bitcoin Developer Guide: Memory Pool
Bitcoin Developer Referenceでも、mempool メッセージへの応答は、その受信ノードのmemory poolにあるTXIDの一覧であり、ネットワーク全体の未承認取引の完全な一覧ではなく、あくまで1ノードから見たネットワークの見え方だと説明されています。
参考: Bitcoin Developer Reference: MemPool
Ethereum公式ドキュメントでも、取引が有効なら実行クライアントがローカルmempoolへ追加し、実行層のgossip networkで他ノードへbroadcastすると説明されています。
参考: Ethereum Documentation: Proof-of-stake - How a Transaction Gets Executed
ここで大切なのは、mempoolはネットワーク全体で完全に1つの共有箱ではない という点です。
各ノードは、自分が見た取引、自分が有効だと判断した取引、自分のポリシーで保持する取引をmempoolに入れます。
そのため、あるノードのmempoolと別のノードのmempoolが完全に一致しているとは限りません。
小さなコードで、取引を受け取り、検証してからローカルmempoolへ入れる流れを見てみます。
# mempoolの考え方を理解するための学習用コードです。
# 実際のBitcoinやEthereumの取引検証・mempool管理を再現するものではありません。
from dataclasses import dataclass
from typing import Dict
@dataclass
class Transaction:
"""学習用の取引データです。実際の取引形式ではありません。"""
tx_id: str
sender: str
receiver: str
amount: int
signature_valid: bool
def is_valid_transaction(tx: Transaction) -> bool:
"""
取引が最低限の条件を満たすかを確認します。
実際のブロックチェーンでは、残高、nonce、手数料、署名形式なども検証されます。
"""
if tx.amount <= 0:
return False
if not tx.signature_valid:
return False
return True
def receive_transaction(tx: Transaction, mempool: Dict[str, Transaction]) -> None:
"""
受け取った取引を検証し、有効ならローカルmempoolへ追加します。
mempoolはネットワーク全体で1つではなく、ここでは各ノードの手元の一時置き場として扱います。
"""
if tx.tx_id in mempool:
print(f"{tx.tx_id}: すでにmempoolにあるため、重複追加しません。")
return
if not is_valid_transaction(tx):
print(f"{tx.tx_id}: 無効な取引として受け入れません。")
return
mempool[tx.tx_id] = tx
print(f"{tx.tx_id}: 有効な取引としてmempoolへ追加しました。")
local_mempool: Dict[str, Transaction] = {}
transactions = [
Transaction(tx_id="tx_001", sender="Alice", receiver="Bob", amount=10, signature_valid=True),
Transaction(tx_id="tx_002", sender="Carol", receiver="Dave", amount=-5, signature_valid=True),
Transaction(tx_id="tx_003", sender="Eve", receiver="Frank", amount=3, signature_valid=False),
Transaction(tx_id="tx_001", sender="Alice", receiver="Bob", amount=10, signature_valid=True),
]
for tx in transactions:
receive_transaction(tx, local_mempool)
print("\n現在のローカルmempool:")
for tx_id in local_mempool:
print(tx_id)
このコードでは、tx_001 は有効な取引としてmempoolへ入ります。
一方、金額が負の tx_002 や、署名が無効という扱いにした tx_003 は受け入れません。さらに、同じ tx_001 がもう一度届いても、重複追加しません。
実際のブロックチェーンでは、取引検証はもっと複雑です。
ただし、入口としては次の流れを押さえておくとよいです。
1. 取引を受け取る
2. 形式や署名などを検証する
3. 有効なら自分のローカルmempoolへ入れる
4. 必要に応じてpeerへ知らせる
5. ブロック作成者はmempoolなどから取引を選んでブロック候補を作る
3.7 request / response:必要な情報を相手に取りに行く
P2Pネットワークでは、情報を一方的に広げるだけでなく、必要な情報を相手に問い合わせることもあります。
Ethereum公式ドキュメントでは、networking layerには、gossipingのようなone-to-many communicationだけでなく、特定ノード間のrequest/response、つまりone-to-one communicationも含まれると説明されています。
参考: Ethereum Documentation: Networking layer
身近な例でいうと、連絡網で「明日の集合時間が変わったらしい」と聞いたあと、詳しい場所や持ち物を知っている人に直接聞くようなものです。
| 通信の形 | 身近な例 | P2Pでのイメージ |
|---|---|---|
| gossip / broadcast | 連絡を周りへ広げる | 新しい取引やブロックを知らせる |
| request / response | 詳細を知っている人に聞く | 足りないブロックや取引データをpeerへ要求する |
BitcoinのP2Pでも、まず inv メッセージで「この取引やブロックを知っています」と知らせ、受信側が必要なら getdata で要求し、実データを受け取る流れがあります。
参考: Bitcoin Developer Reference: Inv
このように、P2Pネットワークは「とにかく全部を全員に投げる」だけではありません。
相手がすでに持っているか、必要としているか、どのデータが不足しているかを見ながら通信します。
3.8 基本用語をまとめて整理する
ここまで出てきた用語を、もう一度整理します。
| 用語 | ざっくりした意味 | ブロックチェーンでの具体例 |
|---|---|---|
| node | ネットワークに参加する1つの点 | Bitcoin full node、Ethereum nodeなど |
| peer | 自分のノードが直接接続している相手 | 取引やブロックをやり取りする接続先 |
| broadcast | 情報をネットワークへ知らせること | 新しい取引やブロックをpeerへ伝える |
| gossip | peerからpeerへ情報が広がる通信のイメージ | Ethereumの取引gossip、Beacon block gossipなど |
| peer discovery | 接続できるpeerを見つけること | BitcoinのDNS seed、Ethereumのbootnodeなど |
| bootnode | 新規ノードへpeerを紹介する入口 | Ethereumの初回接続時の案内役 |
| DNS seed | 接続候補のノードを見つけるための仕組み | Bitcoin Coreなどで使われる初回接続の入口 |
| mempool | 未承認取引の一時置き場 | 各ノードが保持する未ブロック化取引の集合 |
| request / response | 特定peerに必要な情報を要求する通信 | 足りないデータをpeerへ問い合わせる |
この表で特に大切なのは、次の3点です。
1つ目は、nodeとpeerは視点が違う ことです。
ノードはネットワークの参加者そのものです。ピアは、自分のノードから見た直接の接続相手です。
2つ目は、broadcastやgossipは情報を広げる仕組みであって、正しさを保証する仕組みではない ことです。
届いた情報が有効かどうかは、各ノードの検証が必要です。
3つ目は、peer discoveryには入口が必要になる場合がある ことです。
P2Pだからといって、最初から何も頼らずに世界中のノードを見つけられるわけではありません。BitcoinのDNS seedやEthereumのbootnodeのように、最初の接続候補を得るための補助的な仕組みがあります。
3.9 この章のまとめ
この章では、P2Pネットワークを理解するための基本用語を整理しました。
P2Pネットワークは、単に「中央サーバーがない」という言葉だけでは理解しにくいです。
実際には、ノード、ピア、ブロードキャスト、gossip、peer discovery、mempool、request/responseなど、いくつかの部品が組み合わさって動いています。
この章で押さえたいポイントは、次の通りです。
| ポイント | 説明 |
|---|---|
| ノードはネットワーク参加者 | ブロックチェーンでは取引やブロックを受け取り、検証し、必要に応じて中継する |
| ピアは直接つながっている相手 | ネットワーク全体の全員ではなく、自分のノードから見た接続先 |
| ブロードキャストやgossipで情報が広がる | 取引やブロックはpeerからpeerへ伝わっていく |
| peer discoveryで接続先を見つける | DNS seedやbootnodeのような入口が使われる場合がある |
| mempoolはローカルな一時置き場 | 未承認取引を扱うが、全ノードで完全に同じとは限らない |
| request/responseで足りない情報を取りに行く | gossipだけでなく、特定peerへの問い合わせも使われる |
ここまでで、P2Pネットワークを読むための基本用語がそろいました。
次の章では、これらの用語を使いながら、なぜブロックチェーンでP2Pネットワークが必要になるのか を整理します。
単に「分散しているからP2Pが必要」という説明で終わらせず、取引、ブロック、検証、コンセンサスとの関係を見ていきます。
4. ブロックチェーンでP2Pネットワークが必要になる理由
この章では、ここまで整理したP2Pネットワークの用語を使いながら、ブロックチェーンでなぜP2Pネットワークが重要になるのかを見ていきます。
ポイントは、P2Pネットワークが 取引やブロックを広げるための土台 であり、正しさの判断は各ノードの検証やコンセンサスと組み合わさって行われる、という点です。
前の章では、node、peer、broadcast、gossip、peer discovery、mempool、request / response などの基本用語を整理しました。
ここからは、それらの用語をブロックチェーンの動きに重ねて考えていきます。
ブロックチェーンでは、誰かが取引を作ったとき、その取引が自分の手元だけに残っていても意味がありません。
他のノードへ伝わり、検証され、ブロックに取り込まれ、さらにそのブロックがネットワークへ広がる必要があります。
この「情報を広げる道」として使われるのが、P2Pネットワークです。
Bitcoin Developer Guideでは、Bitcoinのフルノードがブロックと取引を交換するためにP2Pネットワークを共同で維持すると説明されています。
また、Bitcoin.orgのフルノード解説でも、フルノードは取引とブロックを完全に検証し、多くのフルノードは他のノードから受け取った取引やブロックを検証して、さらに別のノードへ中継すると説明されています。
参考: Bitcoin Developer Guide: P2P Network
参考: Bitcoin.org: Running A Full Node
Ethereumでも同じように、ノード同士が情報を交換するためのnetworking layerがあります。Ethereum公式ドキュメントでは、networking layerはノードが互いを見つけ、情報を交換するためのプロトコル群であり、gossipとrequest / responseの両方を含むと説明されています。
参考: Ethereum Documentation: Networking layer
ただし、ここで大切なのは、P2Pネットワークだけで「正しい履歴」が決まるわけではないことです。
P2Pネットワークは、取引やブロックを広げます。
しかし、その取引が本当に有効か、そのブロックを受け入れてよいか、複数の履歴候補が出たときにどちらを採用するかは、検証ルールやコンセンサスの領域です。
ざっくり分けると、次のようになります。
| 役割 | ざっくりした問い | 担当するものの例 |
|---|---|---|
| 情報を届ける | 取引やブロックをどう広げるか | P2Pネットワーク、gossip、request / response |
| 情報を確認する | その取引やブロックはルールに合っているか | 各ノードの検証ルール |
| 履歴を選ぶ | 複数の候補があるとき、どれを正しい履歴として扱うか | コンセンサス、フォーク選択 |
身近な例でいうと、P2Pネットワークは「連絡網」に近いです。
連絡網は、情報をみんなへ届けるためにあります。
しかし、届いた情報が正しいかどうかは、別途確認する必要があります。
ブロックチェーンでも同じです。
P2Pネットワークは、取引やブロックを広げるために重要です。
一方で、届いた情報をそのまま信じるのではなく、各ノードが自分で検証することが大切です。
4.1 取引はまずネットワークへ広がる
まず、利用者が新しい取引を作った場面を考えます。
たとえば、AさんがBさんへ送金する取引を作ったとします。
この取引は、最初はAさんのウォレットや接続先ノードの近くにしかありません。
その取引を他のノードへ知らせることで、ネットワーク全体へ広がっていきます。
この図で大切なのは、取引が「広がる」だけではなく、各ノードが受け取った取引を検証する点です。
Bitcoinのホワイトペーパーでは、ネットワークの手順として、まず新しい取引がすべてのノードへブロードキャストされ、各ノードが新しい取引をブロックへ集める、という流れが説明されています。
参考: Bitcoin: A Peer-to-Peer Electronic Cash System
もちろん、現実のBitcoinネットワークでは、全ノードに一気に完全同時に届くというより、peerからpeerへ伝わっていくイメージです。
また、すべてのノードが常に同じmempoolを持っているわけでもありません。
ここでは、まず次の流れを押さえておきます。
1. 利用者が取引を作る
2. 取引が接続先ノードへ送られる
3. ノードが取引を検証する
4. 有効ならローカルmempoolへ入れる
5. 必要に応じてpeerへ知らせる
6. 他のノードも受け取り、検証し、さらに広げる
💡 豆知識
「ブロードキャスト」という言葉を聞くと、全員に一斉送信するイメージを持つかもしれません。
ただ、P2Pネットワークでは、多くの場合、自分のpeerへ知らせ、そのpeerがさらに別のpeerへ知らせることで広がっていきます。
そのため、実際には「連絡網で順番に広がる」イメージに近いです。
4.2 小さなコードで見る:取引がpeerへ広がる流れ
ここで、取引がP2Pネットワーク上で広がる様子を、簡単なPythonコードで見てみます。
このコードは、実際のBitcoinやEthereumの通信を再現するものではありません。
目的は、あるノードが取引を受け取り、それをpeerへ伝えると、数ステップでネットワーク内へ広がっていく感覚をつかむことです。
# P2Pネットワークで取引がpeerへ広がる様子を確認する学習用コードです。
# 実際のBitcoinやEthereumのP2Pプロトコルを再現するものではありません。
from collections import deque
# ノード同士の接続関係を、隣接リストとして表します。
# たとえば node_A は node_B と node_C に直接つながっています。
network = {
"node_A": ["node_B", "node_C"],
"node_B": ["node_A", "node_D"],
"node_C": ["node_A", "node_D", "node_E"],
"node_D": ["node_B", "node_C", "node_F"],
"node_E": ["node_C"],
"node_F": ["node_D"],
}
def propagate_transaction(start_node: str, tx_id: str) -> dict[str, int]:
"""
start_nodeからtx_idが広がる様子を、幅優先探索で単純化して表します。
戻り値:
各ノードが何ステップ目に取引を受け取ったかを表す辞書です。
注意:
- 実際のP2Pネットワークでは通信遅延、重複、検証、帯域制限などが関係します。
- ここでは「どの順番で届き得るか」を理解するために単純化しています。
"""
received_step = {start_node: 0} # start_nodeは0ステップ目に取引を知っているとします。
queue = deque([start_node]) # 次にpeerへ伝えるノードを入れるキューです。
while queue:
current = queue.popleft()
current_step = received_step[current]
# currentのpeerへ取引を知らせます。
for peer in network[current]:
# すでに受け取ったノードには、重複して処理させないようにします。
if peer in received_step:
continue
# peerは、currentより1ステップ後に取引を受け取ったとします。
received_step[peer] = current_step + 1
queue.append(peer)
return received_step
result = propagate_transaction(start_node="node_A", tx_id="tx_001")
print("取引 tx_001 を受け取ったステップ:")
for node, step in sorted(result.items(), key=lambda item: item[1]):
print(f"{node}: {step}ステップ目")
このコードでは、node_A が最初に tx_001 を知っている状態から始めています。
node_A は自分のpeerである node_B と node_C に知らせます。
その後、node_B や node_C がさらに自分のpeerへ知らせることで、取引がネットワークへ広がっていきます。
ここで使っている考え方は、グラフ探索の基本である 幅優先探索 に近いです。
ただし、実際のブロックチェーンネットワークでは、ここまできれいに順番通りに届くわけではありません。
通信遅延、ノードの帯域、peer数、重複、検証、DoS対策など、さまざまな要素があります。
それでも、P2Pネットワークの入口としては、次の感覚が大切です。
取引は、中央サーバーから全員へ一斉配信されるというより、ノードからpeerへ、さらにそのpeerから別のpeerへと広がっていきます。
4.3 ブロックもP2Pネットワークで広がる
取引だけでなく、ブロックもP2Pネットワークを通じて広がります。
Bitcoinのホワイトペーパーでは、各ノードが取引をブロックへ集め、難しいProof of Workを探し、Proof of Workを見つけたらそのブロックを全ノードへブロードキャストすると説明されています。
そして、各ノードは、ブロック内のすべての取引が有効で、まだ使われていない場合だけ、そのブロックを受け入れると説明されています。
参考: Bitcoin: A Peer-to-Peer Electronic Cash System
ここで重要なのは、ブロックが届いたからといって、必ず受け入れるわけではないことです。
ブロックを受け取ったノードは、次のような確認を行います。
| 確認すること | ざっくりした意味 |
|---|---|
| 前のブロックと正しくつながっているか | 履歴の途中に勝手なブロックを差し込んでいないか |
| ブロック形式がルールに合っているか | サイズ、フィールド、構造などが正しいか |
| 含まれる取引が有効か | 署名、残高、二重支払いなどに問題がないか |
| コンセンサス上の条件を満たすか | PoWやPoSなど、そのチェーンのルールに合っているか |
身近な例でいうと、共同編集メモに新しいページが送られてきたとき、単に「届いたから採用」するのではなく、前のページとのつながりや内容の妥当性を確認するようなものです。
このように、P2Pネットワークはブロックを広げます。
しかし、ブロックを受け入れるかどうかは、各ノードの検証にかかっています。
4.4 小さなコードで見る:無効なブロックは受け入れない
次に、ブロックを受け取ったノードが、簡単な検証をしてから受け入れる様子をコードで見てみます。
このコードも、実際のBitcoinやEthereumの検証処理ではありません。
実際には、署名、UTXO、アカウント状態、ガス、Proof of Work、Proof of Stake、ブロックヘッダなど、非常に多くの検証があります。
ここでは、学習用に次の2つだけを確認します。
- ブロックが前のブロックのハッシュを正しく参照しているか
- ブロック内の取引金額がすべて正の値か
# 受け取ったブロックを検証してからチェーンに追加する学習用コードです。
# 実際のBitcoinやEthereumのブロック検証を再現するものではありません。
from dataclasses import dataclass
from typing import List
@dataclass
class Block:
"""学習用のかなり単純化したブロックです。"""
block_id: str
previous_block_id: str
transactions: List[int]
def is_valid_block(block: Block, current_head: Block) -> bool:
"""
ブロックを受け入れてよいか、簡単なルールで確認します。
確認すること:
1. previous_block_id が現在の先頭ブロックを指しているか
2. transactions に0以下の金額が含まれていないか
注意:
- 実際のブロックチェーンでは、これ以外にも多くの検証があります。
- ここでは「届いたブロックをそのまま信じない」ことを確認するために単純化しています。
"""
if block.previous_block_id != current_head.block_id:
return False
if any(amount <= 0 for amount in block.transactions):
return False
return True
# 現在、自分のノードが正しいと見ているチェーンの先頭です。
current_head = Block(
block_id="block_100",
previous_block_id="block_099",
transactions=[500, 1200],
)
# P2Pネットワークから、3つのブロック候補が届いたとします。
received_blocks = [
Block(block_id="block_101_A", previous_block_id="block_100", transactions=[300, 700]),
Block(block_id="block_101_B", previous_block_id="block_999", transactions=[400]), # 前のブロックが合わない
Block(block_id="block_101_C", previous_block_id="block_100", transactions=[100, -50]), # 無効な取引を含む
]
for block in received_blocks:
if is_valid_block(block, current_head):
print(f"{block.block_id}: 有効なブロックとして受け入れます。")
# 学習用なので、最初に有効だったブロックを先頭にします。
current_head = block
else:
print(f"{block.block_id}: 無効なブロックとして受け入れません。")
このコードでは、P2Pネットワークから複数のブロック候補が届いたように見せています。
block_101_A は、現在の先頭 block_100 を正しく参照していて、取引金額も正なので受け入れます。
一方、block_101_B は前のブロックの参照が合っていません。
block_101_C は、取引に負の値が含まれているため、学習用ルールでは無効としています。
ここで伝えたいのは、次の点です。
P2Pネットワークでは、正しい情報も間違った情報も届く可能性があります。
だからこそ、各ノードが自分で検証することが大切です。
4.5 P2Pは「通信」、検証は「判断」、コンセンサスは「履歴選び」
ここまでで、取引やブロックがP2Pネットワークで広がることを見ました。
ただし、P2Pネットワークを学ぶときに混同しやすいことがあります。
それは、P2Pネットワーク、検証、コンセンサスをまとめて同じもののように扱ってしまうことです。
この3つは関係していますが、役割は違います。
| 観点 | 役割 | 身近な例 | ブロックチェーンでの例 |
|---|---|---|---|
| P2Pネットワーク | 情報を届ける | 連絡網で内容を広げる | 取引やブロックをpeerへ伝える |
| 検証 | 届いた情報が正しいか確認する | 連絡内容が本当に先生からのものか確認する | 署名、残高、ブロック形式などを確認する |
| コンセンサス | 複数の履歴候補から採用するものを決める | 複数の版がある議事録から正式版を決める | PoW、PoS、フォーク選択など |
P2Pネットワークは、あくまで情報を届けるための仕組みです。
もしP2Pネットワークがなければ、取引やブロックが一部の参加者にしか届かず、同じ台帳を共有しにくくなります。
一方で、P2Pネットワークだけでは、届いた情報が正しいかどうかは分かりません。
そのため、各ノードは検証ルールに従って、取引やブロックを確認します。
さらに、ネットワークの遅延などで複数のブロック候補が出た場合には、どの履歴を伸ばすかを決める必要があります。
ここでコンセンサスやフォーク選択が関係します。
この関係を図にすると、次のようになります。
この図はかなり単純化していますが、P2Pネットワークの立ち位置を理解するには役立ちます。
💡 豆知識
BitcoinのホワイトペーパーのタイトルにはPeer-to-Peer Electronic Cash Systemとあります。
ただし、Bitcoinの仕組みはP2P通信だけで成り立っているわけではありません。
取引の署名、ハッシュ、Proof of Work、ブロックの検証、チェーン選択などが組み合わさることで、中央の管理者だけに頼らない電子的な支払いシステムを目指しています。
4.6 情報が届く速さは、ネットワークの見え方に影響する
P2Pネットワークでは、情報がすべてのノードへ完全に同時に届くわけではありません。
あるノードには新しい取引やブロックがすぐ届いても、別のノードには少し遅れて届くことがあります。
さらに、ノードがどのpeerとつながっているかによっても、情報の届き方は変わります。
この遅れは、ブロックチェーンの動きに影響します。
たとえば、ほぼ同じタイミングで2つのブロックが作られた場合、一部のノードはブロックAを先に受け取り、別のノードはブロックBを先に受け取るかもしれません。
すると、一時的にネットワーク上で見えている履歴が分かれることがあります。
このような一時的な分岐が起きたとき、どちらの履歴を伸ばすかを決めるルールが必要になります。
ここで、PoWやPoS、フォーク選択の話につながります。
つまり、P2Pネットワークの伝播速度やpeerの選び方は、単なる通信の話にとどまりません。
ブロックがどれだけ早く広がるか、ノード間でどのくらい情報の差が出るかは、フォークや確認時間にも関係します。
ただし、この記事ではネットワーク測定や伝播遅延の厳密な分析には深入りしません。
まずは、次の点を押さえておきます。
P2Pネットワークでは、情報はpeerを通じて順に広がります。
そのため、ノードごとに一時的に見えている情報が違う場合があります。
4.7 小さなコードで見る:ノードごとに取引が届くタイミングは違う
次のコードでは、ノードごとに取引が届くタイミングが違う様子を、かなり単純化して見てみます。
ここでは、各接続に「遅延」を設定し、ある取引がどのノードへ何秒後に届くかを計算します。
実際のネットワーク遅延はもっと複雑ですが、情報が完全同時には届かないことを理解するには十分です。
# P2Pネットワークで情報が届くタイミングに差が出ることを確認する学習用コードです。
# 実際のBitcoinやEthereumのネットワーク遅延を再現するものではありません。
import heapq
# ノード間の接続と、説明用の遅延秒数を表します。
# たとえば node_A から node_B へは1秒で情報が届く、というイメージです。
network_with_delay = {
"node_A": [("node_B", 1), ("node_C", 3)],
"node_B": [("node_A", 1), ("node_D", 2)],
"node_C": [("node_A", 3), ("node_D", 1), ("node_E", 2)],
"node_D": [("node_B", 2), ("node_C", 1), ("node_F", 1)],
"node_E": [("node_C", 2)],
"node_F": [("node_D", 1)],
}
def calculate_arrival_times(start_node: str) -> dict[str, int]:
"""
start_nodeから情報が広がったとき、各ノードに最短で何秒後に届くかを計算します。
ここでは、重み付きグラフの最短距離を求めるDijkstra法の考え方を使っています。
ただし、実際のP2Pネットワークでは、再送、検証時間、帯域、混雑なども関係します。
"""
arrival_times = {start_node: 0}
priority_queue = [(0, start_node)] # (到着時刻, ノード名) の順で管理します。
while priority_queue:
current_time, current_node = heapq.heappop(priority_queue)
# すでにもっと早い時刻で到着できる経路を知っている場合は、古い情報を無視します。
if current_time > arrival_times[current_node]:
continue
# current_nodeのpeerへ情報を送ります。
for peer, delay in network_with_delay[current_node]:
new_time = current_time + delay
# より早くpeerへ届く経路を見つけた場合だけ更新します。
if peer not in arrival_times or new_time < arrival_times[peer]:
arrival_times[peer] = new_time
heapq.heappush(priority_queue, (new_time, peer))
return arrival_times
arrival_times = calculate_arrival_times(start_node="node_A")
print("node_Aから送られた取引が届く時刻:")
for node, time in sorted(arrival_times.items(), key=lambda item: item[1]):
print(f"{node}: {time}秒後")
このコードでは、node_A から送られた取引が、接続関係と遅延に応じて各ノードへ届いていきます。
結果を見ると、すべてのノードに同じタイミングで届くわけではないことが分かります。
近いpeerにはすぐ届き、遠いpeerには少し遅れて届きます。
このズレがあるため、ある瞬間だけを見ると、ノードごとに見えている取引やブロックが違うことがあります。
ブロックチェーンでは、このようなズレがある前提で、検証やコンセンサスの仕組みが設計されています。
4.8 P2Pネットワークがあるから、各ノードが自分で確認できる
P2Pネットワークの大きな意味は、単に情報を広げることだけではありません。
各ノードが取引やブロックを受け取り、自分で検証できることも重要です。
もし中央サーバーだけが正しい履歴を管理しているなら、利用者はそのサーバーを信じる必要があります。
一方、ブロックチェーンでは、ノードが自分でデータを受け取り、ルールに従って検証できます。
NISTIR 8202では、ブロックチェーンは中央の保管場所や中央権限なしに分散的に実装される台帳として説明されています。
また、通常の動作では、ブロックチェーンネットワーク上で公開された取引を変更できないようにする仕組みとして整理されています。
参考: NISTIR 8202: Blockchain Technology Overview
もちろん、これは「すべてのノードが完全に同じ役割を持つ」という意味ではありません。
フルノード、軽量クライアント、マイナー、バリデータ、アーカイブノードなど、役割はチェーンや実装によって異なります。
ただし、P2Pネットワークがあることで、少なくとも次のような構造が作りやすくなります。
| できること | 意味 |
|---|---|
| 複数のノードが同じ情報を受け取れる | 1か所だけに記録を閉じ込めない |
| 各ノードが検証できる | 届いた情報をそのまま信じずに確認できる |
| 一部のノードが止まっても別経路が残る場合がある | 単一障害点を減らしやすい |
| 誰かが不正な情報を流しても拒否できる | 検証ルールに合わない取引やブロックを受け入れない |
ここが、ブロックチェーンにおけるP2Pネットワークの重要な部分です。
P2Pネットワークは、単なる通信方式ではありません。
各ノードが自分で受け取り、自分で確認し、必要なら他のpeerへ中継できるようにするための土台でもあります。
4.9 ただし、P2Pだけではブロックチェーンにならない
ここまで読むと、「P2Pネットワークがあればブロックチェーンになる」と思うかもしれません。
しかし、それは少し違います。
P2Pネットワークは、情報を広げるための仕組みです。
ブロックチェーンとして動くには、それに加えて、取引の検証、ブロック構造、ハッシュ、署名、コンセンサス、フォーク選択、インセンティブ設計などが必要になります。
P2Pだけの仕組みと、ブロックチェーンの違いをかなり大きく整理すると、次のようになります。
| 観点 | P2Pネットワークだけ | ブロックチェーン |
|---|---|---|
| 主な役割 | 参加者同士で情報をやり取りする | 共有台帳として履歴を積み上げる |
| 情報の正しさ | 別途確認が必要 | 取引・ブロックの検証ルールを持つ |
| 履歴の順序 | 通信だけでは決まらない | ブロックの順序やフォーク選択がある |
| 改ざん検知 | 通信方式だけでは不十分 | ハッシュや署名などが関係する |
| 合意形成 | 通信だけでは不十分 | PoWやPoSなどの仕組みが関係する |
たとえるなら、P2Pネットワークは「みんなに連絡を回す仕組み」です。
ブロックチェーンは、そのうえで「どの記録を正式な履歴として積み上げるか」まで決める仕組みです。
この違いを押さえておくと、P2Pネットワークを過大評価しすぎず、逆に役割を小さく見すぎることも避けられます。
💡 豆知識
P2Pという考え方自体は、ブロックチェーン専用のものではありません。
ファイル共有、通話、分散ストレージ、メッセージングなど、さまざまな分野で使われてきました。
ブロックチェーンでは、そのP2P通信の上に、取引検証、ブロック、ハッシュ、署名、コンセンサスなどを組み合わせています。
4.10 この章のまとめ
この章では、ブロックチェーンでP2Pネットワークがなぜ必要になるのかを整理しました。
ブロックチェーンでは、取引やブロックが一部の参加者だけに届いても、共有台帳としてはうまく機能しません。
取引がネットワークへ広がり、各ノードが検証し、ブロックがさらに広がり、複数の履歴候補がある場合はコンセンサスやフォーク選択によって整理されていきます。
この章で押さえたいポイントは、次の通りです。
| ポイント | 説明 |
|---|---|
| P2Pネットワークは取引やブロックを広げる土台 | 利用者が作った取引や、作成されたブロックをpeerへ伝える |
| 各ノードは届いた情報を検証する | P2Pで届いたからといって、そのまま信じるわけではない |
| ブロックもP2Pで広がる | 作成されたブロックはノード間で共有され、検証される |
| P2P、検証、コンセンサスは役割が違う | 通信、判断、履歴選びを分けて考えると理解しやすい |
| 情報は完全同時には届かない | ノードごとに一時的に見えている取引やブロックが違うことがある |
| P2Pだけではブロックチェーンにならない | 署名、ハッシュ、ブロック構造、コンセンサスなどと組み合わさる必要がある |
ここまでで、P2Pネットワークがブロックチェーンの中でどのような役割を持つのかが見えてきました。
次の章では、より具体的に、情報が広がる流れを図とコードで見る ことに焦点を当てます。
取引やブロックがpeerからpeerへ伝わる様子、重複を避ける考え方、途中でノードが止まった場合の影響などを、もう少し手を動かしながら確認していきます。
5. 情報が広がる流れを図とコードで見る
この章では、P2Pネットワーク上で情報がどのように広がるのかを、図と小さなPythonコードで確認します。
ポイントは、全員へ一斉に送るというより、近くのpeerへ伝え、それを受け取ったpeerがさらに別のpeerへ伝えていく という流れです。
前の章では、ブロックチェーンでP2Pネットワークが必要になる理由を整理しました。
取引やブロックは、作られた瞬間に世界中の全ノードへ同時に届くわけではありません。
あるノードが情報を受け取り、接続しているpeerへ伝え、そのpeerがさらに別のpeerへ伝えることで、少しずつネットワーク全体へ広がっていきます。
これは、クラスの連絡を「代表者から全員へ一斉送信する」のではなく、近くの友人同士で「この連絡見た?」と伝え合う様子に似ています。
この図では、最初に情報を知ったノード A が、直接つながっている B と C へ情報を伝えています。
その後、B や C がさらに別のpeerへ情報を伝えます。
Bitcoin Developer Guideでは、Bitcoinのフルノードがブロックや取引を交換するためにP2Pネットワークを共同で維持すると説明されています。
また、Ethereum公式ドキュメントでは、ノード同士が互いを見つけ、gossipやリクエスト/レスポンスによって情報を交換すると説明されています。
参考: Bitcoin Developer Guide: P2P Network
参考: Ethereum Documentation: Networking layer
ただし、この章で扱うコードは、実際のBitcoin CoreやEthereumクライアントの実装ではありません。
あくまで、P2Pネットワークにおける情報伝播の考え方を理解するための学習用コードです。
5.1 まずは「連絡が広がる順番」を考える
P2Pネットワークでは、1つの情報がpeerからpeerへ広がります。
たとえば、研究室で「明日の集合時間が10時に変わりました」という連絡を回すとします。
最初にAさんがその情報を知っていて、AさんはBさんとCさんに伝えます。
BさんはDさんとEさんに伝え、CさんはFさんに伝える、という流れです。
このような広がり方は、次のように考えられます。
| 順番 | 情報を知る人 | 何が起きているか |
|---|---|---|
| 0 | Aさん | 最初に情報を持っている |
| 1 | Bさん、Cさん | Aさんから直接聞く |
| 2 | Dさん、Eさん、Fさん | BさんやCさんから聞く |
| 3 | Gさん | Eさんから聞く |
このように、情報は一気に全員へ届くのではなく、接続関係をたどって段階的に広がります。
ブロックチェーンでも、取引やブロックは同じようにネットワーク上を伝わっていきます。
あるノードが新しい取引を受け取ると、それを検証したうえで、接続しているpeerへ伝えます。
それを受け取ったpeerも、必要に応じてさらに別のpeerへ伝えます。
ここで大切なのは、届いた情報をそのまま何でも広げるわけではない という点です。
実際のブロックチェーンノードは、取引やブロックがルールに合っているかを確認しながら扱います。
5.2 小さなコードで見る:情報がpeerへ広がる流れ
まずは、P2Pネットワーク上で1つのメッセージがどの順番で広がるかを、Pythonで確認してみます。
このコードでは、ノード同士の接続関係を辞書で表します。
そして、最初に情報を持っているノードから、接続しているpeerへ順番に情報を広げていきます。
# P2Pネットワーク上で、情報がpeerからpeerへ広がる様子を確認する学習用コードです。
# 実際のBitcoinやEthereumの通信処理を再現するものではありません。
from collections import deque
# ノード同士の接続関係を表します。
# たとえば "A": ["B", "C"] は、AがBとCに接続していることを意味します。
network = {
"A": ["B", "C"],
"B": ["A", "D", "E"],
"C": ["A", "F"],
"D": ["B"],
"E": ["B", "G"],
"F": ["C"],
"G": ["E"],
}
def spread_message(start_node: str, message: str) -> None:
"""
start_nodeからmessageを広げる簡易シミュレーションです。
queue:
これから処理するノードを入れておく待ち行列です。
seen:
すでにメッセージを受け取ったノードを記録します。
同じノードを何度も処理しないために使います。
"""
queue = deque([start_node])
seen = {start_node}
print(f"最初に {start_node} がメッセージを持っています: {message}")
while queue:
current = queue.popleft()
# currentに接続しているpeerを順番に見ます。
for peer in network[current]:
# すでに受け取ったpeerには、同じメッセージを再処理させません。
if peer in seen:
continue
print(f"{current} -> {peer}: {message}")
seen.add(peer)
queue.append(peer)
spread_message("A", "明日の集合時間は10時です")
このコードでは、A から始まったメッセージが、B、C、D、E、F、G へ順番に広がっていきます。
ここでは、幅優先探索に近い形で「近いpeerから順に情報が届く」ようにしています。
実際のP2Pネットワークでは、通信遅延、接続数、ノードの性能、実装ごとのルールなどが関係するため、このコードよりずっと複雑です。
ただし、入口としては次の感覚が大切です。
P2Pネットワークでは、情報は中央から一斉配信されるのではなく、接続しているpeerをたどって広がっていきます。
5.3 全員に毎回送ると、同じ情報が何度も届く
P2Pネットワークでは、同じ情報が複数の経路から届くことがあります。
たとえば、AさんからBさんに連絡が届き、BさんからDさんに届いたとします。
同時に、AさんからCさんにも届き、CさんからDさんにも届くかもしれません。
この場合、Dさんは同じ連絡を2回受け取る可能性があります。
身近な連絡なら、「それ、もう聞いたよ」と言えば済みます。
P2Pネットワークでも似たように、すでに見たメッセージや取引を記録しておき、同じ情報を何度も処理しないようにします。
これをしないと、同じ情報が何度も回り続け、ネットワークの帯域やノードの処理時間を無駄に使ってしまいます。
5.4 小さなコードで見る:重複したメッセージを無視する
次のコードでは、同じメッセージが複数経路から届いた場合に、2回目以降は処理しないようにします。
ポイントは、メッセージごとにIDを付け、各ノードが「このIDはもう見たか」を記録することです。
# 同じメッセージが複数経路から届いたとき、重複処理を避ける学習用コードです。
# 実際のブロックチェーンでは、取引IDやブロックハッシュなどを使って重複を識別します。
network = {
"A": ["B", "C"],
"B": ["A", "D"],
"C": ["A", "D"],
"D": ["B", "C"],
}
# 各ノードが、すでに見たメッセージIDを記録するための辞書です。
seen_messages = {
node: set() for node in network
}
def receive_message(node: str, from_node: str, message_id: str, body: str) -> None:
"""
nodeがfrom_nodeからメッセージを受け取ったときの処理です。
すでに同じmessage_idを見たことがある場合は、重複として無視します。
初めて見るmessage_idなら、自分のseen_messagesへ記録し、peerへ転送します。
"""
if message_id in seen_messages[node]:
print(f"{node}: {message_id} はすでに見たので無視します")
return
print(f"{node}: {from_node} から {message_id} を受け取りました: {body}")
seen_messages[node].add(message_id)
# 自分に送ってきた相手へすぐ送り返すと無駄が多いため、ここでは除外します。
for peer in network[node]:
if peer == from_node:
continue
receive_message(peer, node, message_id, body)
# Aが最初にメッセージを知った想定です。
seen_messages["A"].add("msg-001")
# AがBとCに同じメッセージを送ります。
receive_message("B", "A", "msg-001", "明日の集合時間は10時です")
receive_message("C", "A", "msg-001", "明日の集合時間は10時です")
このコードでは、D が B 経由と C 経由で同じメッセージを受け取る可能性があります。
しかし、seen_messages によって、2回目以降は重複として無視されます。
実際のブロックチェーンでも、取引やブロックには識別子として使える値があります。
たとえば、Bitcoinでは取引やブロックを識別するためにハッシュが使われ、Ethereumでも取引ハッシュやブロックハッシュが扱われます。
このような識別子があるからこそ、ノードは「これは新しい情報か」「すでに知っている情報か」を判断できます。
5.5 ただし、毎回すべての中身を送るとは限らない
ここまでの例では、メッセージ本文そのものをpeerへ渡していました。
しかし、実際のP2Pネットワークでは、毎回すべてのデータ本体をいきなり送るとは限りません。
大きなブロックや多数の取引を、全peerへ無条件に送り続けると、通信量が大きくなりすぎるからです。
BitcoinのP2Pプロトコルでは、inv や getdata のようなメッセージが使われます。
ざっくり言えば、inv は「こういうデータを持っています」というお知らせで、getdata は「それが欲しいので送ってください」という要求です。
参考: Bitcoin Developer Reference: P2P Network
身近な例で言うと、いきなり資料全文を全員へ送りつけるのではなく、まず「新しい資料があります」と知らせて、必要な人が「その資料をください」と取りに行くようなものです。
| ステップ | Bitcoin風のイメージ | 身近な例 |
|---|---|---|
| 1 |
inv でデータの存在を知らせる |
「新しい資料があります」と知らせる |
| 2 | 必要なら getdata で要求する |
「その資料をください」と頼む |
| 3 |
tx や block で本体を受け取る |
実際の資料を受け取る |
この流れにすると、すでに知っている取引やブロックを何度も受け取る無駄を減らしやすくなります。
💡 豆知識
P2Pネットワークでは、「データ本体を送る」だけでなく、「データを持っていることを知らせる」「必要なデータだけ要求する」といった工夫も重要です。
これは、友人からいきなり重い添付ファイルを何度も送られるより、まず「必要なら送るよ」と言ってもらう方が助かるのに似ています。
5.6 小さなコードで見る:inv と getdata の雰囲気
次のコードでは、Bitcoinの inv と getdata の雰囲気を、かなり単純化して表します。
実際のBitcoinプロトコルでは、メッセージ形式、インベントリベクトル、取引やブロックの識別子、検証処理などが細かく決まっています。
ここでは、「まずIDだけ知らせ、必要なときだけ本体を要求する」という考え方を見るための学習用コードです。
# Bitcoinのinv/getdataの考え方を、かなり単純化して理解するための学習用コードです。
# 実際のBitcoin P2Pメッセージ形式を再現するものではありません。
class SimpleNode:
"""取引IDと取引本体を持つ、学習用の簡易ノードです。"""
def __init__(self, name: str):
self.name = name
self.transactions = {}
def add_transaction(self, tx_id: str, tx_body: str) -> None:
"""ノードが取引本体を保存します。"""
self.transactions[tx_id] = tx_body
def send_inv(self, peer: "SimpleNode", tx_id: str) -> None:
"""
peerに「このtx_idの取引を知っています」と知らせます。
実際のBitcoinでは、invメッセージがこのような役割を持ちます。
"""
print(f"{self.name} -> {peer.name}: inv({tx_id})")
peer.receive_inv(self, tx_id)
def receive_inv(self, peer: "SimpleNode", tx_id: str) -> None:
"""
peerからinvを受け取ったときの処理です。
まだ自分が知らない取引なら、getdataで本体を要求します。
"""
if tx_id in self.transactions:
print(f"{self.name}: {tx_id} はすでに持っているので要求しません")
return
print(f"{self.name} -> {peer.name}: getdata({tx_id})")
tx_body = peer.respond_getdata(tx_id)
if tx_body is not None:
self.transactions[tx_id] = tx_body
print(f"{self.name}: {tx_id} の本体を受け取りました")
def respond_getdata(self, tx_id: str) -> str | None:
"""
getdataに対して、取引本体を返します。
指定されたtx_idを持っていない場合はNoneを返します。
"""
return self.transactions.get(tx_id)
node_a = SimpleNode("NodeA")
node_b = SimpleNode("NodeB")
# NodeAだけが新しい取引を知っている状態にします。
node_a.add_transaction("tx-001", "Alice -> Bob: 1 BTC")
# NodeAは、まず取引本体ではなくtx_idだけをNodeBへ知らせます。
node_a.send_inv(node_b, "tx-001")
このコードでは、NodeA がいきなり取引本体を送るのではなく、まず inv(tx-001) として「この取引を知っています」と知らせています。
NodeB はまだその取引を知らないため、getdata(tx-001) で本体を要求します。
これにより、すでに知っている取引を何度も受け取る無駄を減らしやすくなります。
5.7 gossipは「全員に同じ本文を毎回投げ続ける」だけではない
gossip という言葉は、日本語では「うわさ話」のように訳されることがあります。
P2Pネットワークの文脈では、あるノードが知った情報を、接続しているpeerへ少しずつ広げていく考え方として使われます。
ただし、gossipは単に「全員へ何でも送り続ける」という意味ではありません。
実際のプロトコルでは、次のような工夫が入ることがあります。
| 工夫 | ざっくりした意味 | 目的 |
|---|---|---|
| fanout | 送る相手を一部のpeerに絞る | 通信量を抑える |
| message id | メッセージを識別する | 重複処理を避ける |
| metadata gossip | 本体ではなく、見たメッセージの情報だけ伝える | 必要なものだけ取得する |
| peer score | peerの振る舞いを評価する | 問題のあるpeerを避けやすくする |
Ethereum公式ドキュメントでは、Ethereumのネットワーク層でgossipとリクエスト/レスポンスの両方が使われると説明されています。
また、一般的なP2P pubsubの実装例であるlibp2pのGossipsubでは、mesh内のpeerへメッセージを伝えつつ、gossipでメッセージのメタデータを広げる考え方が仕様として説明されています。
参考: Ethereum Documentation: Networking layer
参考: libp2p specs: Gossipsub v1.1
ただし、ここで注意したいのは、GossipsubがそのままBitcoinのP2Pネットワークを表しているわけではないことです。
チェーンやクライアントによって、実際に使われるプロトコルや細かい設計は異なります。
この章では、gossipを次のようにざっくり押さえておきます。
gossipは、ノードが知った情報を接続先へ広げ、ネットワーク全体へ伝播させるための考え方です。
ただし、効率や安全性のために、送信先の選び方、重複排除、要求応答、peer評価などの工夫が組み合わされることがあります。
5.8 小さなコードで見る:fanoutを絞ったgossip
次のコードでは、各ノードがすべてのpeerへ送るのではなく、接続しているpeerの一部だけへメッセージを送るようにします。
これは、gossipの雰囲気をつかむための簡易モデルです。
実際のGossipsubやEthereumのgossip処理を再現するものではありません。
# fanoutを絞ったgossipの雰囲気を理解するための学習用コードです。
# 実際のGossipsubやEthereumのgossipプロトコルではありません。
import random
from collections import defaultdict
network = {
"A": ["B", "C", "D"],
"B": ["A", "E", "F"],
"C": ["A", "F", "G"],
"D": ["A", "G"],
"E": ["B", "H"],
"F": ["B", "C", "H"],
"G": ["C", "D", "H"],
"H": ["E", "F", "G"],
}
def gossip_rounds(start_node: str, message_id: str, rounds: int = 4, fanout: int = 2) -> None:
"""
start_nodeからmessage_idをgossipで広げる簡易シミュレーションです。
rounds:
何回のラウンドで広げるかを表します。
fanout:
各ノードが1ラウンドで何人のpeerへ送るかを表します。
すべてのpeerではなく、一部のpeerだけへ送ることで通信量を抑えるイメージです。
known:
各ノードが知っているメッセージIDを記録します。
"""
known = defaultdict(set)
known[start_node].add(message_id)
print(f"round 0: {start_node} が {message_id} を知っています")
for round_number in range(1, rounds + 1):
deliveries = []
# 現時点でメッセージを知っているノードだけが送信候補になります。
for node in list(network.keys()):
if message_id not in known[node]:
continue
peers = network[node]
# 接続しているpeerのうち、fanoutで指定した数だけランダムに選びます。
# peer数がfanoutより少ない場合は、全peerを候補にします。
selected_peers = random.sample(peers, k=min(fanout, len(peers)))
for peer in selected_peers:
if message_id not in known[peer]:
deliveries.append((node, peer))
# このラウンドで届いたメッセージを反映します。
for sender, receiver in deliveries:
known[receiver].add(message_id)
print(f"round {round_number}: {sender} -> {receiver}")
reached = [node for node in network if message_id in known[node]]
print("\n最終的にメッセージを知っているノード:", sorted(reached))
random.seed(7) # 実行結果をある程度再現しやすくするため、乱数の種を固定します。
gossip_rounds("A", "msg-001", rounds=4, fanout=2)
このコードでは、各ノードがすべてのpeerへ送るのではなく、ランダムに選んだ一部のpeerへ送っています。
それでも、ラウンドを重ねることで、メッセージは少しずつ広がっていきます。
ここで大切なのは、通信量と到達の速さにはトレードオフがあることです。
| 方針 | 良い点 | 注意点 |
|---|---|---|
| すべてのpeerへ送る | 情報が早く広がりやすい | 通信量が増えやすい |
| 一部のpeerへ送る | 通信量を抑えやすい | 広がるまでに時間がかかる場合がある |
| 必要なものだけ要求する | 無駄なデータ転送を減らしやすい | 往復のやり取りが必要になる |
実際のP2Pネットワークでは、このバランスを取るためにさまざまな工夫が入ります。
5.9 情報は完全同時には届かない
P2Pネットワークでは、ノードごとに情報が届くタイミングが違います。
これは、インターネット上で物理的な距離、回線品質、ノードの処理性能、接続peerの違いなどがあるためです。
同じ取引やブロックでも、あるノードにはすぐ届き、別のノードには少し遅れて届くことがあります。
この「届くタイミングの違い」は、ブロックチェーンでは重要です。
たとえば、あるノードは新しいブロック Block A を先に受け取り、別のノードは同じ高さの Block B を先に受け取るかもしれません。
一時的には、どちらのブロックを先に見ているかがノードごとに違う状態になります。
このような一時的な食い違いがあるため、ブロックチェーンではP2P通信だけでなく、フォーク選択やコンセンサスのルールが必要になります。
5.10 小さなコードで見る:届くタイミングの違い
次のコードでは、各ノードへ情報が届くまでの時間を簡単にシミュレーションします。
接続ごとに遅延を設定し、最初のノードから各ノードへ最短で何秒後に届くかを計算します。
これは、グラフ上の最短経路を求めるDijkstra法に近い考え方です。
# P2Pネットワークで、ノードごとに情報が届くタイミングが違うことを確認する学習用コードです。
# 実際のネットワーク遅延やブロック伝播時間を正確に表すものではありません。
import heapq
# 接続関係と通信遅延を表します。
# たとえば A -> B の遅延が2秒、A -> C の遅延が5秒という意味です。
network_with_delay = {
"A": [("B", 2), ("C", 5)],
"B": [("A", 2), ("D", 3), ("E", 4)],
"C": [("A", 5), ("F", 2)],
"D": [("B", 3)],
"E": [("B", 4), ("F", 1)],
"F": [("C", 2), ("E", 1)],
}
def compute_arrival_times(start_node: str) -> dict[str, int]:
"""
start_nodeから各ノードへ、最短で何秒後に情報が届くかを計算します。
priority_queue:
到着時刻が早いノードから処理するための優先度付きキューです。
arrival_times:
各ノードに情報が届く最短時刻を記録します。
"""
arrival_times = {node: float("inf") for node in network_with_delay}
arrival_times[start_node] = 0
priority_queue = [(0, start_node)]
while priority_queue:
current_time, current_node = heapq.heappop(priority_queue)
# すでにより早い到着時刻が分かっている場合は、古い候補なので無視します。
if current_time > arrival_times[current_node]:
continue
# 隣接するpeerへ情報を伝える場合の到着時刻を計算します。
for peer, delay in network_with_delay[current_node]:
new_time = current_time + delay
if new_time < arrival_times[peer]:
arrival_times[peer] = new_time
heapq.heappush(priority_queue, (new_time, peer))
return arrival_times
arrival_times = compute_arrival_times("A")
for node, time in sorted(arrival_times.items()):
print(f"{node}: {time}秒後に到着")
このコードを実行すると、同じメッセージでもノードごとに届く時間が違うことが分かります。
ここでは単純化して「最短で届く時間」だけを見ています。
実際には、ノードが一時的に混雑していたり、接続が切れていたり、メッセージの検証に時間がかかったりすることもあります。
このような遅延があるため、ブロックチェーンでは「ある瞬間に全ノードが完全に同じ情報を持っている」とは限りません。
だからこそ、後続ブロック、承認数、ファイナリティ、フォーク選択といった考え方が重要になります。
5.11 一部のノードが止まっても、別経路で届く場合がある
P2Pネットワークの強みの一つは、一部のノードや経路が止まっても、別の経路から情報が届く場合があることです。
たとえば、中央サーバー型では、中心のサーバーが止まると全体に大きな影響が出ます。
一方、P2P型では、ネットワークが十分につながっていれば、あるノードが止まっても別経路で情報が広がる可能性があります。
上の図では、B が止まっても、A -> E -> F -> D の経路が残っていれば、情報は別ルートで届く可能性があります。
ただし、ここでも「P2Pなら必ず止まらない」とは言えません。
接続が偏っていたり、重要なノードに依存していたり、ネットワークが分断されたりすると、情報が届かない範囲が出る可能性があります。
5.12 小さなコードで見る:ノード停止時の到達範囲
次のコードでは、一部のノードが停止した場合に、情報がどこまで届くかを確認します。
# 一部のノードが停止したとき、情報がどこまで届くかを確認する学習用コードです。
# 実際のブロックチェーンネットワークの耐障害性を評価するものではありません。
from collections import deque
network = {
"A": ["B", "E"],
"B": ["A", "C"],
"C": ["B", "D"],
"D": ["C", "F"],
"E": ["A", "F"],
"F": ["E", "D"],
}
def reachable_nodes(start_node: str, offline_nodes: set[str]) -> set[str]:
"""
start_nodeから到達できるノードを調べます。
offline_nodes:
停止しているノードの集合です。
停止中のノードは、情報を受け取ることも中継することもできないものとして扱います。
"""
if start_node in offline_nodes:
return set()
visited = {start_node}
queue = deque([start_node])
while queue:
current = queue.popleft()
for peer in network[current]:
# 停止しているpeerには送れないものとして扱います。
if peer in offline_nodes:
continue
if peer not in visited:
visited.add(peer)
queue.append(peer)
return visited
# Bが停止している場合でも、A->E->F->D->C のような別経路が残っているか確認します。
offline_nodes = {"B"}
reached = reachable_nodes("A", offline_nodes)
print("停止しているノード:", sorted(offline_nodes))
print("Aから到達できるノード:", sorted(reached))
このコードでは、B が停止していても、A から E、F、D、C へ到達できる可能性があります。
もちろん、ネットワークの形によって結果は変わります。
たとえば、B だけが橋渡し役になっている構造なら、B が止まることでネットワークが分断されるかもしれません。
つまり、P2Pネットワークの耐障害性は、単に「P2Pであること」だけでなく、接続関係がどれだけ多様で偏っていないか にも左右されます。
5.13 情報伝播を見ると、P2Pの強みと限界が見えてくる
ここまで、情報がpeerからpeerへ広がる様子を見てきました。
P2Pネットワークには、中央の1か所だけに頼らず情報を広げられるという強みがあります。
一部のノードが止まっても、別経路が残っていれば情報が届く可能性があります。
一方で、限界もあります。
| 観点 | 強み | 注意点 |
|---|---|---|
| 情報伝播 | peerをたどって広がる | 全ノードへ完全同時には届かない |
| 耐障害性 | 一部の停止に強い場合がある | 接続が偏ると分断されやすい |
| 通信量 | 一部peerへ送る工夫ができる | 重複や無駄な転送を抑える必要がある |
| 正しさ | 多くのノードが検証に参加できる | P2P通信だけでは正しさは決まらない |
| セキュリティ | 中央1点への依存を減らせる | Sybil攻撃やEclipse攻撃などのリスクがある |
この章で特に大切なのは、P2Pネットワークを「ただつながっているだけの図」として見ないことです。
実際には、次のような処理が組み合わさっています。
- どのpeerへ送るかを決める
- すでに見た情報を重複処理しない
- 必要なデータだけ要求する
- 届いた情報を検証する
- 遅延や一時的な食い違いを前提にする
- 一部のノード停止や接続切れに備える
このように見ると、P2Pネットワークは、ブロックチェーンの「裏側で静かに情報を運ぶ仕組み」として重要な役割を持っていることが分かります。
5.14 この章のまとめ
この章では、P2Pネットワーク上で情報がどのように広がるのかを、図とコードで確認しました。
取引やブロックは、中央のサーバーから全員へ一斉配信されるのではなく、ノードが接続しているpeerへ伝え、それを受け取ったpeerがさらに別のpeerへ伝えることで広がります。
この章で押さえたいポイントは、次の通りです。
| ポイント | 説明 |
|---|---|
| 情報はpeerからpeerへ広がる | 最初のノードから接続関係をたどって段階的に伝わる |
| 同じ情報が複数経路から届くことがある | メッセージIDや取引IDなどで重複処理を避ける必要がある |
| すべての中身を毎回送るとは限らない |
inv / getdata のように、まず存在を知らせ、必要なら本体を要求する考え方がある |
| gossipには効率化の工夫がある | fanout、metadata、peer scoreなどで通信量や安全性を考える |
| 情報は完全同時には届かない | 遅延により、ノードごとに一時的に見えている情報が違うことがある |
| 一部のノード停止には強い場合がある | ただし、接続が偏るとネットワーク分断のリスクもある |
| P2Pだけで正しさは決まらない | 届いた情報は、検証やコンセンサスのルールと組み合わせて扱う必要がある |
ここまでで、P2Pネットワーク上で情報が広がる基本的なイメージを確認しました。
次の章では、より具体的に BitcoinのP2Pネットワーク を見ていきます。
Bitcoinでは、フルノードがどのように取引やブロックを扱い、DNS seedやmempool、inv / getdata のような仕組みがどのように関係するのかを、初学者向けに整理します。
6. BitcoinのP2Pネットワークをざっくり見る
この章では、ここまで見てきたP2Pネットワークの考え方を、Bitcoinの具体例に寄せて整理します。
ポイントは、Bitcoinではフルノードが取引やブロックを受け取り、検証し、必要に応じて他のpeerへ中継することです。
ただし、ここではBitcoin Coreの実装を細かく追うのではなく、初学者が全体像をつかめる範囲に絞ります。
前の章では、P2Pネットワーク上で情報がpeerからpeerへ広がる様子を、図とコードで確認しました。
ここからは、その考え方をBitcoinに当てはめて見ていきます。
Bitcoinは、タイトルにも Peer-to-Peer Electronic Cash System とあるように、中央の管理者だけに頼らず、参加者同士が取引やブロックの情報を共有する設計を取っています。
Bitcoinのホワイトペーパーでは、ネットワークの流れとして、新しい取引が全ノードへブロードキャストされ、各ノードが取引をブロックに集め、Proof of Workを見つけたノードがブロックを全ノードへブロードキャストし、ノードはブロック内の取引が有効で未使用である場合だけ受け入れる、という流れが説明されています。
参考: Bitcoin: A Peer-to-Peer Electronic Cash System
また、Bitcoin Developer Guideでは、Bitcoinのネットワークプロトコルにより、フルノードがブロックと取引を交換するためのP2Pネットワークを共同で維持すると説明されています。
フルノードは、ブロックや取引を他ノードへ中継する前に、それらをダウンロードして検証します。
参考: Bitcoin Developer Guide: P2P Network
ここで大切なのは、BitcoinのP2Pネットワークを「何でも流れてくる掲示板」のように見るのではなく、受け取った情報を検証しながら広げるネットワーク として見ることです。
6.1 BitcoinのP2Pネットワークで登場する主な役割
まず、BitcoinのP2Pネットワークでよく出てくる役割を整理します。
| 用語 | ざっくりした意味 | 身近なイメージ |
|---|---|---|
| フルノード | ブロックや取引を検証し、ネットワークへ参加するノード | 連絡を受け取るだけでなく、内容を確認してから共有する人 |
| peer | 自分のノードが直接つながっている相手 | 直接連絡を取り合う相手 |
| DNS seed | 初回接続時に、接続先候補を得るための入口 | 新しいグループに入るとき、最初に連絡先候補を教えてくれる案内役 |
| addr / addrv2 | peerの接続情報を共有するメッセージ | 「この人とも連絡が取れるよ」と紹介するメッセージ |
| inv | ある取引やブロックを持っていることを知らせるメッセージ | 「この資料を持っています」と知らせるメモ |
| getdata | 必要な取引やブロックの本体を要求するメッセージ | 「その資料を送ってください」とお願いするメッセージ |
| tx | 取引データ本体を送るメッセージ | 取引そのものの共有 |
| block | ブロックデータ本体を送るメッセージ | ブロックそのものの共有 |
| mempool | 未承認取引を一時的に保持する場所 | まだ正式なノートに書かれる前の候補箱 |
Bitcoin Developer Referenceでは、addr メッセージがpeerの接続情報を伝えるために使われ、peer discoveryに関係すると説明されています。
また、inv、getdata、block、tx などのメッセージもP2Pネットワーク上で使われるメッセージとして整理されています。
参考: Bitcoin Developer Reference: P2P Network
この一覧を見ると、BitcoinのP2Pネットワークは単に「取引を流すだけ」ではないことが分かります。
peerを見つける、接続する、情報の存在を知らせる、必要なデータを要求する、受け取ったデータを検証する、検証できたものをさらに広げる、という複数の処理が組み合わさっています。
この図のように、BitcoinのP2Pネットワークでは、情報が届いたあとに検証が入ります。
ここが、「ただ拡散するだけのネットワーク」と大きく違うところです。
6.2 新しいノードは、まず接続先を探す
新しくBitcoinノードを起動したとき、そのノードは最初から世界中のpeerを知っているわけではありません。
新しい学校やサークルに入ったばかりの人が、最初から全員の連絡先を知っているわけではないのと同じです。
まずは、誰か数人とつながり、そこから少しずつ連絡先を広げていく必要があります。
Bitcoin Developer Guideでは、プログラムが初回起動時にアクティブなフルノードのIPアドレスを知らないため、Bitcoin Coreなどに組み込まれたDNS seedへ問い合わせ、接続を受け入れる可能性があるフルノードのIPアドレスを取得すると説明されています。
参考: Bitcoin Developer Guide: Peer Discovery
ただし、DNS seedは「中央管理者そのもの」ではありません。
あくまで、最初にpeer候補を見つけるための入口です。
Bitcoin Developer Guideでも、DNS seedの結果は認証されないため、悪意あるseed運営者や中間者攻撃により、攻撃者が管理するノードだけを返される可能性があると注意されています。
そのため、プログラムはDNS seedだけに排他的に頼るべきではないと説明されています。
参考: Bitcoin Developer Guide: Peer Discovery
💡 豆知識
DNS seedは、P2Pネットワークへ入るための「最初の案内板」のようなものです。
ただし、案内板だけを完全に信じると危ない場合があります。
Bitcoinでは、一度ネットワークへ接続したあと、peerからaddrメッセージで他のpeer情報を得るなど、接続先を広げていく仕組みがあります。
6.3 小さなコードで見る:DNS seedからpeer候補を得るイメージ
ここで、DNS seedからpeer候補を得る流れを、かなり単純化したコードで見てみます。
このコードは、実際にBitcoinネットワークへ接続するものではありません。
実際のDNS seedやBitcoin Coreの挙動を再現するものでもありません。
目的は、最初に接続先候補を得て、その中からpeerを選ぶ という流れを理解することです。
# DNS seedからpeer候補を得るイメージを理解するための学習用コードです。
# 実際のBitcoinネットワークへ接続するコードではありません。
# IPアドレスは説明用の架空の値です。
import random
# DNS seedが返してくれた接続先候補、という想定です。
# 実際にはDNS問い合わせやネットワーク通信が関係します。
dns_seed_results = [
"node_A:8333",
"node_B:8333",
"node_C:8333",
"node_D:8333",
"node_E:8333",
]
def choose_initial_peers(candidates, max_peers=3):
"""
DNS seedなどから得た候補の中から、最初に接続するpeerを選びます。
candidates:
接続先候補の一覧です。
max_peers:
最初に接続するpeer数の上限です。
注意:
- これは説明用の単純な抽選です。
- 実際のノード実装では、接続状態、ネットワーク種別、過去の接続履歴など、
さまざまな条件が関係します。
"""
# random.sampleは、候補から重複なしで指定個数を選びます。
return random.sample(candidates, k=min(max_peers, len(candidates)))
initial_peers = choose_initial_peers(dns_seed_results)
print("DNS seedから得た候補:")
for peer in dns_seed_results:
print(" -", peer)
print("\n最初に接続するpeer:")
for peer in initial_peers:
print(" -", peer)
このコードでは、DNS seedから複数の接続先候補を得たあと、その中からいくつかのpeerを選んでいます。
もちろん、実際のBitcoinノードは、このような単純なリストだけで動いているわけではありません。
しかし、初学者向けには、まず次の流れを押さえると分かりやすいです。
1. ノードを起動する
2. まだpeerを知らない
3. DNS seedなどから接続先候補を得る
4. いくつかのpeerへ接続する
5. 接続後は、peerからさらに他のpeer情報を得る
6.4 peerと接続したあと、さらに接続先を広げる
DNS seedは、最初の入口として便利です。
ただし、P2Pネットワークでは、最初に得た候補だけで終わるわけではありません。
Bitcoin Developer Guideでは、プログラムがネットワークへ接続したあと、peerが addr メッセージで他のpeerのIPアドレスとポート番号を送ることで、分散的なpeer discoveryができると説明されています。
参考: Bitcoin Developer Guide: Peer Discovery
身近な例でいうと、最初は先生から数人の連絡先を教えてもらい、その後は友人から「この人にも連絡できるよ」と紹介してもらうようなものです。
このように、P2Pネットワークでは、接続先の情報もpeerからpeerへ共有されます。
ただし、peerから送られてきた情報をすべて無条件に信じるのは危険です。
存在しないノードや、攻撃者が管理するノードを紹介される可能性もあります。
そのため、実際の実装では、接続先の管理や接続の偏りを避けるための工夫が重要になります。
6.5 小さなコードで見る:peerから紹介された接続先を追加する
次のコードでは、最初に接続したpeerから、さらに別のpeerを紹介してもらう様子を表します。
これも学習用の単純化です。
実際のBitcoinノードのpeer管理やアドレス管理を再現するものではありません。
# peerから紹介された接続先を追加するイメージを理解するための学習用コードです。
# 実際のBitcoin Coreのpeer管理処理ではありません。
# 最初に知っているpeerです。
known_peers = {"node_A:8333", "node_B:8333"}
# 各peerがaddrメッセージで紹介してくれたpeer候補、という想定です。
addr_messages = {
"node_A:8333": ["node_C:8333", "node_D:8333"],
"node_B:8333": ["node_D:8333", "node_E:8333"],
}
def update_known_peers(known_peers, addr_messages):
"""
peerから受け取ったaddr情報をもとに、知っているpeer一覧を更新します。
known_peers:
現在知っているpeerの集合です。
addr_messages:
peerごとに紹介してくれた接続先候補です。
"""
for source_peer, introduced_peers in addr_messages.items():
print(f"{source_peer} から紹介されたpeer:")
for peer in introduced_peers:
print(" -", peer)
# 集合setに追加することで、重複するpeerは自動的に1つにまとまります。
known_peers.add(peer)
return known_peers
updated_peers = update_known_peers(known_peers, addr_messages)
print("\n更新後に知っているpeer一覧:")
for peer in sorted(updated_peers):
print(" -", peer)
このコードでは、node_A と node_B から紹介されたpeer情報を、known_peers に追加しています。
node_D:8333 は2回紹介されていますが、set を使っているため、重複して保存されません。
P2Pネットワークでは、同じ情報が複数の経路から届くことがあります。
そのため、取引やブロックだけでなく、peer情報でも重複を扱う考え方が必要になります。
6.6 Bitcoinでは「まず存在を知らせ、必要なら本体を要求する」
BitcoinのP2Pネットワークでは、取引やブロックの本体をいきなり全peerへ送り続けるだけではありません。
代表的な考え方として、inv と getdata があります。
inv は、inventoryの略で、「この取引やブロックを持っています」と知らせるためのメッセージです。
受け取った側がそのデータをまだ持っておらず、必要だと判断した場合、getdata で本体を要求します。
Bitcoin Developer Guideでは、ブロックの標準的な中継方法として、リレー側が inv メッセージで新しいブロックのinventoryを知らせ、必要なpeerが getdata でフルブロックを要求すると説明されています。
また、受け取ったフルノードはブロックを検証し、その後peerへ広告すると説明されています。
参考: Bitcoin Developer Guide: Block Broadcasting
取引についても、Bitcoin Developer Guideでは、取引をpeerへ送るために inv メッセージを送り、getdata を受け取った場合に tx で取引を送ると説明されています。
その取引を受け取ったpeerは、有効な取引であれば同じように転送します。
参考: Bitcoin Developer Guide: Transaction Broadcasting
この流れは、身近な例でいうと次のようなものです。
Aさん: 「新しい資料があります」
Bさん: 「まだ持っていないので送ってください」
Aさん: 「では資料本体を送ります」
Bさん: 「内容を確認して問題なければ、他の人にも存在を知らせます」
この流れにより、すでに持っているデータの本体を何度も送る無駄を減らしやすくなります。
6.7 小さなコードで見る:inv / getdata / tx の流れ
次のコードでは、inv、getdata、tx の流れを、簡単なメッセージ交換として表します。
実際のBitcoin P2Pメッセージのバイナリ形式や検証ルールは再現していません。
ここでは、存在を知らせる → 必要なら要求する → 本体を受け取る という考え方だけを確認します。
# inv / getdata / tx の流れを理解するための学習用コードです。
# 実際のBitcoin P2Pメッセージ形式ではありません。
class Node:
"""取引データを持つ簡易ノードです。"""
def __init__(self, name):
self.name = name
self.transactions = {}
def has_transaction(self, tx_id):
"""このノードが指定された取引を持っているか確認します。"""
return tx_id in self.transactions
def receive_inv(self, tx_id, from_node):
"""
invメッセージを受け取ったときの処理です。
まだ取引本体を持っていなければ、getdataで要求します。
"""
print(f"{self.name}: {from_node.name} から inv({tx_id}) を受信")
if self.has_transaction(tx_id):
print(f"{self.name}: {tx_id} はすでに持っているので要求しません")
return
print(f"{self.name}: {tx_id} を持っていないので getdata を送信")
tx_data = from_node.send_transaction(tx_id)
if tx_data is not None:
self.receive_tx(tx_id, tx_data)
def send_transaction(self, tx_id):
"""
getdataに対して、取引本体を返す処理です。
実際のBitcoinではtxメッセージとして送られます。
"""
print(f"{self.name}: tx({tx_id}) を送信")
return self.transactions.get(tx_id)
def receive_tx(self, tx_id, tx_data):
"""
取引本体を受け取ったときの処理です。
ここでは検証を単純化し、そのまま保存しています。
"""
print(f"{self.name}: tx({tx_id}) 本体を受信")
self.transactions[tx_id] = tx_data
# Node Aは取引を持っていて、Node Bはまだ持っていない想定です。
node_a = Node("Node A")
node_b = Node("Node B")
node_a.transactions["tx_001"] = {"from": "Alice", "to": "Bob", "amount": 1.0}
# Node AがNode Bへ「tx_001を持っている」と知らせます。
node_b.receive_inv("tx_001", from_node=node_a)
print("\nNode Bが持っている取引:")
print(node_b.transactions)
このコードでは、Node A が tx_001 を持っており、Node B はまだ持っていません。
Node B は inv(tx_001) を受け取ると、まだその取引を持っていないことを確認し、getdata に相当する要求を出します。
その後、Node A が tx に相当する形で取引本体を返し、Node B が保存します。
ここでは検証を単純化していますが、実際のBitcoinでは、受け取った取引がルールに合っているか確認することが重要です。
6.8 フルノードは、受け取った取引やブロックを検証する
BitcoinのP2Pネットワークでは、情報を受け取ったら何でもそのまま信じるわけではありません。
Bitcoin Developer Guideでは、フルノードはすべてのブロックと取引をダウンロードし、他のノードへ中継する前に検証すると説明されています。
参考: Bitcoin Developer Guide: P2P Network
この点は、Bitcoinを理解するうえでとても大切です。
P2Pネットワークは、情報を届けるための道です。
しかし、届いた情報が正しいかどうかは、ノードの検証ルールによって判断されます。
たとえば、身近な連絡網でも、誰かから「明日の集合時間は朝5時に変更です」と連絡が来たとして、それが本当に正しいとは限りません。
公式の予定表や担当者の確認が必要になる場合があります。
Bitcoinでも、取引やブロックを受け取ったノードは、形式、署名、二重支払い、ブロックのつながり、Proof of Workなど、さまざまなルールに照らして検証します。
ここでは細かい検証ルールまでは扱いませんが、流れとしては次のように整理できます。
| 段階 | ざっくりした処理 | 身近な例 |
|---|---|---|
| 受信 | peerから取引やブロックを受け取る | 連絡網で新しい情報が届く |
| 検証 | ルールに合っているか確認する | 公式資料や過去の記録と照合する |
| 保存 | 問題なければローカルに保存する | 自分のメモに反映する |
| 中継 | 必要に応じてpeerへ知らせる | 他の人へ共有する |
6.9 小さなコードで見る:検証してからmempoolへ入れる
次のコードでは、受け取った取引を簡単に検証し、問題なければローカルのmempoolへ入れる流れを表します。
実際のBitcoinの取引検証は、UTXO、スクリプト、署名、手数料、標準性、ロックタイムなど多くの要素を含みます。
ここでは、学習用に「金額が正の値か」「同じ取引IDをすでに持っていないか」だけを確認します。
# 取引を検証してからローカルmempoolへ入れる流れを理解するための学習用コードです。
# 実際のBitcoinの取引検証ルールではありません。
class SimpleBitcoinNode:
"""未承認取引をmempoolに保持する簡易ノードです。"""
def __init__(self, name):
self.name = name
self.mempool = {}
def validate_transaction(self, tx):
"""
取引が最低限の条件を満たすか確認します。
注意:
- これは説明用の簡易検証です。
- 実際のBitcoinでは、署名、UTXO、手数料、スクリプトなどを確認します。
"""
if tx["tx_id"] in self.mempool:
return False, "すでに同じ取引IDをmempoolに持っています"
if tx["amount"] <= 0:
return False, "金額が正の値ではありません"
return True, "簡易検証に通りました"
def receive_transaction(self, tx):
"""
peerから取引を受け取ったときの処理です。
検証に通った場合だけ、mempoolへ追加します。
"""
is_valid, reason = self.validate_transaction(tx)
print(f"{self.name}: {tx['tx_id']} の検証結果: {reason}")
if not is_valid:
print(f"{self.name}: {tx['tx_id']} はmempoolへ追加しません")
return False
self.mempool[tx["tx_id"]] = tx
print(f"{self.name}: {tx['tx_id']} をmempoolへ追加しました")
return True
node = SimpleBitcoinNode("Node A")
transactions = [
{"tx_id": "tx_001", "from": "Alice", "to": "Bob", "amount": 1.0},
{"tx_id": "tx_002", "from": "Carol", "to": "Dave", "amount": -3.0}, # 説明用に無効
{"tx_id": "tx_001", "from": "Alice", "to": "Bob", "amount": 1.0}, # 重複
]
for tx in transactions:
node.receive_transaction(tx)
print("\n最終的なmempool:")
print(node.mempool)
このコードでは、tx_001 はmempoolに追加されます。
一方、tx_002 は金額が負の値なので拒否され、2回目の tx_001 は重複として拒否されます。
実際のBitcoinではもっと多くの検証がありますが、ここで大切なのは次の点です。
BitcoinのP2Pネットワークでは、情報を受け取ったノードが、検証してから保存・中継する。
つまり、P2P通信は「情報を届ける道」であり、検証は「情報を受け入れるか決める門番」のような役割を持つ。
6.10 mempoolはネットワーク全体で完全に1つの箱ではない
Bitcoinを調べていると、mempool という言葉を見かけることがあります。
mempoolは、ざっくり言えば、まだブロックに入っていない未承認取引を一時的に保持する場所です。
ただし、ここで注意したいのは、mempoolが「Bitcoinネットワーク全体に1つだけ存在する共有箱」ではないことです。
各ノードは、それぞれ自分のローカルなmempoolを持ちます。
Bitcoin Developer Guideでは、未承認取引はBitcoinにおいて永続的な状態を持たないため、Bitcoin Coreはそれらを非永続的なメモリに保存し、memory poolまたはmempoolと呼ぶと説明されています。
また、peerが停止すると、そのmempoolはウォレットに保存された取引などを除いて失われるとも説明されています。
参考: Bitcoin Developer Guide: Memory Pool
このため、ノードAのmempoolにはある取引が入っているけれど、ノードBのmempoolにはまだ入っていない、ということがあり得ます。
これは、前の章で見た「情報は完全同時には届かない」という話ともつながります。
6.11 小さなコードで見る:ノードごとにmempoolが違うことがある
次のコードでは、同じネットワーク上のノードでも、情報が届くタイミングによってmempoolの中身が違うことを確認します。
# ノードごとにmempoolの中身が違うことを理解するための学習用コードです。
# 実際のBitcoinネットワークのmempool同期を再現するものではありません。
class NodeWithMempool:
"""ローカルmempoolを持つ簡易ノードです。"""
def __init__(self, name):
self.name = name
self.mempool = set()
def receive_tx(self, tx_id):
"""取引IDを受け取り、自分のmempoolへ追加します。"""
self.mempool.add(tx_id)
def show_mempool(self):
"""現在のmempoolの中身を表示します。"""
print(f"{self.name}: {sorted(self.mempool)}")
node_a = NodeWithMempool("Node A")
node_b = NodeWithMempool("Node B")
node_c = NodeWithMempool("Node C")
# tx_001はAとBには届いたが、Cにはまだ届いていない、という想定です。
node_a.receive_tx("tx_001")
node_b.receive_tx("tx_001")
# tx_002はAにだけ届いた、という想定です。
node_a.receive_tx("tx_002")
print("各ノードのmempool:")
for node in [node_a, node_b, node_c]:
node.show_mempool()
このコードを実行すると、ノードごとにmempoolの中身が違うことが分かります。
これは、Bitcoinのmempoolを理解するときに重要です。
mempoolは、ネットワーク全体で完全に同期された単一のデータベースではありません。
各ノードが、自分が受け取り、検証し、保持している未承認取引の集合です。
そのため、mempoolを説明するときは、次のように表現すると安全です。
mempoolは、各ノードがローカルに持つ未承認取引の一時的な置き場です。
ノードごとに中身が違うことがあり、ブロックに入るまでは永続的な状態ではありません。
6.12 ブロックの伝播では、ヘッダやinventoryも重要になる
取引だけでなく、ブロックの伝播もBitcoinのP2Pネットワークの重要な役割です。
Bitcoin Developer Guideでは、マイナーが新しいブロックを見つけたとき、peerへブロックを知らせる方法として、block メッセージを直接送る方法、inv で知らせて getdata で要求してもらう方法、headers を使って知らせる方法などが説明されています。
参考: Bitcoin Developer Guide: Block Broadcasting
ここでは細かい方式の違いまでは追いません。
初学者向けには、次のように考えると分かりやすいです。
| メッセージ | ざっくりした役割 | 身近な例 |
|---|---|---|
| inv | 新しいブロックを持っていると知らせる | 「新しい議事録があります」 |
| getheaders | 自分の知っているところから先のヘッダを求める | 「目次だけ先に見せてください」 |
| headers | ブロックヘッダを送る | 「議事録のタイトル・前後関係だけ先に送ります」 |
| getdata | 必要なブロック本体を要求する | 「その本文を送ってください」 |
| block | ブロック本体を送る | 「議事録本文を送ります」 |
ブロック本体には、多くの取引が含まれます。
そのため、まずヘッダやinventoryで存在を知らせ、必要な本体を要求することで、無駄な通信を抑えやすくなります。
また、ブロックは前のブロックのハッシュを参照します。
そのため、親ブロックを知らない状態で子ブロックだけを受け取っても、すぐには完全に検証できません。
Bitcoin Developer Guideでは、親ブロックをまだ知らないブロックをorphan blockとして説明し、headers-firstのノードでは先にヘッダを要求することで、この複雑さを避ける工夫が説明されています。
参考: Bitcoin Developer Guide: Orphan Blocks
6.13 小さなコードで見る:親ブロックを知らないと検証できない
次のコードでは、ブロックが前のブロックを参照していることを、簡単な親子関係として表します。
実際のBitcoinでは、ブロックヘッダのハッシュ、Proof of Work、Merkle root、取引検証などが関係します。
ここでは、親ブロックを知っているかどうかだけに注目します。
# 親ブロックを知らないとブロックを検証しにくいことを理解するための学習用コードです。
# 実際のBitcoinのブロック検証処理ではありません。
class BlockNode:
"""ブロックを保存する簡易ノードです。"""
def __init__(self):
# すでに知っているブロックIDを保存します。
self.known_blocks = {"block_0"} # genesis blockのような出発点を想定
self.orphan_blocks = {}
def receive_block(self, block_id, prev_block_id):
"""
ブロックを受け取ったときの処理です。
block_id:
受け取ったブロックのIDです。
prev_block_id:
そのブロックが参照している親ブロックのIDです。
"""
print(f"受信: {block_id} は {prev_block_id} の次のブロック")
if prev_block_id not in self.known_blocks:
# 親ブロックを知らない場合、ここではorphanとして一時的に扱います。
print(f" -> 親ブロック {prev_block_id} を知らないため、すぐには検証できません")
self.orphan_blocks[block_id] = prev_block_id
return
# 親ブロックを知っている場合、ここでは検証できたものとして保存します。
print(" -> 親ブロックを知っているため、チェーンへ追加できます")
self.known_blocks.add(block_id)
node = BlockNode()
# block_2が先に届いてしまったが、親のblock_1をまだ知らない想定です。
node.receive_block("block_2", "block_1")
# その後、親であるblock_1が届きます。
node.receive_block("block_1", "block_0")
print("\n知っているブロック:", sorted(node.known_blocks))
print("orphanとして扱っているブロック:", node.orphan_blocks)
このコードでは、block_2 が先に届いてしまうため、親である block_1 を知らない時点では、すぐにチェーンへ追加できません。
実際のBitcoinでは、このような状況を扱うために、ヘッダの取得や不足ブロックの要求など、より複雑な処理が行われます。
ここでは、ブロックが「前のブロックを参照してつながる」ため、届く順番も重要になることを押さえておけば十分です。
6.14 初回同期では、多くのブロックを取得して検証する
新しいフルノードがBitcoinネットワークへ参加するとき、過去のブロックチェーン履歴を取得し、検証する必要があります。
この処理は Initial Block Download、略して IBD と呼ばれます。
Bitcoin Developer Guideでは、IBDの方式として、過去にはblocks-first、現在のBitcoin Coreではheaders-firstの説明がされています。
headers-firstでは、まずブロックヘッダを取得し、その後必要なブロック本体をpeerから取得して検証します。
参考: Bitcoin Developer Guide: Initial Block Download
身近な例でいうと、共同編集ノートに新しく参加した人が、いきなり最新ページだけを見るのではなく、過去のページのつながりを確認しながら、最新版まで追いつくようなものです。
IBDを見ると、フルノードが単にネットワークに「接続するだけ」ではないことが分かります。
過去の履歴を取得し、自分で検証するからこそ、特定の相手だけを信じずにネットワークへ参加できます。
6.15 小さなコードで見る:ヘッダを先に確認してからブロック本体を要求する
次のコードでは、headers-firstの雰囲気をかなり単純化して表します。
実際のBitcoin CoreのIBDを再現するものではありません。
ここでは、まずヘッダのつながりを確認し、足りないブロック本体を要求する という流れだけを見ます。
# headers-firstの雰囲気を理解するための学習用コードです。
# 実際のBitcoin CoreのInitial Block Downloadではありません。
headers = [
{"block_id": "block_1", "prev": "block_0"},
{"block_id": "block_2", "prev": "block_1"},
{"block_id": "block_3", "prev": "block_2"},
]
available_blocks = {
"block_1": "block_1の本体データ",
"block_2": "block_2の本体データ",
"block_3": "block_3の本体データ",
}
def validate_header_chain(headers, known_tip="block_0"):
"""
ヘッダが前のブロックを順番に参照しているか確認します。
注意:
- 実際のBitcoinでは、Proof of Workや難易度なども確認します。
- ここでは、prevが直前のblock_idと一致するかだけを見ています。
"""
expected_prev = known_tip
for header in headers:
if header["prev"] != expected_prev:
return False
expected_prev = header["block_id"]
return True
def download_blocks(headers, block_store):
"""
ヘッダで存在を確認したブロック本体を取得します。
"""
downloaded = []
for header in headers:
block_id = header["block_id"]
block_body = block_store.get(block_id)
if block_body is None:
print(f"{block_id} の本体が見つかりません")
continue
print(f"{block_id} の本体を取得しました")
downloaded.append(block_body)
return downloaded
if validate_header_chain(headers):
print("ヘッダのつながりを確認できました")
downloaded_blocks = download_blocks(headers, available_blocks)
else:
print("ヘッダのつながりに問題があります")
このコードでは、まずヘッダ同士が順番につながっているかを確認しています。
その後、確認できたヘッダに対応するブロック本体を取得します。
実際のBitcoinでは、ここにProof of Work、難易度、ブロックサイズ、取引検証など多くの処理が加わります。
ただ、初学者向けには、headers-firstを次のように理解すると分かりやすいです。
まず目次やページ番号のつながりを確認し、そのあと必要な本文を取りに行く。
6.16 BitcoinのP2Pネットワークで注意したいこと
BitcoinのP2Pネットワークは、中央サーバーだけに頼らず、世界中のノードが取引やブロックを共有するための重要な土台です。
ただし、P2Pだからといって、何もしなくても常に安全というわけではありません。
この章で見た範囲だけでも、次のような注意点があります。
| 注意点 | 説明 |
|---|---|
| DNS seedだけに頼りすぎない | 初期接続先が偏ると、攻撃者のノードだけにつながるリスクがある |
| peerからの情報を無条件に信じない |
addr などの情報は誤りや悪意を含む可能性がある |
| mempoolはノードごとに違う | 未承認取引はネットワーク全体で完全同期された状態ではない |
| ブロックは検証が必要 | 受け取ったブロックが必ず正しいとは限らない |
| 通信とコンセンサスは別 | P2Pは情報伝播を担うが、履歴選択にはPoWなどのルールが関係する |
| 接続の偏りに注意 | 少数のpeerだけに囲まれると、ネットワークから孤立する可能性がある |
特に、Bitcoin Developer GuideがDNS seedだけに排他的に頼るべきではないと注意している点は、P2Pネットワークを理解するうえで重要です。
参考: Bitcoin Developer Guide: Peer Discovery
この話は、後の章で扱う Eclipse攻撃 とも関係します。
Eclipse攻撃は、あるノードの接続先を攻撃者側のノードで囲い込み、そのノードがネットワーク全体から孤立したような状態にされる攻撃です。
ここではまだ深追いしませんが、BitcoinのP2Pネットワークを理解するときは、次のように考えておくとよいです。
P2Pネットワークは、中央の1か所に頼りすぎないための仕組みです。
しかし、どのpeerとつながるか、どの情報を受け入れるか、接続先が偏っていないかは、セキュリティ上とても重要です。
6.17 この章のまとめ
この章では、BitcoinのP2Pネットワークを初学者向けに整理しました。
Bitcoinでは、フルノードが取引やブロックを受け取り、検証し、必要に応じて他のpeerへ中継します。
新しいノードは、DNS seedなどを使って最初の接続先候補を得て、接続後はpeerから addr メッセージなどでさらに接続先情報を得ます。
取引やブロックの伝播では、inv で存在を知らせ、getdata で必要な本体を要求し、tx や block で本体を送るという考え方が出てきます。
また、未承認取引は各ノードのローカルmempoolに一時的に保持されるため、mempoolはネットワーク全体で完全に1つの共有箱ではありません。
この章で押さえたいポイントは、次の通りです。
| ポイント | 説明 |
|---|---|
| BitcoinはP2Pで取引やブロックを共有する | フルノードがブロックと取引交換のためのP2Pネットワークを維持する |
| フルノードは検証してから中継する | 届いた情報をそのまま信じるのではなく、ルールに合うか確認する |
| DNS seedは初期接続の入口 | ただし、DNS seedだけに頼り切るのは望ましくない |
addr でpeer情報が広がる |
接続後はpeerから他のpeer情報を得られる |
inv / getdata は効率的な共有に関係する |
まず存在を知らせ、必要なら本体を要求する |
| mempoolはローカルな一時置き場 | ノードごとに中身が違う場合がある |
| IBDでは過去の履歴を取得して検証する | 新しいフルノードは、過去のブロックを取得して検証しながら最新状態へ追いつく |
| P2Pだけで正しさは決まらない | 通信、検証、PoW、チェーン選択が組み合わさってBitcoinが動く |
BitcoinのP2Pネットワークを見ると、これまで説明してきた「中央の1か所だけに頼らない情報共有」が、実際のブロックチェーンでどのように使われているかが見えてきます。
次の章では、もう一つの代表例として EthereumのP2Pネットワーク を見ていきます。
Ethereumでは、実行クライアントとコンセンサスクライアント、DevP2P、RLPx、discovery、gossipなど、Bitcoinとは少し違う用語や構成が出てきます。
ただし、基本にあるのは同じく「ノード同士が情報を見つけ、伝え、検証する」という考え方です。
7. EthereumのP2Pネットワークをざっくり見る
この章では、Bitcoinとは少し構成が異なる EthereumのP2Pネットワーク を見ていきます。
ポイントは、現在のEthereumでは、実行クライアントとコンセンサスクライアントが分かれており、それぞれが別のP2Pネットワークを使っていることです。
前の章では、BitcoinのP2Pネットワークを見ました。
Bitcoinでは、フルノードが取引やブロックを受け取り、検証し、必要に応じて他のpeerへ中継します。
inv / getdata のように、まず「こういうデータを持っています」と知らせ、必要なら本体を取りに行く流れも確認しました。
Ethereumでも、P2Pネットワークはとても重要です。
ただし、EthereumはBitcoinと同じ見方だけでは少し分かりにくいです。
特に現在のEthereumでは、ノードを構成するソフトウェアが大きく 実行クライアント と コンセンサスクライアント に分かれています。
ざっくり言うと、次のような役割分担です。
| 観点 | 実行クライアント | コンセンサスクライアント |
|---|---|---|
| 主な役割 | 取引の実行、EVM、状態、transaction poolの管理 | Proof of Stake、ブロック提案・検証、fork choice、finality |
| P2Pで主に扱うもの | 未承認取引の共有 | Beacon block、attestation、slashing関連情報などの共有 |
| 身近なイメージ | 届いた申請内容を実際に処理し、台帳の状態を更新する係 | どの記録案を採用するか、どの履歴を伸ばすかを確認する係 |
| 関係する用語 | transaction pool、EVM、execution payload、DevP2P、RLPx | Beacon block、attestation、libp2p、gossipsub、request/response |
Ethereum公式ドキュメントでは、Ethereumノードはブロックや取引データを検証できるソフトウェアを実行するコンピュータであり、現在のノードは consensus client と execution client の2つを実行する必要があると説明されています。
参考: Ethereum Documentation: Nodes and clients
また、Ethereumのnetworking layerの説明では、Ethereumは多数のノードからなるP2Pネットワークであり、ノード同士が互いを見つけ、gossipやrequest/responseによって情報を交換すると説明されています。
参考: Ethereum Documentation: Networking layer
この章では、EthereumのP2Pネットワークを、次の順番で整理していきます。
Bitcoinの章と同じく、ここでも細かい仕様をすべて追うことはしません。
まずは、Ethereumでは「どの情報が、どのP2Pネットワークで広がるのか」をつかむことを目標にします。
7.1 Ethereumノードは「2つのクライアント」で見ると分かりやすい
EthereumのP2Pネットワークを理解するとき、最初につまずきやすいのが ノードとクライアントの関係 です。
日常的には「Ethereumノードを動かす」と言います。
しかし、現在のEthereumでは、1つのソフトウェアだけを起動すれば全部終わり、というより、基本的には次の2種類のクライアントが協力します。
| 用語 | ざっくりした意味 |
|---|---|
| ノード | Ethereumネットワークに参加しているコンピュータ、またはその環境全体 |
| クライアント | Ethereumのルールに従って動くソフトウェア実装 |
| 実行クライアント | 取引を実行し、EVMや状態、transaction poolを扱うソフトウェア |
| コンセンサスクライアント | PoSの合意形成、ブロックやattestation、fork choiceを扱うソフトウェア |
| バリデータクライアント | ステーキングして、ブロック提案やattestationに参加するための追加要素 |
Ethereum公式のノードアーキテクチャ説明では、Ethereumノードは実行クライアントとコンセンサスクライアントで構成され、ブロックを提案するにはバリデータクライアントも必要だと説明されています。
参考: Ethereum Documentation: Node architecture
身近な例で言うと、共同編集メモを管理するチームの中に、次のような役割があるイメージです。
| 役割 | 身近な例 | Ethereumでの対応 |
|---|---|---|
| 入力内容を処理する係 | 届いた申請内容を確認し、計算して台帳に反映する | 実行クライアント |
| 正式な記録の順番を確認する係 | どの版を正式な履歴として扱うか確認する | コンセンサスクライアント |
| 当番として記録案を出す係 | 今回の議事録案を書く担当者 | バリデータクライアント |
図にすると、次のようになります。
ここで大切なのは、Ethereumには 2つのP2Pネットワークがある と考えると理解しやすい点です。
Ethereum公式のnetworking layerでは、実行クライアントは実行レイヤーのP2Pネットワークで取引をgossipし、コンセンサスクライアントは別のP2PネットワークでBeacon blockをgossipすると説明されています。
参考: Ethereum Documentation: Networking layer
💡 豆知識
Ethereumでは、The Merge以前と以後でノード構成の見方が変わりました。
以前は実行クライアントだけでフルノードとして扱えましたが、Proof of Stake移行後は、実行クライアントとコンセンサスクライアントを組み合わせる構成が基本になっています。
そのため、EthereumのP2Pを読むときは「実行レイヤーの話なのか」「コンセンサスレイヤーの話なのか」を分けると混乱しにくくなります。
7.2 実行クライアントは取引を受け取り、実行する
まずは、実行クライアントから見ていきます。
実行クライアントは、Ethereumの取引を受け取り、EVM上で実行し、状態を管理する側です。
Ethereum公式ドキュメントでは、実行クライアントはネットワーク上でbroadcastされた新しい取引を受け取り、それをEVMで実行し、Ethereumの現在の状態やデータベースを保持すると説明されています。
参考: Ethereum Documentation: Nodes and clients
身近な例で言えば、実行クライアントは「申請内容を実際に処理する係」です。
たとえば、共同会計ノートで次のような申請が届いたとします。
AさんからBさんへ 1,000円を移す
このとき、申請をただ受け取るだけでは不十分です。
本当にAさんに残高があるのか、形式は正しいのか、処理後の残高はどうなるのかを確認する必要があります。
Ethereumで言えば、取引を受け取ったあと、署名、nonce、gas、残高、スマートコントラクトの実行結果などを考慮しながら、状態の更新が妥当かを確認します。
| 実行クライアントが関係すること | ざっくりした説明 |
|---|---|
| 取引の受信 | ユーザーや他ノードから取引を受け取る |
| transaction pool | まだブロックに入っていない取引を一時的に保持する |
| EVM実行 | スマートコントラクトや送金を実行する |
| 状態管理 | アカウント残高やコントラクト状態などを保持する |
| execution payload作成 | ブロックに含める実行結果部分を作る |
| JSON-RPC | ユーザーやアプリがEthereumへアクセスする入口になる |
ここでのP2Pの役割は、取引を実行クライアント同士で広げることです。
つまり、あるユーザーが送信した取引は、接続しているノードへ届き、そこから他のノードへ広がっていきます。
ただし、Bitcoinのmempoolと同じように、Ethereumのtransaction poolもネットワーク全体で完全に1つの箱として同期されているわけではありません。
ノードごとに届いている取引、保持している取引、手数料条件、設定などが異なる場合があります。
そのため、記事では次のように表現すると安全です。
Ethereumの実行クライアントは、未承認取引をP2Pネットワークで共有します。
ただし、各ノードのtransaction poolはローカルな一時置き場であり、ネットワーク全体で完全に同じ中身になるとは限りません。
7.3 コンセンサスクライアントはブロックやattestationを共有する
次に、コンセンサスクライアントを見ていきます。
コンセンサスクライアントは、現在のEthereumにおけるProof of Stakeの合意形成を担当します。
ブロックを受け取り、どのチェーンをheadとして見るかを決めるfork choiceを実行し、finalityにも関係します。
Ethereum公式のノードアーキテクチャ説明では、コンセンサスクライアントはEthereumネットワークと同期し続けるためのロジックを扱い、peerからブロックを受け取り、バリデータの有効残高で重み付けされたattestationの蓄積に基づいてチェーンを追跡すると説明されています。
参考: Ethereum Documentation: Node architecture
コンセンサスレイヤーのP2Pでは、主に次のような情報が流れます。
| 情報 | ざっくりした意味 | 身近なイメージ |
|---|---|---|
| Beacon block | PoSのブロック本体 | 今回の正式な記録案 |
| attestation | バリデータの投票・証言 | 「この記録案を支持します」という確認 |
| aggregate | 複数attestationをまとめたもの | 複数人分の確認印をまとめたもの |
| voluntary exit | バリデータの自主退出 | 記録係をやめる届出 |
| slashing evidence | 重大な違反の証拠 | 二重署名などの不正証拠 |
Ethereum公式のnetworking layerでは、コンセンサスレイヤーのgossip domainに、Beacon block、proof、attestation、exit、slashingなど、ネットワーク全体へ素早く広げる必要がある情報が含まれると説明されています。
参考: Ethereum Documentation: Networking layer
Bitcoinでは、ブロック伝播と取引伝播が同じ「Bitcoin P2Pネットワーク」の話として説明されることが多いです。
一方、Ethereumでは、実行クライアントが取引を広げ、コンセンサスクライアントがブロックやattestationを広げる、という見方をすると整理しやすくなります。
ここで大切なのは、コンセンサスクライアントが「取引を実行しない」わけではなく、実行結果の検証には実行クライアントと連携するという点です。
次の節で、この2つのクライアントがどうつながるのかを見ます。
7.4 2つのクライアントはローカルで連携する
Ethereumでは、実行クライアントとコンセンサスクライアントが別々のP2Pネットワークに参加します。
しかし、両者が完全に独立しているわけではありません。
1台のEthereumノードの中では、実行クライアントとコンセンサスクライアントがローカルRPCで連携します。
この連携に使われるAPIとして、Ethereum公式ドキュメントでは Engine API が紹介されています。
参考: Ethereum Documentation: Networking layer
イメージとしては、次のような分担です。
また、自分のノードがブロック提案者になった場合は、流れが少し変わります。
Ethereum公式のnetworking layerでは、ブロック提案者でない場合、コンセンサスクライアントがブロックをgossipで受け取り、実行ペイロードを実行レイヤーへ渡し、実行レイヤーが取引を実行して状態の妥当性を確認すると説明されています。
また、ブロック提案者になった場合、コンセンサスレイヤーが実行クライアントへブロック作成を依頼し、実行クライアントはtransaction mempoolから取引を束ね、実行し、ブロックハッシュを生成すると説明されています。
参考: Ethereum Documentation: Networking layer
ここまでを見ると、EthereumのP2Pネットワークは少し複雑に感じるかもしれません。
ただ、役割を分けるとかなり見通しがよくなります。
| 場面 | 実行クライアント | コンセンサスクライアント |
|---|---|---|
| 普段の取引共有 | transaction poolに取引を受け取り、P2Pでgossipする | 直接の中心ではない |
| ブロック受信 | execution payloadを検証する | Beacon blockを受け取り、fork choiceに反映する |
| ブロック提案 | 取引を実行してpayloadを作る | payloadをBeacon blockに含めてgossipする |
| ブロック確定へ向かう流れ | 実行結果の正しさを支える | attestationやfinalityに関わる |
このように、Ethereumでは 取引の実行 と 合意形成 を分け、それぞれのクライアントがP2Pネットワークを使いながら協力しています。
7.5 実行レイヤーのP2P:DevP2P、discovery、RLPx
ここからは、少しだけ実行レイヤーのP2P用語を見ていきます。
Ethereumの実行レイヤーでは、ノード同士が互いを見つけ、接続し、取引などを交換するために、複数のプロトコルが関係します。
Ethereum公式のnetworking layerでは、実行レイヤーのネットワークプロトコルは大きく次の2つに分かれると説明されています。
| スタック | 主な役割 | ざっくりしたイメージ |
|---|---|---|
| discovery stack | 新しいノードが接続先peerを見つける | 連絡網に入るための「知り合い探し」 |
| DevP2P stack | 見つけたpeerと情報を交換する | 実際に会話するための通信ルール |
参考: Ethereum Documentation: Networking layer
discovery stack
discoveryは、ノードが他のpeerを見つけるための仕組みです。
Ethereum公式ドキュメントでは、新しいノードは少数のbootnodeを使ってネットワークへ入ると説明されています。bootnodeは、新しいノードへpeerを紹介するための入口であり、通常の同期やブロック処理を担うためのものではない、と説明されています。
参考: Ethereum Documentation: Networking layer
これは、2章で説明した「最初から全員を知っているわけではない」という話につながります。
新しく参加したノード
↓
bootnodeへ接続
↓
近いpeerの情報を得る
↓
そのpeerたちと接続を試す
↓
ネットワーク内の接続が少しずつ増える
Ethereum公式ドキュメントでは、discoveryの初期接続を PING / PONG のやり取りとして説明しています。
PING と PONG が成功すると、ノード同士はbonded、つまり接続確認済みの状態になり、その後 FIND-NEIGHBOURS によって近いpeer情報を取得できます。
参考: Ethereum Documentation: Networking layer
DevP2P / RLPx
peerを見つけたあとは、実際に情報をやり取りする必要があります。
Ethereum公式ドキュメントでは、DevP2PはEthereumがP2Pネットワークを確立・維持するために実装しているプロトコルスタックであり、その中にRLPx transport protocol、wire protocol、複数のsub-protocolが含まれると説明されています。
参考: Ethereum Documentation: Networking layer
また、Ethereumのdevp2p仕様リポジトリでは、devp2pはEthereum P2Pネットワークを形成するネットワークプロトコル群であり、他の参加者の発見や安全な通信を提供することを目的としていると説明されています。
参考: ethereum/devp2p: Ethereum peer-to-peer networking specifications
ここでは、細かい暗号ハンドシェイクやRLPの仕様には入りません。
初学者向けには、次のように見ると十分です。
| 用語 | ざっくりした意味 |
|---|---|
| DevP2P | Ethereumの実行レイヤーP2Pで使われるプロトコル群 |
| RLPx | peer間セッションの開始、認証、維持に関わる通信の土台 |
| wire protocol | peer同士が基本的な通信を行うためのルール |
| sub-protocol | 取引交換や同期など、用途ごとの追加ルール |
| RLP | Ethereumで使われるデータエンコード方式の1つ |
💡 豆知識
DevP2Pという名前を見ると「開発用のP2P」のように見えるかもしれませんが、EthereumのP2Pネットワークを構成する低レベルなプロトコル群を指す名前です。
RLPxのRLPは Recursive Length Prefix の略で、Ethereumでデータをコンパクトに表現するためのエンコード方式に関係します。
名前だけ見ると難しく感じますが、記事の入口では「peer同士が安全に会話を始めるための土台」くらいに押さえると読みやすいです。
7.6 コンセンサスレイヤーのP2P:libp2p、gossipsub、request/response
次に、コンセンサスレイヤーのP2Pを見ます。
Ethereum公式のnetworking layerでは、コンセンサスクライアントは実行クライアントとは別のP2Pネットワークに参加すると説明されています。
コンセンサスレイヤーでは、discv5によるpeer discovery、libp2pによる通信、gossip domain、request-response domainが登場します。
参考: Ethereum Documentation: Networking layer
コンセンサスレイヤーのP2Pを初学者向けに整理すると、次のようになります。
| 用語 | ざっくりした意味 | 身近なイメージ |
|---|---|---|
| discv5 | peerを見つけるための仕組み | 連絡網に入るための参加者探し |
| ENR | ノードの接続情報や対応情報を入れた名刺 | 連絡先カード |
| libp2p | P2P通信を組み立てるためのネットワークスタック | 連絡網アプリの共通部品 |
| gossipsub | 情報を効率よく広げるpub/sub型のgossip方式 | 必要なグループへ連絡を回す仕組み |
| request/response | 特定のpeerへ必要なデータを要求する仕組み | 「その資料を送ってください」と個別依頼する |
| SSZ | コンセンサスレイヤーで使われるデータ表現 | 決まった形式の記録用フォーマット |
Ethereumのconsensus-specsのP2P interfaceでは、Phase 0のネットワーク仕様が、network fundamentals、gossip domain、discovery domain、Req/Resp domainなどに分けて整理されています。
参考: Ethereum consensus-specs: Phase 0 P2P interface
gossip domain
gossip domainは、ネットワーク全体へ素早く広げる必要がある情報を扱います。
たとえば、Beacon blockやattestationは、遅れて届くとブロック提案や投票のタイミングに影響します。
そのため、関係するpeerへ素早く広げる必要があります。
Ethereum公式のnetworking layerでは、gossip domainにはBeacon block、proof、attestation、exit、slashingなど、ネットワーク全体へ素早く広げる必要がある情報が含まれると説明されています。
参考: Ethereum Documentation: Networking layer
request/response domain
一方で、request/responseは、特定のデータを特定のpeerから取り寄せるために使われます。
たとえば、あるBeacon blockを受け取ったものの、その親ブロックを持っていない場合、必要なブロックをpeerへ要求する必要があります。
あるいは、特定範囲のslotのブロックをまとめて取得したい場合にも使えます。
Ethereum公式のnetworking layerでは、request-response domainは、特定のroot hashに一致するBeacon blockや、slot範囲に含まれるBeacon blockなどをpeerへ要求するプロトコルを含むと説明されています。
参考: Ethereum Documentation: Networking layer
このように、Ethereumのコンセンサスレイヤーでは、何でもgossipで流すのではなく、広く素早く伝える情報と、必要に応じて取りに行く情報を分けています。
| 方式 | 向いている情報 | イメージ |
|---|---|---|
| gossip | 新しいブロック、attestation、slashing evidenceなど、すぐ広げたい情報 | 全体連絡 |
| request/response | 特定のブロック、slot範囲のデータなど、必要なものだけ取りたい情報 | 個別依頼 |
この分け方は、5章で見た inv / getdata の考え方とも少し似ています。
「全部を無差別に送り続ける」のではなく、「広げる情報」と「必要になったら取りに行く情報」を分けることで、帯域や処理の無駄を抑えやすくしています。
7.7 小さなコードで見る:Ethereumノードを2つのP2Pネットワークとして表す
ここで、Ethereumノードの構成をコードで単純化して見てみます。
このコードは、実際のEthereumクライアントの実装ではありません。
目的は、Ethereumでは実行クライアントとコンセンサスクライアントが別々のP2Pネットワークへ参加している、という構造をイメージすることです。
# Ethereumノードの構成を理解するための学習用コードです。
# 実際のEthereumクライアントやP2P通信を再現するものではありません。
from dataclasses import dataclass, field
from typing import List
@dataclass
class ExecutionClient:
"""実行クライアントを表す学習用クラスです。"""
name: str
execution_peers: List[str] = field(default_factory=list)
transaction_pool: List[str] = field(default_factory=list)
@dataclass
class ConsensusClient:
"""コンセンサスクライアントを表す学習用クラスです。"""
name: str
consensus_peers: List[str] = field(default_factory=list)
known_blocks: List[str] = field(default_factory=list)
@dataclass
class EthereumNode:
"""1台のEthereumノードを、2つのクライアントの組として表します。"""
node_name: str
execution_client: ExecutionClient
consensus_client: ConsensusClient
def show_node(node: EthereumNode) -> None:
"""ノードが参加している2種類のP2P接続を表示します。"""
print(f"\n{node.node_name}")
print(f" 実行クライアント: {node.execution_client.name}")
print(f" 実行レイヤーのpeer: {node.execution_client.execution_peers}")
print(f" コンセンサスクライアント: {node.consensus_client.name}")
print(f" コンセンサスレイヤーのpeer: {node.consensus_client.consensus_peers}")
node_a = EthereumNode(
node_name="Node_A",
execution_client=ExecutionClient(
name="Execution_A",
execution_peers=["Execution_B", "Execution_C"],
),
consensus_client=ConsensusClient(
name="Consensus_A",
consensus_peers=["Consensus_B", "Consensus_D"],
),
)
show_node(node_a)
このコードでは、1つの EthereumNode の中に、ExecutionClient と ConsensusClient を持たせています。
そして、それぞれが異なるpeer一覧を持っています。
実際のEthereumでは、ここに暗号化通信、peer discovery、gossip、request/response、Engine API、fork choice、EVM実行など多くの仕組みが加わります。
ただ、まずは次のように理解すると分かりやすいです。
Ethereumノードは、1つの箱の中で、実行クライアントとコンセンサスクライアントが協力している。
そして、それぞれが別のP2Pネットワークを通じて、異なる種類の情報をやり取りしている。
7.8 小さなコードで見る:実行レイヤーで取引がgossipされる
次に、実行レイヤーで取引が広がる流れを見てみます。
このコードも学習用です。
ある実行クライアントが取引を受け取り、検証してから自分のtransaction poolへ入れ、peerへgossipする流れを単純化しています。
# 実行レイヤーで取引がgossipされる様子を理解するための学習用コードです。
# 実際のEthereumの取引検証、gas計算、署名検証、txpoolの実装ではありません。
from collections import deque
# 実行クライアント同士の接続関係を表します。
execution_network = {
"Execution_A": ["Execution_B", "Execution_C"],
"Execution_B": ["Execution_A", "Execution_D"],
"Execution_C": ["Execution_A", "Execution_E"],
"Execution_D": ["Execution_B"],
"Execution_E": ["Execution_C"],
}
# 各実行クライアントのローカルtransaction poolを用意します。
transaction_pools = {client: [] for client in execution_network}
def is_valid_transaction(tx: dict) -> bool:
"""
取引が有効かをかなり単純化して確認します。
実際のEthereumでは、署名、nonce、残高、gas、形式などを検証します。
ここでは説明用に、amountが正の値かどうかだけを見ています。
"""
return tx.get("amount", 0) > 0
def gossip_transaction(start_client: str, tx: dict) -> None:
"""取引を受け取った実行クライアントから、peerへ順番に広げます。"""
if not is_valid_transaction(tx):
print("無効な取引なのでgossipしません")
return
seen = set()
queue = deque([start_client])
while queue:
current = queue.popleft()
# すでに処理したclientでは、同じ取引を重複処理しません。
if current in seen:
continue
seen.add(current)
transaction_pools[current].append(tx["tx_id"])
print(f"{current} が {tx['tx_id']} をtransaction poolへ追加しました")
# 接続先peerへ取引を広げます。
for peer in execution_network[current]:
if peer not in seen:
queue.append(peer)
transaction = {"tx_id": "tx_001", "from": "Alice", "to": "Bob", "amount": 3}
gossip_transaction("Execution_A", transaction)
このコードでは、Execution_A から始まった取引が、接続関係をたどって他の実行クライアントへ広がります。
ここでのポイントは、各ノードが取引を受け取ったときに、まず簡単な検証をしていることです。
実際のEthereumでは、検証はもっと複雑ですが、考え方としては「受け取った情報をそのまま無条件に信じるわけではない」と押さえておくとよいです。
また、各クライアントのtransaction poolはローカルに存在します。
このコードでも、transaction_pools はクライアントごとに分かれています。
7.9 小さなコードで見る:コンセンサスレイヤーでBeacon blockがgossipされる
次に、コンセンサスレイヤーでBeacon blockが広がる流れを見てみます。
実際のEthereumでは、Beacon blockのgossipには、提案者が正しいか、未来のslotではないか、署名が正しいか、親ブロックが見えているかなど、多くの検証条件があります。
Ethereumのconsensus-specsでも、gossipで受け取ったBeacon blockを検証するための条件が細かく定義されています。
参考: Ethereum consensus-specs: Phase 0 P2P interface
ここでは、次の3点だけを単純化して確認します。
- ブロックのslotが現在より未来すぎないこと
- 親ブロックをすでに知っていること
- 同じ提案者が同じslotで複数ブロックを出していないこと
# コンセンサスレイヤーでBeacon blockをgossipする流れを理解するための学習用コードです。
# 実際のEthereumのBeacon block検証やBLS署名検証を再現するものではありません。
from collections import deque
consensus_network = {
"Consensus_A": ["Consensus_B", "Consensus_C"],
"Consensus_B": ["Consensus_A", "Consensus_D"],
"Consensus_C": ["Consensus_A", "Consensus_E"],
"Consensus_D": ["Consensus_B"],
"Consensus_E": ["Consensus_C"],
}
# 各コンセンサスクライアントが知っているブロックを表します。
known_blocks = {client: {"genesis"} for client in consensus_network}
# 同じ提案者が同じslotで複数ブロックを出していないかを見るための記録です。
seen_proposer_slots = set()
def validate_beacon_block(block: dict, current_slot: int) -> bool:
"""
Beacon blockの検証をかなり単純化して行います。
実際のEthereumでは、署名、fork、finalized checkpoint、
parent state、proposer indexなど多くの条件を確認します。
"""
if block["slot"] > current_slot:
print("未来のslotのブロックなので、いったん受け入れません")
return False
proposer_slot = (block["proposer"], block["slot"])
if proposer_slot in seen_proposer_slots:
print("同じ提案者が同じslotで複数ブロックを出しています")
return False
return True
def gossip_beacon_block(start_client: str, block: dict, current_slot: int) -> None:
"""Beacon blockをコンセンサスクライアント間で広げます。"""
if not validate_beacon_block(block, current_slot):
return
seen_proposer_slots.add((block["proposer"], block["slot"]))
seen_clients = set()
queue = deque([start_client])
while queue:
current = queue.popleft()
if current in seen_clients:
continue
seen_clients.add(current)
# 親ブロックを知らない場合、本来はrequest/responseなどで取得を試みます。
if block["parent"] not in known_blocks[current]:
print(f"{current} は親ブロック {block['parent']} を知らないため保留します")
continue
known_blocks[current].add(block["block_id"])
print(f"{current} が {block['block_id']} を受け取りました")
for peer in consensus_network[current]:
if peer not in seen_clients:
queue.append(peer)
block = {
"block_id": "beacon_block_001",
"parent": "genesis",
"slot": 1,
"proposer": "validator_10",
}
gossip_beacon_block("Consensus_A", block, current_slot=1)
このコードでは、Beacon blockを受け取る前に、簡単な検証を入れています。
実際のEthereumでは、これよりずっと多くの条件を確認します。
ただ、入口としては次の点を押さえれば十分です。
コンセンサスレイヤーのP2Pでも、受け取ったブロックを無条件に広げるわけではない。
正しいタイミングか、親ブロックがあるか、提案者が妥当かなどを確認しながら扱う。
7.10 小さなコードで見る:request/responseで足りない親ブロックを取りに行く
前のコードでは、親ブロックを知らない場合、そのブロックを保留しました。
しかし、実際のP2Pネットワークでは、足りないデータがあればpeerへ要求することがあります。
Ethereumのコンセンサスレイヤーでも、request/response domainでは、特定のrootに一致するBeacon blockやslot範囲のブロックを要求できます。
参考: Ethereum Documentation: Networking layer
ここでは、親ブロックがない場合にpeerへ問い合わせる処理を、かなり単純化して見てみます。
# request/responseで不足している親ブロックを取得する考え方を理解するための学習用コードです。
# 実際のEthereumのreq/respプロトコル、SSZ、Snappy圧縮などは再現していません。
# peerごとに持っているブロックを簡単な辞書で表します。
peer_block_store = {
"Consensus_B": {
"genesis": {"block_id": "genesis", "parent": None, "slot": 0},
"beacon_block_001": {"block_id": "beacon_block_001", "parent": "genesis", "slot": 1},
},
"Consensus_C": {
"genesis": {"block_id": "genesis", "parent": None, "slot": 0},
},
}
def request_block(peer: str, block_id: str):
"""
指定したpeerに、特定のブロックを要求します。
実際のEthereumでは、rootやslot範囲などで要求し、
SSZエンコードされたデータがSnappy圧縮されて返る場合があります。
ここでは辞書から取り出すだけに単純化しています。
"""
print(f"{peer} に {block_id} を要求します")
return peer_block_store.get(peer, {}).get(block_id)
def fetch_missing_parent(known: set, parent_id: str, peers: list[str]) -> bool:
"""知らない親ブロックを、接続先peerから探して取得します。"""
if parent_id in known:
print("親ブロックはすでに持っています")
return True
for peer in peers:
block = request_block(peer, parent_id)
if block is not None:
known.add(block["block_id"])
print(f"{parent_id} を取得しました")
return True
print(f"{parent_id} を取得できませんでした")
return False
my_known_blocks = {"genesis"}
peers = ["Consensus_B", "Consensus_C"]
fetch_missing_parent(my_known_blocks, "beacon_block_001", peers)
print("現在知っているブロック:", my_known_blocks)
このコードでは、自分が知らない親ブロックをpeerへ問い合わせています。
実際のEthereumでは、データ形式や検証条件はもっと複雑です。
ただ、考え方としては、5章で見た「必要な情報は個別に取りに行く」流れと同じです。
7.11 小さなコードで見る:実行クライアントとコンセンサスクライアントの連携
最後に、実行クライアントとコンセンサスクライアントが連携して、ブロック提案を行う流れを単純化して見ます。
このコードは、Engine APIや実際のブロック作成を再現するものではありません。
目的は、次の流れをイメージすることです。
- 実行クライアントがtransaction poolを持っている
- コンセンサスクライアントがブロック提案者になったと想定する
- コンセンサスクライアントが実行クライアントへpayload作成を依頼する
- 実行クライアントが取引を選び、payloadを返す
- コンセンサスクライアントがBeacon blockとしてgossipする
# 実行クライアントとコンセンサスクライアントの連携を理解するための学習用コードです。
# 実際のEngine API、EVM実行、ブロック提案、署名、gossip処理を再現するものではありません。
from dataclasses import dataclass, field
from typing import List
@dataclass
class SimpleExecutionClient:
"""transaction poolとpayload作成を持つ、かなり単純化した実行クライアントです。"""
transaction_pool: List[dict] = field(default_factory=list)
def create_execution_payload(self, max_transactions: int = 2) -> dict:
"""
transaction poolから取引を選び、execution payloadを作ります。
実際のEthereumでは、gas、手数料、状態更新、MEV、ブロック制約などが関係します。
ここでは先頭から最大max_transactions件を選ぶだけです。
"""
selected = self.transaction_pool[:max_transactions]
self.transaction_pool = self.transaction_pool[max_transactions:]
return {
"transactions": selected,
"state_root": "dummy_state_root_after_execution",
}
@dataclass
class SimpleConsensusClient:
"""execution payloadをBeacon blockに含める、単純化したコンセンサスクライアントです。"""
slot: int
proposer: str
def propose_block(self, execution_client: SimpleExecutionClient) -> dict:
"""
実行クライアントへpayload作成を依頼し、Beacon blockを作ります。
"""
payload = execution_client.create_execution_payload()
beacon_block = {
"slot": self.slot,
"proposer": self.proposer,
"execution_payload": payload,
}
return beacon_block
execution_client = SimpleExecutionClient(
transaction_pool=[
{"tx_id": "tx_001", "from": "Alice", "to": "Bob", "amount": 1},
{"tx_id": "tx_002", "from": "Carol", "to": "Dave", "amount": 2},
{"tx_id": "tx_003", "from": "Eve", "to": "Frank", "amount": 3},
]
)
consensus_client = SimpleConsensusClient(slot=128, proposer="validator_42")
new_block = consensus_client.propose_block(execution_client)
print("提案されたBeacon block:")
print(new_block)
print("残ったtransaction pool:")
print(execution_client.transaction_pool)
このコードでは、コンセンサスクライアントが「ブロック提案者になった」と仮定し、実行クライアントへpayloadを作ってもらっています。
実際のEthereumでは、ブロック提案にはvalidator、署名、RANDAO、fork choice、payload attributes、実行結果の検証など、さらに多くの要素が関係します。
しかし、最初の理解としては、次の流れを押さえるとよいです。
未承認取引は実行レイヤーのP2Pで広がる。
ブロック提案時には、実行クライアントが取引を実行してpayloadを作る。
コンセンサスクライアントがそのpayloadをBeacon blockに含め、コンセンサスレイヤーP2Pで広げる。
7.12 EthereumのP2Pでは「通信内容ごとの場所」を意識する
ここまで見ると、EthereumのP2Pネットワークは用語が多く、少し複雑に感じるかもしれません。
しかし、通信内容ごとに「どこで扱うのか」を分けると整理しやすくなります。
| 通信内容 | 主に関係する場所 | ざっくりした説明 |
|---|---|---|
| 未承認取引 | 実行レイヤーP2P | transaction poolへ入り、実行クライアント同士で広がる |
| execution payload | ローカルRPC / Engine API | 実行クライアントとコンセンサスクライアントの間で受け渡しされる |
| Beacon block | コンセンサスレイヤーP2P | コンセンサスクライアント同士でgossipされる |
| attestation | コンセンサスレイヤーP2P | バリデータの投票・証言として広がる |
| missing block | request/response | 足りないブロックをpeerへ要求して取得する |
| ユーザーからのRPC | 実行クライアントのJSON-RPCなど | ウォレットやアプリが取引送信・状態照会に使う |
この表を押さえておくと、EthereumのP2Pを読むときに迷いにくくなります。
たとえば、次のような疑問が出たとします。
ユーザーが送った取引は、どこで広がるの?
答えは、主に実行レイヤーのP2Pです。
新しいBeacon blockは、どこで広がるの?
答えは、主にコンセンサスレイヤーのP2Pです。
ブロックに含まれる取引の実行結果は、誰が確認するの?
実行クライアントがexecution payloadを実行し、状態更新が妥当か確認します。
その結果をコンセンサスクライアントが受け取り、chain headやattestationの判断へつなげます。
このように、Ethereumでは 通信内容ごとに担当する場所が分かれている と考えると分かりやすいです。
7.13 Bitcoinとの違いをざっくり比較する
ここで、前章のBitcoinと比較してみます。
| 観点 | Bitcoin | Ethereum |
|---|---|---|
| ノード構成の見方 | フルノードを中心に見ると理解しやすい | 実行クライアントとコンセンサスクライアントに分けると理解しやすい |
| 取引伝播 | Bitcoin P2Pで取引が広がる | 実行レイヤーP2Pで取引が広がる |
| ブロック伝播 | Bitcoin P2Pでブロックが広がる | コンセンサスレイヤーP2PでBeacon blockが広がる |
| 未承認取引の一時置き場 | mempool | transaction pool |
| 初期peer発見 | DNS seedなど | bootnode、discovery protocolなど |
| 履歴選択 | PoWと累積作業量などが重要 | PoS、attestation、fork choice、finalityなどが重要 |
| 情報共有の考え方 |
inv / getdata などで存在通知と取得を分ける |
gossipとrequest/responseを使い分ける |
ただし、違いばかりに注目しすぎる必要はありません。
BitcoinもEthereumも、根本には次の共通点があります。
- ノード同士がpeerを見つける
- 取引やブロックなどの情報をネットワークへ広げる
- 届いた情報を無条件に信じず、ルールに従って検証する
- 一時的に見えている情報がノードごとに違う場合がある
- 通信だけで正しさが決まるわけではなく、検証やコンセンサスと組み合わさる
つまり、P2Pネットワークは、BitcoinでもEthereumでも 情報を共有するための土台 です。
そのうえで、チェーンごとの設計に応じて、検証ルールやコンセンサス、ネットワークプロトコルの構成が違っています。
7.14 EthereumのP2Pネットワークで注意したいこと
EthereumのP2Pネットワークも、中央の1か所だけに頼らないための重要な仕組みです。
しかし、Bitcoinの章でも見たように、P2Pだからといって何もしなくても安全というわけではありません。
Ethereumの場合、特に次の点に注意が必要です。
| 注意点 | 説明 |
|---|---|
| クライアントが2種類ある | 実行クライアントとコンセンサスクライアントの役割を混同しやすい |
| P2Pネットワークも2つある | 取引gossipとブロックgossipは別のネットワークで扱われる |
| bootnodeは入口であって中央管理者ではない | 新規ノードにpeerを紹介するが、通常の同期や合意形成を一手に担うものではない |
| transaction poolはローカル | すべてのノードが完全に同じ未承認取引集合を持つとは限らない |
| gossipでも検証が必要 | Beacon blockやattestationは、受信後にルールに沿って検証する必要がある |
| クライアント多様性が重要 | 特定実装への依存が強すぎると、バグや障害が広がる可能性がある |
| peerの偏りに注意 | 特定のpeerだけに囲まれると、情報の見え方が偏る可能性がある |
Ethereum公式のnodes and clientsでは、複数のクライアント実装があることは、単一のコードベースへの依存を減らし、ネットワークを強くする方向に働くと説明されています。
参考: Ethereum Documentation: Nodes and clients
この点は、情報セキュリティの観点でも重要です。
たとえば、もしネットワークの大部分が同じクライアント実装に依存していて、その実装に重大なバグがあった場合、影響が広がりやすくなります。
逆に、複数の実装が適度に分散していれば、1つの実装の問題がネットワーク全体へ直撃するリスクを抑えやすくなります。
💡 豆知識
Ethereumでは、クライアント多様性がよく話題になります。
これは単に「いろいろなソフトがあると楽しい」という話ではなく、同じ実装に依存しすぎることで起こる単一障害点を避ける意味があります。
P2Pネットワークの分散性を考えるときは、ノード数だけでなく、どのクライアント実装が使われているかも重要な観点になります。
7.15 EthereumのP2Pを理解するときの誤解
EthereumのP2Pネットワークについては、次のような誤解が起こりやすいです。
| 誤解しやすい表現 | より丁寧な見方 |
|---|---|
| Ethereumノードは1つのソフトだけで動く | 現在のEthereumノードは、基本的に実行クライアントとコンセンサスクライアントを組み合わせて動く |
| 実行クライアントが全部の合意形成をしている | 実行クライアントは取引実行や状態管理を担い、PoSの合意形成はコンセンサスクライアントが担う |
| コンセンサスクライアントだけで取引を実行できる | 取引実行や状態更新の検証には実行クライアントが必要 |
| P2Pに流れたブロックはそのまま正しい | ブロックやattestationは、受信後に検証される |
| bootnodeは中央サーバーである | bootnodeは主に初期peer発見の入口であり、通常の合意形成を一手に担う中央管理者ではない |
| gossipは全員へ一斉送信するだけ | 実際にはtopic、mesh、metadata、peer選択などの仕組みが関係する |
| P2Pなら完全に止まらない | peerの偏り、ネットワーク分断、実装バグ、運用障害などのリスクは残る |
この章では、これらをすべて詳細に掘り下げるわけではありません。
ただ、EthereumのP2Pを理解するときは、次の3つを分けることが大切です。
1. 取引を広げる通信
2. ブロックやattestationを広げる通信
3. 届いた情報を検証し、履歴として採用する判断
この3つを混ぜてしまうと、「P2Pで届いたから正しい」「gossipされたから確定した」という誤解につながりやすくなります。
7.16 この章のまとめ
この章では、EthereumのP2Pネットワークを初学者向けに整理しました。
Ethereumでは、現在のノード構成を 実行クライアント と コンセンサスクライアント に分けて見ると理解しやすくなります。
実行クライアントは、取引の受信、実行、状態管理、transaction pool、execution payloadなどを担当します。
一方、コンセンサスクライアントは、PoSの合意形成、Beacon block、attestation、fork choice、finalityなどを担当します。
この章で押さえたいポイントは、次の通りです。
| ポイント | 説明 |
|---|---|
| Ethereumノードは2つのクライアントで見ると分かりやすい | 実行クライアントとコンセンサスクライアントが協力して動く |
| P2Pネットワークも2つに分けて考える | 実行レイヤーP2PとコンセンサスレイヤーP2Pがある |
| 実行レイヤーP2Pでは取引が広がる | 未承認取引は実行クライアント同士でgossipされる |
| コンセンサスレイヤーP2PではBeacon blockやattestationが広がる | PoSの合意形成に関係する情報が流れる |
| 2つのクライアントはローカルで連携する | Engine APIなどを通じてpayloadや検証結果をやり取りする |
| DevP2PやRLPxは実行レイヤーP2Pの理解に関係する | peer discovery、セッション確立、sub-protocolなどの土台になる |
| libp2pやgossipsubはコンセンサスレイヤーP2Pの理解に関係する | ブロックやattestationのgossip、request/responseに関係する |
| P2Pだけで正しさは決まらない | 受け取った取引やブロックは、検証やコンセンサスのルールと組み合わせて扱われる |
BitcoinとEthereumを比べると、Ethereumの方が用語や構成が複雑に見えるかもしれません。
しかし、根本にある考え方は同じです。
ノード同士がpeerを見つけ、必要な情報を広げ、届いた情報を検証しながら、同じ履歴を共有しようとする。
次の章では、ここまで見てきたP2Pネットワークの強みを整理します。
中央の1か所だけに頼らないことで、耐障害性、検閲耐性、参加しやすさなどの面でどのようなメリットがあるのかを見ていきます。
8. P2Pネットワークの強み
この章では、ここまで見てきたP2Pネットワークの強みを整理します。
ポイントは、P2Pが「絶対に止まらない魔法」ではなく、中央の1か所だけに頼りすぎないことで、障害・検閲・情報の偏りに強くなりやすい設計 だということです。
前章では、EthereumのP2Pネットワークを見ました。
Bitcoinでは、フルノード同士がブロックや取引を交換します。
Ethereumでは、実行クライアントとコンセンサスクライアントが、それぞれP2Pネットワークを使って取引、Beacon block、attestationなどを広げます。
ここまでで、P2Pネットワークは「情報を広げるための土台」であり、取引やブロックの正しさを単独で決める仕組みではないことを確認しました。
では、なぜブロックチェーンでは、わざわざP2Pネットワークを使うのでしょうか。
一言でいうと、中央の1か所だけに頼ると、その1か所が止まったり、遮断されたり、偏った情報を流したりしたときに、全体へ影響が出やすいから です。
身近な例で考えると、クラス全体への連絡を先生1人だけが送る仕組みと、複数人が互いに連絡を回し合う仕組みの違いに近いです。
先生1人からの連絡は分かりやすく管理しやすい一方で、その先生が連絡できない状態になると、全員への共有が止まってしまうかもしれません。
一方で、複数人が互いに連絡を回し合う仕組みなら、多少遠回りになっても、別の経路から情報が届く可能性があります。
もちろん、これだけで完璧になるわけではありません。
情報が遅れて届くこともありますし、間違った情報が広がる可能性もあります。
だからこそ、P2Pネットワークでは「広げる仕組み」と「検証する仕組み」を分けて考える必要があります。
NISTは、ブロックチェーンを中央の保管場所や中央権限なしに分散的に実装される台帳として説明し、複数のノードが台帳のコピーを維持し、検証とコンセンサスルールに従って新しいブロックを追加すると説明しています。
参考: NIST: Blockchain
参考: NISTIR 8202: Blockchain Technology Overview
この章では、P2Pネットワークの強みを次の観点から整理します。
| 強み | ざっくりした意味 |
|---|---|
| 単一障害点を減らしやすい | 1か所が止まっても、別の経路で情報が届く可能性がある |
| 情報のコピーを複数の場所に置ける | 1台のサーバーだけではなく、複数ノードが履歴を保持できる |
| 検閲や遮断に強くなりやすい | 1つの経路で止められても、別経路から届く可能性がある |
| 参加者が独立して検証できる | 他人の言うことをそのまま信じず、自分のノードで確認できる |
| 地理的・実装的な偏りを減らしやすい | 場所やソフトウェア実装が分散していれば、障害の影響を抑えやすい |
ただし、どの強みも「必ずそうなる」とは言い切れません。
ノードが少なかったり、接続先が偏っていたり、同じクラウドや同じクライアント実装に依存しすぎていたりすると、P2Pの強みは弱くなります。
そのため、この章では強みだけを持ち上げるのではなく、何があると強みとして働きやすいのか も一緒に見ていきます。
8.1 単一障害点を減らしやすい
P2Pネットワークの分かりやすい強みは、単一障害点 を減らしやすいことです。
単一障害点とは、そこが止まると全体が止まってしまうような弱点のことです。
たとえば、クラスの連絡が1つの掲示板だけに集約されていて、その掲示板が見られなくなると、全員が連絡を確認できなくなるかもしれません。
中央サーバー型の通信では、中央サーバーが重要な役割を持ちます。
この構成は、とても分かりやすく、管理もしやすいです。
実際、多くのWebサービスや業務システムでは、中央サーバー型の構成が使われています。
一方で、中央サーバーが止まると、利用者同士の情報共有も止まりやすくなります。
P2Pネットワークでは、中央の1台だけに情報共有を任せるのではなく、ノード同士が複数の経路でつながります。
このような構成では、一部のノードが止まっても、別の経路が残っていれば情報が届く可能性があります。
Bitcoinの説明でも、フルノードは他のフルノードから取引やブロックを受け取り、検証して、さらに別のフルノードへ中継すると説明されています。
つまり、1つの中央サーバーだけがすべての取引やブロックを配る構成ではありません。
参考: Bitcoin: Running A Full Node
参考: Bitcoin Developer Guide: P2P Network
ただし、ここで注意したいのは、P2Pなら必ず止まらないわけではないことです。
ネットワーク分断、接続先の偏り、ソフトウェアのバグ、大規模な通信障害などがあれば、P2Pネットワークでも影響を受けます。
ここでは、まず「中央1点に依存するより、複数経路を持つことで障害に強くなりやすい」と理解しておくとよいです。
8.2 小さなコードで見る:中央サーバー停止とP2P経路の違い
ここで、中央サーバー型とP2P型の違いを、かなり単純化したPythonコードで見てみます。
このコードは実際のBitcoinやEthereumのネットワークを再現するものではありません。
目的は、中央の1か所が止まる場合 と 一部ノードが止まっても別経路が残る場合 の違いを直感的に見ることです。
# 中央サーバー型とP2P型の情報到達を比較するための学習用コードです。
# 実際のBitcoinやEthereumのネットワーク挙動を再現するものではありません。
from collections import deque
# まず、中央サーバー型の例です。
# server_is_online が False の場合、中央サーバーから誰にも連絡が届かない想定にします。
participants = ["A", "B", "C", "D"]
server_is_online = False
def central_server_delivery(participants, server_is_online):
"""中央サーバーがオンラインなら全員へ届き、停止中なら誰にも届かない、という単純なモデルです。"""
if not server_is_online:
return {name: False for name in participants}
return {name: True for name in participants}
print("中央サーバー型:")
print(central_server_delivery(participants, server_is_online))
# 次に、P2P型の例です。
# 各ノードが接続しているpeerをグラフとして表します。
network = {
"A": ["B", "C"],
"B": ["A", "D"],
"C": ["A", "D"],
"D": ["B", "C"],
}
def p2p_delivery(network, start_node, failed_nodes=None):
"""
start_node から情報がどこまで届くかを、幅優先探索で確認します。
failed_nodes:
停止しているノードの集合です。
停止しているノードは、情報を受け取ることも中継することもできない想定です。
"""
failed_nodes = set(failed_nodes or [])
reached = set()
queue = deque([start_node])
while queue:
current = queue.popleft()
# 停止しているノードや、すでに到達済みのノードは処理しません。
if current in failed_nodes or current in reached:
continue
reached.add(current)
# 現在のノードが知っているpeerへ情報を広げます。
for peer in network.get(current, []):
if peer not in reached and peer not in failed_nodes:
queue.append(peer)
return reached
print("\nP2P型:")
print("Bが停止している場合に情報が届くノード:")
print(p2p_delivery(network, start_node="A", failed_nodes={"B"}))
この例では、中央サーバー型ではサーバーが止まると全員に届かない、という結果になります。
一方、P2P型では B が止まっていても、A -> C -> D という別経路があるため、D まで情報が届きます。
もちろん、実際のP2Pネットワークでは、接続数、通信遅延、peer選択、NAT、ファイアウォール、ノードの性能など、もっと多くの要素が関係します。
ただ、この小さな例だけでも、複数経路を持つことの意味は見えてきます。
8.3 情報のコピーを複数の場所に置ける
P2Pネットワークのもう一つの強みは、情報のコピーを複数の場所に置きやすいことです。
ブロックチェーンでは、すべての情報が1台のサーバーだけに保存されるわけではありません。
フルノードは、ブロックや取引を検証しながら、自分の手元にも必要な情報を保持します。
NISTは、ブロックチェーンネットワークでは台帳のコピーがネットワーク内のノードに維持され、新しいブロックは検証とコンセンサスルールに従って追加されると説明しています。
参考: NIST: Blockchain
これは、会計ノートを1冊だけ金庫に入れておくのではなく、複数人が同じ内容のコピーを持っている状態に近いです。
| 構成 | 情報の置き方 | 起こりやすい問題 |
|---|---|---|
| 1台の中央サーバーだけ | その1台に記録が集中する | そのサーバーが壊れると確認しにくくなる |
| 複数ノードがコピーを保持 | いくつかの場所に記録が残る | ノード間で同期や検証が必要になる |
ただし、ここでも丁寧に見る必要があります。
すべてのノードが必ず完全な履歴を持つわけではありません。
Bitcoinでも、全履歴を保持するアーカイバルノードだけでなく、過去のブロックデータを削減するpruned nodeがあります。
また、軽量クライアントはフルノードに依存する部分があります。
Bitcoin Developer Guideでも、フルノードには全ブロックチェーンを保存するarchival nodeと、全体を保存しないpruned nodeがあると説明されています。
参考: Bitcoin Developer Guide: P2P Network
つまり、P2Pの強みは「全員が全部を持つ」という単純な話ではありません。
より正確には、複数の参加者が情報を保持・検証できることで、1か所だけに依存しにくくなる ということです。
💡 豆知識
「分散して保存されている」と聞くと、すべてのノードが完全に同じデータを持っているように感じるかもしれません。
しかし実際には、フルノード、アーカイバルノード、pruned node、軽量クライアントなど、役割や保存範囲が異なります。
「どのノードが、どこまで保存し、どこまで検証できるのか」を分けて見ると、P2Pネットワークの理解がかなり正確になります。
8.4 小さなコードで見る:複数ノードにコピーがある意味
次のコードでは、あるブロックをどのノードが持っているかを確認します。
一部のノードがオフラインでも、別のノードが同じブロックを持っていれば、そこから情報を得られる可能性があります。
# 複数ノードが情報のコピーを持つ意味を確認するための学習用コードです。
# 実際のブロックチェーンの同期処理やデータ保存方式を再現するものではありません。
# 各ノードが持っているブロックIDを、説明用に集合で表します。
ledger_copies = {
"Node_A": {"block_1", "block_2", "block_3"},
"Node_B": {"block_1", "block_2", "block_3"},
"Node_C": {"block_1", "block_2"},
"Node_D": {"block_1", "block_2", "block_3"},
}
# Node_A と Node_D が一時的にオフラインになっている想定です。
offline_nodes = {"Node_A", "Node_D"}
def find_nodes_that_have_block(ledger_copies, block_id, offline_nodes=None):
"""
指定したblock_idを持っていて、かつオンラインのノードを探します。
offline_nodes:
現在アクセスできないノードの集合です。
"""
offline_nodes = set(offline_nodes or [])
holders = []
for node, blocks in ledger_copies.items():
# オフラインのノードは問い合わせ先として使えない想定にします。
if node in offline_nodes:
continue
if block_id in blocks:
holders.append(node)
return holders
block_id = "block_3"
available_nodes = find_nodes_that_have_block(ledger_copies, block_id, offline_nodes)
print(f"{block_id} を取得できる可能性があるノード:")
print(available_nodes)
この例では、Node_A と Node_D がオフラインでも、Node_B が block_3 を持っているため、情報を得られる可能性があります。
もちろん、実際のブロックチェーンでは、単に「データを持っているか」だけでは不十分です。
受け取ったブロックが正しいかどうかを検証する必要があります。
つまり、P2Pで複数のコピーがあることは強みですが、最終的には コピーを受け取ったあとに検証できること が重要です。
8.5 検閲や遮断に強くなりやすい
P2Pネットワークは、検閲や遮断に対しても強くなりやすい性質があります。
ここでいう検閲とは、特定の情報を通さないようにすることです。
たとえば、1つの中央サーバーだけが連絡を配っている場合、そのサーバーが特定のメッセージを配らなければ、利用者には届きません。
一方で、P2Pネットワークでは、ある経路で情報が止められても、別のpeerから届く可能性があります。
この図では、B -> D の経路では情報が止められています。
しかし、A -> C -> D の経路が残っていれば、D は情報を受け取れる可能性があります。
ただし、これも「絶対に検閲されない」という意味ではありません。
ネットワークの大部分が特定の事業者や地域、クラウド環境、少数のpeerに偏っている場合、検閲や遮断の影響を受けやすくなります。
また、ノードの接続先が攻撃者に偏ってしまうと、外の情報が見えにくくなる可能性もあります。
そのため、P2Pネットワークの検閲耐性は、次のような条件に左右されます。
| 観点 | 検閲耐性に関係する理由 |
|---|---|
| peerの数 | 接続先が少なすぎると、情報経路が限られる |
| peerの多様性 | 同じ地域・同じ事業者・同じネットワークに偏ると、同時に影響を受けやすい |
| ノード数 | 参加ノードが多いほど、複数経路が生まれやすい |
| 検証ルール | 届いた情報を無条件に受け入れず、正しさを確認できる |
| クライアント実装の多様性 | 同じ実装のバグに一斉に巻き込まれるリスクを抑えやすい |
Ethereum公式ドキュメントでは、Ethereumは標準化されたプロトコルで通信する多数のノードからなるP2Pネットワークであり、情報のgossipと特定ノード間のrequest/responseが使われると説明されています。
参考: Ethereum Documentation: Networking layer
このような仕組みにより、情報は1つの中央サーバーだけではなく、複数のpeerを通じて広がります。
その結果、特定の経路が止められても、別経路から届く可能性が生まれます。
8.6 小さなコードで見る:1つの経路が止まっても別経路で届く
次のコードでは、一部の経路がブロックされても、別の経路から情報が届く様子を確認します。
# 一部の通信経路がブロックされても、別経路で情報が届く可能性を確認する学習用コードです。
# 実際の検閲耐性やネットワーク攻撃を評価するものではありません。
from collections import deque
# AからDへは、A->B->D と A->C->D の2つの経路があります。
network = {
"A": ["B", "C"],
"B": ["D"],
"C": ["D"],
"D": [],
}
# BからDへの経路だけがブロックされている想定です。
blocked_edges = {("B", "D")}
def propagate_with_blocked_edges(network, start_node, blocked_edges):
"""
start_nodeから情報を広げます。
blocked_edges に含まれる経路では、情報が中継されない想定です。
"""
reached = {start_node}
queue = deque([start_node])
log = []
while queue:
current = queue.popleft()
for peer in network.get(current, []):
# ブロックされている経路なら、情報を送れません。
if (current, peer) in blocked_edges:
log.append(f"{current} -> {peer}: ブロックされました")
continue
# まだ届いていないpeerなら、情報を届けて次の中継候補にします。
if peer not in reached:
reached.add(peer)
queue.append(peer)
log.append(f"{current} -> {peer}: 届きました")
return reached, log
reached, log = propagate_with_blocked_edges(network, "A", blocked_edges)
print("中継ログ:")
for line in log:
print(line)
print("\n最終的に情報が届いたノード:")
print(reached)
この例では、B -> D の経路がブロックされています。
しかし、A -> C -> D の経路が残っているため、D へ情報が届きます。
P2Pネットワークの強みは、このような 経路の冗長性 にあります。
ただし、すべての経路が同じ場所や同じ管理主体に偏っている場合、この強みは弱くなります。
たとえば、全ノードが同じクラウド、同じ地域、同じネットワーク事業者に偏っていると、その場所の障害や制限の影響を受けやすくなります。
P2Pの強みを活かすには、「ノード数が多い」だけでなく、どのように分散しているか も重要です。
8.7 参加者が独立して検証できる
P2Pネットワークの強みは、情報が広がることだけではありません。
ブロックチェーンでは、受け取った情報を各ノードが検証できることも重要です。
Bitcoinのフルノードは、取引やブロックを完全に検証するプログラムとして説明されています。
また、多くのフルノードは、他のフルノードから受け取った取引やブロックを検証し、さらに別のフルノードへ中継します。
参考: Bitcoin: Running A Full Node
この「自分で検証できる」という点は、かなり大きな意味を持ちます。
中央サーバー型のシステムでは、利用者はサーバーが返した結果を信じる場面が多くなります。
もちろん、認証、監査、ログ、冗長化などによって安全性を高めることはできます。
一方、ブロックチェーンのフルノードでは、他のノードから届いたブロックや取引を、そのまま鵜呑みにしません。
自分のノードがルールに従って検証し、無効なものは受け入れません。
| 観点 | 中央サーバー型のイメージ | P2P + 検証のイメージ |
|---|---|---|
| 情報の受け取り | サーバーから結果を受け取る | peerから情報を受け取る |
| 正しさの確認 | サーバー側の処理を信頼する場面が多い | ノードがルールに従って検証する |
| 不正な情報 | サーバーや管理者の防御に依存しやすい | 各ノードが無効な情報を拒否できる |
| 信頼の置き方 | 特定の運営主体に寄りやすい | プロトコルと検証ルールに寄せやすい |
もちろん、利用者全員がフルノードを動かすわけではありません。
軽量クライアントやウォレット、取引所、ノードプロバイダを使う場合は、別の信頼前提が入ります。
それでも、「誰かが独立して検証できる」という性質は、ブロックチェーンの透明性や監査可能性を支える重要な要素です。
8.8 小さなコードで見る:届いた情報をそのまま信じない
次のコードでは、peerから届いた取引をそのまま受け入れるのではなく、簡単な検証を行ってから扱う例を見ます。
このコードは、実際のBitcoinやEthereumの検証ルールではありません。
署名、残高、nonce、UTXO、ガス、スマートコントラクト実行などは大幅に単純化しています。
# peerから届いた取引を、簡単なルールで検証してから受け入れる学習用コードです。
# 実際のBitcoinやEthereumの検証ルールを再現するものではありません。
transactions = [
{"tx_id": "tx_1", "sender": "Alice", "amount": 30, "signature_valid": True},
{"tx_id": "tx_2", "sender": "Alice", "amount": 120, "signature_valid": True},
{"tx_id": "tx_3", "sender": "Bob", "amount": 10, "signature_valid": False},
]
# 説明用の残高です。
# 実際のブロックチェーンでは、UTXOやアカウント状態などに基づいてもっと厳密に確認します。
balances = {
"Alice": 100,
"Bob": 50,
}
def validate_transaction(tx, balances):
"""
取引を受け入れてよいかを、説明用の簡単なルールで確認します。
確認すること:
- 署名が有効か
- 金額が0より大きいか
- 送信者の残高が足りているか
"""
if not tx["signature_valid"]:
return False, "署名が無効です"
if tx["amount"] <= 0:
return False, "金額が0以下です"
if balances.get(tx["sender"], 0) < tx["amount"]:
return False, "残高が足りません"
return True, "有効な取引として扱えます"
for tx in transactions:
is_valid, reason = validate_transaction(tx, balances)
print(f"{tx['tx_id']}: {reason}")
# 有効な取引だけを、ローカルのmempoolやtransaction poolへ入れるイメージです。
if is_valid:
print(" -> ローカルの一時置き場へ追加する候補になります")
else:
print(" -> 受け入れず、中継もしない候補になります")
この例では、tx_1 だけが有効な取引として扱われます。
tx_2 は残高不足、tx_3 は署名が無効なので受け入れません。
このように、P2Pネットワークで情報が届いたとしても、それだけで正しいとは判断しません。
各ノードが検証するからこそ、悪意あるpeerから不正な情報が届いた場合でも、無効なものを拒否できます。
ここは、P2Pネットワークを理解するうえでとても重要です。
P2Pは情報を届ける仕組みです。
その情報を受け入れてよいかは、検証ルールによって判断します。
8.9 地理的・運用的な分散があると障害に強くなりやすい
P2Pネットワークの強みは、ノードが単にたくさんあるだけでは十分に発揮されません。
どこにノードがあるのか、誰が運用しているのか、どのクライアント実装を使っているのかも重要です。
たとえば、ノード数が多く見えても、実はほとんどが同じクラウド事業者、同じ地域、同じ組織、同じソフトウェア実装に依存していたらどうでしょうか。
その場合、見た目には分散していても、実際には同じ原因でまとめて影響を受ける可能性があります。
Ethereum公式のnodes and clientsでは、複数のクライアント実装があることは、単一のコードベースへの依存を減らし、ネットワークをより強くする方向に働くと説明されています。
参考: Ethereum Documentation: Nodes and clients
これは、情報セキュリティの観点でも大切です。
同じ実装に重大なバグがあると、その実装を使っているノードがまとめて影響を受ける可能性があります。
一方で、複数の実装が適度に使われていれば、1つの実装の問題がネットワーク全体に広がるリスクを抑えやすくなります。
もちろん、クライアント多様性があれば何でも安全、というわけではありません。
異なる実装間で仕様解釈がずれると、それはそれで問題になる場合があります。
それでも、「同じものに依存しすぎない」という考え方は、P2Pネットワークの強みを活かすうえで重要です。
8.10 透明性や監査可能性を高めやすい
P2Pネットワークとブロックチェーンを組み合わせると、透明性や監査可能性も高めやすくなります。
ここでいう透明性とは、すべての個人情報が丸見えになるという意味ではありません。
ブロックチェーンの文脈では、ルールに従って公開されている取引やブロックを、多くの参加者が確認できるという意味に近いです。
NISTは、ブロックチェーン技術について、参加者のコミュニティが共有された、改ざんを検知しやすく耐性を持つデジタル台帳を維持する方法を提供すると説明しています。
参考: NIST: Blockchain
P2Pネットワークによって、取引やブロックが複数ノードへ広がります。
そして、各ノードがそれを検証し、自分の持つ台帳へ反映します。
その結果、次のような確認がしやすくなります。
- ある取引がブロックに含まれているか
- どのブロックの後にどのブロックが続いているか
- 取引やブロックがルールに沿っているか
- 特定のノードだけが違う履歴を見ていないか
- ネットワークの一部で情報伝播が遅れていないか
ただし、透明性にも限界があります。
ブロックチェーン上に見えている情報だけで、現実世界の本人性や意図まで完全に分かるわけではありません。
また、ノードを運用していない利用者は、ウォレット、RPCプロバイダ、ブロックエクスプローラなどの表示に依存することがあります。
そのため、透明性を語るときも、次のように分けると安全です。
| 観点 | できること | 注意点 |
|---|---|---|
| ブロックや取引の確認 | 公開された履歴を検証できる | 表示サービスの解釈に依存する場合がある |
| ノードによる検証 | 自分でルールに沿って確認できる | フルノード運用にはコストがある |
| 監査・分析 | 履歴をもとに動きを追いやすい | アドレスと現実の人物が常に対応するわけではない |
| ネットワーク観測 | 伝播や接続の状況を調べられる | 観測地点によって見え方が変わる |
P2Pネットワークは、こうした透明性や検証可能性を支える通信の土台です。
8.11 P2Pの強みは「組み合わせ」で効いてくる
ここまで、P2Pネットワークの強みをいくつか見てきました。
ただし、P2Pネットワークだけでブロックチェーンの安全性が成立するわけではありません。
P2Pは、あくまで情報を届ける仕組みです。
ブロックチェーンでは、そこにハッシュ、デジタル署名、検証ルール、コンセンサス、インセンティブ設計などが組み合わさります。
たとえば、P2Pネットワークで取引が広がっても、署名が無効なら受け入れられません。
ブロックが届いても、コンセンサスルールに反していれば採用されません。
たくさんのノードがあっても、全員が同じ実装のバグに依存していれば、別のリスクが残ります。
そのため、P2Pネットワークの強みは、次のように整理すると分かりやすいです。
| P2Pの強み | それだけでは足りない理由 | 組み合わせるもの |
|---|---|---|
| 情報を複数経路で広げられる | 間違った情報も広がり得る | 検証ルール |
| 中央1点への依存を減らせる | peerが偏ると弱くなる | peer discovery、peer管理、運用分散 |
| 複数ノードが履歴を持てる | 古い情報や不正な情報を信じる可能性がある | 同期、検証、コンセンサス |
| 検閲に強くなりやすい | ネットワーク分断や接続支配のリスクがある | 経路の多様性、ノード多様性 |
| 独立検証できる | すべての利用者がフルノードを動かすわけではない | フルノード、軽量クライアント設計、監査 |
この見方をしておくと、「P2Pだから安全」「分散しているから絶対に止まらない」といった強すぎる表現を避けられます。
8.12 この章のまとめ
この章では、P2Pネットワークの強みを整理しました。
P2Pネットワークは、中央の1か所だけに頼らず、ノード同士が情報をやり取りすることで、障害や遮断に強くなりやすい構成を作れます。
また、複数ノードが情報のコピーを保持し、各ノードが届いた情報を検証できることで、透明性や監査可能性を高めやすくなります。
この章で押さえたいポイントは、次の通りです。
| ポイント | 説明 |
|---|---|
| 単一障害点を減らしやすい | 中央の1か所が止まっても、別経路で情報が届く可能性がある |
| 複数の場所にコピーを置ける | 1台のサーバーだけに履歴を集中させず、複数ノードで保持・検証できる |
| 検閲や遮断に強くなりやすい | 1つの経路で止められても、別経路から情報が届く可能性がある |
| 独立検証ができる | peerから届いた情報をそのまま信じず、各ノードがルールに従って検証できる |
| 多様性が重要 | 地理的分散、運用主体の分散、クライアント実装の多様性が強みに関係する |
| P2Pだけでは十分ではない | 検証ルール、暗号技術、コンセンサス、運用設計と組み合わせて機能する |
P2Pネットワークは、ブロックチェーンの分散性を支える重要な土台です。
ただし、P2Pだからといって無条件に安全になるわけではありません。
次の章では、この裏側にある P2Pネットワークの限界とセキュリティ上の注意点 を見ていきます。
Sybil攻撃、Eclipse攻撃、ネットワーク分断、peerの偏りなどを整理しながら、「中央だけに頼らない仕組み」にもどのような弱点があるのかを確認します。
9. P2Pネットワークの限界とセキュリティ上の注意点
この章では、P2Pネットワークの弱点や注意点を整理します。
ポイントは、P2Pが「中央の1か所だけに頼らない」ための強い考え方である一方で、P2Pにしただけで自動的に安全になるわけではない という点です。
前の章では、P2Pネットワークの強みを見ました。
中央の1か所だけに頼らず、複数のノードが情報を持ち、別経路からも情報が届くようにすることで、障害や遮断に強くなりやすい構成を作れます。
また、ブロックチェーンでは、各ノードが取引やブロックを検証することで、届いた情報をそのまま信じずに確認できます。
ただし、ここで誤解してはいけないのは、P2Pネットワークが万能ではないということです。
身近な連絡網でも、友人同士で情報を伝え合う形にすれば、代表者1人に頼るより強くなる場合があります。
しかし、もし悪意ある人が大量の偽アカウントでグループに入ってきたり、特定の人の周りを悪意ある人だけで囲んだり、通信経路を分断したりすれば、情報の伝わり方はゆがめられてしまいます。
P2Pネットワークでも、これに近い問題が起こり得ます。
| 注意点 | 身近なイメージ | P2Pネットワークでのイメージ |
|---|---|---|
| Sybil攻撃 | 1人が大量の偽アカウントで多数派に見せる | 攻撃者が多数の偽ノードを用意する |
| Eclipse攻撃 | ある人の周りを悪意ある人だけで囲む | 被害ノードの接続先を攻撃者ノードで独占する |
| ネットワーク分断 | グループが2つに割れて情報が行き来しない | ノード群が分断され、別々の情報を見てしまう |
| スパム・DoS | 大量の不要メッセージで連絡網を埋める | 大量の無効な取引や接続でリソースを消費させる |
| peerの偏り | 同じ地域・同じ組織の人だけとつながる | 接続先が同じAS、地域、クラウド、実装に偏る |
この章では、こうした注意点を、できるだけ身近な例と小さなコードで整理します。
ただし、本記事では攻撃を再現するための具体的な手順や、実際のノードを攻撃する方法は扱いません。
あくまで、P2Pネットワークを安全に理解するための概念整理として扱います。
9.1 P2Pは「情報を広げる仕組み」であり、情報を正しくする魔法ではない
まず、もっとも大切な点から確認します。
P2Pネットワークは、情報をノードからノードへ広げる仕組みです。
しかし、広がった情報が正しいかどうか までは、P2Pネットワークだけでは決まりません。
たとえば、研究室の連絡網で、誰かが次のような連絡を流したとします。
明日の集合時間は10時です。
この連絡が友人から友人へ広がったとしても、その内容が本当に正しいかどうかは別問題です。
正式な予定表や先生からの連絡と照合しなければ、誤情報かもしれません。
ブロックチェーンでも同じです。
P2Pネットワークは、取引やブロックを広げるために使われます。
しかし、その取引が有効か、ブロックがルールに合っているか、どの履歴を採用するかは、検証ルールやコンセンサスの役割です。
Bitcoin Developer Guideでは、フルノードはブロックや取引を他のノードへ中継する前に検証すると説明されています。
参考: Bitcoin Developer Guide: P2P Network
この関係を整理すると、次のようになります。
| 役割 | 担当すること | それだけでは足りないこと |
|---|---|---|
| P2Pネットワーク | 取引やブロックを広げる | 内容が正しいかは判断しない |
| 検証ルール | 署名、残高、形式、ブロック条件などを確認する | どの履歴を伸ばすかは別途考える必要がある |
| コンセンサス | 多くの参加者が同じ履歴を扱えるようにする | ネットワーク伝播がなければ情報が届かない |
| ノード運用 | 実際に接続し、同期し、検証する | 接続先が偏ると見える情報がゆがむ場合がある |
つまり、P2Pはブロックチェーンを支える重要な土台ですが、単独で安全性を完成させるものではありません。
この図のように、P2Pは最初の入口です。
しかし、その先にある検証や履歴選択まで含めて考えないと、ブロックチェーン全体の安全性は理解できません。
9.2 Sybil攻撃:偽の参加者を大量に作る問題
P2Pネットワークで代表的な注意点の一つが、Sybil攻撃 です。
Sybil攻撃とは、1人の攻撃者が大量の偽の参加者を作り、まるで多数派であるかのように振る舞う攻撃です。
身近な例で言えば、オンラインアンケートで1人がアカウントを100個作って投票するようなものです。
表面上は100人が投票しているように見えても、実際には1人の意見かもしれません。
P2Pネットワークでも、もし参加者の数だけで信頼してしまうと、攻撃者が大量の偽ノードを用意することで、情報の流れや多数派の見え方をゆがめられる可能性があります。
DouceurのSybil攻撃に関する論文では、大規模P2Pシステムでは、単一の故障または悪意ある主体が複数のIDを提示できると、冗長性の前提を崩せると説明されています。
また、論理的な中央機関なしにSybil攻撃を完全に防ぐことは、極端で非現実的な仮定を除いて難しいと述べられています。
参考: Microsoft Research: The Sybil Attack
💡 豆知識
Sybil攻撃の名前は、複数の人格を持つ人物を扱った書籍名に由来するとされています。
P2Pやブロックチェーンの文脈では、「1人がたくさんの顔を持って、多数派のように見せる攻撃」と考えると分かりやすいです。
9.3 小さなコードで見る:参加者数だけを見ると危ない
次のコードは、Sybil攻撃の考え方を理解するための学習用コードです。
実際のブロックチェーンの投票やコンセンサスを再現するものではありません。
ここでは、単純に「どの情報を多くのノードが支持しているか」だけで判断すると、偽ノードの数にだまされる可能性があることを確認します。
# Sybil攻撃の考え方を理解するための学習用コードです。
# 実際のBitcoinやEthereumのコンセンサスを再現するものではありません。
from collections import Counter
# 正直なノード3台は、正しい情報Aを支持しているとします。
honest_nodes = [
{"node_id": "honest_1", "message": "情報A"},
{"node_id": "honest_2", "message": "情報A"},
{"node_id": "honest_3", "message": "情報A"},
]
# 攻撃者は、偽ノードを10台分作り、情報Bを支持させます。
# ここでは1人の攻撃者が複数のnode_idを持っている、というイメージです。
sybil_nodes = [
{"node_id": f"sybil_{i}", "message": "情報B"}
for i in range(1, 11)
]
# すべてのノードから届いた情報をまとめます。
all_nodes = honest_nodes + sybil_nodes
# 単純にメッセージの数だけを数えると、偽ノードが多い情報Bが優勢に見えます。
message_count = Counter(node["message"] for node in all_nodes)
print("単純な数え上げ結果:")
for message, count in message_count.items():
print(f"{message}: {count}件")
winner = message_count.most_common(1)[0][0]
print(f"\n数だけで見ると採用されそうな情報: {winner}")
このコードでは、正直なノードは3台しかありません。
一方で、攻撃者は10個の偽ノードを用意しています。
そのため、単純に数だけで見ると、攻撃者が流した 情報B が多数派に見えてしまいます。
ここで大切なのは、P2Pネットワークでは「たくさんのpeerから同じ情報が来たから正しい」とは言い切れないことです。
peerの数だけでなく、情報の検証、接続先の多様性、コストを伴う参加条件、コンセンサスルールなどが重要になります。
9.4 Eclipse攻撃:特定のノードを孤立させる問題
Sybil攻撃と並んで、P2Pネットワークで重要なのが Eclipse攻撃 です。
Eclipse攻撃とは、ざっくり言えば、特定のノードの接続先を攻撃者が支配し、そのノードをネットワークの本来の情報から孤立させる攻撃です。
身近な例で考えると、ある学生Aさんの周りの連絡相手を、悪意ある人たちだけで固めるようなものです。
Aさんは「みんながそう言っている」と思っていても、実際には悪意ある人たちからだけ情報を受け取っているかもしれません。
Eclipse攻撃では、被害ノードはネットワークに参加しているつもりでも、実際には攻撃者が用意したpeerとだけ接続している状態になります。
その結果、ブロックや取引の見え方がゆがめられたり、最新情報の到達が遅れたりする可能性があります。
HeilmanらのUSENIX Security 2015の研究では、十分なIPアドレスを制御する攻撃者が、Bitcoinノードの接続を独占できる可能性を示し、その状態を利用して二重支払い、selfish mining、敵対的なフォークなどに悪用できると説明しています。
参考: USENIX Security 2015: Eclipse Attacks on Bitcoin's Peer-to-Peer Network
また、Bitcoin Core Academyでも、Eclipse攻撃は攻撃者が被害ノードをネットワークの残りから孤立させる攻撃として説明されています。
参考: Bitcoin Core Academy: P2P attacks
9.5 小さなコードで見る:peerが偏ると見える情報が偏る
次のコードでは、被害ノードが接続しているpeerによって、見える情報がどう変わるかを単純化して確認します。
これはEclipse攻撃を再現するコードではありません。
あくまで、接続先peerが偏ると、受け取る情報も偏り得る という考え方を理解するための学習用コードです。
# peerの偏りによって見える情報が変わることを確認する学習用コードです。
# 実際のEclipse攻撃を再現するものではありません。
from collections import Counter
# 正常なpeerは、最新ブロックとして block_100 を見ているとします。
honest_peers = {
"honest_1": "block_100",
"honest_2": "block_100",
"honest_3": "block_100",
"honest_4": "block_100",
}
# 攻撃者peerは、古い情報や別の情報を返すとします。
attacker_peers = {
"attacker_1": "block_95",
"attacker_2": "block_95",
"attacker_3": "block_95",
"attacker_4": "block_95",
}
def observe_chain_head(connected_peers):
"""接続中のpeerから見える最新ブロック情報を集計します。"""
observed = Counter(connected_peers.values())
return observed
# ケース1: 正直なpeerと広くつながっている場合
normal_connections = {
"honest_1": honest_peers["honest_1"],
"honest_2": honest_peers["honest_2"],
"attacker_1": attacker_peers["attacker_1"],
}
# ケース2: 攻撃者peerだけとつながっている場合
isolated_connections = attacker_peers
print("通常に近い接続で見える情報:")
print(observe_chain_head(normal_connections))
print("\n攻撃者peerだけに偏った接続で見える情報:")
print(observe_chain_head(isolated_connections))
この例では、通常に近い接続では block_100 が見えています。
しかし、攻撃者peerだけに接続が偏ると、被害ノードからは block_95 が多数派に見えてしまいます。
実際のブロックチェーンでは、ブロックヘッダ、PoW、署名、フォーク選択、同期処理など多くの検証があります。
そのため、このコードのように単純に多数決で決めるわけではありません。
それでも、接続先が偏ること自体が危険である、という感覚は重要です。
9.6 ネットワーク分断:同じネットワークが別々の世界に見える
P2Pネットワークでは、ネットワーク分断も重要な問題です。
ネットワーク分断とは、ノード群が複数のグループに分かれ、グループ間で情報が届きにくくなる状態です。
身近な例で言うと、同じクラスの連絡網なのに、AグループとBグループの間で連絡が途切れてしまい、それぞれが別々の予定を信じてしまうような状態です。
分断が起きると、片方のグループには新しい取引やブロックが届いているのに、もう片方には届いていない、ということが起こり得ます。
その結果、一時的に異なる履歴候補が伸びる可能性があります。
ブロックチェーンでは、こうした分断が永遠に続かない限り、ネットワークが再接続された後にコンセンサスルールに従って履歴が整理される場合があります。
しかし、分断中に取引を信用しすぎると、あとから履歴が変わるリスクがあります。
ここでも、P2P、検証、コンセンサスを分けて考えることが大切です。
| 観点 | 分断時に起こり得ること |
|---|---|
| P2Pネットワーク | グループ間で情報が届きにくくなる |
| 検証ルール | 各グループ内では有効なブロックを検証する |
| コンセンサス | 再接続後に、どの履歴を採用するかが問題になる |
| 利用者 | 十分な確定性を待たずに取引を信用するとリスクがある |
9.7 小さなコードで見る:ネットワーク分断で情報が届かない範囲
次のコードでは、ネットワークが2つに分かれると、メッセージが片方のグループにしか届かないことを確認します。
これは実際のブロックチェーンネットワークを再現するものではありません。
グラフ構造として、どのノードに情報が到達できるかを見るための学習用コードです。
# ネットワーク分断によって、情報が届く範囲が変わることを確認する学習用コードです。
# 実際のブロックチェーンネットワークを再現するものではありません。
from collections import deque
# グループAとグループBが分断されたネットワークを表します。
# A3 と B1 の間の接続がないため、A側からB側へ情報が届きません。
network = {
"A1": ["A2"],
"A2": ["A1", "A3"],
"A3": ["A2"],
"B1": ["B2"],
"B2": ["B1", "B3"],
"B3": ["B2"],
}
def reachable_nodes(start_node):
"""start_nodeから到達できるノードを幅優先探索で調べます。"""
visited = set([start_node])
queue = deque([start_node])
while queue:
current = queue.popleft()
for peer in network[current]:
if peer not in visited:
visited.add(peer)
queue.append(peer)
return visited
print("A1から到達できるノード:")
print(sorted(reachable_nodes("A1")))
print("\nB1から到達できるノード:")
print(sorted(reachable_nodes("B1")))
このコードでは、A1 からは A1、A2、A3 にしか到達できません。
B1 からは B1、B2、B3 にしか到達できません。
つまり、同じP2Pネットワークに参加しているつもりでも、接続が分断されると、見えている世界が分かれてしまいます。
P2Pネットワークでは、ノード数だけでなく、ノード同士がどのようにつながっているかも重要です。
9.8 スパムやDoS:大量の不要な情報でリソースを消費させる問題
P2Pネットワークでは、誰でも参加しやすいことが強みになります。
しかし、誰でも情報を送れるということは、大量の不要な情報や無効な情報が流れてくる可能性もあるということです。
身近な例で言えば、グループチャットに大量の意味のないメッセージが連投されると、本当に重要な連絡が埋もれてしまいます。
また、スマホの通知や通信量、読む人の時間も消費されます。
P2Pネットワークでも、大量の接続、大量のメッセージ、無効な取引、低手数料のスパム取引などによって、ノードのCPU、メモリ、帯域、ディスク、処理時間が圧迫される可能性があります。
Bitcoin Developer Referenceでは、feefilter メッセージにより、指定した手数料率より低い取引の inv を送らないようpeerへ伝えられること、またmempool limitingが低手数料でブロックに入りにくいスパム取引に対する保護として導入されたことが説明されています。
参考: Bitcoin Developer Reference: P2P Network - FeeFilter
ここで大切なのは、P2Pネットワークでは「受け取ったものを全部処理する」と危険になりやすいことです。
| 対策の考え方 | ざっくりした意味 |
|---|---|
| 検証してから受け入れる | 無効な取引やブロックを拒否する |
| mempool policy | 自分のmempoolに入れる条件を決める |
| rate limit | 同じpeerからの大量メッセージを制限する |
| peer管理 | 振る舞いの悪いpeerを切断・入れ替える |
| 手数料フィルタ | 明らかに優先度の低い取引通知を減らす |
9.9 小さなコードで見る:簡単なrate limitの考え方
次のコードは、同じpeerから短時間に大量のメッセージが来た場合に、一定数を超えたものを処理しない例です。
実際のBitcoin CoreやEthereumクライアントの実装ではありません。
P2Pネットワークでリソースを守るために、受け取る量を制限する考え方を学ぶための簡易モデルです。
# rate limitの考え方を理解するための学習用コードです。
# 実際のBitcoin CoreやEthereumクライアントの実装ではありません。
from collections import defaultdict
# peerごとに、1区間で処理してよいメッセージ数の上限を決めます。
MESSAGE_LIMIT = 3
# peerごとの処理済みメッセージ数を記録します。
message_count_by_peer = defaultdict(int)
# 受信したメッセージの例です。
# peer_bad は短時間に大量のメッセージを送っている想定です。
incoming_messages = [
{"peer": "peer_good", "message": "tx_1"},
{"peer": "peer_bad", "message": "spam_1"},
{"peer": "peer_bad", "message": "spam_2"},
{"peer": "peer_bad", "message": "spam_3"},
{"peer": "peer_bad", "message": "spam_4"},
{"peer": "peer_good", "message": "tx_2"},
]
for item in incoming_messages:
peer = item["peer"]
message = item["message"]
# すでに上限を超えているpeerからのメッセージは処理しません。
if message_count_by_peer[peer] >= MESSAGE_LIMIT:
print(f"{peer} からの {message} は上限超過のため処理しません")
continue
# 上限内であれば処理し、カウントを増やします。
message_count_by_peer[peer] += 1
print(f"{peer} からの {message} を処理します")
このコードでは、peer_bad から4件目のメッセージが来たときに、上限を超えたため処理しません。
実際のP2Pネットワークでは、もっと複雑な基準が使われます。
ただ、基本的な考え方は同じです。
すべてのpeerを無条件に信頼せず、リソースを守るための制限や検証を入れる必要があります。
9.10 peer discoveryやbootnodeへの依存にも注意が必要
P2Pネットワークでは、新しく参加するノードが最初から全peerを知っているわけではありません。
そこで、peer discovery、DNS seed、bootnodeなどを使って、最初の接続先候補を見つけます。
これはネットワーク参加の入口として重要です。
ただし、入口があるということは、その入口への依存にも注意が必要です。
Bitcoin Developer Guideでは、Bitcoin Coreが初回起動時にDNS seedを使ってIPアドレスを取得すること、ただしDNS seedの応答は認証されていないため、DNS seedだけに排他的に依存するべきではないと説明されています。
参考: Bitcoin Developer Guide: P2P Network - Peer Discovery
Ethereumでも、bootnodeは新しいノードがpeerを見つけるための入口として使われます。
Ethereum公式ドキュメントでは、bootnodeは新しいノードに接続先となるノード情報を提供するノードとして説明されています。
参考: Ethereum Documentation: Networking layer
ここでも、P2Pを「完全に中央がない」と言い切らない方が安全です。
より正確には、次のように表現できます。
P2Pネットワークは、中央の1か所だけに依存しない通信を目指します。
ただし、初期接続やpeer discoveryでは、DNS seedやbootnodeのような入口が使われることがあります。
この点を理解しておくと、「P2Pなのにbootnodeがあるのは矛盾では?」という疑問にも答えやすくなります。
9.11 peerの偏り:同じ地域・クラウド・実装に寄りすぎる問題
P2Pネットワークでは、ノードの数だけでなく、ノードの多様性も重要です。
たとえば、ノードが1000台あっても、その大半が同じクラウド事業者、同じ地域、同じネットワーク、同じクライアント実装に集中していたらどうでしょうか。
見た目のノード数は多くても、同じ原因でまとめて影響を受ける可能性があります。
身近な例で言うと、連絡網のメンバーがたくさんいても、全員が同じ建物にいて、同じWi-Fiを使っているような状態です。
その建物やWi-Fiに問題が起きると、一気に連絡が止まるかもしれません。
Ethereum公式ドキュメントでは、複数のクライアント実装があることで、単一のコードベースへの依存を減らし、ネットワークをより強くする方向に働くと説明されています。
参考: Ethereum Documentation: Nodes and clients
ただし、クライアントが多様であれば何でも安全というわけではありません。
仕様の解釈違いや実装差分が新しい問題につながる場合もあります。
それでも、P2Pネットワークの健全性を考えるうえで、次のような偏りは意識しておく必要があります。
| 偏りの種類 | 起こり得る問題 |
|---|---|
| 地理的な偏り | 災害や規制、通信障害の影響を受けやすくなる |
| クラウド事業者の偏り | 特定事業者の障害や制限が広範囲に影響する |
| AS・ネットワークの偏り | 経路障害やBGP関連の問題を受けやすくなる |
| クライアント実装の偏り | 特定実装のバグが大きく影響する |
| peer接続の偏り | Eclipse攻撃や情報の偏りに弱くなる |
9.12 小さなコードで見る:接続先の多様性をざっくり確認する
次のコードでは、自分のノードが接続しているpeerの所属ネットワークや実装が偏っていないかを、かなり単純化して確認します。
実際のノード運用では、IPアドレス、AS番号、接続方向、クライアント実装、地理情報などを正確に扱う必要があります。
ここでは、考え方を理解するために、peer情報を辞書で用意しています。
# peerの多様性をざっくり確認するための学習用コードです。
# 実際のノード運用やセキュリティ評価にそのまま使うものではありません。
from collections import Counter
connected_peers = [
{"peer": "peer_1", "region": "JP", "cloud": "CloudA", "client": "ClientX"},
{"peer": "peer_2", "region": "JP", "cloud": "CloudA", "client": "ClientX"},
{"peer": "peer_3", "region": "US", "cloud": "CloudB", "client": "ClientY"},
{"peer": "peer_4", "region": "DE", "cloud": "CloudC", "client": "ClientZ"},
{"peer": "peer_5", "region": "JP", "cloud": "CloudA", "client": "ClientX"},
]
def summarize_distribution(peers, key):
"""指定した項目ごとにpeer数を集計します。"""
return Counter(peer[key] for peer in peers)
for key in ["region", "cloud", "client"]:
print(f"{key} の分布:")
distribution = summarize_distribution(connected_peers, key)
for value, count in distribution.items():
print(f" {value}: {count} peer")
print()
このコードでは、CloudA や ClientX に接続先がやや偏っていることが分かります。
実際のネットワークでは、単純に均等ならよいという話ではありません。
それでも、自分のpeerがどのような属性に偏っているかを意識することは、P2Pネットワークのリスクを考える入口になります。
9.13 プライバシー面の注意:P2Pでは通信の見え方にも気をつける
P2Pネットワークでは、取引やブロックがpeer間で広がります。
このとき、情報が「どのpeerから最初に見えたか」「どのタイミングで届いたか」を観測されると、ネットワーク上の動きについて推測される可能性があります。
たとえば、ある取引を非常に早いタイミングで特定のpeerから受け取った場合、そのpeerが取引の発信元に近いのではないか、と推測されるかもしれません。
もちろん、実際のネットワークでは伝播遅延、接続関係、ノードの実装、リレー方針など多くの要素があるため、単純には決められません。
しかし、P2Pネットワークは「みんなで通信するからプライバシー面も自動的に安全」とは言えません。
この問題に対して、ブロックチェーンやP2Pプロトコルでは、情報伝播方式、peer管理、リレー方針、匿名通信との組み合わせなど、さまざまな研究や実装上の工夫が行われています。
ただし、本記事ではネットワークプライバシーの詳細な攻撃・防御手法には踏み込みません。
ここでは、次の点だけ押さえておきます。
| 観点 | 注意点 |
|---|---|
| 取引の伝播 | 最初に見えたpeerから発信元を推測される可能性がある |
| 接続関係 | peer関係が観測されるとネットワーク構造を推測される場合がある |
| ノード運用 | 自分のIPアドレスや接続先が見える場合がある |
| 外部サービス利用 | 自分でノードを使わずRPCやエクスプローラに頼ると、問い合わせ内容が相手に見える |
P2Pネットワークを理解するときは、「情報を広げる」だけでなく、「情報がどのように見えるか」も大切です。
9.14 対策の考え方:完全防御ではなく、攻撃しにくくする
ここまで見ると、P2Pネットワークには多くの注意点があるように見えます。
しかし、これはP2Pが弱いという話ではありません。
むしろ、中央だけに頼らない仕組みを作るためには、こうした攻撃面を理解したうえで、設計や運用で攻撃しにくくする必要があります。
Bitcoin Core Academyでは、P2P領域の攻撃に対して、peer address bucketing、block-relay-only connections、misbehaving peerの切断、peer rotation / eviction、protected peers、anchor peersなどの対策が整理されています。
参考: Bitcoin Core Academy: P2P attacks
また、Gossipsubに関するProtocol Labsの記事では、Sybil攻撃やEclipse攻撃など、permissionless blockchain networkで考慮すべき攻撃が取り上げられ、mesh管理やpeer scoringなどの対策が説明されています。
参考: Protocol Labs Research: GossipSub
対策を大きく分けると、次のようになります。
| 対策の方向性 | ざっくりした意味 |
|---|---|
| peerを多様にする | 同じ種類のpeerだけに接続しない |
| peerを入れ替える | 長期間同じ接続先だけに依存しない |
| 悪い振る舞いを制限する | スパムや無効な情報を送るpeerを制限・切断する |
| 情報を検証する | peerから届いた情報をそのまま信じない |
| リソースを守る | rate limit、mempool policy、手数料フィルタなどを使う |
| 実装を多様にする | 単一クライアント実装への依存を減らす |
| 観測と更新を続ける | 新しい攻撃や研究に合わせて改善する |
ここで大切なのは、完全防御を前提にしない ことです。
セキュリティでは、「絶対に攻撃されない仕組み」を作るというより、攻撃のコストを高め、検知しやすくし、影響を限定し、回復しやすくする設計が重要になります。
P2Pネットワークでも同じです。
9.15 「P2Pだから安全」と言わないための整理
ここまでの内容を踏まえると、P2Pネットワークについては、強すぎる表現を避けることが大切です。
たとえば、次のような表現は誤解につながりやすいです。
| 避けたい表現 | より丁寧な表現 |
|---|---|
| P2Pなら絶対に止まらない | 中央1点への依存を減らせるため、障害に強くなりやすい |
| P2Pなら検閲されない | 複数経路で届く可能性があるため、検閲に強くなりやすい |
| P2Pならサーバーが不要 | 中央サーバーだけに頼らないが、初期接続にはDNS seedやbootnodeが使われることがある |
| P2Pなら情報は正しい | 情報は広がるが、正しさは検証やコンセンサスで判断する |
| ノード数が多ければ安全 | ノードの分布、接続先、実装、運用主体の多様性も重要 |
| SybilやEclipseは理論上だけの話 | 実際に研究・対策されているP2Pネットワーク上の重要な脅威 |
P2Pネットワークの価値は、弱点がないことではありません。
弱点を理解したうえで、中央の1か所だけに頼らない通信、複数経路での情報伝播、各ノードの独立検証、継続的な改善を組み合わせられる点にあります。
9.16 この章のまとめ
この章では、P2Pネットワークの限界とセキュリティ上の注意点を整理しました。
P2Pネットワークは、中央の1か所だけに頼らない通信を実現しやすくする重要な仕組みです。
しかし、P2Pにしただけで自動的に安全になるわけではありません。
この章で押さえたいポイントは、次の通りです。
| ポイント | 説明 |
|---|---|
| P2Pは情報を広げる仕組み | 情報の正しさは検証ルールやコンセンサスと組み合わせて判断する |
| Sybil攻撃に注意 | 1人が大量の偽ノードを作り、多数派のように見せる可能性がある |
| Eclipse攻撃に注意 | 特定ノードの接続先を攻撃者が独占し、孤立させる可能性がある |
| ネットワーク分断に注意 | グループ間で情報が届かず、別々の履歴を見てしまう場合がある |
| スパムやDoSに注意 | 大量の不要メッセージでCPU、メモリ、帯域などを消費させられる可能性がある |
| peer discoveryへの依存に注意 | DNS seedやbootnodeは便利だが、入口への依存も意識する必要がある |
| 多様性が大切 | 地域、クラウド、AS、クライアント実装、peer接続の偏りを減らすことが重要 |
| 完全防御ではなく攻撃しにくくする | 検証、peer管理、rate limit、mempool policy、client diversityなどを組み合わせる |
P2Pネットワークは、「分散しているから安心」と短くまとめるには少し複雑です。
むしろ、分散しているからこそ、peerの選び方、情報伝播、検証、リソース管理、攻撃耐性を丁寧に設計する必要があります。
次の章では、ここまでの内容を受けて、P2Pネットワークについてよくある誤解を整理します。
「P2Pならサーバーがない」「全員が全員とつながる」「P2Pなら必ず安全」といった表現を、どのように言い換えると正確で分かりやすくなるのかを確認していきます。
10. よくある誤解
この章では、P2Pネットワークについてよく見かける誤解を整理します。
ポイントは、P2Pを「すごく分散していて、何でも自動で安全になる仕組み」と見ないことです。
P2Pはとても重要な通信の考え方ですが、検証、コンセンサス、peer管理、攻撃対策と組み合わせてはじめて、ブロックチェーンの土台として機能します。
ここまで、P2Pネットワークについて、身近な連絡網の例から始めて、BitcoinやEthereumでの使われ方、強み、セキュリティ上の注意点まで見てきました。
ここまで読んでくると、P2Pネットワークは「中央の1か所だけに頼らない」という意味で、とても魅力的に見えると思います。
一方で、P2Pは便利な言葉だからこそ、少し強く言いすぎると誤解につながりやすいです。
たとえば、次のような説明を見かけることがあります。
- P2Pだからサーバーが存在しない
- P2Pだから全員が全員と直接つながっている
- P2Pだから改ざんされない
- P2Pだから絶対に止まらない
- ノード数が多いから必ず安全
これらは、入門として雰囲気をつかむには分かりやすいかもしれません。
ただし、Qiitaの記事として一般公開するなら、もう少し丁寧に書いた方が安全です。
Bitcoin Developer Guideでは、フルノードがブロックや取引を交換するためにP2Pネットワークを共同で維持すると説明されています。
また、初回接続時にはDNS seedを使って接続候補を得る仕組みも説明されています。
参考: Bitcoin Developer Guide: P2P Network
Ethereum公式ドキュメントでも、Ethereumは多数のノードが標準化されたプロトコルで通信するP2Pネットワークであり、gossipやrequest/responseによって情報を交換すると説明されています。
参考: Ethereum Documentation: Networking layer
つまり、P2Pネットワークは「情報を広げるための通信の土台」です。
しかし、それだけで取引の正しさやブロックチェーンの安全性が自動的に決まるわけではありません。
この章では、よくある誤解を1つずつ取り上げて、どのように言い換えると分かりやすく、かつ正確に近づくのかを整理します。
10.1 誤解1:P2Pならサーバーが一切ない
まず多いのが、P2Pを「サーバーが一切ない仕組み」と説明してしまうことです。
たしかに、P2Pネットワークは、中央サーバー1台だけがすべてを管理する構成とは違います。
参加者同士がpeerとして接続し、取引やブロックなどの情報をやり取りします。
ただし、だからといって「サーバー的な役割を持つものが一切ない」と言い切るのは正確ではありません。
たとえばBitcoinでは、新しいノードが最初から全世界のpeerを知っているわけではありません。
初回接続時には、DNS seedから接続先候補を取得する仕組みがあります。
Ethereumでも、bootnodeは新しいノードがネットワークへ参加するときの入口として使われます。
つまり、P2Pネットワークでも、最初の接続先を見つけるための入口や、peer情報を得るための補助的な仕組みが使われることがあります。
| 説明 | 注意点 |
|---|---|
| P2Pならサーバーがない | 強すぎる。初期接続や補助的な仕組みを見落としやすい |
| P2Pは中央サーバーだけに頼らない | より正確。中央の1点にすべてを依存しないという意味が伝わりやすい |
| P2Pでは参加者同士が情報をやり取りする | 初学者向けにも分かりやすい |
このブログ記事では、次のように表現するのがよさそうです。
P2Pネットワークは、中央サーバーを一切使わない仕組みというより、中央の1か所だけに頼らず、参加者同士が情報をやり取りできる通信の考え方です。
この表現なら、P2Pの大事な特徴を保ちつつ、「サーバーが完全に存在しない」という誤解を避けられます。
💡 豆知識
DNS seedやbootnodeは、P2Pネットワークの「入口」に近い役割を持ちます。
たとえるなら、転校初日にクラス全員の連絡先を知らない人が、まず先生や代表者から数人の連絡先を教えてもらうようなものです。
その後は、その人たちを通じてさらに他の人を知っていく、というイメージです。
10.2 誤解2:P2Pでは全員が全員と直接つながっている
次に多いのが、「P2Pでは全ノードが全ノードと直接つながっている」という誤解です。
P2Pという言葉だけを見ると、すべての参加者が互いに直接つながっているように感じるかもしれません。
しかし、実際には、各ノードは一部のpeerと接続し、そのpeerを通じて情報が広がっていきます。
身近な連絡網で考えると分かりやすいです。
クラスの全員が、全員の電話番号を知っていて、全員に同時に連絡するわけではありません。
多くの場合、数人の友人や班のメンバーに連絡し、その人たちがさらに別の人へ伝える形になります。
P2Pネットワークでも同じように、各ノードは接続しているpeerへ情報を伝えます。
その情報がpeerからpeerへ広がることで、ネットワーク全体へ伝播していきます。
この図では、Node A は Node F と直接つながっていません。
それでも、Node B や Node C を通じて情報が届く可能性があります。
| 誤解 | より丁寧な理解 |
|---|---|
| 全員が全員と直接つながる | 各ノードは一部のpeerとつながり、情報が段階的に広がる |
| 直接つながっていないノードには情報が届かない | 中継によって届く場合がある |
| 接続数が多いほど必ずよい | 多様性、品質、リソース消費とのバランスが大切 |
P2Pネットワークでは、接続数を増やせばよいという単純な話ではありません。
接続を増やしすぎると、帯域やCPU、メモリなどのリソースを使います。
一方で、接続先が少なすぎたり、偏りすぎたりすると、情報が届きにくくなったり、Eclipse攻撃のようなリスクが高まったりします。
そのため、「どのpeerとつながるか」「どのpeerを維持するか」「悪い振る舞いをするpeerをどう扱うか」も、P2Pネットワークでは重要です。
10.3 誤解3:P2Pだけで情報の正しさが決まる
P2Pネットワークについて最も注意したい誤解の一つが、P2Pで広がった情報は正しい と考えてしまうことです。
P2Pネットワークは、取引やブロックなどの情報をノード間へ広げるための仕組みです。
しかし、情報が届いたことと、その情報が正しいことは別です。
身近な例で考えてみます。
友人から「明日の集合時間が10時に変わったらしい」と聞いたとします。
その情報は自分に届きました。
しかし、本当に代表者からの正式な連絡なのか、古い情報ではないのか、誰かの勘違いではないのかは、別途確認する必要があります。
ブロックチェーンでも同じです。
取引やブロックがpeerから届いても、ノードはそれをそのまま信じるわけではありません。
Bitcoin Developer Guideでは、フルノードはブロックや取引を他ノードへ中継する前に検証すると説明されています。
参考: Bitcoin Developer Guide: P2P Network
この関係を整理すると、次のようになります。
| 役割 | 担当すること | 担当しないこと |
|---|---|---|
| P2Pネットワーク | 情報をpeer間へ広げる | その情報が正しいかを単独で決めること |
| 検証ルール | 取引やブロックが形式・署名・残高などの条件を満たすか確認する | 情報を全ノードへ届けること |
| コンセンサス | どの履歴を正しい候補として扱うかを決める | ネットワーク接続そのものを維持すること |
つまり、P2Pネットワークは「郵便や連絡網」に近いです。
手紙を届けることはできますが、手紙の内容が正しいかどうかまでは、受け取った人が確認する必要があります。
10.4 誤解4:P2Pなら改ざんされない
「P2Pなら改ざんされない」という表現も、少し注意が必要です。
ブロックチェーンの改ざん耐性は、P2Pネットワークだけで実現されているわけではありません。
ハッシュ、デジタル署名、ブロック同士のリンク、各ノードによる検証、コンセンサス、インセンティブ設計などが組み合わさって、過去の履歴を書き換えにくくしています。
P2Pネットワークは、その中で情報を広げる土台です。
NISTIR 8202では、ブロックチェーンを、暗号技術でリンクされたブロックからなる分散デジタル台帳として説明しています。
この説明からも分かるように、ブロックチェーンはP2P通信だけではなく、暗号技術や検証ルールと組み合わさった仕組みです。
参考: NISTIR 8202: Blockchain Technology Overview
より丁寧に言うなら、次のようになります。
| 避けたい表現 | より丁寧な表現 |
|---|---|
| P2Pなら改ざんされない | P2Pは情報を分散的に共有する土台になる |
| ブロックチェーンはP2Pだから安全 | P2P、検証、暗号技術、コンセンサスなどが組み合わさって安全性を支える |
| 分散しているので変更できない | 多くのノードが検証し、履歴を書き換えるには大きなコストが必要になる場合がある |
P2Pは大事ですが、P2Pだけでブロックチェーンの改ざん耐性を説明しきることはできません。
10.5 誤解5:P2Pなら絶対に止まらない
P2Pネットワークは、中央の1か所だけに頼る構成と比べて、障害に強くなりやすいです。
一部のノードが止まっても、別のノードや別の経路で情報が広がる可能性があるからです。
ただし、だからといって「絶対に止まらない」と言うのは強すぎます。
P2Pネットワークでも、次のような問題は起こり得ます。
- 多くのノードが同時に停止する
- ネットワークが分断される
- 特定ノードがEclipse攻撃で孤立する
- 大量のスパムやDoSでリソースを消費させられる
- peer discoveryや接続先が偏る
- クラウド事業者や地域にノードが偏る
- 主要クライアント実装に重大なバグが見つかる
つまり、P2Pは障害に強くなりやすい設計ですが、障害や攻撃を完全に消すものではありません。
USENIX Security 2015のEclipse攻撃の研究では、攻撃者がBitcoinノードの接続を独占し、対象ノードをネットワークから孤立させられる可能性が示されています。
参考: Eclipse Attacks on Bitcoin's Peer-to-Peer Network
そのため、記事では次のように書くとバランスがよいです。
P2Pネットワークは、中央の1か所だけに依存する構成と比べて、一部の障害に強くなりやすいです。
ただし、ネットワーク分断、peerの偏り、Eclipse攻撃、DoSなどのリスクは残るため、P2Pにしただけで絶対に止まらないわけではありません。
10.6 誤解6:ノード数が多ければ必ず安全
P2Pネットワークでは、ノード数が多いことは大切です。
多くのノードが参加していれば、情報のコピーが複数の場所に置かれ、特定の1か所が止まっても全体が続きやすくなります。
ただし、ノード数だけを見て安全性を判断するのは危険です。
たとえば、ノードがたくさんあっても、その多くが同じクラウド事業者、同じ地域、同じネットワーク、同じクライアント実装に偏っていたらどうでしょうか。
見かけ上は分散していても、実際には共通の弱点を持っているかもしれません。
| 見たい観点 | なぜ大切か |
|---|---|
| ノード数 | 参加者やコピーの多さに関係する |
| 地理的分散 | 特定地域の障害・規制・通信断への偏りを減らす |
| ネットワーク分散 | 同じASや同じクラウドへの集中を避ける |
| クライアント多様性 | 同じ実装のバグが全体へ広がるリスクを下げる |
| peer接続の多様性 | Eclipse攻撃や情報の偏りを受けにくくする |
| 運用主体の多様性 | 少数の組織に依存しすぎないようにする |
つまり、P2Pネットワークの健全性を見るには、単に「ノードが何台あるか」だけでなく、どこにあり、誰が動かし、どの実装で、どのようにつながっているか も重要です。
💡 豆知識
「分散している」という言葉には、いくつかの種類があります。
ノード数が多いという意味の分散、地理的に散らばっているという意味の分散、クライアント実装が分かれているという意味の分散、運用主体が偏っていないという意味の分散などです。
ブロックチェーンの文脈では、どの分散について話しているのかを分けると、かなり読みやすくなります。
10.7 誤解7:mempoolはネットワーク全体で完全に同じ
BitcoinやEthereumの説明で、mempoolという言葉が出てくることがあります。
mempoolは、ざっくり言えば、まだブロックに入っていない取引を一時的に置いておく場所です。
ただし、ここで注意したいのは、mempoolがネットワーク全体で完全に1つの共有箱になっているわけではないことです。
Bitcoin Developer Referenceでは、mempool メッセージへの応答は、そのpeerから見た未承認取引の一覧であり、ネットワーク全体のすべての未承認取引の一覧ではないと説明されています。
参考: Bitcoin Developer Reference: P2P Networking
身近な例で言えば、各人が持っている「未処理の連絡メモ」に近いです。
Aさんのメモにはある連絡が入っているけれど、Bさんのメモにはまだ入っていないかもしれません。
逆に、Bさんは別の連絡を先に受け取っているかもしれません。
| 誤解 | より丁寧な理解 |
|---|---|
| mempoolはネットワーク全体で1つ | 各ノードがローカルに持つ未承認取引の集合として見る方が自然 |
| 全ノードのmempoolは常に同じ | 伝播タイミング、手数料ポリシー、ノード設定などで差が出る場合がある |
| mempoolにある取引は確定済み | ブロックに入る前の候補であり、確定した履歴ではない |
mempoolにある取引は、まだブロックに入っていない候補です。
そのため、mempoolに見えているからといって、それだけで取引が確定したとは言えません。
この点は、ブロックチェーンの取引を理解するうえでとても重要です。
10.8 誤解8:ブロードキャストすれば一瞬で全員に届く
P2Pネットワークでは、取引やブロックがブロードキャストされます。
ただし、ここでいうブロードキャストは、全員に同時に一瞬で届くという意味ではありません。
実際には、情報はpeerからpeerへ段階的に広がります。
そのため、ノードによって情報を受け取るタイミングが異なることがあります。
この図のように、Node D は Node B から先に受け取るかもしれませんし、Node C から先に受け取るかもしれません。
同じ情報が複数経路から届くこともあります。
そのため、P2Pネットワークでは、重複処理を避ける仕組みや、どのpeerからどの情報を受け取ったかを管理する仕組みが重要になります。
| 誤解 | より丁寧な理解 |
|---|---|
| ブロードキャスト = 全員へ一瞬で届く | peerからpeerへ段階的に広がる |
| 全ノードが同時に同じ情報を見る | ネットワーク遅延により一時的に見えている情報が異なる |
| 同じ情報は1回だけ届く | 複数経路から重複して届くことがある |
この遅延や一時的な食い違いがあるからこそ、ブロックチェーンではフォークや再編成、確認数、ファイナリティといった考え方が重要になります。
10.9 誤解9:BitcoinとEthereumのP2Pは同じように説明できる
BitcoinとEthereumは、どちらもP2Pネットワークを使います。
ただし、細かい仕組みは同じではありません。
Bitcoinでは、フルノードがブロックや取引を交換し、検証し、ネットワークへ中継します。
inv、getdata、tx、block、headers などのP2Pメッセージが関係します。
Ethereumでは、実行クライアントとコンセンサスクライアントという2つの視点で見る必要があります。
実行レイヤーでは取引などを扱い、コンセンサスレイヤーではBeacon blockやattestationなどを扱います。
Ethereum公式ドキュメントでは、実行クライアントは実行レイヤーP2Pで取引をgossipし、コンセンサスクライアントは別のP2PネットワークでBeacon blockをgossipすると説明されています。
参考: Ethereum Documentation: Networking layer
| 観点 | Bitcoin | Ethereum |
|---|---|---|
| 主な説明の入口 | フルノード、取引、ブロック | 実行クライアント、コンセンサスクライアント |
| 情報伝播 |
inv / getdata / tx / block など |
gossip、request/response、DevP2P、libp2pなど |
| コンセンサスとの関係 | PoWとチェーン選択が関係 | PoS、Beacon chain、attestation、finalityなどが関係 |
| 注意点 | Bitcoinの説明をそのままEthereumへ当てはめない | Ethereumの説明をそのままBitcoinへ当てはめない |
そのため、記事では「ブロックチェーンではP2Pネットワークが使われる」と広く説明したあと、BitcoinとEthereumを分けて具体例として扱うのが安全です。
10.10 誤解10:P2Pなら攻撃されない
9章でも見たように、P2Pネットワークには攻撃面があります。
代表的なものとして、Sybil攻撃やEclipse攻撃があります。
Sybil攻撃は、1人の攻撃者が多数の偽ノードや偽IDを作り、多数派のように見せる攻撃です。
Douceurの論文では、単一の主体が複数のIDを提示できると、冗長性に依存する分散システムの前提を崩せることが示されています。
参考: The Sybil Attack
Eclipse攻撃は、特定ノードの接続先を攻撃者側で埋め、対象ノードをネットワークから孤立させる攻撃です。
Heilmanらの研究では、BitcoinのP2Pネットワークに対するEclipse攻撃が示されています。
参考: Eclipse Attacks on Bitcoin's Peer-to-Peer Network
| 攻撃 | ざっくりした内容 | 身近なイメージ |
|---|---|---|
| Sybil攻撃 | 偽ノードや偽IDを大量に作る | 1人が大量のアカウントで投票する |
| Eclipse攻撃 | 対象ノードの接続先を攻撃者が独占する | ある人の連絡先を全員偽の相手で埋める |
| DoS / スパム | 大量の不要メッセージでリソースを消費させる | 大量の迷惑連絡で本来の連絡を見えにくくする |
| ネットワーク分断 | グループ間の通信が切れる | クラスが2つの連絡網に分かれてしまう |
P2Pネットワークは攻撃されない仕組みではありません。
むしろ、誰でも参加しやすいネットワークだからこそ、悪意あるpeerが混ざる前提で設計する必要があります。
10.11 小さなコードで見る:強すぎる表現を見つける
ここで、記事を書くときに使える小さなコードを見てみます。
P2Pネットワークを説明するときは、「絶対」「完全」「一切」「必ず」のような言葉を使いすぎると、実際より強い主張になってしまうことがあります。
次のコードは、文章の中に強すぎる表現が含まれていないかを確認するための学習用コードです。
実際の校正ツールではありませんが、記事を推敲するときの考え方として役立ちます。
# P2Pネットワークの記事で、強すぎる表現を見つけるための学習用コードです。
# 実際の校正ツールではありません。
# 「絶対」「完全」「一切」「必ず」などを使っている箇所を確認し、
# 必要に応じて、より慎重な表現に直すための補助として使います。
sentences = [
"P2Pなら絶対に止まらない。",
"P2Pネットワークは中央の1か所だけに頼らない通信の考え方です。",
"P2Pならサーバーを一切使わない。",
"P2Pは情報を広げる土台ですが、正しさは検証やコンセンサスと組み合わせて判断します。",
]
# 強すぎる可能性がある言葉をリストにしておきます。
strong_words = ["絶対", "完全", "一切", "必ず", "100%"]
for sentence in sentences:
# 文の中に、強すぎる可能性がある言葉が含まれているか確認します。
found_words = [word for word in strong_words if word in sentence]
if found_words:
print("注意したい表現:", sentence)
print(" 見つかった言葉:", found_words)
print(" → 断定しすぎていないか確認しましょう。")
else:
print("そのままでも比較的安全そうな表現:", sentence)
このコードでは、強すぎる可能性がある言葉を含む文を見つけています。
もちろん、「絶対」や「完全」という言葉を必ず使ってはいけないわけではありません。
ただし、技術記事では、事実以上に強く見える表現を避けることが大切です。
たとえば、次のように言い換えると、より丁寧になります。
| 強すぎる表現 | 言い換え例 |
|---|---|
| P2Pなら絶対に止まらない | P2Pは中央1点への依存を減らせるため、障害に強くなりやすい |
| P2Pならサーバーを一切使わない | P2Pは中央サーバーだけに頼らない通信の考え方 |
| P2Pなら完全に安全 | P2Pは強みがある一方で、Sybil攻撃やEclipse攻撃などの注意点もある |
| ノード数が多ければ必ず安全 | ノード数だけでなく、接続先や運用主体、実装の多様性も重要 |
このように、言い切りすぎを避けるだけで、記事全体の信頼性がかなり上がります。
10.12 小さなコードで見る:P2P・検証・コンセンサスを分ける
もう一つ、P2Pを説明するときに大切なのが、役割を分けることです。
次のコードは、ブロックチェーンで出てくる処理を、かなり単純化して分類する学習用コードです。
実際のBitcoinやEthereumの実装ではありません。
「これはP2Pの話なのか、検証の話なのか、コンセンサスの話なのか」を分けて考えるための補助です。
# ブロックチェーンに関係する処理を、P2P・検証・コンセンサスに分類する学習用コードです。
# 実際のBitcoin CoreやEthereumクライアントの実装ではありません。
# 記事を書くときに、役割を混同しないための補助として使います。
actions = [
"peerへ新しい取引を知らせる",
"取引の署名が正しいか確認する",
"ブロックのハッシュ条件を確認する",
"どのチェーンを正しい候補として伸ばすか決める",
"接続するpeer候補を探す",
"無効なブロックを拒否する",
]
# かなり単純化した分類ルールです。
# 実際には、処理が複数の領域にまたがることもあります。
def classify_action(action: str) -> str:
"""処理内容をざっくり分類します。"""
if "peer" in action or "知らせる" in action or "探す" in action:
return "P2Pネットワーク"
if "署名" in action or "確認" in action or "拒否" in action:
return "検証ルール"
if "チェーン" in action or "正しい候補" in action:
return "コンセンサス / フォーク選択"
return "分類が難しい処理"
for action in actions:
category = classify_action(action)
print(f"{action} -> {category}")
このコードはとても単純ですが、記事を書くときの考え方としては役立ちます。
P2Pネットワークは、主に情報を届ける役割を持ちます。
取引やブロックが正しいかは検証ルールで確認します。
複数の履歴候補があるときにどれを選ぶかは、コンセンサスやフォーク選択の領域です。
この3つを分けておくと、P2Pネットワークについてかなり正確に説明しやすくなります。
10.13 この章のまとめ
この章では、P2Pネットワークについてよくある誤解を整理しました。
P2Pネットワークは、中央の1か所だけに頼らず、参加者同士で情報をやり取りできる重要な仕組みです。
ただし、P2Pにしただけで、すべてが自動的に安全になるわけではありません。
この章で押さえたいポイントは、次の通りです。
| 誤解 | より丁寧な理解 |
|---|---|
| P2Pならサーバーが一切ない | 中央サーバーだけに頼らない。初期接続にはDNS seedやbootnodeが使われることがある |
| 全員が全員と直接つながる | 各ノードは一部のpeerと接続し、情報が段階的に広がる |
| P2Pだけで情報の正しさが決まる | P2Pは情報を届ける土台。正しさは検証やコンセンサスと組み合わせて判断する |
| P2Pなら改ざんされない | 改ざん耐性は、P2P、ハッシュ、署名、検証、コンセンサスなどの組み合わせで考える |
| P2Pなら絶対に止まらない | 一部障害に強くなりやすいが、分断、DoS、Eclipse攻撃などのリスクは残る |
| ノード数が多ければ必ず安全 | ノード数だけでなく、地理・ネットワーク・実装・運用主体の多様性も重要 |
| mempoolは全体で完全に同じ | 各ノードがローカルに持つ未承認取引の集合として見る方が自然 |
| ブロードキャストすれば一瞬で全員に届く | peerからpeerへ段階的に広がるため、ノードごとに到着タイミングが異なる |
| BitcoinとEthereumのP2Pは同じ | 共通点はあるが、具体的なプロトコルや構成は異なる |
| P2Pなら攻撃されない | Sybil攻撃、Eclipse攻撃、DoS、ネットワーク分断などに注意が必要 |
P2Pネットワークを理解するときは、次の一文に戻ると整理しやすいです。
P2Pネットワークは、中央の1か所だけに頼らず、参加者同士で情報を広げるための通信の土台です。
ただし、届いた情報の正しさは、各ノードの検証やコンセンサスルールと組み合わせて判断します。
次の章では、P2Pネットワークに関する最近の動向を整理します。
BitcoinやEthereumのP2Pは、最初に作られたまま固定されているわけではありません。
攻撃研究、ノード運用の変化、クライアント多様性、gossipプロトコルの改善などに合わせて、P2Pネットワークも少しずつ改善されています。
11. 最近の動向
この章では、P2Pネットワークに関する最近の動きを、BitcoinとEthereumを中心に整理します。
ポイントは、P2Pネットワークが「昔からある通信方式」で終わっているわけではなく、暗号化、帯域削減、gossipの改善、軽量ノード、データ可用性 などの観点で今も改善され続けていることです。
ここまで、P2Pネットワークを「中央の1か所だけに頼らず、参加者同士で情報を広げる通信の土台」として見てきました。
ただし、ブロックチェーンのP2Pネットワークは、一度作られたらそのまま固定されるものではありません。
ノード数が増えたり、攻撃研究が進んだり、Layer 2やRollupのように扱うデータ量が増えたりすると、P2Pネットワークにも新しい課題が出てきます。
たとえば、次のような問いが出てきます。
- ノード同士の通信内容や通信パターンを、どこまで見えにくくできるのか
- 同じ情報を何度も送りすぎて、帯域を無駄にしていないか
- 悪いpeerや役に立たないpeerを、どう扱うべきか
- 軽量な端末でも、中央集権的なRPCサービスだけに頼らずEthereumへアクセスできるか
- Rollupなどで増えるデータを、すべてのノードが丸ごと持たなくても安全に扱えるか
この章では、細かい仕様をすべて追うのではなく、P2Pネットワークの流れとして押さえておきたい動向を、初学者向けに整理します。
11.1 BitcoinではP2P通信の暗号化が進んでいる
BitcoinのP2Pネットワークでは、ノード同士が取引やブロックなどの情報を交換します。
ここで注意したいのは、Bitcoinで流れる取引やブロック自体は、もともと公開されることを前提にしたデータだという点です。
そのため、「P2P通信を暗号化する」と聞くと、少し不思議に感じるかもしれません。
公開されるデータなら、通信を暗号化しても意味がないのでは?
実は、公開データであっても、誰がいつどのpeerと通信したか というメタデータは、プライバシーや検閲耐性に関わります。
たとえば、手紙の本文があとで公開されるとしても、封筒に書かれた「誰から誰へ、いつ送られたか」という情報は別の意味を持ちます。
BitcoinのP2P通信でも、取引そのものが公開されることと、通信経路やタイミングが観測されやすいことは分けて考える必要があります。
この文脈で重要なのが、BIP324 です。
BIP324は、BitcoinのP2P通信におけるVersion 2 Transport Protocol、つまりv2 transportを定義する提案です。
BIP324の仕様では、v2 transportは opportunistic encryption、軽い帯域削減、アプリケーションメッセージ交換前のアップグレード交渉などを特徴としていると説明されています。
参考: BIP 324: Version 2 P2P Encrypted Transport Protocol
参考: Bitcoin Core 27.0 Release Notes
Bitcoin Core 26.0ではBIP324のv2 transportが実験的に追加され、デフォルトでは無効でした。
その後、Bitcoin Core 27.0ではBIP324 v2 transportがデフォルトで有効になったと説明されています。
参考: Bitcoin Core 26.0 Release Notes
参考: Bitcoin Core 27.0 Release Notes
ただし、ここでも強く言い切りすぎないことが大切です。
BIP324は、P2P通信の盗聴や改ざん、プロトコル識別を難しくする方向の改善です。
一方で、これだけでpeerの身元が完全に保証されるわけでも、Eclipse攻撃やSybil攻撃が完全になくなるわけでもありません。
| 観点 | v1 transportのイメージ | v2 transportのイメージ |
|---|---|---|
| 通信内容 | 平文で扱われる | opportunistic encryptionにより見えにくくする |
| プロトコル識別 | 観測者にBitcoin P2P通信だと分かりやすい | バイト列を疑似ランダムに見せ、識別を難しくする方向 |
| 改ざん耐性 | 通信経路上の改ざんに弱い面がある | 改ざんコストを上げる方向 |
| 注意点 | 古い方式として残る場合がある | peer認証や攻撃対策のすべてを解決するわけではない |
ここで押さえたいのは、P2Pネットワークの改善が「もっと速くする」だけではないことです。
通信の見え方、観測されにくさ、改ざんされにくさも、P2Pネットワークの大切な改善対象になります。
💡 豆知識
BIP324の暗号化は、一般的なWebサイトで使われるTLSと同じものとして考えると少し誤解しやすいです。
BitcoinのP2Pでは、公開データを扱う一方で、通信メタデータやプロトコル識別が攻撃の手がかりになることがあります。
そのため、v2 transportは「秘密の取引を隠す」というより、P2P通信を観測・改ざん・検閲しにくくする方向の改善として見ると分かりやすいです。
11.2 小さなコードで見る:対応している通信方式を交渉する
BIP324そのものの暗号処理は複雑なので、ここでは実装しません。
代わりに、ノード同士が対応している通信方式を見て、使える方式を選ぶ という雰囲気だけを、学習用コードで確認します。
次のコードは、実際のBitcoin Coreの実装ではありません。
「お互いがv2 transportに対応していればv2を使い、片方が未対応ならv1に戻る」という考え方を、かなり単純化したものです。
# P2P通信方式の交渉を理解するための学習用コードです。
# 実際のBIP324の暗号処理やBitcoin Coreの実装ではありません。
# ここでは「両方がv2に対応していればv2を使う」という雰囲気だけを確認します。
from dataclasses import dataclass
@dataclass
class Node:
"""ノードが対応している通信方式を表す学習用データ構造です。"""
name: str
supports_v1: bool
supports_v2: bool
def negotiate_transport(a: Node, b: Node) -> str:
"""
2つのノードが共通して使える通信方式を選びます。
実際のBIP324では、handshake、鍵交換、暗号化、改ざん検出などが関係します。
この関数では、説明のために対応可否だけを見ています。
"""
if a.supports_v2 and b.supports_v2:
return "v2 transport を使用"
if a.supports_v1 and b.supports_v1:
return "v1 transport にフォールバック"
return "共通して使える通信方式がありません"
alice = Node(name="Alice", supports_v1=True, supports_v2=True)
bob = Node(name="Bob", supports_v1=True, supports_v2=True)
carol = Node(name="Carol", supports_v1=True, supports_v2=False)
for peer in [bob, carol]:
result = negotiate_transport(alice, peer)
print(f"{alice.name} - {peer.name}: {result}")
このコードでは、AliceとBobはどちらもv2に対応しているため、v2 transportを使うことにしています。
一方、Carolはv2に対応していないため、v1へ戻るという結果になります。
実際のプロトコルでは、ここに暗号化のためのhandshakeや、互換性のための細かい処理が加わります。
ただし、入口としては「P2Pネットワークでは、古いノードと新しいノードが混ざるため、互換性を考えながら少しずつ改善していく」と見ると分かりやすいです。
11.3 帯域を減らす工夫:全部を毎回そのまま送らない
P2Pネットワークでは、同じ情報が複数経路から何度も届くことがあります。
これは冗長性という意味では強みです。
一方で、何でも毎回そのまま全peerへ送ると、帯域を使いすぎる可能性があります。
このため、Bitcoinでは以前から、inv で「この取引やブロックを知っています」と知らせ、必要な場合に getdata で本体を要求するような設計が使われています。
また、BitcoinのP2Pレイヤーでは、さらに効率的な取引リレーを目指す提案として Erlay や BIP330 があります。
BIP330では、Erlayのような効率的なreconciliation-based protocolを可能にするためのP2Pレイヤー拡張が提案されています。
そこでは、各ノードがpeerごとのreconciliation setを持ち、一定タイミングで差分を照合することで、双方が知らない取引を学べるようにする考え方が説明されています。
参考: BIP 330: Transaction announcements reconciliation
参考: Bitcoin Optech: Erlay
身近な例でいうと、次のような違いです。
| 方法 | 身近な例 | P2Pでのイメージ |
|---|---|---|
| 毎回全部送る | 友達に毎回ノート全文を送る | すべての取引データを何度も送る |
| 存在だけ知らせる | 「この資料ある?」と一覧だけ見せる |
inv で存在を知らせる |
| 足りない分だけ送る | 相手が持っていないページだけ送る |
getdata や差分照合で必要分を送る |
P2Pネットワークでは、情報を早く広げることと、帯域を節約することのバランスが重要です。
情報を絞りすぎると、伝播が遅くなったり、一部のノードに届きにくくなったりします。
逆に、何でも大量に送りすぎると、ノード運用の負担が増え、個人がフルノードを動かしにくくなるかもしれません。
つまり、P2Pネットワークの改善では、速さ・冗長性・帯域・プライバシー・攻撃耐性 のバランスを考える必要があります。
11.4 小さなコードで見る:差分だけを交換する
次のコードは、reconciliation-based relayの雰囲気をつかむための学習用コードです。
実際のErlayやBIP330のset reconciliationを再現するものではありません。
ここでは、2つのノードがそれぞれ知っている取引IDを比較し、相手が持っていないものだけを送る例を見ます。
# 取引リレーの帯域削減をイメージするための学習用コードです。
# 実際のErlayやBIP330のset reconciliationではありません。
# ここでは「相手が持っていない取引だけを送る」考え方を単純化して表します。
node_a_mempool = {"tx001", "tx002", "tx003", "tx004"}
node_b_mempool = {"tx003", "tx004", "tx005"}
# Aが知っていてBが知らない取引です。
# Bへ送る候補になります。
missing_for_b = node_a_mempool - node_b_mempool
# Bが知っていてAが知らない取引です。
# Aへ送る候補になります。
missing_for_a = node_b_mempool - node_a_mempool
print("Bへ送る必要がある取引:", sorted(missing_for_b))
print("Aへ送る必要がある取引:", sorted(missing_for_a))
このコードでは、AとBの両方が tx003 と tx004 を知っています。
そのため、それらをもう一度送り直す必要はありません。
AからBへは tx001 と tx002、BからAへは tx005 だけを送れば、両者の情報をそろえやすくなります。
実際のP2Pプロトコルでは、取引IDの短縮、衝突への対策、DoS耐性、プライバシー、タイミングなど、もっと多くの要素が関係します。
ただし、入口としては「全部を毎回送るのではなく、相手が持っていない差分を見つけて送る」という方向の改善があると押さえておくと十分です。
11.5 gossipプロトコルは「ただの伝言ゲーム」から進化している
P2Pネットワークでは、gossipという考え方がよく使われます。
gossipは、身近な言葉で言えば「うわさ話」のように、近くの相手へ情報を伝え、その相手がまた別の相手へ伝えていく方式です。
ただし、実際のP2Pプロトコルでは、何も考えずに全員へばらまくだけではありません。
Ethereumのコンセンサスレイヤーでも使われるlibp2pのGossipsubでは、mesh、metadata-only peering、peer scoringなどの考え方があります。
Gossipsub v1.1の仕様では、peer scoringにより、各peerが他のpeerのスコアをローカルに計算し、悪い振る舞いをするpeerをmeshから外したり、gossip対象から外したりする仕組みが説明されています。
参考: libp2p Gossipsub v1.1 Specification
参考: libp2p Docs: What is Publish/Subscribe
ここで大切なのは、gossipを「ただ全員に投げるだけ」と考えないことです。
| 観点 | 単純なgossip | 改善されたgossipのイメージ |
|---|---|---|
| 送信先 | 適当にpeerへ送る | meshやpeer scoreを見ながら送る |
| 悪いpeerへの対応 | 何度も相手にする可能性がある | スコアに応じてpruneや無視を行う |
| 帯域 | 重複が多くなりやすい | metadataやmeshで調整する |
| 攻撃耐性 | SybilやDoSに弱くなりやすい | scoringや接続方向の制約で耐性を高める |
もちろん、peer scoringも万能ではありません。
スコア設計を間違えると、良いpeerを不当に低く評価したり、悪いpeerを見逃したりする可能性があります。
それでも、P2Pネットワークでは「誰とつながるか」「誰からの情報をどれくらい信用して中継するか」を、少しずつ賢く調整する方向に進んでいると見ることができます。
11.6 小さなコードで見る:peer scoreで中継先を選ぶ
次のコードは、peer scoreの雰囲気を理解するための学習用コードです。
実際のGossipsubのスコア計算ではありません。
ここでは、peerごとにスコアを持ち、スコアが低すぎるpeerにはメッセージを中継しない、という単純な例を見ます。
# peer scoring の雰囲気を理解するための学習用コードです。
# 実際のlibp2p Gossipsubのスコア計算ではありません。
# ここでは「スコアが低いpeerには中継しない」という考え方だけを確認します。
peers = {
"peer_A": {"score": 1.2, "reason": "よくメッセージを転送する"},
"peer_B": {"score": 0.4, "reason": "普通に参加している"},
"peer_C": {"score": -0.8, "reason": "無効なメッセージが多い"},
"peer_D": {"score": -2.0, "reason": "悪い振る舞いが続いている"},
}
# この値より低いpeerには、説明用にメッセージを送らないことにします。
# 実際のGossipsubには、複数のしきい値やスコア項目があります。
GOSSIP_THRESHOLD = -0.5
def select_gossip_targets(peers: dict[str, dict[str, float]]) -> list[str]:
"""スコアがしきい値以上のpeerだけを中継先として選びます。"""
targets = []
for peer_id, info in peers.items():
if info["score"] >= GOSSIP_THRESHOLD:
targets.append(peer_id)
return targets
targets = select_gossip_targets(peers)
print("今回メッセージを中継するpeer:")
for peer_id in targets:
print(f"- {peer_id}: score={peers[peer_id]['score']}, reason={peers[peer_id]['reason']}")
このコードでは、peer_D のスコアがしきい値を大きく下回っているため、中継対象から外れます。
一方、peer_A や peer_B は中継先として選ばれます。
実際のP2Pネットワークでは、スコアをどう計算するかがとても重要です。
メッセージを正しく転送しているか、無効なデータを送っていないか、接続が安定しているかなど、複数の要素を見ます。
このような仕組みは、P2Pネットワークの「誰でもつながれる」という開かれた性質を保ちながら、悪いpeerの影響を減らすために使われます。
💡 豆知識
peer scoreは、SNSのフォロワー数のように全員へ公開される評価ではありません。
Gossipsub v1.1では、各peerが観測した振る舞いにもとづいてローカルにスコアを計算すると説明されています。
つまり、「ネットワーク全体でこのpeerは何点」と決めるのではなく、自分から見た振る舞いをもとに接続や中継を調整するイメージです。
11.7 Ethereumではデータ可用性とP2Pがさらに重要になっている
EthereumのP2Pネットワークでは、取引やブロックだけでなく、Rollupなどに関係するデータの扱いも重要になっています。
EIP-4844では、blob transactionが導入され、blobに関係するデータ、commitment、proofなどのネットワーク上の扱いが定義されています。
EIP-4844のNetworking項目では、blob transactionにはネットワーク上での表現があり、ノードがblob、commitment、proofを検証する必要があることが説明されています。
参考: EIP-4844: Shard Blob Transactions
さらに、Ethereumのロードマップでは PeerDAS という考え方も重要です。
PeerDASは、Data Availability Sampling、つまりデータ可用性サンプリングをEthereumで実現するためのP2Pネットワーク上の仕組みとして整理されています。
EIP-7594では、PeerDASはノードがblob dataの一部だけをダウンロードしながら、そのデータが利用可能であることを確認するためのネットワークプロトコルとして説明されています。
また、gossipによる配布、特定のdata custodyを持つpeerの発見、peer requestsによるsamplingを使うと説明されています。
参考: EIP-7594: PeerDAS - Peer Data Availability Sampling
参考: Ethereum Roadmap: PeerDAS
ここは少し難しいので、身近な例で考えてみます。
大量の資料を全員が丸ごと保存するのは大変です。
そこで、資料を細かい断片に分け、各参加者が一部だけを持つとします。
ただし、それだけでは「本当に資料全体がどこかに存在するのか」「一部の人が偽物の断片を配っていないか」が分かりません。
そこで、ランダムにいくつかの断片を確認し、暗号学的な証明と組み合わせて、データが正しく利用可能であることを確認しようとします。
| 観点 | 従来のイメージ | DAS / PeerDASのイメージ |
|---|---|---|
| データの持ち方 | 多くのノードが丸ごと持つ | ノードが一部の断片を担当する |
| 確認方法 | 全部をダウンロードして確認する | ランダムに一部をサンプリングして確認する |
| P2Pの役割 | 取引やブロックを広げる | 断片を持つpeerを探し、必要な断片を要求する |
| 注意点 | 帯域・保存容量が重い | サンプリング、証明、peer探索の設計が重要 |
このように、Ethereumのスケーリングでは、P2Pネットワークが「ただ情報を広げるだけ」ではなく、誰がどのデータ断片を持つのか、どうやって探すのか、どう確認するのか という問題にも関わってきます。
11.8 小さなコードで見る:データの一部をサンプリングする
次のコードは、Data Availability Samplingの雰囲気をつかむための学習用コードです。
実際のPeerDAS、EIP-7594、KZG証明、erasure codingを再現するものではありません。
ここでは、大きなデータを断片に分け、そのうちいくつかをランダムに確認するという考え方だけを見ます。
# Data Availability Sampling の雰囲気を理解するための学習用コードです。
# 実際のPeerDAS、KZG証明、erasure codingを再現するものではありません。
# ここでは「全部ではなく、一部の断片をランダムに確認する」考え方を示します。
import random
# 大きなデータを、説明用に10個の断片へ分けたとします。
# 実際のEthereumのblobやPeerDASの構造とは異なります。
data_pieces = {
f"piece_{i}": f"data_fragment_{i}"
for i in range(1, 11)
}
def sample_pieces(data: dict[str, str], sample_size: int) -> dict[str, str]:
"""データ断片の中から、指定した数だけランダムに選びます。"""
piece_ids = list(data.keys())
sampled_ids = random.sample(piece_ids, sample_size)
# 選ばれた断片だけを取り出します。
return {piece_id: data[piece_id] for piece_id in sampled_ids}
# 再現しやすいように乱数の種を固定します。
random.seed(42)
sampled = sample_pieces(data_pieces, sample_size=3)
print("確認する断片:")
for piece_id, value in sampled.items():
print(f"- {piece_id}: {value}")
このコードでは、10個の断片のうち3個だけをランダムに確認しています。
もちろん、実際のData Availability Samplingでは、ただランダムに見るだけでは不十分です。
データが正しいことを示す暗号学的な証明、データが十分に分散されていること、悪意あるpeerへの対策、サンプリング確率、ネットワーク遅延など、多くの要素が関係します。
ただし、入口としては次のように考えると分かりやすいです。
データ量が増え続けると、全員がすべてを丸ごと持つ設計は重くなります。
そこで、一部のデータを担当し、必要に応じてpeerから取得し、暗号学的な証明と組み合わせて確認する方向が重要になります。
11.9 Portal Network:軽量な端末でも分散的にEthereumへアクセスする方向
Ethereumでは、Portal NetworkもP2Pネットワークの最近の動向として重要です。
Ethereum公式ドキュメントでは、Portal Networkは、軽量なPortal clientの分散ネットワークによって、チェーンのhead追跡、最近および過去のチェーンデータの同期、状態データの取得、取引のブロードキャストなどを可能にすることを目指していると説明されています。
また、中央集権的なプロバイダへの依存を減らすこと、帯域使用量を減らすこと、低リソース端末でもアクセスしやすくすることが利点として挙げられています。
参考: Ethereum Documentation: The Portal Network
これは、スマートフォンや軽量端末からEthereumにアクセスするときのことを考えると分かりやすいです。
現在、多くのアプリケーションでは、ユーザーが自分でフルノードを動かすのではなく、RPCプロバイダを通じてEthereumへアクセスします。
これは便利ですが、特定のプロバイダに依存しすぎると、障害、検閲、プライバシー、信頼前提の面で注意が必要です。
Portal Networkは、このような依存を減らし、軽量な端末でもP2Pネットワークから必要なデータを取得しやすくする方向の取り組みです。
| 観点 | 中央集権的RPCへ強く依存する場合 | Portal Networkの方向性 |
|---|---|---|
| 使いやすさ | すぐ使いやすい | 軽量端末でも分散的にアクセスしやすくする |
| 信頼前提 | RPC事業者への依存が大きい | P2Pネットワークからデータ取得する方向 |
| 障害耐性 | プロバイダ障害の影響を受けやすい | 複数peerから取得できる可能性がある |
| プライバシー | アクセス先に問い合わせ内容が見えやすい | 分散取得で改善の余地がある |
もちろん、Portal Networkも「これだけで完全に分散化できる」という魔法ではありません。
データをどう分散するか、どう検証するか、悪意あるpeerをどう扱うか、どの程度のユーザー体験を実現できるかなど、実装・運用上の課題があります。
ただ、P2Pネットワークの役割が、フルノード同士のブロック・取引伝播だけでなく、軽量クライアントや低リソース端末のアクセスにも広がっている点は重要です。
💡 豆知識
Portal Networkは、1つの巨大なネットワークというより、History、State、Beacon light client、Transaction gossipなど、用途ごとのサブネットワークとして考えると分かりやすいです。
「必要なデータを、必要なときに、分散的に探す」方向の取り組みだと見ると、P2Pネットワークの応用範囲が見えてきます。
11.10 小さなコードで見る:必要なデータを持つpeerを探す
次のコードは、Portal NetworkやDHTの考え方をかなり単純化した学習用コードです。
実際のEthereum Portal Network、Discovery v5、Kademlia DHTを再現するものではありません。
ここでは、peerごとに持っているデータが違う状況で、必要なデータを持つpeerを探す例を見ます。
# 必要なデータを持つpeerを探す考え方を理解するための学習用コードです。
# 実際のPortal Network、Discovery v5、Kademlia DHTの実装ではありません。
# ここでは「どのpeerがどのデータを持っているか」を辞書で表します。
peer_storage = {
"peer_A": {"header_100", "header_101", "receipt_100"},
"peer_B": {"state_account_X", "state_account_Y"},
"peer_C": {"header_102", "receipt_101", "receipt_102"},
"peer_D": {"state_account_Z", "header_100"},
}
def find_peers_with_data(target_data: str) -> list[str]:
"""指定したデータを持っているpeerの一覧を返します。"""
holders = []
for peer_id, data_set in peer_storage.items():
if target_data in data_set:
holders.append(peer_id)
return holders
target = "header_100"
holders = find_peers_with_data(target)
print(f"{target} を持っているpeer:")
for peer_id in holders:
print(f"- {peer_id}")
このコードでは、header_100 を持つpeerを探しています。
結果として、peer_A と peer_D が見つかります。
実際のP2Pネットワークでは、誰がどのデータを持っているかを単純な辞書で管理できるわけではありません。
そのため、DHT、peer discovery、request/response、検証、キャッシュ、スコアリングなどが組み合わさります。
ただ、考え方としては次のように整理できます。
P2Pネットワークでは、情報をただ流すだけでなく、「必要なデータを持つpeerを探し、取得し、検証する」ことも重要になっている。
11.11 最近の動向を整理すると、P2Pの課題が見えてくる
ここまで見てきた動向をまとめると、P2Pネットワークの改善は、単に「ノードをたくさん増やす」だけではないことが分かります。
| 動向 | 何を改善しようとしているか | 関連する例 |
|---|---|---|
| P2P通信の暗号化 | 盗聴、改ざん、プロトコル識別、検閲のコストを上げる | Bitcoin BIP324 / v2 transport |
| 帯域削減 | 重複した取引・ブロック伝播を減らす |
inv / getdata、Erlay、BIP330 |
| gossipの改善 | 悪いpeerを避けつつ、効率よく情報を広げる | libp2p Gossipsub、peer scoring |
| データ可用性 | 大量データを全員が丸ごと持たずに確認する | EIP-4844、PeerDAS、DAS |
| 軽量アクセス | 低リソース端末でも分散的にデータへアクセスする | Ethereum Portal Network |
| 接続の多様性 | 単一障害点や偏りを減らす | client diversity、peer diversity |
P2Pネットワークの改善は、ブロックチェーンの分散性を支える重要なテーマです。
ただし、どの改善にもトレードオフがあります。
- 暗号化を入れると、互換性や実装複雑性を考える必要がある
- 帯域を減らすと、伝播遅延や実装複雑性とのバランスが必要になる
- peer scoringを入れると、スコア設計を誤った場合の副作用に注意が必要になる
- データを分割して持つと、可用性と検証方法が重要になる
- 軽量クライアントを増やすと、どのデータを誰が保持するかが課題になる
つまり、P2Pネットワークは「つながれば終わり」ではありません。
つながり方、情報の流し方、相手の選び方、データの持ち方、検証方法まで含めて、少しずつ改善され続けています。
11.12 この章のまとめ
この章では、P2Pネットワークに関する最近の動向を整理しました。
Bitcoinでは、BIP324によるv2 transportのように、P2P通信を暗号化し、観測や改ざん、プロトコル識別を難しくする方向の改善が進んでいます。
また、ErlayやBIP330のように、取引リレーの帯域を減らしつつ、ネットワーク接続性を高めるための提案もあります。
Ethereumでは、実行レイヤーとコンセンサスレイヤーのP2Pが分かれており、gossip、request/response、libp2p、Gossipsubなどが重要な役割を持っています。
さらに、EIP-4844、PeerDAS、Portal Networkのように、P2Pネットワークは大量データの扱いや軽量アクセスにも関わるようになっています。
この章で押さえたいポイントは、次の通りです。
| ポイント | 説明 |
|---|---|
| P2P通信の暗号化が進んでいる | BitcoinではBIP324 / v2 transportが重要な動向になっている |
| 帯域削減は分散性にも関係する | ノード運用の負担を下げることは、参加しやすさにもつながる |
| gossipは単純な伝言ゲームではない | mesh、metadata、peer scoreなどを使って効率と攻撃耐性を調整する |
| Ethereumではデータ可用性が重要になっている | Rollupやblobの増加により、データをどう配り、どう確認するかが課題になる |
| 軽量アクセスもP2Pの重要な応用 | Portal Networkのように、低リソース端末でも分散的にデータへアクセスする方向がある |
| 改善にはトレードオフがある | 暗号化、帯域削減、スコアリング、DASはいずれも設計上の注意点を持つ |
P2Pネットワークは、ブロックチェーンの裏側で動く地味な仕組みに見えるかもしれません。
しかし、取引がどれだけ速く広がるか、ノード運用の負担がどれくらいか、ネットワークがどれだけ検閲や障害に耐えられるかは、P2Pの設計に大きく左右されます。
次の章では、ここまでの内容をまとめます。
P2Pネットワークを、中央サーバー型との違い、ブロックチェーンでの役割、BitcoinとEthereumの具体例、強みと限界の観点から振り返ります。
12. まとめ
この章では、記事全体を振り返りながら、P2Pネットワークをどのように理解するとよいかを整理します。
ポイントは、P2Pネットワークを「サーバーが一切ない仕組み」としてではなく、中央の1か所だけに頼らず、参加者同士で情報をやり取りする通信の土台 として見ることです。
ここまで、P2Pネットワークについて、身近な連絡共有の例から順番に整理してきました。
最初に見たのは、クラスや研究室で急ぎの連絡を回すような場面です。
代表者が1人いて、その人が全員へ連絡する形は分かりやすく、管理もしやすいです。
しかし、その代表者が連絡できなくなったらどうなるでしょうか。
連絡の入口が1か所に集中していると、そこが止まっただけで、後ろの人に情報が届かなくなるかもしれません。
一方で、参加者同士が近くの人へ情報を伝え合う形なら、一部の人や経路が止まっても、別の経路から情報が広がる可能性があります。
この「中央の1か所だけに頼らず、参加者同士で情報をやり取りする」という考え方が、P2Pネットワークを理解する入口でした。
12.1 記事全体の振り返り
この記事では、P2Pネットワークをいきなり定義から覚えるのではなく、次の流れで整理しました。
最初に確認したように、P2Pネットワークは、単に「サーバーがない仕組み」と言い切れるものではありません。
Bitcoinでは、初回接続時にDNS seedを使って接続先候補を得ることがあります。
Ethereumでも、新しいノードがネットワークへ参加するときにbootnodeが入口として使われることがあります。
そのため、P2Pネットワークは 中央的な要素が一切ない仕組み ではなく、中央の1か所だけに依存しすぎない通信の考え方 として見る方が自然です。
また、P2Pネットワークは情報を広げるための仕組みです。
取引やブロックがネットワークへ届いたとしても、それだけで正しいと決まるわけではありません。
各ノードが届いた情報を検証し、さらにコンセンサスルールと組み合わさることで、ブロックチェーンらしい「分散した記録の共有」に近づいていきます。
12.2 本記事で整理したこと
本記事で整理した内容を、章ごとに振り返ると次のようになります。
| 章 | 整理したこと | 大事なポイント |
|---|---|---|
| 1章 | 中央の1か所だけに頼ると何が困るのか | 中央サーバー型は便利だが、単一障害点になりやすい場合がある |
| 2章 | P2Pネットワークの基本的な捉え方 | P2Pは、中央の1か所だけに頼らず参加者同士が情報をやり取りする考え方 |
| 3章 | P2Pネットワークの基本用語 | ノード、ピア、gossip、peer discovery、mempoolなどを分けて理解する |
| 4章 | ブロックチェーンでP2Pが必要になる理由 | 取引やブロックをノード間へ広げる土台になる |
| 5章 | 情報が広がる流れ | gossip、重複処理、到達タイミングの違いを図とコードで確認する |
| 6章 | BitcoinのP2Pネットワーク | フルノード、DNS seed、inv / getdata、mempool、IBDなどを整理する |
| 7章 | EthereumのP2Pネットワーク | 実行クライアントとコンセンサスクライアントの役割を分けて見る |
| 8章 | P2Pネットワークの強み | 単一障害点の低減、情報の複製、検閲耐性、独立検証につながる |
| 9章 | 限界とセキュリティ上の注意点 | Sybil攻撃、Eclipse攻撃、ネットワーク分断、DoSなどに注意する |
| 10章 | よくある誤解 | 「P2Pなら完全に安全」「P2Pだけで正しさが決まる」などを避ける |
| 11章 | 最近の動向 | BIP324、Erlay、PeerDAS、Portal Networkなどの改善を確認する |
こうして見ると、P2Pネットワークは、ブロックチェーンの表側からは見えにくいものの、取引やブロックを届けるための重要な基盤だと分かります。
ただし、P2Pネットワークだけを見ても、ブロックチェーン全体は説明できません。
検証ルール、暗号技術、データ構造、コンセンサス、ノード運用、攻撃対策などと組み合わせて理解する必要があります。
12.3 P2Pは「情報を届ける道」である
この記事全体を一言でまとめるなら、次のようになります。
P2Pネットワークは、ブロックチェーンにおいて取引やブロックを広げるための通信の土台です。
ただし、情報が届くことと、その情報が正しいことは別の問題です。
この区別はとても大切です。
P2Pネットワークによって、取引やブロックは多くのノードへ広がります。
しかし、各ノードは届いた情報をそのまま信じるのではなく、ルールに従って検証します。
たとえば、Bitcoinではフルノードがブロックや取引を交換するためにP2Pネットワークを維持し、他ノードへ中継する前に検証します。
参考: Bitcoin Developer Guide: P2P Network
Ethereumでも、networking layerはノード同士の発見、gossip、request/responseによる情報交換を扱います。
そのうえで、実行クライアントやコンセンサスクライアントが、それぞれの役割に応じて取引、ブロック、attestationなどを扱います。
参考: Ethereum Documentation: Networking layer
つまり、P2Pネットワークは「情報を届ける道」です。
その道を通って届いた情報をどう確認し、どの履歴を採用するかは、検証やコンセンサスの領域になります。
| 役割 | ざっくりした意味 | ブロックチェーンでの例 |
|---|---|---|
| P2Pネットワーク | 情報をノード間へ届ける | 取引やブロックをpeerへ伝える |
| 検証ルール | 届いた情報がルールに合うか確認する | 署名、残高、ブロック形式などを確認する |
| コンセンサス | 複数の候補があるときに、どの履歴を採用するか決める | PoW、PoS、fork choice、finalityなど |
| データ構造 | 記録同士のつながりを確認しやすくする | ハッシュ、Merkle tree、ブロックチェーン |
ここを分けて考えると、「P2Pなら正しい」「P2Pなら改ざんされない」といった誤解を避けやすくなります。
💡 豆知識
P2Pネットワークは、ブロックチェーンの画面にはあまり出てきません。
しかし、取引を送ったあと、その情報がどのノードへ届き、どのpeerへ中継され、どのタイミングでブロックに入るのかを考えると、P2Pネットワークの存在感が見えてきます。
目立ちにくいですが、かなり重要な裏方です。
12.4 P2Pネットワークはブロックチェーン全体の一部である
本記事ではP2Pネットワークを中心に扱いましたが、ここで一度、ブロックチェーン全体の中での位置づけを整理しておきます。
ブロックチェーンの仕組みは、P2Pネットワークだけで成り立っているわけではありません。
たとえば、次のような要素も関係します。
- 取引を作るためのウォレット
- 取引の正当性を確認するデジタル署名
- ブロック同士をつなぐハッシュ
- 取引をまとめるMerkle tree
- 取引やブロックを伝えるP2Pネットワーク
- ブロックや取引を確認する検証ルール
- 複数の履歴候補から選ぶコンセンサス周辺の仕組み
- ノードソフトウェアの実装や運用
P2Pネットワークは、その中でも特に 情報を届ける層 に関係します。
この図のように、P2Pネットワークはブロックチェーン全体の中の重要な一部分です。
P2Pネットワークがなければ、分散したノードへ取引やブロックを届けることが難しくなります。
一方で、P2Pネットワークだけがあっても、届いた情報を検証するルールや、複数の履歴候補を選ぶ仕組みがなければ、ブロックチェーンとしての信頼性は成り立ちません。
そのため、P2Pネットワークを理解するときは、次のように考えると整理しやすいです。
P2Pは、ブロックチェーンの「通信の土台」です。
その上に、検証ルール、暗号技術、データ構造、コンセンサスが重なって、分散した記録の共有が実現されます。
12.5 P2Pを学ぶときに意識したいこと
P2Pネットワークを学ぶときは、次の点を意識すると理解しやすくなります。
| 意識したいこと | 理由 |
|---|---|
| 「サーバーが一切ない」と言い切らない | DNS seedやbootnodeなど、参加の入口になる仕組みが使われる場合があるため |
| 「情報が届くこと」と「情報が正しいこと」を分ける | P2Pは伝播を担い、正しさは検証ルールやコンセンサスで確認するため |
| ノード数だけで安全性を判断しない | 地理、クラウド、実装、接続先の偏りも影響するため |
| BitcoinとEthereumを同じものとして扱わない | P2Pのプロトコル、レイヤー構成、合意形成との関係が異なるため |
| 攻撃者の視点も考える | Sybil攻撃、Eclipse攻撃、DoS、ネットワーク分断などがあるため |
| 最新動向を確認する | BIP324、PeerDAS、Portal Networkなど、P2P周辺の改善は続いているため |
特に、次のような表現には注意が必要です。
| 避けたい表現 | より丁寧な表現 |
|---|---|
| P2Pはサーバーが一切ない仕組み | P2Pは中央の1か所だけに頼らない通信の考え方 |
| P2Pなら完全に分散している | P2Pは分散しやすい構造だが、接続先や運用には偏りがあり得る |
| P2Pなら情報は必ず正しい | P2Pは情報を広げる仕組みであり、正しさは検証ルールで確認する |
| P2Pなら絶対に止まらない | 一部障害に強くなりやすいが、ネットワーク分断や攻撃の影響はあり得る |
| ノード数が多ければ必ず安全 | ノードの多様性や接続構造も重要になる |
技術記事として書く場合も、学習する場合も、強すぎる言い切りを避けることが大切です。
P2Pネットワークは、ブロックチェーンの分散性や耐障害性を支える重要な仕組みです。
一方で、「P2Pだから安全」と単純に片付けられるものではありません。
どの部分が強く、どの部分に限界があり、どこから検証やコンセンサス、運用の問題になるのかを分けて考えることが重要です。
12.6 今後深掘りしたいテーマ
P2Pネットワークを理解すると、次に気になってくるテーマがいくつかあります。
| テーマ | ざっくりした内容 | P2Pとの関係 |
|---|---|---|
| ノード運用 | 実際にノードを動かすときの帯域、ストレージ、peer管理 | P2Pネットワークを実感しやすい |
| Proof of Work / Proof of Stake | どの履歴を採用するかに関わる仕組み | P2Pで届いたブロック候補をどう扱うかにつながる |
| fork choice | 複数の履歴候補が出たとき、どれを選ぶか | P2Pで複数候補が届くことがあるため重要 |
| finality | あるブロックをどの程度確定と見るか | ネットワーク伝播や投票と関係する |
| Data Availability | データが本当に利用可能か確認する考え方 | PeerDASやRollupの理解につながる |
| 軽量クライアント | すべてのデータを持たずに検証へ近づく仕組み | Portal NetworkやDASと関係する |
| プライバシー | 取引伝播や接続情報から何が観測され得るか | P2Pの通信経路やメタデータが関係する |
| ブリッジ | 異なるチェーン間で価値を移す仕組み | 異なるP2Pネットワークや合意形成をまたぐリスクにつながる |
| ネットワーク攻撃 | Sybil攻撃、Eclipse攻撃、DoSなど | P2Pの接続構造を悪用する攻撃を理解できる |
この記事では、P2Pネットワークの全体像を初学者向けに整理することを優先しました。
そのため、Bitcoin Coreの実装詳細、EthereumのDevP2P仕様、libp2p Gossipsubのpeer scoring、PeerDASの詳細、実際のノード運用手順などは深く扱っていません。
これらは、別記事として一つずつ掘り下げる価値があります。
12.7 最後に
P2Pネットワークは、ブロックチェーンを理解するうえで避けて通れないテーマです。
ただ、最初からプロトコル仕様や実装詳細、ネットワーク攻撃の研究に入ると、かなり難しく感じるかもしれません。
そのため、本記事では、クラスや研究室での連絡共有のような身近な例から始めました。
みんなへ同じ情報を届けたい。
でも、代表者1人だけに連絡を任せると、その人が止まったときに全体へ影響が出てしまう。
一方で、参加者同士が情報を伝え合う形にすれば、別の経路から情報が広がる可能性があります。
この考え方をブロックチェーンに当てはめると、取引やブロックをノード同士で伝え合うP2Pネットワークの役割が見えてきます。
もちろん、P2Pネットワークにも限界があります。
Sybil攻撃、Eclipse攻撃、ネットワーク分断、スパム、peer discoveryへの依存、ノードの偏りなど、考えるべき点は多くあります。
それでも、P2Pネットワークは「中央の1か所だけに頼らず、分散した参加者が情報を共有するにはどうすればよいか」という大きな問いに対する、重要な設計の一つです。
この記事を通じて、P2Pネットワークを単なる用語としてではなく、次のように捉えられるようになっていれば幸いです。