理解 BLS 簽名算法
來源 https://medium.com/cryptoadvance/bls-signatures-better-than-schnorr-5a7fe30ea716
原文標題:《干貨:理解 BLS 簽名算法》
作者:Stepan
翻譯 & 校對:wuwei & 阿劍
之前的文章中,我介紹了 Schnorr 簽名算法和它的優勢。現在,我來介紹下 BLS (Boneh-Lynn-Shacham)簽名算法以及它相比 Schnorr 的優勝之處。
長話短說,我們已經知道:
ECDSA 簽名算法已經足夠勝任它的工作,但也僅限於此。它無法做簽名聚合或者密鑰聚合,因此只能挨個對簽名進行驗證。在驗證多重簽名的交易時,此舉過於繁瑣,我們需要逐個驗證所有的簽名及其對應的公鑰,耗費大量的區塊空間和交易費。
Schnorr 簽名算法就好多了,它可以把一筆交易中的所有簽名和公鑰合並成單個簽名和公鑰,且合並過程不可見(無從追溯這個簽名或公鑰是否通過合並而來)。另外,可以一次性對合並后的簽名做驗證,加快了區塊驗證的速度。當然,該算法也有一些不足 :
- 多重簽名需要多次(簽名者之間的)通信,這對冷錢包來說過於麻煩;
- 聚合簽名算法依賴隨機數生成器,而不像 ECDSA 那樣可以使用指定的隨機點(R);
- m-n 多重簽名機制比較取巧,需要構建公鑰的默克爾樹。當 m 和 n 較大時,樹所占空間會相當大;
- 無法把一個區塊中的所有簽名聚合成一個簽名。
BLS 簽名算法可以解決以上問題。它不需要隨機數生成器,可以將區塊中的所有簽名聚合成一個,容易實現 m-n 多重簽名,也可以避免簽名者之間的多余通信。除此之外,BLS 簽名的長度更短(簽名為橢圓曲線上的一個點而非兩個),是 Schnorr 或 ECDSA 的 2 分之一。聽起來完美!那么讓我們看看 BLS 簽名算法的工作原理。
BLS 簽名算法的魔力
進入正題前,我們先來了解兩個基礎概念,曲線哈希(hashingto the curve,或譯作 “哈希成曲線上的點”)和曲線配對(curves pairing)。
曲線哈希
在 ECDSA 和 Schnorr 簽名算法中,我們對消息進行哈希計算后,結果(哈希值)是數字。BLS 簽名算法則不同,它略微修改了哈希算法,結果對應到橢圓曲線上(的一個點)。最簡單的修改是:哈希函數保持不變,將得到的哈希值作為點的 x 值尋找橢圓曲線上的對應點。通常來說(比如比特幣所用的曲線),橢圓曲線有 2²⁵⁶ 個點,而 SHA-256 哈希算法的值也恰好是 256 位。不過,一個有效的 x 坐標,會對應一正一負兩個 y 坐標(因為(x, y)和(x, -y)都是曲線y²=x³+ax+b上的點)。換句話說,新的哈希算法大約有 50% 的概率在曲線上找到 2 個對應點,另 50% 的概率則一個點也找不到(校對注:因為橢圓曲線只有 2^256 個點,如果要讓每個哈希值都能找到對應點,橢圓曲線得有 2^257 個點才行)。
以在模為 23 的有限域上定義的橢圓曲線 y²=x³+7為例。只有一半的 x 坐標在曲線上能找到對應點。此例中,我們嘗試三次才成功找到對應點。
對消息求哈希時,為確保能在曲線上找到對應的點,可以在消息體后附加一個數,若(尋找對應點)失敗則累加該數並重新計算。即如果hash(m||0)沒有找到對應點,則持續嘗試 hash(m||1)\, hash(m||2)等,直到找到為止。當找到對應點后,在 2 個點中選擇 y 坐標較小的那個作為結果即可。
曲線配對
我們還需要一個特殊的函數,能夠把一條(或 2 條不同的)曲線上的兩個點 P 和 Q映射為一個數:
e(P, Q) → n
此函數還要有一個重要的特性。即對於未知數 x 和兩個點 P 、 Q ,無論哪個點乘以 x,結果相同,即:
e(x*P\, Q) = e(P\, x*Q)
如此,除了乘數交換仍能保持等式成立外,更進一步,以下所有的交換都要保持等式成立:
e(a*P\, b*Q) = e(P\, ab*Q) = e(ab*P\, Q) = e(P\, Q)^(ab)
我不准備解釋這個函數是如何工作的。背后的數學原理相當復雜,如果你想知道所有 “令人厭煩”的細節,我建議你參考這篇文章。如果你還想深入,可以研讀這篇論文,它通篇講述配對(pairings)理論。當前,我們只要假設這種函數存在,並且不會暴露 x 的任何相關信息。
值得注意的是,配對函數中不能使用任何橢圓曲線(特別是比特幣的 secp256k1 橢圓曲線)。我們必須使用非常特殊的曲線(通常出自易於配對的曲線簇),才能保證函數的效率和安全。
BLS 簽名方案
現在,所有構建BLS簽名算法的基礎知識已經齊備。我們用pk 代表私鑰,P = pk*G 代表公鑰,m代表要簽名的消息。
為了計算簽名,先對消息求曲線哈希 H(m),再將獲取的結果(曲線坐標點)乘以私鑰即可:S = pk*H(m)。大功告成!不需要隨機數,不需要額外的步驟,僅僅將哈希結果乘以私鑰即可。簽名結果是一個曲線上的點,用壓縮的序列化格式保存,只占 33 個字節。
生成 BLS 簽名,將消息的哈希結果乘以私鑰即可
我們可以使用公鑰 P 來驗證簽名,即 e(P, H(m)) = e(G, S) 。這是為什么呢?
如前所述,配對函數的特性使得如下等式成立:
e(P\, H(m)) = e(pk*G\, H(m)) = e(G\, pk*H(m)) = e(G\, S)
BLS 簽名驗證,我們只需驗證 公鑰和消息的哈希值(曲線上兩個點)與曲線生成點和簽名(曲線上另兩個點)是否映射到同一個數(若是則說明這是一個有效的 BLS 簽名)
這個簽名方案優美簡單。對於比特幣來說,該方案還有以下更棒的特性。
簽名聚合
現在讓我們把區塊中的簽名都聚合在一起。假設一個區塊中有 1000 筆交易,每筆交易都由Si(簽名),Pi(公鑰)和 mi(消息)組成(i在這里表示序號)。如果這些簽名可以被合並,那又何必分開保存呢?畢竟,我們只關心區塊中所有的簽名(而不是每一個)是否正確。為獲得聚合簽名,只需要將區塊中的所有簽名加起來:
S = S1 + S2 +…+ S1000
為驗證該區塊是否正確,我們需要保證以下等式成立:
e(G, S) = e(P1, H(m1)) * e(P2, H(m2)) … e(P1000, H(m1000))
如果簽名都有效,那么該等式的確是成立的:
e(G\, S) = e(G\, S1+S2+…+S1000) = e(G\, S1) × e(G\, S2) *…* e(G\, S1000) = e(G\, pk1×H(m1)) *…* e(G\, pk1000×H(m1000)) = e(pk1×G\, H(m1)) *…* e(pk1000×G\, H(m1000)) = e(P1\, H(m1)) × e(P2\, H(m2)) *…* e(P1000\, H(m1000))
這里我們仍需用到所有的公鑰,並計算 1001 次配對函數,好處是,區塊中的簽名只占 33 字節了。簽名聚合可以由礦工在挖礦時完成,節省大量的區塊空間。
密鑰聚合和 n-n 多重簽名
使用多重簽名的地址時,我們會對同一筆交易用不同的密鑰進行簽名。這種情況下,可以和 Schnorr 算法一樣使用聚合密鑰,把所有密鑰和簽名聚合成單個公鑰和簽名。下面我們以 3-3 多重簽名方案為例(同理可推導任意數量的多重簽名方案)。
一種簡單的聚合方法,是把所有的簽名和密鑰加起來即可。如此,簽名聚合結果為S=S1+S2+S3,密鑰聚合結果P=P1+P2+P3。可以驗證以下等式依然成立:
e(G, S) = e(P, H(m))
因為:
e(G, S) = e(G, S1+S2+S3) = e(G, (pk1+pk2+pk3)×H(m)) = e((pk1+pk2+pk3)×G, H(m)) = e(P1+P2+P3, H(m)) = e(P, H(m))
和 Schnorr 一樣,我們也需要杜絕偽造密鑰攻擊。一種方法是要求每個簽名參與者證明它擁有公鑰對應的私鑰(用私鑰給公鑰簽名)。另一種方法是加入非線性系數,使得攻擊無法實施。要做到這一點,聚合不再是簡單的將多個密鑰和簽名相加,而是將它們分別乘以某個系數后再相加:
S = a1×S1+a2×S2+a3×S3
P = a1×P1+a2×P2+a3×P3
公式中簽名和密鑰的系數,可以通過簽名者以及其它所有人的公鑰計算得出,公式如下:
ai = hash(Pi, {P1,P2,P3})
舉個例子,可以簡單的將簽名者的公鑰和所有人公鑰拼接在一起算出系數:
ai = hash(Pi||P1||P2||P3)
此時,上面的驗證公式依然成立。雖然多了系數ai,但計算邏輯未變。
該方案的好處是,無需在設備間進行多輪通信,只需知曉其它簽名者的相應信息即可。它可比 Schnorr 算法(需要 3 輪通信)的多重簽名方案簡單多了。這個方案也不依賴隨機性,是一種具有完全確定性的簽名算法。
子群多重簽名方案(m-n 多重簽名)
n-n 多重簽名並不常見,我們更傾向使用 m-n 多重簽名(比如 2-3 多重簽名)。我們不想因為丟失(n 個密鑰中的)一個密鑰而一無所有。密鑰聚合非常適合這種場景。在 Schnorr 簽名算法中,我們用公鑰組成的默克爾樹來實現密鑰聚合,這種方式效率不高,但是將將堪用。不幸地是,當 m 和 n 的值變大時,默克爾樹的大小會呈指數增長。
BLS 使用了另一種方法,不過略復雜。我們需要一個普通哈希函數hash(x)(結果為一個數)和一個曲線哈希函數H(x)。開始多重簽名時,還需要一個初始化過程,這之后,簽名者之間就不再需要通信了,只需提供交易簽名即可。
舉個簡單的例子,我們要創建一個 2-3 多重簽名,3 個簽名存儲在不同的設備上(這個例子可以擴展為任意的 m-n 多重簽名)。
初始化(生成成員密鑰)
用i = 1,2,3來表示集合中相應位置的設備,用pki表示私鑰,用Pi = pki×G表示公鑰。我們用前面說的方法來聚合公鑰:
P = a1×P1+a2×P2+a3×P3, ai = hash(Pi, {P1,P2,P3})
現在,每個設備都需要對每個 i 簽名,以證明該 i 是聚合公鑰中的一員。將簽名聚合后,保存在對應的設備中:
MKi = (a1_pk1)_H(P, i) + (a2_pk2)_H(P, i) + (a3_pk3)_H(P, i)
這個簽名被稱作“成員密鑰”,稍后簽名時我們會用到。每個成員密鑰都是對消息體H(P,i)的 n-n 多重簽名,即:
e(G, MKi)=e(P, H(P,i))
因為:
e(G\,Mki) = e(G\, (a1*pk1)*H(P\, i) + (a2*pk2)*H(P\, i) + (a3*pk3)*H(P\, i)) = e(G\, (a1*pk1 + a2*pk2 + a3*pk3)*H(P\, i))
= e(G*(a1*pk1 + a2*pk2 + a3*pk3)\, H(P\, i))
= e((G*a1*pk1 + G*a2*pk2 + G*a3*pk3)\, H(P\, i))
= e(P, H(P,i))
記住這個等式,稍后還會用到。它用來證明某個設備是多重簽名中的合法參與者。
簽名
假設只用私鑰 pk1 和 pk3 給交易簽名,我們會生成 2 個簽名 S1 和 S3:
S1 = pk1×H(P, m) + MK1, S3 = pk3×H(P, m) + MK3
將它們加起來,聚合成單一的簽名和公鑰:
(S’, P’) = (S1+S3, P1+P3)
用 P’ 和 S’,是為了強調它們只是由部分簽名者參與計算的(公鑰和簽名),而不像 P 那樣是由所有簽名者參與計算的(公鑰)。為了驗證 2-3 多重簽名,需證明如下等式成立:
e(G, S’) = e(P’, H(P, m)) * e(P, H(P, 1)+H(P, 3))
上面說過,成員密鑰 MK1 和 MK3 是對消息 H(P, 1) 和 H(P, 3) (消息本身由聚合公鑰P簽名)的簽名,所以有:
e(G, S’) = e(G, S1+S3)
= e(G, pk1H(P, m)+pk3×H(P, m) + MK1 + MK3)
= e(G, pk1H(P, m)+pk3×H(P, m)) * e(G, MK1+MK3)
= e(pk1*G+pk3*G\, H(P\, m)) * e(P\, H(P\, 1) + H(P\, 3))
= e(P’, H(P, m)) * e(P, H(P, 1)+H(P, 3))
證明完畢。比 n-n 多重簽名復雜一些,但仍然可以理解。
在比特幣中的實現
要在比特幣上部署一個 BLS 多簽名錢包時,我們需要往某個地址(該地址由聚合公鑰 P 生成)打錢。假設我們希望這是一個 2-n 多簽名合約,那么可以用比特幣的鎖定腳本來描述,聲明如下:
OP_2_
_OP_CHECK_BLS_MULTISIG
OP_2表示需要 2 個密鑰進行簽名。 這里沒有指明總共有 3 個簽名,因此它可以表示 2-3 或者 2-100 等不同的多重簽名地址。如此,總簽名數永遠不會暴露。
為了花掉輸出(output)地址中的錢,則需要提供 P’(公鑰),S’(簽名 )以及參與簽名者的編號(這個例子中是 1 和 3)。解鎖腳本如下:
OP_1 OP_3
腳本中有了這些信息,就足夠用來驗證交易了。其中,OP_1和 OP_3 告訴我們需要計算的曲線哈希為 H(P, 1)和H(P, 3)。
如此,對於任意 m 和 n的多重簽名,我們只需要以下信息:
- 一個用在加鎖腳本中的聚合公鑰 P;
- 一個由部分參與者(m個)生成的聚合公鑰 P’(用在解鎖中);
- 一個聚合簽名 S’;
- 解鎖要求的參與者數量m。
一切簡單而優美!
由於比特幣地址通常只用一次,需要使用 BIP32 這種密鑰推導算法生成新的私鑰和地址。因此,每次生成新的私鑰時,我們也需要生成對應的成員私鑰,我不太喜歡這一點。為避免每次交易后都要初始化生成成員私鑰,可以一次性地生成一批(比如 1000 個)成員私鑰,畢竟每個私鑰只占 32 字節。如此,當 1000 個地址用完后,我們才需要再次進行初始化。
缺點
鑒於大家在評論和 twitter 的提醒,我認識到有些地方我考慮欠周,會讓你們誤以為 BLS 簽名算法是完美的。它並不完美。謝謝你們的提醒!
我完全忽略了配對計算的高復雜度。我們曾認為配對函數e(P, Q)既高效又安全,實際上它並不完全是。
配對函數並不那么高效
BLS 簽名驗證的復雜度要比 ECDSA 高上一個數量級。 在驗證區塊中 1000 筆交易的聚合簽名時,仍需要進行 1000 次配對計算,這可能比使用 ECDSA 時(對 1000 個單獨簽名進行驗證)還要慢。唯一的好處在於,可以在區塊中放更多筆的交易,畢竟聚合簽名只占 32 字節。
與 BLS 不同,Schnorr 驗證算法的效率很高,它可以把簽名放一起驗證,效率是 ECDSA 算法的三倍。這樣,問題來了,效率和安全哪個對我們更重要?
想要安全,更難!
配對函數十分復雜,使用不當就會反受其累。一方面,我們希望用高效的配對函數來加快簽名驗證,另一方面,我們又希望密鑰信息暴露越少越好。兩者互相矛盾,我們需要異常小心地選擇適宜配對的曲線。
有一種針對橢圓曲線加密體系的攻擊,叫 MOV 攻擊(用攻擊發現者 Menezes、Okamoto 和 Vanstone 的名字命名),它能利用配對函數來危害系統安全。所以,使用配對函數必須萬分小心。
總結
BLS 簽名算法很出色——它能將區塊中的簽名聚合成單一簽名; 能進行密鑰聚合和 m-n 多重簽名(無需額外通信);能避免使用隨機數生成器。這些優點使它顯得如此簡單優雅。當然,它仍有改進空間,標准化和優化也尚需時日。但我希望有朝一日,它能變得足夠好,可以被納入比特幣協議中。這樣的話,我們不但能獲得它出色的功能,還可享受體積更小、聚合度更強的簽名算法帶來的好處。
BLS 算法的第一作者 Dan Boneh,目前正投身於對加密貨幣的研究,這使我異常興奮。他是一位傑出的密碼學家,他所執教的密碼學課程也很棒。在這里,我還要推薦下他尚未完成的密碼學著作。相信不久的將來,他和他的團隊會帶來更多針對加密貨幣的有趣方案和協議改進。
=============== End