13. 線路協定:框架和可擴展性
在本章中,我們深入探討閃電網路的線路協定,並涵蓋協定中內建的各種可擴展性槓桿。在本章結束時,有雄心的讀者應該能夠為閃電網路編寫自己的線路協定解析器。除了能夠編寫自訂線路協定解析器外,本章的讀者還將深入理解協定中內建的各種升級機制。
13.1. 閃電協定套件中的訊息層
訊息層在本章中詳細介紹,包括「框架和訊息格式」、「類型-長度-值」編碼和「功能位元」。這些組件在協定套件中以輪廓突顯,如 閃電協定套件中的訊息層 所示。
13.2. 線路框架
我們首先描述協定中線路 框架 的高層結構。當我們說框架時,我們指的是位元組在線路上打包以 編碼 特定協定訊息的方式。如果不了解協定中使用的框架系統,線路上的位元組字串會像是一系列隨機位元組,因為沒有強加任何結構。通過應用適當的框架來解碼線路上的這些位元組,我們將能夠提取結構,並最終在我們的更高級語言中將此結構解析為協定訊息。
重要的是要注意,閃電網路是一個 端對端加密 的協定,線路框架本身封裝在 加密 的訊息傳輸層中。正如我們在 閃電網路的加密訊息傳輸 中看到的,閃電網路使用 Noise 協定的自訂變體來處理傳輸加密。在本章中,每當我們給出線路框架的範例時,我們假設加密層已經被剝離(解碼時),或者我們尚未加密我們在線路上發送的位元組集(編碼時)。
13.2.1. 高層線路框架
話雖如此,我們準備描述用於在線路上編碼訊息的高層架構:
-
線路上的訊息以 2 位元組 類型欄位開始,後面跟著訊息有效載荷。
-
訊息有效載荷本身最大可達 65 KB。
-
所有整數都以大端序(網路順序)編碼。
-
在定義的訊息之後跟隨的任何位元組都可以安全地忽略。
是的,就是這樣。由於協定依賴於 封裝 的傳輸協定加密層,我們不需要為每種訊息類型提供明確的長度。這是因為傳輸加密在 訊息 級別工作,所以當我們準備解碼下一條訊息時,我們已經知道訊息本身的總位元組數。使用 2 個位元組作為訊息類型(以大端序編碼)意味著協定可以有多達 2^16 – 1 或 65,535 種不同的訊息。繼續,因為我們知道所有訊息必須小於 65 KB,這簡化了我們的解析,因為我們可以使用 固定大小 的緩衝區,並對解析傳入線路訊息所需的總記憶體量保持強邊界。
最後一點允許一定程度的 向後 相容性,因為新節點能夠在舊節點(可能不理解它們)可以安全忽略的線路訊息中提供資訊。正如我們隨後看到的,這個功能與非常靈活的線路訊息可擴展性格式相結合,也允許協定實現 向前 相容性。
13.2.2. 類型編碼
有了這個高層背景,我們現在從最原始的層開始:解析原始類型。除了編碼整數外,閃電協定還允許編碼大量類型,包括可變長度位元組切片、橢圓曲線公鑰、比特幣地址和簽名。當我們在本章後面描述線路訊息的 結構 時,我們引用高層類型(抽象類型)而不是該類型的低層表示。在本節中,我們剝離這個抽象層,以確保我們未來的線路解析器能夠正確編碼/解碼任何更高層的類型。
在 高層訊息類型 中,我們將給定訊息類型的名稱映射到用於編碼/解碼該類型的高層例程。
| 高層類型 | 框架 | 註解 |
|---|---|---|
|
32 位元組固定長度位元組切片 |
解碼時,如果內容不是有效的 UTF-8 字串則拒絕 |
|
32 位元組固定長度位元組切片,將出點映射到 32 位元組值 |
給定一個出點,可以通過取出點的 TxID 並與索引(解釋為低 2 位元組)進行 XOR 來將其轉換為 |
|
無符號 64 位整數( |
由區塊高度(24 位元)、交易索引(24 位元)和輸出索引(16 位元)打包成 8 位元組組成 |
|
無符號 64 位整數( |
代表千分之一聰 |
|
無符號 64 位整數( |
比特幣的基本單位 |
|
以 壓縮 格式編碼的 secp256k1 公鑰,佔用 33 位元組 |
在線路上佔用固定的 33 位元組長度 |
|
secp256k1 橢圓曲線的 ECDSA 簽名 |
編碼為 固定 64 位元組的位元組切片,打包為 |
|
8 位整數 |
|
|
16 位整數 |
|
|
64 位整數 |
|
|
可變長度位元組切片 |
以表示位元組長度的 16 位整數為前綴 |
|
RGB 顏色編碼 |
編碼為一系列 8 位整數 |
|
網路地址的編碼 |
以表示地址類型的 1 位元組前綴編碼,後面跟著地址主體 |
在下一節中,我們描述每個線路訊息的結構,包括訊息的前綴類型以及其訊息主體的內容。
13.3. 類型-長度-值訊息擴展
在本章前面,我們提到訊息最大可達 65 KB,如果在解析訊息時,剩餘額外的位元組,那麼這些位元組將被忽略。乍一看,這個要求可能看起來有些武斷;然而,這個要求允許閃電協定本身的解耦非同步演進。我們在本章末尾更多地討論這一點。但首先,我們把注意力轉向訊息末尾的那些「額外位元組」到底可以用來做什麼。
13.3.1. Protocol Buffers 訊息格式
Protocol Buffers(Protobuf)訊息序列化格式最初是 Google 內部使用的格式,現已發展成為全球開發人員使用的最流行的訊息序列化格式之一。Protobuf 格式描述了訊息(通常是與 API 相關的某種資料結構)如何在線路上編碼並在另一端解碼。數十種語言中存在幾種「Protobuf 編譯器」,它們充當橋樑,允許任何語言編碼的 Protobuf 能夠被另一種語言中的相容解碼器解碼。這種跨語言的資料結構相容性允許廣泛的創新,因為可以跨語言和抽象邊界傳輸結構甚至類型化的資料結構。
Protobuf 還以其在處理底層訊息結構變化方面的靈活性而聞名。只要遵守欄位編號架構,較新的 Protobuf 編寫器就可以在 Protobuf 中包含任何較舊讀取器可能不知道的資訊。當舊讀取器遇到新的序列化格式時,如果有它不理解的類型/欄位,那麼它只是 忽略 它們。這允許舊客戶端和新客戶端共存,因為所有客戶端都可以解析較新訊息格式的某些部分。
13.3.2. 向前和向後相容性
Protobuf 在開發人員中非常流行,因為它們內建支援向前和向後相容性。大多數開發人員可能熟悉向後相容性的概念。簡單來說,該原則指出,對訊息格式或 API 的任何更改都應以不破壞對舊客戶端支援的方式進行。在我們前面的 Protobuf 可擴展性範例中,通過確保對 Protobuf 格式的新添加不會破壞舊讀取器的已知部分來實現向後相容性。另一方面,向前相容性對於非同步更新同樣重要;然而,它不太為人所知。為了使更改向前相容,客戶端只需忽略他們不理解的任何資訊。升級比特幣共識系統的軟分叉機制可以說是向前和向後相容的:任何不更新的客戶端仍然可以使用比特幣,如果他們遇到任何不理解的交易,那麼他們只是忽略它們,因為他們的資金沒有使用那些新功能。
13.4. 類型-長度-值格式
為了能夠以向前和向後相容的方式升級訊息,除了功能位元(稍後會詳細介紹)外,閃電網路還使用一種自訂訊息序列化格式,簡稱為類型-長度-值,或簡稱 TLV。該格式受到廣泛使用的 Protobuf 格式的啟發,借用了許多概念,同時大大簡化了實現以及與訊息解析互動的軟體。好奇的讀者可能會問,「為什麼不直接使用 Protobuf?」作為回應,閃電開發人員會說,我們能夠擁有 Protobuf 可擴展性的最佳特性,同時也有更小的實現優勢,因此攻擊面更小。截至 3.15.6 版本,Protobuf 編譯器有超過 656,671 行程式碼。相比之下,LND 的 TLV 訊息格式實現只有 2.3k 行程式碼(包括測試)。
有了必要的背景介紹,我們現在準備詳細描述 TLV 格式。TLV 訊息擴展被稱為單個 TLV 記錄 的串流。單個 TLV 記錄有三個組件:記錄的類型、記錄的長度,最後是記錄的不透明值:
type-
表示正在編碼的記錄名稱的整數
length-
記錄的長度
value-
記錄的不透明值
type 和 length 都使用受比特幣 P2P 協定中使用的可變大小整數(varint)啟發的可變大小整數編碼,簡稱為 BigSize。
13.4.1. BigSize 整數編碼
在其最完整的形式中,BigSize 整數可以表示最多 64 位元的值。與比特幣的 varint 格式相比,BigSize 格式使用大端序位元組排序來編碼整數。
BigSize varint 有兩個組件:判別式和主體。在 BigSize 整數的上下文中,判別式向解碼器傳達後面跟隨的可變大小整數的大小。請記住,可變大小整數的獨特之處在於它們允許解析器使用比較大整數更少的位元組來編碼較小的整數,從而節省空間。BigSize 整數的編碼遵循以下四個選項之一:
-
如果值小於
0xfd(253):則判別式實際上不使用,編碼只是整數本身。這允許我們編碼非常小的整數而沒有額外開銷。 -
如果值小於或等於
0xffff(65535):判別式編碼為0xfd,這表示後面的值大於0xfd,但小於0xffff。然後數字編碼為 16 位整數。包括判別式,我們可以使用 3 個位元組編碼大於 253 但小於 65,535 的值。 -
如果值小於
0xffffffff(4294967295):判別式編碼為0xfe。主體使用 32 位整數編碼,包括判別式,我們可以使用 5 個位元組編碼小於4,294,967,295的值。 -
否則,我們只是將值編碼為完整大小的 64 位整數。
13.4.2. TLV 編碼約束
在 TLV 訊息的上下文中,低於 2^16 的記錄類型被稱為 保留 供將來使用。超出此範圍的類型用於更高級應用協定使用的「自訂」訊息擴展。
記錄的 value 取決於 type。換句話說,它可以採取任何形式,因為解析器將嘗試根據類型本身的上下文來解釋它。
13.4.3. TLV 規範編碼
Protobuf 格式的一個問題是,當由兩個不同版本的編譯器編碼時,相同訊息的編碼可能輸出完全不同的位元組集。在閃電的上下文中,這種非規範編碼的情況是不可接受的,因為許多訊息包含訊息摘要的簽名。如果一條訊息可能以兩種不同的方式編碼,那麼通過使用線路上稍微不同的位元組集重新編碼訊息,可能會無意中破壞簽名的認證。
為了確保所有編碼的訊息都是規範的,在編碼時定義了以下約束:
-
TLV 串流中的所有記錄必須按嚴格遞增類型的順序編碼。
-
所有記錄必須最小化編碼
type和length欄位。換句話說,必須始終使用整數的最小BigSize表示。 -
每個
type在給定的 TLV 串流中只能出現一次。
除了這些編碼約束外,還根據給定 type 整數的 元數 定義了一系列更高級的解釋要求。我們在本章末尾更深入地探討這些細節,一旦我們描述了閃電協定在實踐和理論上是如何升級的。
13.5. 功能位元和協定可擴展性
因為閃電網路是一個去中心化系統,沒有單一實體可以對系統的所有用戶強制執行協定更改或修改。這一特性也出現在其他去中心化網路中,如比特幣。然而,與比特幣不同的是,壓倒性的共識 不是 改變閃電網路子集所必需的。閃電網路能夠隨意演進,而無需強烈的協調要求,因為與比特幣不同,閃電網路中不需要全域共識。由於這個事實以及閃電網路中嵌入的幾種升級機制,只有希望使用這些新閃電網路功能的參與者需要升級,然後他們就能夠彼此互動。
在本節中,我們探索開發人員和用戶能夠設計和部署閃電網路新功能的各種方式。原始閃電網路的設計者知道網路和底層協定有許多可能的未來方向。因此,他們確保在系統中實現了幾種可擴展性機制,可用於以解耦、非同步和去中心化的方式部分或完全升級它。
13.5.1. 功能位元作為升級發現機制
精明的讀者可能已經注意到閃電協定中包含功能位元的各個位置。功能位元 是一個位元欄位,可用於宣傳對可能的網路協定更新的理解或遵守。功能位元通常成對分配,這意味著每個潛在的新功能/升級總是在位元欄位中定義兩個位元。一個位元表示宣傳的功能是 可選的,意味著該節點知道該功能並可以使用它,但不認為它對正常操作是必需的。另一個位元表示該功能是 必需的,意味著如果潛在的對等節點不理解該功能,該節點將不會繼續操作。
使用這兩個位元(可選和必需),我們可以構建一個簡單的相容性矩陣,節點/用戶可以參考它來確定對等節點是否與所需功能相容,如 功能位元相容性矩陣 所示。
| 位元類型 | 遠端可選 | 遠端必需 | 遠端未知 |
|---|---|---|---|
本地可選 |
✅ |
✅ |
✅ |
本地必需 |
✅ |
✅ |
❌ |
本地未知 |
✅ |
❌ |
❌ |
從這個簡化的相容性矩陣中,我們可以看到,只要對方知道我們的功能位元,那麼我們就可以使用該協定與他們互動。如果對方甚至不知道我們指的是什麼位元 並且 他們需要該功能,那麼我們與他們不相容。在網路中,可選功能使用 奇數位元號 發信號,而必需功能使用 偶數位元號 發信號。例如,如果一個對等節點發信號說他們知道一個使用位元 15 的功能,那麼我們知道這是一個可選功能,即使我們不知道該功能,我們也可以與他們互動或回應他們的訊息。如果他們使用位元 16 發信號該功能,那麼我們知道這是一個必需功能,除非我們的節點也理解該功能,否則我們無法與他們互動。
閃電開發人員想出了一個易於記憶的短語來編碼這個矩陣:「奇數是可以的」(it’s OK to be odd)。這個簡單的規則允許協定內的豐富互動集,因為兩個功能位元向量之間的簡單位元遮罩操作允許對等節點確定某些互動是否彼此相容。換句話說,功能位元用作升級發現機制:它們很容易讓對等節點根據可選、必需和未知功能位元的概念來理解它們是否相容。
功能位元在協定中的 node_announcement、channel_announcement 和 init 訊息中找到。因此,這三種訊息可用於在網路內發信號傳輸中協定更新的知識和/或理解。node_announcement 訊息中的功能位元可以讓對等節點確定其 連接 是否相容。channel_announcement 訊息中的功能位元允許對等節點確定給定的付款類型或 HTLC 是否可以通過給定的對等節點傳輸。init 訊息中的功能位元允許對等節點理解他們是否可以維持連接,以及為給定連接的生命週期協商了哪些功能。
13.5.2. TLV 用於向前和向後相容性
正如我們在本章前面學到的,TLV 記錄可用於以向前和向後相容的方式擴展訊息。隨著時間的推移,這些記錄已被用來擴展現有訊息,而不會通過利用訊息中超出已知位元組集的「未定義」區域來破壞協定。
例如,原始的閃電協定沒有「最大金額 HTLC」的概念,這是由路由策略規定的可以通過通道傳輸的最大金額。後來,max_htlc 欄位被添加到 channel_update 訊息中,以隨著時間的推移逐步引入這個概念。接收設置了這樣欄位的 channel_update 但甚至不知道該升級存在的對等節點不受變化影響,但如果他們的 HTLC 超過限制,他們的 HTLC 會被拒絕。另一方面,較新的對等節點能夠解析、驗證和利用新欄位。
熟悉比特幣中軟分叉概念的人現在可能會看到兩種機制之間的一些相似之處。與比特幣共識級軟分叉不同,閃電網路的升級不需要壓倒性的共識才能被採用。相反,至少只有網路中的兩個對等節點需要理解新的升級才能開始使用它。通常這兩個對等節點可能是付款的接收者和發送者,或者可能是新支付通道的通道合作夥伴。
13.5.3. 升級機制的分類
與其在網路中有一個廣泛使用的單一升級機制(如比特幣的軟分叉),閃電網路中存在幾種可能的升級機制。在本節中,我們列舉這些升級機制並提供過去使用它們的真實範例。
內部網路升級
我們從需要最多協定級協調的升級類型開始:內部網路升級。內部網路升級的特點是需要預期付款路徑中的 每個節點 都理解新功能。這種升級類似於網際網路中需要在升級的核心中繼部分進行硬體級升級的任何升級。然而,在閃電網路的上下文中,我們處理的是純軟體,所以這種升級更容易部署,但它們仍然比網路中的任何其他升級機制需要更多的協調。
網路中這種升級的一個例子是引入 TLV 編碼用於洋蔥封包中編碼的路由資訊。之前的格式使用硬編碼的固定長度訊息格式來傳達諸如下一跳之類的資訊。因為這種格式是固定的,這意味著新的協定級升級是不可能的。轉向更靈活的 TLV 格式意味著在此升級之後,任何修改每一跳傳達的資訊類型的功能都可以隨意推出。
值得一提的是,TLV 洋蔥升級是一種「軟」內部網路升級,因為如果付款沒有使用超出新路由資訊編碼的任何新功能,那麼付款可以使用混合節點集傳輸。
端對端升級
為了與內部網路升級形成對比,在本節中我們描述 端對端 網路升級。這種升級機制與內部網路升級的不同之處在於,它只需要付款的「兩端」,即發送者和接收者,進行升級。
這種類型的升級允許在網路內進行廣泛的不受限制的創新。由於網路內付款的洋蔥加密性質,在網路中心轉發 HTLC 的那些節點甚至可能不知道正在使用新功能。
網路中端對端升級的一個例子是多部分付款(MPP)的推出。MPP 是一種協定級功能,使單個付款能夠分成多個部分或路徑,在接收者處組裝進行結算。MPP 的推出與一個新的 node_announcement 級功能位元相結合,該功能位元表示接收者知道如何處理部分付款。假設發送者和接收者彼此了解(可能通過 BOLT #11 發票),那麼他們就能夠使用新功能而無需任何進一步協商。
端對端升級的另一個例子是網路中部署的各種類型的 自發 付款。一種早期類型的自發付款稱為 keysend,它通過簡單地將付款的原像放在加密洋蔥中來工作。收到後,目的地將解密原像,然後使用它來結算付款。因為整個封包是端對端加密的,所以這種付款類型是安全的,因為沒有任何中間節點能夠完全解開洋蔥以揭示付款原像。
13.5.4. 通道構建級更新
最後一類廣泛的更新是那些發生在通道構建級別的更新,但不會修改網路中廣泛使用的 HTLC 結構。當我們說通道構建時,我們指的是通道是如何資金充足或創建的。例如,eltoo 通道類型可以使用新的 node_announcement 級功能位元以及 channel_announcement 級功能位元在網路中推出。只有通道兩側的兩個對等節點需要理解和宣傳這些新功能。然後這個通道對可以用來轉發任何付款類型,前提是通道支援它。
另一個是 錨定輸出 通道格式,它允許通過比特幣的子付父(CPFP)費用管理機制來提高承諾費用。
13.6. 結論
閃電網路的線路協定極其靈活,允許快速創新和互操作性,而無需嚴格的共識。這是閃電網路經歷更快發展並吸引許多開發人員的原因之一,否則這些開發人員可能會發現比特幣的開發風格過於保守和緩慢。