精通以太坊

第 4 章密碼學

第 4 章

密碼學

約 54 分鐘閱讀 · 9 個小節

以太坊的基礎技術之一是密碼學,這是數學的一個分支,廣泛應用於電腦安全。密碼學(cryptography)在希臘文中的意思是「祕密書寫」,但密碼學研究的範圍不只是祕密書寫,也就是所謂的加密(encryption)。舉例來說,密碼學也可以用來證明自己知道某個祕密、卻不洩漏這個祕密(例如使用數位簽章),或證明資料的真實性(例如使用數位指紋,也就是雜湊值)。這類密碼學證明是以太坊平台(其實也是所有區塊鏈系統)運作不可或缺的數學工具,在以太坊應用中也被大量使用。

請注意,在本書出版時,以太坊協議的任何部分都沒有用到加密;也就是說,所有與以太坊平台的通訊以及節點之間的通訊(包括交易資料)都沒有加密,任何人都能(也必須能夠)讀取。這樣每個人才能驗證狀態更新是否正確,進而達成共識。未來,零知識證明(zero-knowledge proof)與同態加密(homomorphic encryption)等進階的密碼學工具,將能讓某些加密過的運算記錄在區塊鏈上,同時仍能達成共識;不過,雖然已經為它們預留了空間,這些工具還沒有完全部署。

本章將介紹以太坊使用的一些密碼學,也就是公開金鑰密碼學(public key cryptography,PKC)。它以私鑰與地址的形式,用來控制資金的所有權。

4.1. 金鑰與地址

本書前面提過,以太坊有兩種不同類型的帳戶:外部擁有帳戶(EOA)與合約。EOA 對以太幣(ether)的所有權,是透過數位的私鑰、以太坊地址與數位簽章確立的。私鑰是使用者與以太坊一切互動的核心。事實上,帳戶地址是直接從私鑰衍生出來的:一把私鑰唯一決定一個以太坊地址,也就是一個帳戶。

以太坊系統不會以任何方式直接使用私鑰;私鑰永遠不會在以太坊上傳輸或儲存。也就是說,私鑰應該保持私密,絕不應該出現在傳送到網路的訊息中,也不應該儲存在鏈上;在以太坊系統中傳輸與儲存的,只有帳戶地址與數位簽章。關於如何確保私鑰的安全,請參閱「錢包最佳實踐」。

資金的存取與控制是透過數位簽章達成的,而數位簽章同樣是用私鑰產生的。以太坊交易必須帶有有效的數位簽章,才能被納入區塊鏈。任何持有私鑰副本的人,都能控制對應的帳戶以及其中所有的以太幣。只要使用者妥善保管自己的私鑰,以太坊交易中的數位簽章就能證明誰是資金真正的擁有者,因為這些簽章證明了私鑰的所有權。

在以太坊所使用的這類以 PKC 為基礎的系統中,金鑰是成對出現的,由一把私鑰(祕密金鑰)與一把公鑰組成。你可以把公鑰想成銀行帳號,把私鑰想成祕密的 PIN 碼;後者讓你能控制帳戶,前者則讓別人辨識這個帳戶。以太坊使用者很少直接看到私鑰本身;大多數情況下,私鑰會(以加密的形式)儲存在特殊的檔案中,由以太坊錢包軟體管理。

在以太坊交易的付款部分,預定的收款人以一個以太坊地址表示,用法就像銀行轉帳時的收款帳戶資料。稍後我們會更詳細地看到,EOA 的以太坊地址是由金鑰對中的公鑰部分產生的。不過,並不是所有以太坊地址都代表一組公鑰與私鑰的金鑰對;地址也可以代表合約,而我們會在第 7 章看到,合約背後並沒有私鑰。

在本章接下來的部分,我們將:

  • 更深入了解密碼學的基礎,並探討它在以太坊中的數學原理

  • 檢視金鑰的產生、儲存與管理流程

  • 介紹私鑰、公鑰與地址所使用的各種編碼格式

  • 研究驗證者金鑰的密碼學與 KZG 承諾方案,這兩者代表以太坊密碼學基礎設施最新的更新

4.2. PKC 與加密貨幣

PKC,也稱為 非對稱密碼學(asymmetric cryptography),是現代資訊安全的核心。Martin Hellman、Whitfield Diffie 與 Ralph Merkle 在 1970 年代首次發表的 金鑰交換協議(key exchange protocol),是一項劃時代的突破,引發了大眾對密碼學領域的第一波熱潮。在 1970 年代以前,強大的密碼學知識一直被各國政府保密。

PKC 使用獨一無二的金鑰來保護資訊。這些金鑰以具有一種特殊性質的數學函數為基礎:計算函數本身很容易,計算它的反函數卻很困難。以這些函數為基礎,密碼學讓我們能建立數位祕密與無法偽造的數位簽章,而它們的安全性是由數學定律保障的。

例如,把兩個很大的質數相乘是輕而易舉的事。但如果只給你兩個大質數的乘積,要找出它的質因數就非常困難(這個問題稱為質因數分解)。假設我們給你 8,018,009 這個數字,並告訴你它是兩個質數的乘積。你要找出這兩個質數,遠比我把它們相乘得到 8,018,009 困難得多。

如果你知道某些祕密資訊,其中有些數學函數就能輕易地反推回去。在前面的例子中,如果我告訴你其中一個質因數是 2,003,你只要做個簡單的除法就能找出另一個:8,018,009 ÷ 2,003 = 4,003。這類函數通常稱為 陷門函數(trapdoor function),因為除非你拿到一項可以當作捷徑來反轉函數的祕密資訊,否則它們非常難以反推。

在密碼學中很有用的另一類更進階的數學函數,是以橢圓曲線上的算術運算為基礎的。在橢圓曲線算術中,對質數取模的乘法很簡單,但除法(也就是反運算)實際上是不可能的。這稱為離散對數問題,目前還沒有已知的陷門。橢圓曲線密碼學(elliptic curve cryptography)在現代電腦系統中被廣泛使用,也是以太坊(以及其他加密貨幣)使用私鑰與數位簽章的基礎。

備註

如果你有興趣進一步了解密碼學,以及現代密碼學所使用的數學函數,可以參考以下資源:

在以太坊中,我們使用 PKC 來建立本章一直在談的公鑰與私鑰金鑰對。之所以稱為「一對」,是因為公鑰是從私鑰衍生出來的。兩者一起代表一個以太坊帳戶:公鑰提供公開可存取的帳戶代號(也就是地址),私鑰則讓擁有者私下掌控帳戶中的以太幣,以及帳戶在使用智能合約時所需的任何身分驗證。私鑰之所以能控制存取,是因為它是產生數位簽章唯一需要的那項資訊,而要花用帳戶中的任何資金,都必須用數位簽章簽署交易。我們會在第 7 章看到,數位簽章也用來驗證合約擁有者或使用者的身分。

提示

在大多數錢包實作中,為了方便起見,私鑰與公鑰會以金鑰對的形式一起儲存。不過,公鑰可以輕易地從私鑰算出來,所以只儲存私鑰也是可行的。

數位簽章可以用來簽署任何訊息。對以太坊交易來說,交易本身的細節就是被簽署的訊息。密碼學的數學(在這裡是橢圓曲線密碼學)提供了一種方法,把訊息(也就是交易細節)與私鑰結合起來,產生一個只有知道私鑰才能產生的值。這個值就稱為數位簽章。

請注意,以太坊交易基本上就是一個請求,要求存取某個以太坊地址所對應的帳戶。為了轉移資金或與智能合約互動而把交易送到以太坊網路時,交易必須附上一個數位簽章,而這個簽章必須是用該以太坊地址對應的私鑰產生的。橢圓曲線數學讓任何人都能驗證交易是否有效:只要檢查數位簽章是否與交易細節,以及請求存取的以太坊地址相符即可。驗證過程完全不需要用到私鑰,私鑰始終保持私密。然而,驗證過程能毫無疑問地確定,這筆交易只可能來自持有私鑰的人,而這把私鑰對應的正是該以太坊地址背後的公鑰。這就是 PKC 的「魔法」。

提示

以太坊協議中沒有任何加密——以太坊網路運作時傳送的所有訊息,每個人都能(也必須能夠)讀取。因此,私鑰只用來產生驗證交易所需的數位簽章。

4.3. 私鑰

私鑰就只是一個隨機選出的數字。擁有並控制私鑰,是使用者能控制對應以太坊地址的所有資金、以及存取授權給該地址的合約的根本。花用以太幣時,需要用私鑰產生簽章,以證明自己擁有交易中所使用的資金。私鑰必須隨時保密,因為把私鑰洩漏給第三方,就等於把該私鑰所保護的以太幣與合約的控制權交給對方。私鑰也必須備份,並防止意外遺失。私鑰一旦遺失就無法復原,它所保護的資金也會永遠消失。

提示

以太坊私鑰就只是一個數字。隨機挑選私鑰的方法之一,是準備一枚硬幣、一支鉛筆和一張紙:擲硬幣 256 次,你就得到了一把隨機私鑰的二進位數字,可以用在以太坊錢包中(大概可以——請見接下來的段落)。接著就能從私鑰產生公鑰與地址。

產生金鑰的第一步,也是最重要的一步,是找到一個安全的熵(entropy)來源,也就是隨機性的來源。建立以太坊私鑰,基本上就是在 1 到 2^256 之間挑一個數字。只要這個數字無法預測、不是確定性的,用什麼方法挑選都無所謂。以太坊軟體會使用底層作業系統的隨機數產生器(random number generator,RNG)來產生 256 個隨機位元。作業系統的 RNG 通常會以人為的隨機來源來初始化,所以你可能會被要求晃動滑鼠幾秒鐘,或在鍵盤上隨意按幾個鍵。另一種做法是利用電腦麥克風通道上的宇宙輻射雜訊。

更精確地說,私鑰可以是任何非零的數字,上限是一個略小於 2^256 的極大數字——一個 78 位數的龐大數字,大約是 1.158 × 10^77。這個數字的前 38 位與 2^256 相同,它被定義為以太坊所使用的橢圓曲線的階。要建立私鑰,我們會隨機挑選一個 256 位元的數字,並檢查它是否在有效範圍內。在程式設計上,通常的做法是把一串更長的隨機位元(從密碼學上安全的隨機來源收集而來)輸入 Keccak-256 或 SHA-256 這類 256 位元的雜湊演算法,兩者都會方便地產生一個 256 位元的數字。如果結果落在有效範圍內,我們就得到一把合適的私鑰;否則,只要用另一個隨機數再試一次就行了。

備註

以太坊私鑰空間的大小——2^256——是一個大到難以想像的數字。以十進位表示大約是 10^77,也就是一個 78 位數。相較之下,可觀測宇宙估計含有 10^80 個原子。因此,私鑰的數量幾乎足以讓宇宙中的每一個原子都擁有一個以太坊帳戶。如果你隨機挑選一把私鑰,任何人都不可能猜到它,或自己剛好挑中同一把。

請注意,產生私鑰的過程是離線進行的;它不需要與以太坊網路通訊——事實上,根本不需要與任何人通訊。因此,要挑出一個其他人永遠不會挑到的數字,這個數字必須是真正隨機的。如果你自己決定數字,別人剛好試到它(然後捲走你的以太幣)的機會太高了。使用不好的 RNG(例如大多數程式語言中偽隨機的 rand 函式)更糟,因為它的結果更明顯,也更容易重現。就像線上帳戶的密碼一樣,私鑰必須讓人無法猜到。幸好你永遠不需要記住自己的私鑰,所以可以用最好的方法來挑選它:真正的隨機性。

警告

不要自己寫程式碼來產生隨機數,也不要使用程式語言提供的「簡易」RNG。請注意,瀏覽器錢包中以 JavaScript 實作的 RNG,除非背後有作業系統的熵支援,否則可能並不安全。務必使用密碼學上安全的偽隨機數產生器(例如 CSPRNG),並以來自足夠熵來源的種子初始化。請仔細研讀你所選擇的 RNG 函式庫的文件,確認它在密碼學上是安全的。CSPRNG 函式庫的正確實作,對金鑰的安全至關重要。

以下是一把隨機產生的私鑰,以十六進位格式顯示(256 個位元以 64 個十六進位數字表示,每個數字 4 個位元):

f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315

4.4. 公鑰

以太坊公鑰是橢圓曲線上的一個點,也就是一組滿足橢圓曲線方程式的 x 與 y 座標。

簡單地說,以太坊公鑰就是兩個接在一起的數字。這兩個數字是透過一種只能單向進行的計算,從私鑰產生出來的。這表示如果你有私鑰,要算出公鑰輕而易舉;但你無法從公鑰算出私鑰。

警告

接下來要開始講數學了!別慌。如果你在接下來的段落中開始覺得迷惑,可以跳過接下來幾節。有很多工具與函式庫會替你完成這些數學運算。

公鑰是用橢圓曲線乘法從私鑰計算出來的,這種運算實際上是不可逆的:K = k * G,其中 k 是私鑰,G 是一個稱為生成點的常數點,K 是算出來的公鑰,* 則是特殊的橢圓曲線「乘法」運算子。請注意,橢圓曲線乘法和一般的乘法不一樣。它和一般乘法有一些相同的函數性質,但也僅止於此。例如,它的反運算(對一般數字來說就是除法)稱為「求離散對數」,也就是在知道 K 的情況下計算出 k,其困難程度相當於嘗試 k 所有可能的值(這種暴力搜尋所需的時間,很可能比這個宇宙存在的時間還要長)。

簡單地說,橢圓曲線上的算術與「一般」的整數算術不同。一個點(G)可以乘上一個整數(k),得到另一個點(K)。但這裡沒有除法這回事,所以無法單純地把公鑰 K「除以」點 G 來算出私鑰 k。這就是前面「PKC 與加密貨幣」一節所描述的單向數學函數。

備註

橢圓曲線乘法是密碼學家所謂的「單向」函數:朝一個方向(乘法)計算很容易,朝反方向(除法)計算則是不可能的。私鑰的擁有者可以輕鬆地產生公鑰並公開給全世界,因為他知道沒有人能反轉這個函數、從公鑰算出私鑰。這個數學技巧成了無法偽造的安全數位簽章的基礎,用來證明以太坊資金的所有權與合約的控制權。

在示範如何從私鑰產生公鑰之前,我們先更仔細地看看橢圓曲線密碼學。

4.4.1. 橢圓曲線密碼學解說

橢圓曲線密碼學是一種非對稱密碼學,也就是公開金鑰密碼學,它以橢圓曲線上各點的加法與乘法所表達的離散對數問題為基礎。圖 4-1 是一條橢圓曲線的例子,與以太坊所使用的曲線相似。

備註

以太坊使用的橢圓曲線與比特幣完全相同,稱為 secp256k1。因此,許多比特幣的橢圓曲線函式庫與工具都能重複使用。

一條橢圓曲線
圖 4-1. 一條橢圓曲線

以太坊使用一條特定的橢圓曲線與一組數學常數,定義在一個稱為 secp256k1 的標準中,這個標準由美國國家標準暨技術研究院(National Institute of Standards and Technology,NIST)制定。secp256k1 曲線由下面這個函數定義,它會產生一條橢圓曲線:

y^2 = (x^3 + 7) over (Fp)

或者:

y^2 mod p = (x^3 + 7) mod p

mod p(對質數 p 取模)表示這條曲線位於質數階 p 的有限體上,也寫成 Fp,其中 p = 2^256 – 2^32 – 2^9 – 2^8 – 2^7 – 2^6 – 2^4 – 1,是一個非常大的質數。

因為這條曲線定義在質數階的有限體上,而不是實數上,所以它看起來就像散布在二維平面上的一堆點,很難具體想像。不過,它的數學與實數上的橢圓曲線完全相同。舉例來說,圖 4-2 顯示同一條橢圓曲線在一個小得多、質數階為 17 的有限體上的樣子,呈現為網格上的一組點。以太坊的 secp256k1 橢圓曲線,可以想成是一個大到難以想像的網格上,一組複雜得多的點。

橢圓曲線密碼學:將 F(p) 上的橢圓曲線視覺化,p=17
圖 4-2. 橢圓曲線密碼學:將 F(p) 上的橢圓曲線視覺化,p=17

舉例來說,下面是座標為 (x, y) 的點 Q,它是 secp256k1 曲線上的一個點:

Q = (49790390825249384486033144355916864607616083520101638681403973749255924539515, 59574132161899900045862086493921015780032175291755807399284007721050341297360)

範例 4-1 示範如何用 Python 自己驗證這一點。變數 x 與 y 是點 Q 的座標,和前面的例子相同。變數 p 是橢圓曲線的質數階(也就是所有取模運算所使用的質數)。最後一行 Python 是橢圓曲線方程式(Python 的 % 運算子就是取模運算子)。如果 x 與 y 確實是橢圓曲線上某個點的座標,它們就會滿足方程式,結果為零。你可以自己試試看:在命令列輸入 python(或 python3),然後從列表中複製每一行(提示字元 >>> 之後的內容)。

範例 4-1. 用 Python 確認這個點在橢圓曲線上
Python 3.12.4 (main, Jun  6 2024, 18:26:44) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> p = 115792089237316195423570985008687907853269984665640564039457584007908834671663
>>> x = 49790390825249384486033144355916864607616083520101638681403973749255924539515
>>> y = 59574132161899900045862086493921015780032175291755807399284007721050341297360
>>> (x ** 3 + 7 - y**2) % p
0

4.4.2. 橢圓曲線的算術運算

許多橢圓曲線數學看起來、運作起來都很像我們在學校學過的整數算術。具體來說,我們可以定義一個加法運算子,只是它不是在數線上跳動,而是跳到曲線上的其他點。有了加法運算子之後,我們也可以定義點與整數的乘法,這相當於重複做加法。

橢圓曲線加法的定義是:給定橢圓曲線上的兩個點 P1 與 P2,會有第三個點 P3 = P1 + P2,它同樣在橢圓曲線上。

從幾何上來看,第三個點 P3 的算法是在 P1 與 P2 之間畫一條直線。這條直線會與橢圓曲線恰好再相交於另一個地方(相當神奇)。把這個點稱為 P3′ = (x, y)。接著對 x 軸做鏡射,就得到 P3 = (x, –y),如圖 4-3 所示。

橢圓曲線加法:將橢圓曲線上的兩個點相加
圖 4-3. 橢圓曲線加法:將橢圓曲線上的兩個點相加

如果 P1 與 P2 是同一個點,那麼 P1 與 P2「之間」的直線就應該延伸為曲線在 P1 這一點的切線。這條切線會與曲線恰好相交於一個新的點,如圖 4-4 所示。你可以用微積分的技巧來決定切線的斜率。奇妙的是,即使我們只關心曲線上兩個座標都是整數的點,這些技巧依然有效!

橢圓曲線加法:將一個點與自己相加
圖 4-4. 橢圓曲線加法:將一個點與自己相加

在橢圓曲線數學中,還有一個稱為無窮遠點的點,它的角色大致相當於加法中的數字零。在電腦上,它有時以 x = y = 0 表示(這並不滿足橢圓曲線方程式,但這是一個容易另外檢查的特殊情況)。有幾種特殊情況可以說明為什麼需要無窮遠點。

在某些情況下(例如 P1 與 P2 的 x 值相同但 y 值不同,如圖 4-5 所示),這條直線會是完全垂直的,此時 P3 = 無窮遠點。

橢圓曲線加法:一種結果為無窮遠點的特殊情況
圖 4-5. 橢圓曲線加法:一種結果為無窮遠點的特殊情況

如果 P1 是無窮遠點,那麼 P1 + P2 = P2。同樣地,如果 P2 是無窮遠點,那麼 P1 + P2 = P1。由此可見,無窮遠點扮演的正是零在「一般」算術中的角色。

事實上,+ 滿足結合律,也就是 (A + B) + C = A + (B + C)。這表示我們可以寫成 A + B + C(不加括號),而不會產生歧義。

定義了加法之後,我們就能以延伸加法的標準方式來定義乘法。對橢圓曲線上的一個點 P,如果 k 是一個整數,那麼 k * P = P + P + P + … + P(共 k 次)。請注意,在這種情況下,k 有時(也許令人困惑地)被稱為指數。

4.4.3. 產生公鑰

我們從一把私鑰開始,它是一個隨機產生的數字 k。把它乘上曲線上一個預先決定、稱為生成點 G 的點,就會得到曲線上另一處的另一個點,也就是對應的公鑰 K:

K = k * G

生成點是 secp256k1 標準的一部分;所有 secp256k1 的實作都使用相同的生成點,從這條曲線衍生的所有金鑰也都使用同一個點 G。因為對所有以太坊使用者來說,生成點永遠相同,所以私鑰 k 乘上 G 永遠會得到同一把公鑰 K。k 與 K 之間的關係是固定的,但只能朝一個方向計算,也就是從 k 算到 K。這就是為什麼以太坊地址(從 K 衍生而來)可以分享給任何人,而不會洩漏使用者的私鑰(k)。

如上一節所述,k * G 這個乘法相當於重複做加法,也就是 G + G + G + … + G,重複 k 次。總而言之,要從私鑰 k 產生公鑰 K,就是把生成點 G 與自己相加 k 次。

提示

私鑰可以轉換成公鑰,但公鑰無法轉換回私鑰,因為這種數學只能單向運作。

我們來套用這個計算,找出前面「私鑰」一節中那把私鑰所對應的公鑰:

K = f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315 * G

密碼學函式庫可以幫我們用橢圓曲線乘法算出 K。算出來的公鑰 K 定義為以下這個點:

K = (x, y)

其中:

x = 6e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b
y = 83b5c38e5e2b0c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0

在以太坊中,你可能會看到公鑰以 130 個十六進位字元(65 個位元組)的序列化形式表示。這是採用產業聯盟 Standards for Efficient Cryptography Group(SECG)所提出的標準序列化格式,記載於 Standards for Efficient Cryptography(SEC1)。這個標準定義了四種可以用來識別橢圓曲線上的點的前綴,列於表 4-1。

表 4-1. 序列化橢圓曲線公鑰的前綴
前綴 意義 長度(位元組,含前綴)

0x00

無窮遠點

1

0x04

未壓縮的點

65

0x02

y 為偶數的壓縮點

33

0x03

y 為奇數的壓縮點

33

以太坊只使用未壓縮的公鑰,所以唯一相關的前綴是(十六進位的)04。序列化的方式是把公鑰的 x 與 y 座標串接起來:

04 + x-coordinate (32 bytes / 64 hex) + y-coordinate (32 bytes / 64 hex)

因此,我們前面算出的公鑰序列化之後是:

046e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b83b5c38e5e2b0c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0

4.4.4. 橢圓曲線函式庫

加密貨幣相關的專案會使用以下幾種 secp256k1 橢圓曲線的實作:

OpenSSL 函式庫提供一套完整的密碼學基本元件(cryptographic primitive),包括 secp256k1 的完整實作。例如,要衍生公鑰,可以使用 EC_POINT_mul 函式。

Bitcoin Core 的 libsecp256k1 是以 C 語言實作的 secp256k1 橢圓曲線與其他密碼學基本元件。它是為了取代 Bitcoin Core 軟體中的 OpenSSL 而從頭寫成的,一般認為它在效能與安全性上都更勝一籌。

4.5. 密碼學雜湊函數

以太坊處處都用到密碼學雜湊函數。事實上,幾乎所有的密碼學系統都大量使用雜湊函數——密碼學家 Bruce Schneier 就說過:「單向雜湊函數比加密演算法更像是現代密碼學的主力。」

本節將討論雜湊函數,探討它們的基本性質,並說明這些性質為什麼讓雜湊函數在現代密碼學的許多領域都如此有用。我們在這裡介紹雜湊函數,是因為以太坊把公鑰轉換成地址的過程中會用到它。雜湊函數也可以用來產生數位指紋,協助驗證資料。

簡單地說,雜湊函數就是任何能把任意大小的資料映射成固定大小資料的函數。雜湊函數的輸入稱為 原像(preimage)、訊息,或者就叫輸入資料。輸出則稱為雜湊值。密碼學雜湊函數是其中一個特殊的子類別,具備一些對以太坊這類安全平台很有用的特定性質。

密碼學雜湊函數是一種單向雜湊函數(one-way hash function),它把任意大小的資料映射成固定長度的位元字串。「單向」的意思是,如果只知道輸出的雜湊值,在計算上不可能重建出輸入資料。要找出可能的輸入,唯一的方法是暴力搜尋,逐一檢查每個候選輸入的輸出是否相符;由於搜尋空間幾乎是無限的,不難理解這件事實際上是不可能做到的。就算你找到某筆輸入資料能產生相符的雜湊值,它也不一定是原本的輸入資料:雜湊函數是「多對一」的函數。找到兩筆雜湊後得到相同輸出的輸入資料,稱為找到 雜湊碰撞(hash collision)。粗略地說,雜湊函數越好,雜湊碰撞就越罕見。對以太坊來說,雜湊碰撞實際上是不可能發生的。

我們來仔細看看密碼學雜湊函數的主要性質:

確定性

給定的輸入訊息永遠產生相同的雜湊輸出。

可驗證性

計算訊息的雜湊值很有效率(線性複雜度)。

不相關性

訊息的一點點改變(例如改變 1 個位元),就應該讓雜湊輸出大幅改變,以致無法與原始訊息的雜湊值產生關聯。

不可逆性

從雜湊值算出訊息是不可行的,相當於對所有可能的訊息做暴力搜尋。

抗碰撞性

要算出兩則產生相同雜湊輸出的不同訊息,應該是不可行的。

抵抗雜湊碰撞的能力,對於避免以太坊中的數位簽章偽造特別重要。

這些性質結合起來,使密碼學雜湊函數可以用在範圍廣泛的安全應用上,包括:

  • 資料指紋

  • 訊息完整性(錯誤偵測)

  • 工作量證明(proof of work,PoW)

  • 身分驗證(密碼雜湊與金鑰延展(key stretching))

  • 偽隨機數產生器

  • 訊息承諾,也就是承諾—揭示(commit-reveal)機制

  • 唯一識別碼

隨著我們逐一探討系統的各個層次,會在以太坊中看到其中許多應用。

4.5.1. 以太坊的密碼學雜湊函數:Keccak-256

以太坊在許多地方使用 Keccak-256 密碼學雜湊函數。Keccak-256 是為了參加 NIST 在 2007 年舉辦的 SHA-3 密碼學雜湊函數競賽而設計的候選演算法。Keccak 最後勝出,並在 2015 年被標準化為聯邦資訊處理標準(Federal Information Processing Standard,FIPS)202。

然而,在以太坊開發期間,NIST 的標準化還沒有定案。NIST 在標準程序完成後調整了 Keccak 的部分參數,據稱是為了提高效率。就在同一時期,英勇的吹哨者 Edward Snowden 揭露的文件暗示,NIST 可能受到美國國家安全局(National Security Agency)的不當影響,故意削弱 Dual_EC_DRBG 這個 RNG 標準,等於在標準的 RNG 中放了一個後門。這場爭議引發了對這些修改提案的強烈反彈,也讓 SHA-3 的標準化大幅延宕。當時,以太坊基金會決定實作由發明者提出的原始 Keccak 演算法,而不是經 NIST 修改後的 SHA-3 標準。

警告

雖然你可能會在以太坊的文件與程式碼中到處看到「SHA-3」,但其中許多(甚至全部)實際上指的是 Keccak-256,而不是定案後的 FIPS-202 SHA-3 標準。兩者在實作上的差異很小,只與填充參數有關,但影響很大:對於相同的輸入,Keccak-256 產生的雜湊輸出與 FIPS-202 SHA-3 不同。

4.5.2. 我用的是哪一種雜湊函數?

如果兩者都可能被稱為「SHA-3」,你要怎麼判斷自己所使用的軟體函式庫實作的是 FIPS-202 SHA-3 還是 Keccak-256?

一個簡單的判斷方法是使用 測試向量(test vector),也就是給定輸入時預期的輸出。雜湊函數最常用的測試是空輸入。如果你以空字串作為輸入來執行雜湊函數,應該會看到以下結果:

Keccak256("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
SHA3("") = a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a

無論函數叫什麼名字,只要執行這個簡單的測試,就能知道它是原始的 Keccak-256,還是 NIST 最終的標準 FIPS-202 SHA-3。請記住,以太坊使用的是 Keccak-256,即使程式碼中常把它稱為 SHA-3。

備註

由於以太坊使用的雜湊函數(Keccak-256)與定案的標準(FIP-202 SHA-3)之間的差異造成了混淆,所有程式碼、操作碼與函式庫中的 sha3 都已改名為 keccak256。詳情請參閱 ERC-59。

接下來,我們來看看 Keccak-256 在以太坊中的第一個應用:從公鑰產生以太坊地址。

4.6. 以太坊地址

以太坊地址是唯一識別碼,是用 Keccak-256 單向雜湊函數從公鑰或合約衍生出來的。

在前面的例子中,我們從一把私鑰開始,用橢圓曲線乘法衍生出公鑰。

私鑰 k:

k = f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315

公鑰 K(x 與 y 座標串接起來,以十六進位顯示):

K = 6e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b83b5c38e5e...
備註

值得注意的是,計算地址時,公鑰並不會加上(十六進位的)04 前綴。

我們用 Keccak-256 計算這把公鑰的雜湊值:

Keccak256(K) = 2a5bc342ed616b5ba5732269001d3f1ef827552ae1114027bd3ecf1f086ba0f9

接著只保留最後 20 個位元組(最低有效位元組),這就是我們的以太坊地址:

001d3f1ef827552ae1114027bd3ecf1f086ba0f9

你最常看到的以太坊地址會帶有 0x 前綴,表示它們是以十六進位編碼的,像這樣:

0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9

4.6.1. 以太坊地址的格式

以太坊地址是十六進位的數字,是從公鑰的 Keccak-256 雜湊值的最後 20 個位元組衍生而來的識別碼。

比特幣地址在所有客戶端的使用者介面中,都會編碼成內建校驗和(checksum)的形式,以防止地址輸入錯誤;以太坊地址則不同,它以不含任何校驗和的原始十六進位形式呈現。這項決定背後的理由是:以太坊地址最終會隱藏在系統較高層次的抽象(例如名稱服務)之後,如果需要校驗和,應該在較高的層次加上。

實際上,這些較高的層次開發得太慢,這個設計選擇在生態系早期造成了許多問題,包括因為地址輸入錯誤與輸入驗證錯誤而損失資金。此外,由於以太坊名稱服務的開發比原先預期的慢,錢包開發者採用其他編碼方式的速度也非常緩慢。接下來我們會介紹幾種編碼選項。

備註

值得一提的是 Alex Van de Sande 與 Nick Johnson 在 2017 年推出的以太坊域名服務(Ethereum Name Service,ENS)。ENS 提供一種鏈上的解決方案,能把 masteringethereum.eth 這類人類可讀的名稱轉換成以太坊地址。

4.6.2. 以大小寫表示校驗和的十六進位編碼(ERC-55)

由於名稱服務的部署進度緩慢,ERC-55 提出了一個標準。ERC-55 藉由修改十六進位地址的大小寫,為以太坊地址提供一種向後相容的校驗和。它的構想是:以太坊地址不區分大小寫,所有錢包都應該接受以大寫或小寫字元表示的以太坊地址,而且解讀上沒有任何差別。藉由修改地址中英文字母的大小寫,我們就能傳達一個校驗和,用來保護地址的完整性,避免輸入或讀取錯誤。不支援 ERC-55 校驗和的錢包會直接忽略地址中大小寫混合的情況,支援的錢包則能驗證它,並以 99.986% 的準確度偵測出錯誤。

這種大小寫混合的編碼並不顯眼,你一開始可能不會注意到。我們的範例地址是:

0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9

加上 ERC-55 的大小寫混合校驗和之後,它會變成:

0x001d3F1ef827552Ae1114027BD3ECF1f086bA0F9

你看得出差別嗎?十六進位編碼字母表中的英文字母(A–F),現在有些是大寫,有些是小寫。

ERC-55 實作起來相當簡單。我們先計算小寫十六進位地址的 Keccak-256 雜湊值。這個雜湊值就像地址的數位指紋,可以當作方便的校驗和。輸入(地址)的任何微小變化,都應該使得到的雜湊值(checksum)大幅改變,讓我們能有效地偵測錯誤。接著,地址的雜湊值會被編碼進地址本身的大小寫中。我們一步一步來拆解:

  1. 對不含 0x 前綴的小寫地址計算雜湊值:

    Keccak256("001d3f1ef827552ae1114027bd3ecf1f086ba0f9") = 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9695d9a19d8f673ca991deae1
  2. 如果雜湊值中對應位置的十六進位數字大於或等於 0x8,就把地址中對應的英文字母改成大寫。把地址與雜湊值對齊排列,會比較容易說明:

    Address: 001d3f1ef827552ae1114027bd3ecf1f086ba0f9
    Hash   : 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9...

我們的地址在第四個位置有一個英文字母 d。雜湊值的第四個字元是 6,小於 8,所以 d 維持小寫。地址中的下一個英文字母是第六個位置的 f。十六進位雜湊值的第六個字元是 c,大於 8,因此我們把地址中的 F 改成大寫,依此類推。如你所見,我們只使用雜湊值的前 20 個位元組(40 個十六進位字元)作為校驗和,因為地址只有 20 個位元組(40 個十六進位字元)需要調整大小寫。

請自己檢查得到的大小寫混合地址,看看你能不能看出哪些字元被改成大寫,以及它們對應到地址雜湊值中的哪些字元:

Address: 001d3F1ef827552Ae1114027BD3ECF1f086bA0F9
Hash   : 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9...

4.6.3. 偵測 ERC-55 編碼地址中的錯誤

現在來看看 ERC-55 地址如何幫助我們找出錯誤。假設我們印出了一個以 ERC-55 編碼的以太坊地址:

0x001d3F1ef827552Ae1114027BD3ECF1f086bA0F9

現在,我們在讀這個地址時犯一個基本的錯誤。倒數第二個字元是大寫的 F。在這個例子中,假設我們把它誤讀成大寫的 E,並在錢包中輸入下面這個(錯誤的)地址:

0x001d3F1ef827552Ae1114027BD3ECF1f086bA0E9

幸好,我們的錢包符合 EIP-55!它注意到大小寫混合,於是嘗試驗證這個地址。它把地址轉成小寫,並計算校驗和雜湊值:

Keccak256("001d3f1ef827552ae1114027bd3ecf1f086ba0e9") = 5429b5d9460122fb4b11af9cb88b7bb76d8928862e0a57d46dd18dd8e08a6927

如你所見,即使地址只改變了一個字元(事實上只改變了一個位元,因為 e 與 f 只差一個位元),地址的雜湊值卻發生了劇烈的變化。這正是讓雜湊函數非常適合用來做校驗和的性質!

現在,我們把兩者對齊,檢查大小寫:

001d3F1ef827552Ae1114027BD3ECF1f086bA0E9
5429b5d9460122fb4b11af9cb88b7bb76d892886...

全都不對!有好幾個英文字母的大小寫是錯的。請記住,大小寫就是正確校驗和的編碼。

我們輸入的地址的大小寫與剛剛算出的校驗和不符,表示地址中有某些東西被改變了,出現了錯誤。

4.7. 驗證者的密碼學

本節將探討驗證者在新的、以權益證明(PoS)為基礎的共識協議中所使用的密碼學。雖然核心概念始終是能夠以數位方式簽署訊息並驗證它們,但有一些有趣的需求,使得這裡的設計選擇與最終實作,和以太坊在交易或地址上使用的密碼學有所不同。

4.7.1. 簡介

以太坊還在使用 Ethash(PoW 共識演算法)的時候,區塊提議者(當時是礦工)沒有理由要簽署自己產生的區塊。事實上,以 PoW 為基礎的共識演算法不需要知道是誰在建立區塊,也能正確運作。如果區塊提議者行為不當,協議會讓他們白白浪費電力與時間,也就是浪費金錢,藉此隱含地懲罰他們。 我們以一個試圖藉由同時建立兩個區塊來進行雙重支付(double spend)的礦工為例。為了更了解這種「漏洞利用」背後的核心概念,想像有個礦工想買一輛車,並與車商約定用 ETH 付款,假設是 10 ETH。接著,這個礦工同時建立兩個區塊。假設兩個區塊都有效:在第一個區塊中,礦工加入了付款交易,把 10 ETH 付給車商;在另一個區塊中則沒有。礦工試圖把鏈分裂成兩條。

依照(幾乎是)隨機的因素,車商可能先收到礦工付了 10 ETH 的那個區塊。所以車商可以假設付款順利完成。但車商還不會把車交給礦工。事實上,車商知道在 PoW 系統中,通常要等上好幾個區塊,才能把一筆交易視為確定完成結算。

大約 12 秒後,另一個礦工在不含那筆 10 ETH 付款的區塊上產生了一個新區塊。這完全有可能發生,因為通常取決於下一個礦工先收到哪一個區塊。現在有一條較長的區塊鏈,而根據 最重鏈規則(heaviest chain rule)(也被稱為 最長鏈規則(longest chain rule),但這個說法不太正確),所有節點都會認為這條鏈是唯一有效的鏈。

車商看到有效的區塊鏈並不包含先前收到的那個含有 10 ETH 付款的區塊,因此認定付款沒有完成,也就不會把車交給礦工。

請記住,在 PoW 系統中,礦工必須花費實際的能源與時間,才能產生一個通過 PoW 檢查的有效區塊,也就是區塊雜湊值低於一個動態的門檻;所以,花了能源與時間建立區塊,卻看到其中一個區塊被整個網路拒絕,這件事本身就已經是一種隱含的懲罰。他們損失了寶貴的能源與時間,而這些資源原本可以更好地用在只建立有效區塊、遵守所有規則、不試圖作弊上。

PoS 系統對行為不當的區塊產生者(也就是驗證者)沒有同樣的隱含懲罰。取而代之的是一種明確的方法:罰沒(slashing)。

罰沒是指扣除不遵守規則的驗證者部分(或全部)質押的 ETH,藉此懲罰他們。這也說明了為什麼驗證者必須質押最低數量的 ETH,才能成為活躍的驗證者。如果他們沒有質押任何東西,一旦開始做出惡意行為,協議就無法懲罰他們。

要明確地懲罰不遵守規則的驗證者,就必須確切知道每個驗證者在做什麼。為此,驗證者送出的每一則訊息(包括區塊)都必須以他們的數位簽章驗證身分。

我們把前面礦工的例子用類似的方式重演一次。現在有一個驗證者同時提議兩個區塊。在 PoW 中,礦工要同時產生兩個有效區塊,必須花費兩倍的能源與時間;但在 PoS 中,這件事(幾乎)完全不用成本。

這時就輪到罰沒登場了。雙重簽署(Double-signing)(同時提議兩個區塊)是會被罰沒的行為,所以這個驗證者會立刻受到懲罰:在這個例子中,他們質押的 ETH 會全部被銷毀。

既然知道了驗證者為什麼必須驗證自己所有訊息的身分,我們就可以深入探討為此使用的密碼學:BLS 簽章。下一節將說明有哪些需求,促成了最後決定使用 BLS 密碼學。

4.7.2. 需求

對於這個問題(也就是必須驗證驗證者彼此傳送的每一則訊息),你可能想到的第一個解決方案,是採用以太坊已經用來為每筆交易做數位簽署的同一套數位簽章演算法:橢圓曲線數位簽章演算法(Elliptic Curve Digital Signature Algorithm,ECDSA)。

但以太坊的驗證者數量很多,超過一百萬個,這個方法行不通。事實上,這個數位簽章演算法必須符合一項基本需求:必須能把簽章濃縮起來,以減少它們在區塊中所需的空間,並縮短節點驗證所有驗證者簽章所需的時間。

目前,每個驗證者每個 epoch(也就是 32 個 slot)可以投一次票,而投票是一則需要驗證身分的訊息(我們會在第 15 章進一步說明)。這些訊息需要經由點對點(P2P)的 gossip 層傳送給其他驗證者與節點,由其他每個節點驗證簽章是否有效,再被放進區塊中。

這裡的瓶頸顯而易見:加入網路的驗證者越多,節點需要處理的訊息就越多。連區塊都會變大,因為區塊必須為這些驗證者的簽章保留越來越多的空間。

備註

你可以根據活躍驗證者的數量,輕鬆算出節點需要處理的訊息數量:

N = number of active validators
1 msg / 1 epoch × N =
= 1 msg / 32 slots × N = ← one epoch is made of 32 slots
= 1 msg / 32 × 12 seconds × N = ← every slot takes 12 seconds
= 1 msg / 384 seconds × N =
= 0.0026041667 msg / second × N

所以每秒大約有 0.0026 則訊息,再乘上網路中活躍驗證者的數量 N:

N = 1,000 → ~2.6 messages per second
N = 10,000 → ~26 messages per second
N = 100,000 → 260 messages per second
N = 1,000,000 → 2,600 messages per second

目前,以太坊的 PoS 協議每秒處理大約 2,600 則訊息。

基於這些原因,大多數 PoS 區塊鏈的驗證者集合都非常小,最多只有數十或數百個。就連以太坊最初的提案(請參閱 EIP-1011),目標也是最多九百個驗證者,而且要被選入活躍驗證者集合,最低存入金額是 1,500 ETH。

如果不是以太坊基金會研究員 Justin Drake 在 2018 年 5 月於 ethresearch 網站上發表的一篇長文中提出 BLS 簽章聚合的構想,這原本會是以太坊 PoS 協議的最終規格。

4.7.3. BLS 數位簽章

BLS 簽章是以作者的名字命名的。BLS 代表 Boneh–Lynn–Shacham,指的是 Dan Boneh、Ben Lynn 與 Hovav Shacham 這三位密碼學家,他們在 2001 年的論文〈Short Signatures from the Weil Pairing〉中提出了這個簽章方案。

和 ECDSA(第 6 章會進一步說明)一樣,BLS 簽章同樣以橢圓曲線密碼學為基礎。具體來說,以太坊使用的是 BLS12-381 曲線,由 Sean Bowe 在 2017 年為 Zcash 協議工作時設計。它由下面這個函數定義:

y^2 = x^3 + 4

定義在對 q 取模的整數上,其中 q 是一個 115 位數(十進位)的數字:0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab。

它是怎麼運作的?

核心概念與 ECDSA 的運作方式非常相似:有一把私鑰,從它衍生出公鑰。接著,每一則訊息都用私鑰簽署,其他所有人都能用對應的公鑰驗證訊息的完整性。

私鑰(sk)是一個介於 1 與 r – 1 之間的整數,其中 r 是這個巨大的數字:0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001。它以十進位表示有 77 位數!

公鑰(pk)是由 sk * g1 得到的,這是橢圓曲線點的純量乘法,其中 g1 是一個稱為 G1 的群的生成元。pk 以壓縮並序列化的格式表示,結果是一個 48 位元組的字串。

被簽署的訊息(m)一律會被映射到一個橢圓曲線點上,這個點屬於另一個稱為 G2 的群。你可以把這個映射想成一種雜湊函數:它接收訊息 m,也就是驗證者實際的見證投票(attestation),然後輸出 G2 中的一個點 H(m),以壓縮序列化的形式表示為一個 96 位元組的字串。

最後,我們以 sk * H(m) 得到訊息 m 的簽章(σ),它是 G2 中一個新的橢圓曲線點。

ECDSA 與 BLS 的關鍵差異在於兩種協議的底層細節,也就是它們用來驗證簽章是否正確的數學技巧。事實上,ECDSA 涉及的是線性的數學計算,例如橢圓曲線上的純量乘法與加法;BLS 簽章則仰賴更複雜的橢圓曲線雙線性配對(bilinear pairing)算術。

事實上,簽章 σ 有效,若且唯若下面這個等式成立:

e(g1,σ) = e(pk, H(m))

其中 e 是一個橢圓曲線雙線性配對。

深入橢圓曲線的性質

要完全理解前面的等式,需要對橢圓曲線、pairing 以及那一整個無底洞有深入的認識。不過,有個簡單的理解方式,就是跟著下面的步驟走。你不會因此掌握 pairing 為什麼具有這個性質、又是如何具有這個性質的所有細節,但它仍然能幫助你熟悉這個性質:

e(pk, H(m)) = e(sk * g1,H(m)) = ← pk = sk * g1
= e(g1, H(m))^sk = ← thanks to a pairing property
= e(g1, sk * H(m)) = ← thanks to the same property
= e(g1,σ) ← σ = sk * H(m)

正如 Vitalik Buterin 在一篇 Medium 文章中指出的:

如果你把橢圓曲線點看成單向加密過的數字——encrypt(p) = p * G = P,其中 G 是生成點——那麼傳統的橢圓曲線數學讓你能檢查這些數字的線性約束(例如,如果 P = G * p、Q = G * q 且 R = G * r,檢查 5 * P + 7 * Q = 11 * R 其實就是在檢查 5 * p + 7 * q = 11 * r),而 pairing 則讓你能檢查二次約束(例如,檢查 e(P, Q) * e(G, G * 5) = 1 其實就是在檢查 p * q + 1 * 5 = 0)。

可以想見,更複雜的算術也意味著產生與驗證簽章需要更多時間。以太坊之所以讓驗證者使用 BLS 簽章,唯一的原因就是它極為重要的簽章聚合性質。

事實上,如果你把兩個 ECDSA 簽章加在一起,並不會得到有意義的結果。如果你試圖用這個結果來證明原本被簽署訊息的完整性,任何檢驗都不會通過。

相反地,如果你把兩個 BLS 簽章加在一起,你只是在做一次橢圓曲線加法。結果是一個新的橢圓曲線點(位於 G2),你可以拿它與兩把對應公鑰的總和(仍然是 G1 中的一個橢圓曲線點)比對,正確地驗證原本被簽署訊息的完整性。

備註

聚合簽章與聚合公鑰,和單一簽章與單一公鑰無從區別,所以你可以用完全相同的演算法來驗證聚合簽章是否正確。

聚合簽章與公鑰:

σagg = σ1 + σ2 + σ3 + … + σn
pkagg = pk1 + pk2 + pk3 + … + pkn

聚合簽章的驗證:

e(pkagg,H(m)) =
= e(pk1 + pk2 + pk3 + ... + pkn,H(M)) =
= e((sk1 + sk2 + sk3 + ... + skn) * g1,H(m)) =
= e(g1,H(m))^(sk1 + sk2 + sk3 + ... + skn) =
= e(g1,(sk1 + sk2 + sk3 + ... + skn) * H(m)) =
= e(g1,σ1 + σ2 + σ3 + ... + σn) =
= e(g1,σagg)

小結

密碼學真的很難,需要深厚的數學背景。而本書並不是寫給密碼學家看的,所以把前面幾節簡短談到的內容整理一下是很重要的。我們說明了 BLS 簽章如何運作,以及為什麼它被選為驗證者在新的、以 PoS 為基礎的共識協議中所使用的數位簽章演算法:它的聚合性質讓你能把多個數位簽章濃縮成一個,減少儲存所需的空間與驗證所需的時間,而且完全不損失安全性。

現在我們可以舉個簡單的例子,示範驗證者在「實際生活」中如何使用 BLS 演算法,以及簽章聚合如何發揮作用。圖 4-6 描繪了一個情境:有三個驗證者想對某個區塊(區塊 A)表達他們的投票。於是他們投下票、簽署選票,並彼此分享。

BLS 簽章聚合:三個驗證者投票並簽署
圖 4-6. BLS 簽章聚合:三個驗證者投票並簽署

沒有 BLS 的話,你需要把三張已簽署的選票全部存進區塊,才能永久保存它們。有了 BLS,你可以把三張已簽署的選票聚合成一張新的聚合選票,只把它存進區塊。

這不僅節省空間,也大幅減少所有以太坊節點驗證全部已簽署選票時所需的時間與計算量,因為節點可以直接驗證聚合後的選票,而不必逐一驗證每一張已簽署的選票。BLS 密碼學的神奇之處在於,聚合後的結果和一般的簽章沒有兩樣:也就是說,驗證聚合簽章的有效性,並不比驗證單一簽章的有效性困難。因此,必須驗證的已簽署選票數量大幅減少,而驗證每個簽章所需的計算量不變,完整節點必須執行的總計算量(也就是總時間)就遠低於沒有 BLS 聚合簽章時的情況,如圖 4-7 所示。

BLS 簽章聚合縮短了驗證時間
圖 4-7. BLS 簽章聚合縮短了驗證時間

如果有驗證者不遵守規則呢?如果驗證者做出惡意行為,例如 double-signing,也就是同時投票給兩個不同的區塊,協議就能偵測到這種行為,並對驗證者施以相應的懲罰,如圖 4-8 所示。

BLS 簽章讓惡意的驗證者能被偵測出來
圖 4-8. BLS 簽章讓惡意的驗證者能被偵測出來

事實上,由於所有選票都是用 BLS 簽章方案做數位簽署的,要找出應為不當行為負責的驗證者並對他們施以罰沒,是輕而易舉的事。

4.8. KZG 密碼學

2024 年 3 月 13 日,以太坊升級到 Cancun-Deneb(Dencun)硬分叉。這次升級包含許多變更,但最重要的無疑是引進了一種新的交易類型:EIP-4844 blob 交易。

我們不會在這裡深入探討 blob 交易為什麼對以太坊的路線圖很重要,第 16 章會更詳細地說明。接下來幾節要探討的,是這種新交易類型底層的密碼學:多項式承諾方案,更具體地說,是 KZG 承諾方案。

4.8.1. 簡介

在探討多項式承諾方案之前,先了解我們面對的問題以及想達成的最終目標是很重要的。

備註

本節會簡短提到第二層(layer 2,L2)這個詞。雖然第 16 章才會好好說明並深入介紹 L2,但至少先有個簡短的認識會很有幫助。L2 是以太坊一種新的擴容方案:它們是真正的區塊鏈,擁有自己獨特的歷史與狀態,並定期把所有更新與資料發布到以太坊主網上。其背後的原理是:必須能夠只看以太坊主網,就推導出整條 L2 區塊鏈。

不談太多細節的話,L2 每天都會把大量資料發布到以太坊主鏈上。網路中的所有節點都必須下載並驗證這些資料。這種情況通常會導致 gas 費用飆升,在嚴重的情況下,光是一筆 ETH 轉帳就可能要 10 到 20 美元。

我們最終希望不要把這些資料全部直接發布到鏈上,而是只儲存對它的承諾(也就是一個密碼學雜湊值),把完整的資料留在以太坊鏈外。但只發布雜湊值會產生一個新問題:網路中的所有節點都必須確保承諾所指向的資料確實存在於某處;否則,我們可能已經遺失了關鍵的資料。

第一個(顯而易見的)解決方案,是要求所有節點暫時下載完整的資料,這樣它們就能輕鬆驗證承諾是有效的,也就是承諾確實指向它們手上的完整資料。這就是撰寫本章時(2025 年 6 月)的現況。

長期的解決方案,是不要求所有節點下載完整的資料,只需下載其中非常小的一部分。接著,節點可以使用密碼學證明來確保資料完整可用。這些證明必須很小,而且要能快速驗證。其構想是:證明(以及驗證)資料可用,比下載資料再直接驗證要快得多、輕量得多。這種策略稱為資料可用性抽樣(data availability sampling,DAS),是以太坊社群最重要的研究領域之一。

為了達成 DAS 的最終目標,我們需要一套密碼學系統,讓我們(所有節點)能夠:

  • 對某些資料建立承諾(也就是你想證明確實存在、而且完整可用的資料)

  • 針對與先前建立的承諾相關的資料,產生很小的證明

  • 輕鬆驗證這些證明

這套密碼學系統最好(但不是必要)不要洩漏太多它所指涉資料的資訊。

下一節將探討這樣的一套密碼學系統:多項式承諾方案,更具體地說,是 KZG 承諾方案。

備註

你可能會好奇,為什麼我們談的是多項式承諾方案,而不是資料承諾方案。這是個非常好的問題:我們的目標是能對某些資料做出承諾,而不是對多項式。但訣竅就在這裡:資料可以表示成一個多項式。所以只要我們能做出一個多項式承諾方案,問題就解決了。

4.8.2. 多項式承諾方案

多項式承諾方案讓證明方能計算出對某個多項式的承諾,而這個承諾具有一個性質:之後可以在任何位置「打開」它,也就是求值。證明方可以證明多項式在某個位置的值等於所宣稱的值,並為這項宣稱產生證明。驗證方同時持有承諾與證明,而只有在證明方沒有作弊的情況下(也就是被承諾的多項式在所選位置的值確實等於宣稱的值),驗證方才能驗證證明是有效的。

多項式簡介

本節會大量談到多項式,所以得先確定大家都知道什麼是多項式,畢竟你可能已經很久沒在學校碰過它們了。

多項式是像這樣的數學式:

x^3 + 3x^2 – 9

其中 x 是變數,所有的數字(1、3、0、–9)是係數。請注意,我們把第三個係數寫成 0,是因為這個多項式其實是下面這個:

x^3 + 3x^2 + 0x – 9

在本節中,我們會把多項式寫成 p(x):

p(x) = x^3 + 3x^2 + 0x – 9

我們也會用到多項式的次數 n,也就是多項式中最高次方的指數。我們的範例多項式的次數是 3,所以 n = 3。

描述多項式的一般公式如下:

p(x) = p₀ + p₁x + p₂x² + ... + pₙxⁿ

或者更正式地寫成:

p(x) = Σ pᵢxⁱ  (for i = 0 to n)

其中 n 是多項式的次數,pi 是它所有的係數。

4.8.3. 使用默克爾樹

我們先來看一個使用經典的默克爾樹資料結構的多項式承諾方案。如果你有興趣深入了解默克爾樹,我們會在第 14 章詳細說明。

只要把樹的所有葉節點,設為我們想承諾的多項式的所有係數 pi,就能對一個次數為 n = 2^d – 1 的多項式做出承諾,其中 d 是默克爾樹的深度。

請看下面這個例子:

p(x) = x^7 + 5x^5 – 2x^4 + 3
n = 7 ← degree of p(x)
d = 3 ← depth of the required Merkle tree

圖 4-9 顯示把多項式承諾方案套用在這個例子上的情形,以默克爾根作為核心的 cryptographic primitive。

使用默克爾樹的多項式承諾
圖 4-9. 使用默克爾樹的多項式承諾

最終的承諾,就是建構出完整的默克爾樹後得到的默克爾根。 現在,假設我們想(向驗證方)證明 p(1) = 7。這個陳述是正確的,因為如果你取 p(x) 並令 x = 1,很容易就能算出多項式在這個位置的值是 7。

為了證明我們所宣稱的 p(1) = 7,我們需要傳送給驗證方:

  • 我們想證明的陳述:p(1) = 7

  • 默克爾根,也就是對多項式 p(x) 的承諾

  • 多項式的所有係數 pi

這樣一來,驗證方就能取得所有係數 pi,計算多項式在 x = 1 時的值。這一步讓驗證方驗證我們的宣稱確實為真。接著,驗證方用所有係數 pi 計算出默克爾樹,並把算出的默克爾根與我們提供的默克爾根比對。這一步讓驗證方驗證我們一開始承諾的多項式,確實就是我們傳給他們的那一個。

總結一下使用默克爾樹的多項式承諾所具有的性質:

承諾大小

承諾就是默克爾根,也就是單一個雜湊值,長度通常是 32 個位元組。

證明大小

要證明一個求值結果,我們必須傳送多項式的所有係數 pi。這表示證明的大小與多項式的次數 n 成線性關係。

驗證方的複雜度

驗證方必須做(與多項式次數成)線性的工作,才能完整驗證我們的宣稱。事實上,驗證方手上有所有係數 pi,必須計算默克爾樹,也必須計算多項式在所宣稱的點上的值。

方案的隱私性

這個方案完全沒有隱藏多項式的任何資訊,因為證明方傳送了它所有的係數 pi。

這些性質並不理想,因為在實際的以太坊協議中,多項式的次數 n 可能非常大,在網路上傳送所有係數所需的頻寬太高。此外,我們希望有一種協議,讓證明方盡可能少洩漏被承諾多項式的資訊。

幸好,有一個方案能滿足我們所有的需求,這就是 KZG 登場的時候了。

4.8.4. KZG 承諾

KZG 是三位作者 Kate、Zaverucha 與 Goldberg 姓氏的縮寫。這幾位密碼學家在 2010 年的論文〈Constant-Size Commitments to Polynomials and Their Applications〉中提出了這個承諾方案。

可信設定

KZG 承諾方案需要一個可信設定。你可以把它看成一個由密碼學協議所有參與者(也就是證明方與驗證方)共享的共同知識庫。之所以稱為可信設定,是因為要產生這個共同知識庫,必須有一些參與者產生隨機數(祕密)、將它們加密,再建立最終的資料。接著,他們必須刪除這些祕密,以確保協議保持安全。由於必須信任這些參與者會刪除自己的祕密,這整個儀式就稱為可信設定。

現代的設定通常採用 Powers-of-Tau 設定,它採用 1-of-N 的信任模型。這表示只要有一個誠實的參與者,整個可信設定就能被視為安全的。

以太坊的 KZG 可信設定儀式有超過 140,000 名不同的參與者,如圖 4-10 所示。

以太坊 KZG 可信設定儀式的參與者
圖 4-10. 以太坊 KZG 可信設定儀式的參與者

更具體地說,可信設定會針對 i = 0, 1, … , n – 1 產生元素 [s^i]₁ 與 [s^i]₂,其中:

  • s 是沒有人知道的可信設定祕密(由每位參與者產生的祕密加總而成)

  • [s^i]₁ 與 [s^i]₂ 其實是橢圓曲線點(分別位於曲線 G1 與 G2 上)

  • n 是多項式的階

提示

當你看到方括號裡的東西時,它代表一個橢圓曲線點。

承諾

如你所見,KZG 承諾方案(再次)用到了橢圓曲線與 pairing,這些我們在 BLS 數位簽章中已經看過了。

請記得,在橢圓曲線上,如果你有一個祕密數字 a,就能用橢圓曲線乘法得到一個橢圓曲線點 [a]:

[a]₁ = aG₁

而且在計算上不可能反推回去。因此,如果你只有 [a],就無法得到祕密 a。

雖然證明方與驗證方都不知道可信設定的祕密 s,他們仍然可以對它進行某些運算:

c[s^i]₁ = cs^iG₁ = [cs^i]₁  ← elliptic curve multiplication; c is an integer number
c[s^i]₁ + d[s^i]₁ = (cs^i + ds^i)G₁ = [cs^i + ds^i]₁  ← elliptic curve points addition

所以,如果 p(x) = p₀ + p₁x + p₂x² + ... + pₙxⁿ 是一個多項式,證明方可以計算:

[p(s)]₁ = [p₀ + p₁s₁ + p₂s²₁ + ... + pₙsⁿ₁] = p₀ + p₁[s]₁ + p₂[s²]₁ + ... + pₙ[sⁿ]₁

由於 [s^i]₁ 是可信設定的一部分,因此是大家都知道的資訊,證明方就能在一個沒有人知道的祕密點 s 上計算多項式的值。聽起來很神奇,對吧?不過要注意一件重要的事:前面這個運算的輸出不是整數,而是一個橢圓曲線點。這個橢圓曲線點 [p(s)]₁,就是我們對多項式 p(x) 的 KZG 承諾。

KZG 的可靠性(soundness):為什麼證明方無法偽造承諾

在不知道祕密 s 的情況下,證明方能不能找到另一個多項式 q(x) != p(x),使兩者擁有相同的 KZG 承諾 [q(s)]₁ = [p(s)]₁?如果可以,證明方就能讓驗證方以為他們知道某個多項式 p,即使事實並非如此。

假設可以,那麼就存在一個次數為 n、不是常數的多項式 r(x) = p(x) – q(x)。因為 q(x) != p(x),這表示 r(x) 最多有 n 個零點,也就是 r(x) = 0 最多在 n 個位置成立。這是代數的一個基本性質。你可以輕易驗證,它對你在學校學過的所有基本幾何圖形都成立:直線(1 次多項式)有一個零點,拋物線(2 次多項式)最多有兩個,依此類推。

證明方要達成 q(s) = p(s),唯一的方法就是讓 r(x) = p(x) - q(x) = 0 在盡可能多的位置成立。但如前所述,他們最多只能選擇 n 個零點。

由於證明方不知道 s,他們能猜中它的機率微乎其微。事實上,n(多項式的階)<< p(橢圓曲線的階)。

如果 n = 228 且 p = 2^256,s 恰好是所選零點之一的機率是 2 × 10^-69。[5]

所以我們可以說,雖然存在許多擁有相同承諾 C = [p(s)]₁ 的多項式,但它們是不可能被算出來的。這稱為 計算綁定性(computational binding)。

到目前為止,我們已經能藉由在祕密點 s 上計算多項式 p(x) 的值來對它做出承諾,但還缺少一種方法,能在不把所有係數 pi 傳給驗證方的情況下,證明證明方最初的宣稱 p(z) = y 為真。這時橢圓曲線 pairing 又派上用場了。

4.8.5. KZG 證明

要理解最終的 KZG 證明,我們需要回顧多項式的一些重要性質。

如果多項式 p(x) 在 z 有一個零點,它就能被 (x – z) 整除。這很容易理解。以多項式 p(x) = x² – 4 為例,它也可以寫成 p(x) = (x + 2)(x – 2)。因為 p(2) = 4 – 4 = 0,所以 p(x) 能被 (x – 2) 整除。

反過來也成立。如果 p(x) 能被 (x – z) 整除,多項式 p(x) 就在 z 有一個零點。用前面同一個例子,就能輕鬆證明這一點。

請記得,我們(身為證明方)想證明 p(z) = y。這時,我們取多項式 p(x) – y。這個多項式在 x = z 時的值為零,所以我們可以運用前面描述的性質,特別是:因為 p(x) – y 在 z 有一個零點,所以它能被 (x – z) 整除。

所以我們可以計算商多項式 q(x):

q(x) = [p(x) – y] / (x – z)

也可以寫成等價的形式:

q(x)(x – z) = p(x) – y
備註

這裡非常重要的一點是:之所以能算出商多項式 q(x),是因為 p(x) – y 能被 x – z 整除。否則就不可能算出來,因為一定會有餘數。

為了更清楚地理解這一點,我們來看一個例子。再次以多項式 p(x) = (x + 2)(x – 2) 為例。因為這個多項式能被 (x + 2) 整除,我們可以計算出商多項式:

q(x) = p(x) / (x + 2) = [(x + 2)(x – 2)] / (x + 2) = x – 2

如果我們試著除以 (x – 3),就無法得到商多項式。有興趣的話,你可以用前面的例子試試看……

現在,我們已經完全準備好產生證明了。事實上,對宣稱 p(z) = y 的 KZG 證明就是 π = [q(s)]₁。基本上,它就是商多項式在祕密點 s 上的值(以橢圓曲線點表示)。

驗證方藉由計算下面這個等式,來檢查這個證明是否有效:

e(π, [s – z]₂) = e(C – [y]₁, H)

其中:

  • π = [q(s)]₁ 是 KZG 證明

  • C = [p(s)]₁ 是對多項式 p(x) 的承諾

  • H 是群 G2 的生成元,證明方與驗證方都知道

我們來證明這個密碼學方案同時具備以下兩種性質:

正確

如果證明方遵守所有規則,就一定能產生一個驗證方能正確驗證的有效證明。

Sound

證明方無法藉由計算一個假的證明來欺騙驗證方;也就是說,對於任何 y′ != y,證明方都無法讓驗證方相信 p(z) = y′。

正確性

如果我們把驗證方的等式寫到 pairing 群中,基本上也就是計算 pairing 運算,就會得到:

[q(s)(s – z)]ₜ = [p(s) – y]ₜ

其中 T 是一個有限體擴張的乘法子群。

你一眼就能看出,這個等式和我們之前算出的等式完全相同:

q(x)(x – z) = p(x) – y

只是在祕密點 s 上求值。唯一的差別在於,這裡是橢圓曲線點,而不是數字。

依照建構方式,這個等式永遠成立。正確性的部分就到此為止。

可靠性

證明方能作假嗎?也就是說:證明方能不能在 p(z) = y′ 不成立的情況下宣稱它成立,而且仍然通過驗證方的檢查?

要嘗試這麼做,驗證方有兩種選擇:[6]

  1. 驗證方可以試著照一般的程序進行,只改變所宣稱的值 y′。這樣一來,他們就必須把 p(x) – y′ 除以 (x – z) 來計算商多項式。但這是不可能的,因為 p(z) – y′ != 0,這裡無法進行多項式除法。你可能會想:如果證明方能選擇一個 y′ != y,使得 p(x) – y′ = 0 呢?其實我們在前面的一則備註中已經回答過了:這個機率趨近於零。證明方無法算出這樣的 y′。所以第一種做法並不可行。

  2. 驗證方可以在建構證明時,直接在橢圓曲線上操作。事實上,如果他們能產生這樣的證明:

    π′ = [C – y′] / [s – z]₂

    那麼他們就能證明任何想證明的事:

    e(π′, [s – z]₂) = e(C – [y′]₁, H) →
    → e(π′, [s – z]₂) = e(C – [y′]₁, H) →
    → (C - y`)/(s - z)*(s - z) = C - y` →
    → C - y` = C - y`  ← This is always true.

    但同樣地,對證明方來說,計算這個證明在計算上是不可能的,因為這需要知道 s,而它是可信設定的祕密,沒有人知道它的值。

4.8.6. KZG 的性質

總結一下使用 KZG 證明系統的多項式承諾所具有的性質:

承諾大小

承諾是一條支援 pairing 的橢圓曲線上的單一個群元素;例如,使用 BLS12_381 曲線時,它的壓縮形式長 48 個位元組。

證明大小

證明的大小與我們所承諾的多項式 p(x) 的大小無關,永遠是橢圓曲線上的單一個群元素。

驗證方的複雜度

驗證同樣與多項式的大小無關,只需要兩次(橢圓曲線)群乘法與兩次 pairing。

方案的隱私性

這個方案能隱藏證明方所承諾的多項式 p(x) 的大部分資訊。事實上,使用默克爾樹時,證明方必須傳送所有係數;使用 KZG 時,他們只需要傳送商多項式在祕密點 s 上的值,而且這個值是一個橢圓曲線點,不是整數。

4.8.7. 多點證明(Multiproof)

我們已經能證明多項式 p(x) 的單一個求值結果,但還能做得更多。我們可以證明多項式在任意多個點上的求值結果,而且證明的大小仍然相同。

我們來看看這是怎麼運作的。我們想證明,對每一個 zᵢ,p(x) 的值都是 yᵢ:

(z₀, y₀), (z₁, y₁), …, (zₖ₋₁, yₖ₋₁)

我們一定能找到一個次數低於 k、通過所有這些點的多項式。這是一個代數性質,而且有一個數學公式能產生這個多項式,稱為插值多項式 I(x):

I(x) = Σ yᵢ · ∏ (x – zⱼ)/(zᵢ – zⱼ)  (for i,j = 0 to k-1, j ≠ i)

雖然聽起來很難,其實相當容易理解。假設我們有兩個不同的點,如圖 4-11 所示:

A = (1, 0)
B = (2, 1)
兩個不同的點 A 與 B
圖 4-11. 兩個不同的點 A 與 B

很容易理解,有一條直線(一個 1 次多項式,次數小於點的數量 2)同時通過 A 與 B。

這條直線是:

I(x) = x – 1

圖 4-12 更清楚地說明了這一點。

通過點 A 與 B 的插值多項式(直線)
圖 4-12. 通過點 A 與 B 的插值多項式(直線)

這裡我們靠的是直覺,但也可以用公式證明結果相同。請記得,我們有 k = 2:

I(x) = y₀ · (x – z₁)/(z₀ – z₁) + y₁ · (x – z₀)/(z₁ – z₀)
     = 0 · (x – 2)/(1 – 2) + 1 · (x – 1)/(2 – 1)
     = 0 + (x – 1) = x – 1

由於證明方想證明 p(x) 通過本節一開始提到的所有點,而我們假設這是真的(也就是證明方值得信任),所以 p(x) – I(x) 在 z₀, z₁, …, zₖ₋₁ 每一個點上都是零。

這表示它能被 (x – z₀)(x – z₁)...(x – zₖ₋₁) 整除。我們把這個多項式 Z(x) 稱為 零化多項式(zerofier)。

接下來照一般的方法進行。證明方計算商多項式 q(x):

q(x) = [p(x) – I(x)] / Z(x)

並產生證明 π = [q(s)]₁。請注意,這個證明就是一般的(單一橢圓曲線點的)KZG 證明。

要檢查這個證明是否有效,驗證方需要:

  1. 套用前面描述的公式計算 I(x),再算出 [I(s)]₁。

  2. 計算 Z(x),再算出 [Z(s)]₂。請注意,驗證方之所以能計算 Z(x),是因為他們知道證明方想證明的所有宣稱的點(與值)。

  3. 接著套用一般的等價檢查:

e(π, [Z(s)]₂) = e(C – [I(s)]₁, H)

把等式寫到 pairing 群中,就是:

q(s)Z(s) = p(s) – I(s)

這和先前證明方計算商多項式時的等式相同,唯一的差別是這裡是在祕密點 s 上求值。

4.8.8. 小結

我們來總結 KZG 承諾方案中證明方↔驗證方的完整流程。

一開始是證明方,他知道某些資料。他立刻把這些資料編碼成一個多項式,如圖 4-13 所示。

證明方把資料編碼成多項式 p
圖 4-13. 證明方把資料編碼成多項式 p

證明方的目標,是讓驗證方相信自己知道某筆資料,而這筆資料現在已經編碼成一個多項式。不過,證明方不想洩漏整個資料集,而是希望驗證方能驗證關於這筆資料的特定宣稱。具體來說,證明方希望驗證方確認這個多項式在特定的點上的值等於預期的值,如圖 4-14 所示。

證明方想證明多項式在特定點上的求值結果
圖 4-14. 證明方想證明多項式在特定點上的求值結果

為了達成這個目標,證明方計算這個多項式的 KZG 承諾,並傳送給驗證方,如圖 4-15 所示。

證明方計算 KZG 承諾 C,並傳送給驗證方
圖 4-15. 證明方計算 KZG 承諾 C,並傳送給驗證方

接著,證明方需要針對所有想向驗證方證明的求值結果,計算 KZG 證明,如圖 4-16 所示。

證明方計算多項式各個求值結果的 KZG 證明,並傳送給驗證方
圖 4-16. 證明方計算多項式各個求值結果的 KZG 證明,並傳送給驗證方

現在輪到驗證方了。為了確定證明方是誠實的,驗證方需要使用證明方先前傳來的資訊,再加上可信設定,計算橢圓曲線 pairing 檢查,如圖 4-17 所示。

驗證方透過橢圓曲線 pairing 運算,檢查求值結果是否有效
圖 4-17. 驗證方透過橢圓曲線 pairing 運算,檢查求值結果是否有效

4.9. 結論

本章概略介紹了 PKC,並著重說明以太坊如何使用公鑰與私鑰,以及如何在建立與驗證以太坊地址時使用雜湊函數等密碼學工具。我們也看了數位簽章,以及它如何在不洩漏私鑰的情況下,證明自己擁有這把私鑰。在第 5 章,我們會把這些概念整合起來,看看如何用錢包管理一組組的金鑰。

註腳

  1. 譯註:依同一句的數字驗算,228 ÷ 2^256 約為 2 × 10-75;228 ÷ 2^256 = 2^-228 約為 2.3 × 10^-69,才與原文的 2 × 10^-69 相符。原文的 n = 228 應是 n = 2^28 漏了次方符號。 ↩
  2. 譯註:本段與下面兩個清單項目中的「驗證方」(verifier),從上下文判斷應為「證明方」(prover):本節討論的是證明方能否偽造證明(見上一段),第 1 項結尾也說「證明方無法算出這樣的 y′」,第 2 項結尾則說計算這個證明「對證明方而言」在計算上不可能。譯文依原文譯為「驗證方」。 ↩