オニオンルーティング(Sphinxプロトコル)
オニオンルーティングの全体像1. 送信者が最終受取ノードまでの経路を組み立てる
送信者(支払いをするノード)は、Lightningネットワーク内の公開チャネル情報や、請求書(Invoice)に含まれるルートヒント等を使って、
「自分 → 中継ノード1 → 中継ノード2 → … → 最終受取ノード」
というルートを探索(Path Finding)し、ホップの順番を決めます。
2. 送信者が“オニオンパケット”を生成
Sphinxプロトコルでは、各ホップに渡すデータを多層の暗号データ(オニオン)として準備します。
このときエフェメラル(使い捨て)秘密鍵などの暗号材料を用い、それぞれのホップの公開鍵とECDH(鍵共有)を行いながら、各ホップ用の暗号化レイヤーを構築します。
出来上がったオニオンパケットには、
「どのノードに転送すべきか」
「そのノードが読むべき支払い関連情報(fee、時間制限など)」
が暗号化されて格納されます。
3. 中継ノードは自分のレイヤーのみ復号して次ホップへ転送
最初の中継ノードは送信者から受け取ったオニオンを、自分の秘密鍵を使って一層だけ解読します。
すると「次に送る先のノードの情報」と「自分が受け取るべき手数料やHTLCタイムアウト情報」などが出てきます。
それを使って次ホップにパケットを渡し、自分は最終ノードや他のホップの情報を知ることはできません。
これを各ホップで繰り返すことにより、最終的に受取ノードへ到達する仕組みになっています。
具体的な暗号フロー(Sphinxプロトコルの概念)2-1. 送信者側の処理(暗号化)
1. エフェメラル鍵ペア生成
送信者は、一時的に使う秘密鍵 𝑠 と公開鍵 $ S=s⋅G(楕円曲線の生成元 G を使ったもの)を作る。
これがオニオンパケット全体を暗号化する基点になる鍵(エフェメラル鍵)です。
2. 各ホップとのECDHと鍵導出
送信者は、あらかじめ決めた経路のホップ(中継ノード)それぞれの公開鍵$ (𝑃1,𝑃2,…,𝑃𝑛)を持っています。
ホップ 𝑖 について、𝑆(エフェメラル公開鍵)と 𝑃𝑖 (ホップのノード公開鍵)を使い、ECDHにより共有秘密 𝑘𝑖 を導出します。
$ k i=ECDH(s,P i) (内部的には k i=s⋅P i )
この 𝑘𝑖 から派生した暗号キー(暗号化やHMAC用のキー)を用いて「ホップ 𝑖 向けのペイロード情報」を暗号化し、順番にパケットに詰めていきます。
3. ペイロードと“HMAC”の連結
各ホップが読むべき情報(amount, cltv_expiry, next_hop など)を暗号化したうえでHMAC(メッセージ認証コード)を付加していきます。
これを「多層の暗号データ(オニオン構造)」として組み上げたものが、最終的なSphinxパケット(LNにおけるonion packet)となります。
2-2. 中継ノード側の処理(復号)
1. 受信したオニオンパケットから自分向けのレイヤーを復号
中継ノードは自分の秘密鍵 𝑥𝑖 を用いて、パケット内に含まれるエフェメラル公開鍵 𝑆 とのECDHを行い、共有秘密 𝑘𝑖 = 𝑥𝑖 ⋅ 𝑆 を得ます。
この 𝑘𝑖 から派生した暗号キーでパケットを部分的に復号すると、自分あてのペイロード(次ホップのIDや手数料情報など)が読めます。
2. 次ホップ情報の取得・オニオンの再構築
復号した結果、次のノードのアドレスや公開鍵、支払額などを読み取り、
「次に誰に送るか」
「次に渡すべき暗号化データ(オニオンの残り部分)」を明らかにします。
同時に、自分用の暗号化部分を取り除く処理を行い、一段皮が剥けたオニオンを次のノードに転送します。
それ以降のレイヤー(次ノードに関する暗号データ)は依然として暗号化されているので、自分は読むことができません。
3. 最終ノードに到達
このプロセスが各ホップで繰り返され、最後には受取ノードが自分用のレイヤーを復号して「この支払いが自分宛である」と認識し、HTLCを受理することになります。
ki の計算をより詳しく
$ ki=s⋅Pi=s⋅(xi⋅G)=(s⋅xi)⋅G
$ ki=xi⋅S=xi⋅(s⋅G)=(xi⋅s)⋅G
code:mmd
flowchart LR
A((送信者<br>Onion生成)) -- オニオンパケット送付 --> B((中継ノード1<br>自ノード情報を復号))
B -- 一段皮を剥いたオニオン --> C((中継ノード2<br>自ノード情報を復号))
C -- 最終的なオニオン --> D((最終受取ノード<br>最終レイヤを復号))
style A fill:#CCE5FF,stroke:#003366,stroke-width:2px
style B fill:#E2FFE2,stroke:#006600,stroke-width:2px
style C fill:#E2FFE2,stroke:#006600,stroke-width:2px
style D fill:#FFE5CC,stroke:#FF6600,stroke-width:2px
code:svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generated by graphviz version 7.1.0 (20230312.1610)
-->
<!-- Title: onion_routing Pages: 1 -->
<svg width="704pt" height="224pt"
viewBox="0.00 0.00 704.00 224.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="translate(4,220) scale(0.05 -0.05)">
<title>onion_routing</title>
<polygon fill="#ffffff" stroke="transparent" points="-80,4400 -80,-4400 14152,-4400 14152,4400 -80,4400"/>
<!-- A -->
<g id="node1" class="node">
<title>A</title>
<path fill="#CCE5FF" stroke="#003366" stroke-width="40" d="M465 1185C465 1121 514 1072 578 1072L1420 1072C1484 1072 1533 1121 1533 1185L1533 1615C1533 1679 1484 1728 1420 1728L578 1728C514 1728 465 1679 465 1615L465 1185 465 1185"/>
<text text-anchor="middle" x="999" y="1350" font-family="sans-serif" font-size="240.00">送信者
(オニオン生成)
</text>
</g>
<!-- B -->
<g id="node2" class="node">
<title>B</title>
<path fill="#E2FFE2" stroke="#006600" stroke-width="40" d="M2062 1185C2062 1121 2111 1072 2175 1072L3221 1072C3285 1072 3334 1121 3334 1185L3334 1615C3334 1679 3285 1728 3221 1728L2175 1728C2111 1728 2062 1679 2062 1615L2062 1185 2062 1185"/>
<text text-anchor="middle" x="2747" y="1350" font-family="sans-serif" font-size="240.00">中継ノード1
(自ノード情報を復号)
</text>
</g>
<!-- C -->
<g id="node3" class="node">
<title>C</title>
<path fill="#E2FFE2" stroke="#006600" stroke-width="40" d="M3663 1185C3663 1121 3712 1072 3776 1072L4822 1072C4886 1072 4935 1121 4935 1185L4935 1615C4935 1679 4886 1728 4822 1728L3776 1728C3712 1728 3663 1679 3663 1615L3663 1185 3663 1185"/>
<text text-anchor="middle" x="4299" y="1350" font-family="sans-serif" font-size="240.00">中継ノード2
(自ノード情報を復号)
</text>
</g>
<!-- D -->
<g id="node4" class="node">
<title>D</title>
<path fill="#FFE5CC" stroke="#FF6600" stroke-width="40" d="M5265 1185C5265 1121 5314 1072 5378 1072L6420 1072C6484 1072 6533 1121 6533 1185L6533 1615C6533 1679 6484 1728 6420 1728L5378 1728C5314 1728 5265 1679 5265 1615L5265 1185 5265 1185"/>
<text text-anchor="middle" x="5899" y="1350" font-family="sans-serif" font-size="240.00">最終受取ノード
(最終レイヤを復号)
</text>
</g>
<!-- A&#45;&gt;B -->
<g id="edge1" class="edge">
<title>A&#45;&gt;B</title>
<path fill="none" stroke="#000000" stroke-width="20" d="M1534 1400C1642 1400 1740 1400 1853 1400"/>
<text text-anchor="middle" x="1693" y="1334" font-family="sans-serif" font-size="200.00">オニオンパケット
(多層暗号)</text>
</g>
<!-- B&#45;&gt;C -->
<g id="edge2" class="edge">
<title>B&#45;&gt;C</title>
<path fill="none" stroke="#000000" stroke-width="20" d="M3335 1400C3442 1400 3540 1400 3653 1400"/>
<text text-anchor="middle" x="3493" y="1334" font-family="sans-serif" font-size="200.00">一段皮を剥いた
オニオンパケット</text>
</g>
<!-- C&#45;&gt;D -->
<g id="edge3" class="edge">
<title>C&#45;&gt;D</title>
<path fill="none" stroke="#000000" stroke-width="20" d="M4936 1400C5042 1400 5140 1400 5253 1400"/>
<text text-anchor="middle" x="5093" y="1334" font-family="sans-serif" font-size="200.00">さらに一段皮を剥いた
オニオンパケット</text>
</g>
</g>
</svg>