1. 程式人生 > >精通比特幣-第4章 金鑰、地址、錢包

精通比特幣-第4章 金鑰、地址、錢包

4.1 簡介

比特幣的所有權是通過數字金鑰、比特幣地址和數字簽名來確立的。數字金鑰實際上並不是儲存在網路中,而是由使用者生成並存儲在一個檔案或簡單的資料庫中,稱為錢包。儲存在使用者錢包中的數字金鑰完全獨立於比特幣協議,可由使用者的錢包軟體生成並管理,而無需區塊鏈或網路連線。金鑰實現了比特幣的許多有趣特性,包括去中心化信任和控制、所有權認證和基於密碼學證明的安全模型。

每筆比特幣交易都需要一個有效的簽名才會被儲存在區塊鏈。只有有效的數字金鑰才能產生有效的數字簽名,因此擁有比特幣的金鑰副本就擁有了該帳戶的比特幣控制權。金鑰是成對出現的,由一個私鑰和一個公鑰所組成。公鑰就像銀行的帳號,而私鑰就像控制賬戶的PIN碼或支票的簽名。比特幣的使用者很少會直接看到數字金鑰。一般情況下,它們被儲存在錢包檔案內,由比特幣錢包軟體進行管理。

在比特幣交易的支付環節,收件人的公鑰是通過其數字指紋表示的,稱為比特幣地址,就像支票上的支付物件的名字(即“收款方”)。一般情況下,比特幣地址由一個公鑰生成並對應於這個公鑰。然而,並非所有比特幣地址都是公鑰;他們也可以代表其他支付物件,譬如指令碼,我們將在本章後面提及。這樣一來,比特幣地址把收款方抽象起來了,使得交易的目的地更靈活,就像支票一樣:這個支付工具可支付到個人賬戶、公司賬戶,進行賬單支付或現金支付。比特幣地址是使用者經常看到的金鑰的唯一代表,他們只需要把比特幣地址告訴其他人即可。

在本章中,我們將介紹錢包,也就是金鑰所在之處。我們將瞭解金鑰如何被產生、儲存和管理。我們將回顧私鑰和公鑰、地址和指令碼地址的各種編碼格式。最後,我們將講解金鑰的特殊用途:生成簽名、證明所有權以及創造比特幣靚號地址和紙錢包。

4.1.1 公鑰加密和加密貨幣

公鑰加密發明於20世紀70年代。它是計算機和資訊保安的數學基礎。

自從公鑰加密被髮明之後,一些合適的數學函式被提出,譬如:素數冪和橢圓曲線乘法。這些數學函式都是不可逆的,就是說很容易向一個方向計算,但不可以向相反方向倒推。基於這些數學函式的密碼學,使得生成數字金鑰和不可偽造的數字簽名成為可能。比特幣正是使用橢圓曲線乘法作為其公鑰加密的基礎演算法。

在比特幣系統中,我們用公鑰加密建立一個金鑰對,用於控制比特幣的獲取。金鑰對包括一個私鑰,和由其衍生出的唯一的公鑰。公鑰用於接收比特幣,而私鑰用於比特幣支付時的交易簽名。

公鑰和私鑰之間的數學關係,使得私鑰可用於生成特定訊息的簽名。此簽名可以在不洩露私鑰的同時對公鑰進行驗證。

支付比特幣時,比特幣的當前所有者需要在交易中提交其公鑰和簽名(每次交易的簽名都不同,但均從同一個私鑰生成)。比特幣網路中的所有人都可以通過所提交的公鑰和簽名進行驗證,並確認該交易是否有效,即確認支付者在該時刻對所交易的比特幣擁有所有權。


大多數比特幣錢包工具為了方便會將私鑰和公鑰以金鑰對的形式儲存在一起。然而,公鑰可以由私鑰計算得到,所以只儲存私鑰也是可以的。

4.1.2 私鑰和公鑰

一個比特幣錢包中包含一系列的金鑰對,每個金鑰對包括一個私鑰和一個公鑰。私鑰(k)是一個數字,通常是隨機選出的。有了私鑰,我們就可以使用橢圓曲線乘法這個單向加密函式產生一個公鑰(K)。有了公鑰(K),我們就可以使用一個單向加密雜湊函式生成比特幣地址(A)。在本節中,我們將從生成私鑰開始,講述如何使用橢圓曲線運算將私鑰生成公鑰,並最終由公鑰生成比特幣地址。私鑰、公鑰和比特幣地址之間的關係如下圖所示。

4.1.3 私鑰

私鑰就是一個隨機選出的數字而已。一個比特幣地址中的所有資金的控制取決於相應私鑰的所有權和控制權。在比特幣交易中,私鑰用於生成支付比特幣所必需的簽名以證明資金的所有權。私鑰必須始終保持機密,因為一旦被洩露給第三方,相當於該私鑰保護之下的比特幣也拱手相讓了。私鑰還必須進行備份,以防意外丟失,因為私鑰一旦丟失就難以復原,其所保護的比特幣也將永遠丟失。


比特幣私鑰只是一個數字。你可以用硬幣、鉛筆和紙來隨機生成你的私鑰:擲硬幣256次,用紙和筆記錄正反面並轉換為0和1,隨機得到的256位二進位制數字可作為比特幣錢包的私鑰。該私鑰可進一步生成公鑰。

從一個隨機數生成私鑰

生成金鑰的第一步也是最重要的一步,是要找到足夠安全的熵源,即隨機性來源。生成一個比特幣私鑰在本質上與“在1到2256之間選一個數字”無異。只要選取的結果是不可預測或不可重複的,那麼選取數字的具體方法並不重要。比特幣軟體使用作業系統底層的隨機數生成器來產生256位的熵(隨機性)。通常情況下,作業系統隨機數生成器由人工的隨機源進行初始化,也可能需要通過幾秒鐘內不停晃動滑鼠等方式進行初始化。對於真正的偏執狂,可以使用擲骰子的方法,並用鉛筆和紙記錄。

更準確地說,私鑰可以是1和n-1之間的任何數字,其中n是一個常數(n=1.158*1077,略小於2256),並由比特幣所使用的橢圓曲線的階所定義(見4.1.5 橢圓曲線密碼學解釋)。要生成這樣的一個私鑰,我們隨機選擇一個256位的數字,並檢查它是否小於n-1。從程式設計的角度來看,一般是通過在一個密碼學安全的隨機源中取出一長串隨機位元組,對其使用SHA256雜湊演算法進行運算,這樣就可以方便地產生一個256位的數字。如果運算結果小於n-1,我們就有了一個合適的私鑰。否則,我們就用另一個隨機數再重複一次。


本書強烈建議讀者不要使用自己寫的程式碼或使用程式語言內建的簡易隨機數生成器來獲得一個隨機數。我們建議讀者使用密碼學安全的偽隨機數生成器(CSPRNG),並且需要有一個來自具有足夠熵值的源的種子。使用隨機數發生器的程式庫時,需仔細研讀其文件,以確保它是加密安全的。對CSPRNG的正確實現是金鑰安全性的關鍵所在。

以下是一個隨機生成的私鑰(k),以十六進位制格式表示(256位的二進位制數,以64位十六進位制數顯示,每個十六進位制數佔4位):

1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD


比特幣私鑰空間的大小是2256,這是一個非常大的數字。用十進位制表示的話,大約是1077,而可見宇宙被估計只含有1080個原子。

要使用比特幣核心客戶端生成一個新的金鑰(參見第3章),可使用getnewaddress命令。出於安全考慮,命令執行後只顯示生成的公鑰,而不顯示私鑰。如果要bitcoind顯示私鑰,可以使用dumpprivkey命令。dumpprivkey命令會把私鑰以Base58校驗和編碼格式顯示,這種私鑰格式被稱為錢包匯入格式(WIF,Wallet Import Format),在“私鑰的格式”一節有詳細講解。下面給出了使用這兩個命令生成和顯示私鑰的例子:

$ bitcoind getnewaddress
1J7mdg5rbQyUHENYdx39WVWK7fsLpEoXZy
$ bitcoind dumpprivkey 1J7mdg5rbQyUHENYdx39WVWK7fsLpEoXZy
KxFC1jmwwCoACiCAWZ3eXa96mBM6tb3TYzGmf6YwgdGWZgawvrtJ

dumpprivkey命令只是讀取錢包裡由getnewaddress命令生成的私鑰,然後顯示出來。bitcoind的並不能從公鑰得知私鑰。除非金鑰對都儲存在錢包裡,dumpprivkey命令才有效。


dumpprivkey命令無法從公鑰得到對應的私鑰,因為這是不可能的。這個命令只是提取錢包中已有的私鑰,也就是提取由getnewaddress命令生成的私鑰。

你也可以使用命令列sx工具 (參見“3.3.1 Libbitcoin和sx Tools”)用newkey命令來生成並顯示私鑰:

$ sx newkey
5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1Jcn

4.1.4 公鑰

通過橢圓曲線演算法可以從私鑰計算得到公鑰,這是不可逆轉的過程:K = k * G 。其中k是私鑰,G是被稱為生成點的常數點,而K是所得公鑰。其反向運算,被稱為“尋找離散對數”——已知公鑰K來求出私鑰k——是非常困難的,就像去試驗所有可能的k值,即暴力搜尋。在演示如何從私鑰生成公鑰之前,我們先稍微詳細學習下橢圓曲線加密學。

4.1.5 橢圓曲線密碼學解釋

橢圓曲線加密法是一種基於離散對數問題的非對稱(或公鑰)加密法,可以用對橢圓曲線上的點進行加法或乘法運算來表達。

上圖是一個橢圓曲線的示例,類似於比特幣所用的曲線。

比特幣使用了secp256k1標準所定義的一條特殊的橢圓曲線和一系列數學常數。該標準由美國國家標準與技術研究院(NIST)設立。secp256k1曲線由下述函式定義,該函式可產生一條橢圓曲線:

y2 = (x3 + 7)} over (Fp)

y2 mod p = (x3 + 7) mod p

上述mod p(素數p取模)表明該曲線是在素數階p的有限域內,也寫作Fp,其中p = 2256 – 232 – 29 – 28 – 27 – 26 – 24 – 1,這是一個非常大的素數。

因為這條曲線被定義在一個素數階的有限域內,而不是定義在實數範圍,它的函式影象看起來像分散在兩個維度上的散點圖,因此很難畫圖表示。不過,其中的數學原理與實數範圍的橢圓曲線相似。作為一個例子,下圖顯示了在一個小了很多的素數階17的有限域內的橢圓曲線,其形式為網格上的一系列散點。而secp256k1的比特幣橢圓曲線可以被想象成一個極大的網格上一系列更為複雜的散點。

圖為:橢圓曲線密碼學F(p)上的橢圓曲線,其中p = 17

下面舉一個例子,這是secp256k1曲線上的點P,其座標為(x,y)。可以使用Python對其檢驗:

P =(55066263022277343669578718895168534326250603453777594175500187360389116729240,32670510020758816978083085130507043184471273380659243275938904335757337482424)

Python 3.4.0 (default, Mar 30 2014, 19:23:13)
[GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.38)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> p = 115792089237316195423570985008687907853269984665640564039457584007908834671663
>>> x = 55066263022277343669578718895168534326250603453777594175500187360389116729240
>>> y = 32670510020758816978083085130507043184471273380659243275938904335757337482424
>>> (x ** 3 + 7 - y**2) % p
0

在橢圓曲線的數學原理中,有一個點被稱為“無窮遠點”,這大致對應於0在加法中的作用。計算機中,它有時表示為X = Y = 0(雖然這不滿足橢圓曲線方程,但可作為特殊情況進行檢驗)。 還有一個 + 運算子,被稱為“加法”,就像小學數學中的實數相加。給定橢圓曲線上的兩個點P1和P2,則橢圓曲線上必定有第三點 P3 = P1 + P2

幾何圖形中,該第三點P3可以在P1和P2之間畫一條線來確定。這條直線恰好與橢圓曲線上的一點相交。此點記為 P3'=(x,y)。然後,在x軸做對映獲得 P3=(x,-y)。

下面是幾個可以解釋“無窮遠點”之存在需要的特殊情況。 若 P1和 P2是同一點,P1和P2間的連線則為點P1 的切線。曲線上有且只有一個新的點與該切線相交。該切線的斜率可用微分求得。即使限制曲線點為兩個整數座標也可求得斜率!

在某些情況下(即,如果P1和P2具有相同的x值,但不同的y值),則切線會完全垂直,在這種情況下,P3 = “無窮遠點”。

若P1就是“無窮遠點”,那麼其和 P1 + P2= P2。類似地,當P2是無窮遠點,則P1+ P2 = P1。這就是把無窮遠點類似於0的作用。

事實證明,在這裡 + 運算子遵守結合律,這意味著(A+B)C = A(B+C)。這就是說我們可以直接不加括號書寫 A + B + C,而不至於混淆。

至此,我們已經定義了橢圓加法,為擴充套件加法下面我們對乘法進行標準定義。給定橢圓曲線上的點P,如果k是整數,則 kP = P + P + P + …+ P(k次)。注意,k被有時被混淆而稱為“指數”。

4.1.6 生成公鑰

以一個隨機生成的私鑰k為起點,我們將其與曲線上已定義的 生成點G相乘以獲得曲線上的另一點,也就是相應的公鑰K。生成點是secp256k1標準的一部分,比特幣金鑰的生成點都是相同的:

{K = k * G}

其中k是私鑰,G是生成點,在該曲線上所得的點K是公鑰。因為所有比特幣使用者的生成點是相同的,一個私鑰k乘以G將得到相同的公鑰K。k和K之間的關係是固定的,但只能單向運算,即從k得到K。這就是可以把比特幣地址(K的衍生)與任何人共享而不會洩露私鑰(k)的原因。



因為其中的數學運算是單向的,所以私鑰可以轉換為公鑰,但公鑰不能轉換回私鑰。

為實現橢圓曲線乘法,我們以之前產生的私鑰k和與生成點G相乘得到公鑰K:

K = 1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD * G

公鑰K 被定義為一個點 K = (x, y):

K = (x, y)

其中,

x = F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341A
y = 07CF33DA18BD734C600B96A72BBC4749D5141C90EC8AC328AE52DDFE2E505BDB

為了展示整數點的乘法,我們將使用較為簡單的實數範圍的橢圓曲線。請記住,其中的數學原理是相同的。我們的目標是找到生成點G的倍數kG。也就是將G相加k次。在橢圓曲線中,點的相加等同於從該點畫切線找到與曲線相交的另一點,然後對映到x軸。

上圖顯示了在曲線上得到 G、2G、4G 的幾何操作。


大多數比特幣程式使用OpenSSL加密庫進行橢圓曲線計算。例如,呼叫EC_POINT_mul() 函式,可計算得到公鑰。

4.2 比特幣地址

比特幣地址是一個由數字和字母組成的字串,可以與任何想給你比特幣的人分享。由公鑰(一個同樣由數字和字母組成的字串)生成的比特幣地址以數字“1”開頭。下面是一個比特幣地址的例子:

1J7mdg5rbQyUHENYdx39WVWK7fsLpEoXZy

在交易中,比特幣地址通常以收款方出現。如果把比特幣交易比作一張支票,比特幣地址就是收款人,也就是我們要寫入收款人一欄的內容。一張支票的收款人可能是某個銀行賬戶,也可能是某個公司、機構,甚至是現金支票。支票不需要指定一個特定的賬戶,而是用一個普通的名字作為收款人,這使它成為一種相當靈活的支付工具。與此類似,比特幣地址的使用也使比特幣交易變得很靈活。比特幣地址可以代表一對公鑰和私鑰的所有者,也可以代表其它東西,比如會在132頁的“P2SH (Pay-to-Script-Hash)”一節講到的付款指令碼。現在,讓我們來看一個簡單的例子,由公鑰生成比特幣地址。

比特幣地址可由公鑰經過單向的加密雜湊演算法得到。雜湊演算法是一種單向函式,接收任意長度的輸入產生指紋摘要。加密雜湊函式在比特幣中被廣泛使用:比特幣地址、指令碼地址以及在挖礦中的工作量證明演算法。由公鑰生成比特幣地址時使用的演算法是Secure Hash Algorithm (SHA)和the RACE Integrity Primitives Evaluation Message Digest (RIPEMD),特別是SHA256和RIPEMD160。

以公鑰 K 為輸入,計算其SHA256雜湊值,並以此結果計算RIPEMD160 雜湊值,得到一個長度為160位元(20位元組)的數字:

A = RIPEMD160(SHA256(K))

公式中,K是公鑰,A是生成的比特幣地址。


比特幣地址與公鑰不同。比特幣地址是由公鑰經過單向的雜湊函式生成的。

通常使用者見到的比特幣地址是經過“Base58Check”編碼的(參見72頁“Base58和Base58Check編碼”一節),這種編碼使用了58個字元(一種Base58數字系統)和校驗碼,提高了可讀性、避免歧義並有效防止了在地址轉錄和輸入中產生的錯誤。Base58Check編碼也被用於比特幣的其它地方,例如比特幣地址、私鑰、加密的金鑰和指令碼雜湊中,用來提高可讀性和錄入的正確性。下一節中我們會詳細解釋Base58Check的編碼機制,以及它產生的結果。下圖描述瞭如何從公鑰生成比特幣地址。

4.2.1 Base58和Base58Check編碼

為了更簡潔方便地表示長串的數字,許多計算機系統會使用一種以數字和字母組成的大於十進位制的表示法。例如,傳統的十進位制計數系統使用0-9十個數字,而十六進位制系統使用了額外的 A-F 六個字母。一個同樣的數字,它的十六進位制表示就會比十進位制表示更短。更進一步,Base64使用了26個小寫字母、26個大寫字母、10個數字以及兩個符號(例如“+”和“/”),用於在電子郵件這樣的基於文字的媒介中傳輸二進位制資料。Base64通常用於編碼郵件中的附件。Base58是一種基於文字的二進位制編碼格式,用在比特幣和其它的加密貨幣中。這種編碼格式不僅實現了資料壓縮,保持了易讀性,還具有錯誤診斷功能。Base58是Base64編碼格式的子集,同樣使用大小寫字母和10個數字,但捨棄了一些容易錯讀和在特定字型中容易混淆的字元。具體地,Base58不含Base64中的0(數字0)、O(大寫字母o)、l(小寫字母L)、I(大寫字母i),以及“+”和“/”兩個字元。簡而言之,Base58就是由不包括(0,O,l,I)的大小寫字母和數字組成。

例4-1 比特幣的Base58字母表

123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz

Base58Check是一種常用在比特幣中的Base58編碼格式,增加了錯誤校驗碼來檢查資料在轉錄中出現的錯誤。校驗碼長4個位元組,新增到需要編碼的資料之後。校驗碼是從需要編碼的資料的雜湊值中得到的,所以可以用來檢測並避免轉錄和輸入中產生的錯誤。使用Base58check編碼格式時,編碼軟體會計算原始資料的校驗碼並和結果資料中自帶的校驗碼進行對比。二者不匹配則表明有錯誤產生,那麼這個Base58Check格式的資料就是無效的。例如,一個錯誤比特幣地址就不會被錢包認為是有效的地址,否則這種錯誤會造成資金的丟失。

為了使用Base58Check編碼格式對資料(數字)進行編碼,首先我們要對資料新增一個稱作“版本位元組”的字首,這個字首用來明確需要編碼的資料的型別。例如,比特幣地址的字首是0(十六進位制是0x00),而對私鑰編碼時字首是128(十六進位制是0x80)。 表4-1會列出一些常見版本的字首。

接下來,我們計算“雙雜湊”校驗碼,意味著要對之前的結果(字首和資料)執行兩次SHA256雜湊演算法:

checksum = SHA256(SHA256(prefix+data))

在產生的長32個位元組的雜湊值(兩次雜湊運算)中,我們只取前4個位元組。這4個位元組就作為校驗碼。校驗碼會新增到資料之後。

結果由三部分組成:字首、資料和校驗碼。這個結果採用之前描述的Base58字母表編碼。下圖描述了Base58Check編碼的過程。

Base58Check編碼:一種Base58格式的、有版本的、經過校驗的格式,可以明確的對比特幣資料編碼的編碼格式

在比特幣中,大多數需要向用戶展示的資料都使用Base58Check編碼,可以實現資料壓縮,易讀而且有錯誤檢驗。Base58Check編碼中的版本字首是資料的格式易於辨別,編碼之後的資料頭包含了明確的屬性。這些屬性使使用者可以輕鬆明確被編碼的資料的型別以及如何使用它們。例如我們可以看到他們的不同,Base58Check編碼的比特幣地址是以1開頭的,而Base58Check編碼的私鑰WIF是以5開頭的。表4-1展示了一些版本字首和他們對應的Base58格式。

表4-1 Base58Check版本字首和編碼後的結果

種類 版本字首 (hex) Base58格式
Bitcoin Address 0x00 1
Pay-to-Script-Hash Address 0x05 3
Bitcoin Testnet Address 0x6F m or n
Private Key WIF 0x80 5, K or L
BIP38 Encrypted Private Key 0x0142 6P
BIP32 Extended Public Key 0x0488B21E xpub

我們回顧比特幣地址產生的完整過程,從私鑰、到公鑰(橢圓曲線上某個點)、再到兩次雜湊的地址,最終產生Base58Check格式的比特幣地址。例4-2的C++程式碼完整詳細的展示了從私鑰到Base58Check編碼後的比特幣地址的步驟。程式碼中使用“3.3 其他客戶端、資料庫、工具包 ”一節中介紹的libbitcoin library來實現某些輔助功能。

例4-2 從私鑰產生一個Base58Check格式編碼的比特幣地址

#include <bitcoin/bitcoin.hpp>

int main() {
    // Private secret key.
    bc::ec_secret secret = bc::decode_hash(
        "038109007313a5807b2eccc082c8c3fbb988a973cacf1a7df9ce725c31b14776");
    // Get public key.
    bc::ec_point public_key = bc::secret_to_public_key(secret);
    std::cout << "Public key: " << bc::encode_hex(public_key) << std::endl;

    // Create Bitcoin address.
    // Normally you can use:
    //   bc::payment_address payaddr;
    //   bc::set_public_key(payaddr, public_key);
    //   const std::string address = payaddr.encoded();

    // Compute hash of public key for P2PKH address.
    const bc::short_hash hash = bc::bitcoin_short_hash(public_key);
    bc::data_chunk unencoded_address; // Reserve 25 bytes
    // [ version:1 ]
    // [ hash:20 ]
    //   [ checksum:4 ]
    unencoded_address.reserve(25);
    // Version byte, 0 is normal BTC address (P2PKH).     unencoded_address.push_back(0);
    // Hash data
    bc::extend_data(unencoded_address, hash);
    // Checksum is computed by hashing data, and adding 4 bytes from hash. bc::append_checksum(unencoded_address);
    // Finally we must encode the result in Bitcoin's base58 encoding assert(unencoded_address.size() == 25);
    const std::string address = bc::encode_base58(unencoded_address);
    std::cout << "Address: " << address << std::endl;
    return 0; 
}

正如編譯並執行addr程式碼中展示的,由於程式碼使用預定義的私鑰,所以每次執行都會產生相同的比特幣地址。如例4-3所示。

例4-3 編譯並執行addr程式碼

# Compile the addr.cpp code
$ g++ -o addr addr.cpp $(pkg-config --cflags --libs libbitcoin)
# Run the addr executable
$ ./addr
Public key: 0202a406624211f2abbdc68da3df929f938c3399dd79fac1b51b0e4ad1d26a47aa Address: 1PRTTaJesdNovgne6Ehcdu1fpEdX7913CK

4.2.2 金鑰的格式

公鑰和私鑰的都可以有多種編碼格式。一個金鑰被不同的格式編碼後,雖然結果看起來可能不同,但是金鑰所編碼數字並沒有改變。這些不同的編碼格式主要是用來方便人們無誤地使用和識別金鑰。

私鑰的格式

私鑰可以以許多不同的格式表示,所有這些都對應於相同的256位的數字。表4-2展示了私鑰的三種常見格式。

表4-2 私鑰表示法(編碼格式)

種類 版本 描述
Hex None 64 hexadecimal digits
WIF 5 Base58Check encoding: Base58 with version prefix of 128 and 32-bit checksum
WIF-compressed K or L As above, with added suffix 0x01 before encoding

表4-3展示了用這三種格式所生成的私鑰。

表4-3 示例:同樣的私鑰,不同的格式

格式 私鑰
Hex 1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD
WIF 5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1Jcn
WIF-compressed KxFC1jmwwCoACiCAWZ3eXa96mBM6tb3TYzGmf6YwgdGWZgawvrtJ

這些表示法都是用來表示相同的數字、相同的私鑰的不同方法。雖然編碼後的字串看起來不同,但不同的格式彼此之間可以很容易地相互轉換。

將Base58Check編碼解碼為十六進位制

sx工具包(參見“3.3.1 Libbitcoin和sx Tools”)可用來編寫一些操作比特幣金鑰、地址及交易的shell指令碼和命令列“管道”。你也可以使用sx工具從命令列對Base58Check格式進行解碼。

我們使用的命令是base58check-decode

$ sx base58check-decode 5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1Jcn
1e99423a4ed27608a15a2616a2b0e9e52ced330ac530edcc32c8ffc6a526aedd 128

所得結果是十六進位制的金鑰,緊接著是錢包匯入格式(Wallet Import Format,WIF)的版本字首128。

將十六進位制轉換為Base58Check編碼

要轉換成Base58Check編碼(和之前的命令正好相反),我們需提供十六進位制的私鑰和錢包匯入格式(Wallet Import Format,WIF)的版本號字首128:

$sx base58check-encode 1e99423a4ed27608a15a2616a2b0e9e52ced330ac530edcc32c8ffc6a526aedd 128 
5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1Jcn

將十六進位制(壓縮格式金鑰)轉換為Base58Check編碼

要將壓縮格式的私鑰編碼為Base58Check(參見“壓縮格式私鑰”一節),我們需在十六進位制私鑰的後面新增字尾01,然後使用跟上面一樣的方法:

$ sx base58check-encode 1e99423a4ed27608a15a2616a2b0e9e52ced330ac530edcc32c8ffc6a526aedd01 128
KxFC1jmwwCoACiCAWZ3eXa96mBM6tb3TYzGmf6YwgdGWZgawvrtJ

生成的WIF壓縮格式的私鑰以字母“K”開頭,用以表明被編碼的私鑰有一個字尾“01”,且該私鑰只能被用於生成壓縮格式的公鑰(參見“壓縮格式公鑰”一節)。

公鑰的格式

公鑰也可以用多種不同格式來表示,最重要的是它們分為非壓縮格式或壓縮格式公鑰這兩種形式。

我們從前文可知,公鑰是在橢圓曲線上的一個點,由一對座標(x,y)組成。公鑰通常表示為字首04緊接著兩個256位元的數字。其中一個256位元數字是公鑰的x座標,另一個256位元數字是y座標。字首04是用來區分非壓縮格式公鑰,壓縮格式公鑰是以02或者03開頭。

下面是由前文中的私鑰所生成的公鑰,其座標x和y如下:

x = F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341A
y = 07CF33DA18BD734C600B96A72BBC4749D5141C90EC8AC328AE52DDFE2E505BDB

下面是同樣的公鑰以520位元的數字(130個十六進位制數字)來表達。這個520位元的數字以字首04開頭,緊接著是x及y座標,組成格式為04 x y:

K = 04F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341A07CF33DA18BD734C600B96A72BBC4749D5141C90EC8AC328AE52DDFE2E505BDB

壓縮格式公鑰

引入壓縮格式公鑰是為了減少比特幣交易的位元組數,從而可以節省那些執行區塊鏈資料庫的節點磁碟空間。大部分比特幣交易包含了公鑰,用於驗證使用者的憑據和支付比特幣。每個公鑰有520位元(包括字首,x座標,y座標)。如果每個區塊有數百個交易,每天有成千上萬的交易發生,區塊鏈裡就會被寫入大量的資料。

正如我們在“4.1.4 公鑰”一節所見,一個公鑰是一個橢圓曲線上的點(x, y)。而橢圓曲線實際是一個數學方程,曲線上的點實際是該方程的一個解。因此,如果我們知道了公鑰的x座標,就可以通過解方程y2mod p = (x3 + 7) mod p得到y座標。這種方案可以讓我們只儲存公鑰的x座標,略去y座標,從而將公鑰的大小和儲存空間減少了256位元。每個交易所需要的位元組數減少了近一半,隨著時間推移,就大大節省了很多資料傳輸和儲存。

未壓縮格式公鑰使用04作為字首,而壓縮格式公鑰是以02或03作為字首。需要這兩種不同字首的原因是:因為橢圓曲線加密的公式的左邊是y2 ,也就是說y的解是來自於一個平方根,可能是正值也可能是負值。更形象地說,y座標可能在x座標軸的上面或者下面。從圖4-2的橢圓曲線圖中可以看出,曲線是對稱的,從x軸看就像對稱的鏡子兩面。因此,如果我們略去y座標,就必須儲存y的符號(正值或者負值)。換句話說,對於給定的x值,我們需要知道y值在x軸的上面還是下面,因為它們代表橢圓曲線上不同的點,即不同的公鑰。當我們在素數p階的有限域上使用二進位制算術計算橢圓曲線的時候,y座標可能是奇數或者偶數,分別對應前面所講的y值的正負符號。因此,為了區分y座標的兩種可能值,我們在生成壓縮格式公鑰時,如果y是偶數,則使用02作為字首;如果y是奇數,則使用03作為字首。這樣就可以根據公鑰中給定的x值,正確推匯出對應的y座標,從而將公鑰解壓縮為在橢圓曲線上的完整的點座標。下圖闡釋了公鑰壓縮:

下面是前述章節所生成的公鑰,使用了264位元(66個十六進位制數字)的壓縮格式公鑰格式,其中字首03表示y座標是一個奇數:

K = 03F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341A

這個壓縮格式公鑰對應著同樣的一個私鑰,這意味它是由同樣的私鑰所生成。但是壓縮格式公鑰和非壓縮格式公鑰差別很大。更重要的是,如果我們使用雙雜湊函式(RIPEMD160(SHA256(K)))將壓縮格式公鑰轉化成比特幣地址,得到的地址將會不同於由非壓縮格式公鑰產生的地址。這種結果會讓人迷惑,因為一個私鑰可以生成兩種不同格式的公鑰——壓縮格式和非壓縮格式,而這兩種格式的公鑰可以生成兩個不同的比特幣地址。但是,這兩個不同的比特幣地址的私鑰是一樣的。

壓縮格式公鑰漸漸成為了各種不同的比特幣客戶端的預設格式,它可以大大減少交易所需的位元組數,同時也讓儲存區塊鏈所需的磁碟空間變小。然而,並非所有的客戶端都支援壓縮格式公鑰,於是那些較新的支援壓縮格式公鑰的客戶端就不得不考慮如何處理那些來自較老的不支援壓縮格式公鑰的客戶端的交易。這在錢包應用匯入另一個錢包應用的私鑰的時候就會變得尤其重要,因為新錢包需要掃描區塊鏈並找到所有與這些被匯入私鑰相關的交易。比特幣錢包應該掃描哪個比特幣地址呢?新客戶端不知道應該使用哪個公鑰:因為不論是通過壓縮的公鑰產生的比特幣地址,還是通過非壓縮的公鑰產生的地址,兩個都是合法的比特幣地址,都可以被私鑰正確簽名,但是他們是完全不同的比特幣地址。

為了解決這個問題,當私鑰從錢包中被匯出時,較新的比特幣客戶端將使用一種不同的錢包匯入格式(Wallet Import Format)。這種新的錢包匯入格式可以用來表明該私鑰已經被用來生成壓縮的公鑰,同時生成的比特幣地址也是基於該壓縮的公鑰。這個方案可以解決匯入私鑰來自於老錢包還是新錢包的問題,同時也解決了通過公鑰生成的比特幣地址是來自於壓縮格式公鑰還是非壓縮格式公鑰的問題。最後新錢包在掃描區塊鏈時,就可以使用對應的比特幣地址去查詢該比特幣地址在區塊鏈裡所發生的交易。我們將在下一節詳細解釋這種機制是如何工作的。

壓縮格式私鑰

實際上“壓縮格式私鑰”是一種名稱上的誤導,因為當一個私鑰被使用WIF壓縮格式匯出時,不但沒有壓縮,而且比“非壓縮格式”私鑰長出一個位元組。這個多出來的一個位元組是私鑰被加了字尾01,用以表明該私鑰是來自於一個較新的錢包,只能被用來生成壓縮的公鑰。私鑰是非壓縮的,也不能被壓縮。“壓縮的私鑰”實際上只是表示“用於生成壓縮格式公鑰的私鑰”,而“非壓縮格式私鑰”用來表明“用於生成非壓縮格式公鑰的私鑰”。為避免更多誤解,應該只可以說匯出格式是“WIF壓縮格式”或者“WIF”,而不能說這個私鑰是“壓縮”的。

要注意的是,這些格式並不是可互換使用的。在較新的實現了壓縮格式公鑰的錢包中,私鑰只能且永遠被匯出為WIF壓縮格式(以K或L為字首)。對於較老的沒有實現壓縮格式公鑰的錢包,私鑰將只能被匯出為WIF格式(以5為字首)匯出。這樣做的目的就是為了給匯入這些私鑰的錢包一個訊號:到底是使用壓縮格式公鑰和比特幣地址去掃描區塊鏈,還是使用非壓縮格式公鑰和比特幣地址。

如果一個比特幣錢包實現了壓縮格式公鑰,那麼它將會在所有交易中使用該壓格式縮公鑰。錢包中的私鑰將會被用來生成壓縮格式公鑰,壓縮格式公鑰然後被用來生成交易中的比特幣地址。當從一個實現了壓縮格式公鑰的比特幣錢包匯出私鑰時,錢包匯入格式(WIF)將會被修改為WIF壓縮格式,該格式將會在私鑰的後面附加一個位元組大小的字尾01。最終的Base58Check編碼格式的私鑰被稱作WIF(“壓縮”)私鑰,以字母“K”或“L”開頭。而以“5”開頭的是從較老的錢包中以WIF(非壓縮)格式匯出的私鑰。

表4-4展示了同樣的私鑰使用不同的WIF和WIF壓縮格式編碼。

表4-4 示例:同樣的私鑰,不同的格式

格式 私鑰
Hex 1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD
WIF 5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1Jcn
Hex-compressed 1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD01
WIF-compressed KxFC1jmwwCoACiCAWZ3eXa96mBM6tb3TYzGmf6YwgdGWZgawvrtJ


“壓縮格式私鑰”是一個不當用詞!私鑰不是壓縮的。WIF壓縮格式的私鑰只是用來表明他們只能被生成壓縮的公鑰和對應的比特幣地址。相反地,“WIF壓縮”編碼的私鑰還多出一個位元組,因為這種私鑰多了字尾“01”。該字尾是用來區分“非壓縮格式”私鑰和“壓縮格式”私鑰。

4.3 用Python實現金鑰和比特幣地址

最全面的比特幣Python庫是 Vitalik Buterin寫的 pybitcointools。在例4-4中,我們使用pybitcointools庫(匯入為“bitcoin”)來生成和顯示不同格式的金鑰和比特幣地址。

例4-4 使用pybitcointools庫的金鑰和比特幣地址的生成和格式化過

import bitcoin

# Generate a random private key
valid_private_key = False while not valid_private_key:
    private_key = bitcoin.random_key()
    decoded_private_key = bitcoin.decode_privkey(private_key, 'hex')
    valid_private_key =  0 < decoded_private_key < bitcoin.N

print "Private Key (hex) is: ", private_key
print "Private Key (decimal) is: ", decoded_private_key

# Convert private key to WIF format
wif_encoded_private_key = bitcoin.encode_privkey(decoded_private_key, 'wif')
print "Private Key (WIF) is: ", wif_encoded_private_key

# Add suffix "01" to indicate a compressed private key
compressed_private_key = private_key + '01'
print "Private Key Compressed (hex) is: ", compressed_private_key

# Generate a WIF format from the compressed private key (WIF-compressed)
wif_compressed_private_key = bitcoin.encode_privkey(
    bitcoin.decode_privkey(compressed_private_key, 'hex'), 'wif')
print "Private Key (WIF-Compressed) is: ", wif_compressed_private_key

# Multiply the EC generator point G with the private key to get a public key point
public_key = bitcoin.base10_multiply(bitcoin.G, decoded_private_key) print "Public Key (x,y) coordinates is:", public_key

# Encode as hex, prefix 04
hex_encoded_public_key = bitcoin.encode_pubkey(public_key,'hex') print "Public Key (hex) is:", hex_encoded_public_key

# Compress public key, adjust prefix depending on whether y is even or odd
(public_key_x, public_key_y) = public_key if (public_key_y % 2) == 0:
    compressed_prefix = '02' 
else:
    compressed_prefix = '03'
hex_compressed_public_key = compressed_prefix + bitcoin.encode(public_key_x, 16) print "Compressed Public Key (hex) is:", hex_compressed_public_key
# Generate bitcoin address from public key
print "Bitcoin Address (b58check) is:", bitcoin.pubkey_to_address(public_key)

# Generate compressed bitcoin address from compressed public key
print "Compressed Bitcoin Address (b58check) is:", \             bitcoin.pubkey_to_address(hex_compressed_public_key)

例4-5顯示了上段程式碼執行結果。

例4-5 執行 key-to-address-ecc-example.py

$ python key-to-address-ecc-example.py
Private Key (hex) is:
 3aba4162c7251c891207b747840551a71939b0de081f85c4e44cf7c13e41daa6
Private Key (decimal) is:
 26563230048437957592232553826663696440606756685920117476832299673293013768870
Private Key (WIF) is:
 5JG9hT3beGTJuUAmCQEmNaxAuMacCTfXuw1R3FCXig23RQHMr4K
Private Key Compressed (hex) is:
 3aba4162c7251c891207b747840551a71939b0de081f85c4e44cf7c13e41daa601
Private Key (WIF-Compressed) is:
 KyBsPXxTuVD82av65KZkrGrWi5qLMah5SdNq6uftawDbgKa2wv6S
Public Key (x,y) coordinates is:
 (41637322786646325214887832269588396900663353932545912953362782457239403430124L,
 16388935128781238405526710466724741593761085120864331449066658622400339362166L)
Public Key (hex) is:
 045c0de3b9c8ab18dd04e3511243ec2952002dbfadc864b9628910169d9b9b00ec↵
243bcefdd4347074d44bd7356d6a53c495737dd96295e2a9374bf5f02ebfc176
Compressed Public Key (hex) is:
 025c0de3b9c8ab18dd04e3511243ec2952002dbfadc864b9628910169d9b9b00ec
Bitcoin Address (b58check) is:
 1thMirt546nngXqyPEz532S8fLwbozud8
Compressed Bitcoin Address (b58check) is:
 14cxpo3MBCYYWCgF74SWTdcmxipnGUsPw3

例4-6是另外一個示例,使用的是Python ECDSA庫來做橢圓曲線計算而非使用bitcoin的庫。

例4-6 使用在比特幣金鑰中的橢圓曲線演算法的指令碼

import ecdsa
import random
from ecdsa.util import string_to_number, number_to_string

# secp256k1, http://www.oid-info.com/get/1.3.132.0.10
_p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2FL
_r = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141L
_b = 0x0000000000000000000000000000000000000000000000000000000000000007L
_a = 0x0000000000000000000000000000000000000000000000000000000000000000L
_Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798L
_Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8L
curve_secp256k1 = ecdsa.ellipticcurve.CurveFp(_p, _a, _b)
generator_secp256k1 = ecdsa.ellipticcurve.Point(curve_secp256k1, _Gx, _Gy, _r)
oid_secp256k1 = (1, 3, 132, 0, 10)
SECP256k1 = ecdsa.curves.Curve("SECP256k1", curve_secp256k1, generator_secp256k1,
oid_secp256k1)
ec_order = _r

curve = curve_secp256k1
generator = generator_secp256k1

def random_secret():
    random_char = lambda: chr(random.randint(0, 255))
    convert_to_int = lambda array:     int("".join(array).encode("hex"), 16) 
    byte_array = [random_char() for i in range(32)]
    return convert_to_int(byte_array)

def get_point_pubkey(point): 
    if point.y() & 1:
        key = '03' + '%064x' % point.x() 
    else:
        key = '02' + '%064x' % point.x() 
    return key.decode('hex')

def get_point_pubkey_uncompressed(point): 
    key='04'+\
        '%064x' % point.x() + \
        '%064x' % point.y() 
    return key.decode('hex')

# Generate a new private key.
secret = random_secret() 
print "Secret: ", secret

# Get the public key point.
point = secret * generator 
print "EC point:", point

print "BTC public key:", get_point_pubkey(point).encode("hex")

# Given the point (x, y) we can create the object using:
point1 = ecdsa.ellipticcurve.Point(curve, point.x(), point.y(), ec_order) 
assert point1 == point

例4-7顯示了執行指令碼的結果。

例4-7 安裝Python ECDSA庫,執行ec_math.py指令碼

running the ec_math.py script
$ # Install Python PIP package manager
$ sudo apt-get install python-pip
$ # Install the Python ECDSA library
$ sudo pip install ecdsa
$ # Run the script
$ python ec-math.py
Secret:
38090835015954358862481132628887443905906204995912378278060168703580660294000
EC point:
(70048853531867179489857750497606966272382583471322935454624595540007269312627,
105262206478686743191060800263479589329920209527285803935736021686045542353380)
BTC public key: 029ade3effb0a67d5c8609850d797366af428f4a0d5194cb221d807770a1522873

4.4 比特幣錢包

錢包是私鑰的容器,通常通過有序檔案或者簡單的資料庫實現。另外一種製作私鑰的途徑是 確定性金鑰生成。在這裡你可以用原先的私鑰,通過單向雜湊函式來生成每一個新的私鑰,並將新生成的金鑰按順序連線。只要你可以重新建立這個序列,你只需要第一個私鑰(稱作種子、主私鑰)來生成它們。在本節中,我們將會檢查不同的私鑰生成方法及其錢包結構。


比特幣錢包只包含私鑰而不是比特幣。每一個使用者有一個包含多個私鑰的錢包。錢包中包含成對的私鑰和公鑰。使用者用這些私鑰來簽名交易,從而證明它們擁有交易的輸出(也就是其中的比特幣)。比特幣是以交易輸出的形式來儲存在區塊鏈中(通常記為vout或txout)。

4.4.1 非確定性(隨機)錢包

在最早的一批比特幣客戶端中,錢包只是隨機生成的私鑰集合。這種型別的錢包被稱作零型非確定錢包。舉個例子,比特幣核心客戶端預先生成100個隨機私鑰,從最開始就生成足夠多的私鑰並且每把鑰匙只使用一次。這種型別的錢包有一個暱稱“Just a Bunch Of Keys(一堆私鑰)”簡稱JBOK。這種錢包現在正在被確定性錢包替換,因為它們難以管理、備份以及匯入。隨機鑰匙的缺點就是如果你生成很多,你必須儲存它們所有的副本。這就意味著這個錢包必須被經常性地備份。每一把鑰匙都必須備份,否則一旦錢包不可訪問時,錢包所控制的資金就付之東流。這種情況直接與避免地址重複使用的原則相沖突——每個比特幣地址只能用一次交易。地址通過關聯多重交易和對方的地址重複使用會減少隱私。0型非確定性錢包並不是錢包的好選擇,尤其是當你不想重複使用地址而創造過多的私鑰並且要儲存它們。雖然比特幣核心客戶包含0型錢包,但比特幣的核心開發者並不想鼓勵大家使用。下圖表示包含有鬆散結構的隨機鑰匙的集合的非確定性錢包。

4.4.2 確定性(種子)錢包

確定性,或者“種子”錢包包含通過使用單項離散方程而可從公共的種子生成的私鑰。種子是隨機生成的數字。這個數字也含有比如索引號碼或者可生成私鑰的“鏈碼”(參見“4.4.4 分層確定性錢包(BIP0032/BIP0044)”一節)。在確定性錢包中,種子足夠收回所有的已經產生的私鑰,所以只用在初始建立時的一個簡單備份就足以搞定。並且種子也足夠讓錢包輸入或者輸出。這就很容易允許使用者的私鑰在錢包之間輕鬆轉移輸入。

4.4.3 助記碼詞彙

助記碼詞彙是英文單詞序列代表(編碼)用作種子對應所確定性錢包的隨機數。單詞的序列足以重新建立種子,並且從種子那裡重新創造錢包以及所有私鑰。在首次建立錢包時,帶有助記碼的,執行確定性錢包的錢包的應用程式將會向使用者展示一個12至24個詞的順序。單詞的順序就是錢包的備份。它也可以被用來恢復以及重新創造應用程式相同或者相容的錢包的鑰匙。助記碼程式碼可以讓使用者複製錢包更容易一些,因為它們相比較隨機數字順序來說,可以很容易地被讀出來並且正確抄寫。

助記碼被定義在比特幣的改進建議39中(參見"附錄2 比特幣改進協議[bip0039]”),目前還處於草案狀態。需注意的是,BIP0039是一個建議草案而不是標準方案。具體地來說,電子錢包和BIP0039使用不同的標準且對應不同組的詞彙。Trezor錢包以及一些其他錢包使用BIP0039,但是BIP0039和電子錢包的執行不相容。

BIP0039定義助記碼和種子的建立過程如下:

1.創造一個128到256位的隨機順序(熵)。 
2.提出SHA256雜湊前幾位,就可以創造一個隨機序列的校驗和。 
3.把校驗和加在隨機順序的後面。 
4.把順序分解成11位的不同集合,並用這些集合去和一個預先已經定義的2048個單詞字典做對應。 
5.生成一個12至24個詞的助記碼。

表4-5表示了熵資料的大小和助記碼單詞的長度之間的關係。

表4-5 助記碼:熵及欄位長度

熵(bits) 校驗符(bits) 熵+校驗符 欄位長
128 4 132 12
160 5 165 15
192 6 198 18
224 7 231 21
256 8 264 24

助記碼錶示128至256位數。這可以通過使用私鑰抻拉函式PBKDF2來匯出更長的(512位)的種子。所得的種子可以用來創造一個確定性錢包以及其所派生的所有鑰匙。

表4-6和表4-7展示了一些助記碼的例子和它所生成的種子。

表4-6 128位熵的助記碼以及所產生的種子

負熵輸入 (128 bits) 0c1e24e5917779d297e14d45f14e1a1a
助記碼 (12 個單詞) army van defense carry jealous true garbage claim echo media make crunch
種子 (512 bits)           3338a6d2ee71c7f28eb5b882159634cd46a898463e9d2d0980f8e80dfbba5b0fa0291e5fb88 8a599b44b93187be6ee3ab5fd3ead7dd646341b2cdb8d08d13bf

表4-7 256位熵的助記碼以及所產生的種子

負熵輸入 (256 bits) 2041546864449caff939d32d574753fe684d3c947c3346713dd8423e74abcf8c
助記碼 (24個單詞) cake apple borrow silk endorse fitness top denial coil riot stay wolf luggage oxygen faint major edit measure invite love trap field dilemma oblige
種子 (512 bits)           3972e432e99040f75ebe13a660110c3e29d131a2c808c7ee5f1631d0a977fcf473bee22 fce540af281bf7cdeade0dd2c1c795bd02f1e4049e205a0158906c343

4.4.4 分層確定性錢包(BIP0032/BIP0044)

確定性錢包被開發成更容易從單個“種子”中生成許多關鍵的鑰匙。最高階的來自確定性錢包的形是通過BIP0032標準生成的 the hierarchical deterministic wallet or HD wallet defined。分層確定性錢包包含從數結構所生成的鑰匙。這種母鑰匙可以生成子鑰匙的序列。這些子鑰匙又可以衍生出孫鑰匙,以此無窮類推。這個樹結構表如下圖所示。


如果你想安裝執行一個比特幣錢包,你需要建造一個符合BIP0032和BIP0044標準的HD錢包。

HD錢包提供了隨機(不確定性)鑰匙有兩個主要的優勢。第一,樹狀結構可以被用來表達額外的組織含義。比如當一個特定分支的子金鑰被用來接收交易收入並且有另一個分支的子金鑰用來負責支付花費。不同分支的金鑰都可以被用在企業環境中,這就可以支配不同的分支部門,子公司,具體功能以及會計類別。

HD錢包的第二個好處就是它可以允許讓使用者去建立一個公共金鑰的序列而不需要訪問相對應的私鑰。這可允許HD錢包在不安全的伺服器中使用或者在每筆交易中發行不同的公共鑰匙。公共鑰匙不需要被預先載入或者提前衍生,但是在伺服器中不具有可用來支付的私鑰。

從種子中創造HD錢包

HD錢包從單個root seed中建立,為128到256位的隨機數。HD錢包的所有的確定性都衍生自這個根種子。任何相容HD錢包的根種子也可重新創造整個HD錢包。所以簡單的轉移HD錢包的根種子就讓HD錢包中所包含的成千上百萬的金鑰被複制,儲存匯出以及匯入。根種子一般總是被表示為a mnemonic word sequence,正如"4.4.3 助記碼詞彙"一節所表述的,助記碼詞彙可以讓人們更容易地抄寫和儲存。

建立主金鑰以及HD錢包地主鏈程式碼的過程如下圖所示。

根種子輸入到HMAC-SHA512演算法中就可以得到一個可用來創造master private key(m) 和 a master chain code的雜湊。主私鑰(m)之後可以通過使用我們在本章先前看到的那個普通橢圓曲線m * G過程生來成相對應的主公鑰(M)。鏈程式碼可以給從母金鑰中創造子金鑰的那個方程中引入的熵。

私有子金鑰的衍生

分層確定性錢包使用CKD(child key derivation)方程去從母金鑰衍生出子金鑰。

子金鑰衍生方程是基於單項雜湊方程。這個方程結合了:

• 一個母私鑰或者公共鑰匙(ECDSA未壓縮鍵) 
• 一個叫做鏈碼(256 bits)的種子 
• 一個索引號(32 bits)

鏈碼是用來給這個過程引入看似的隨機資料的,使得索引不能充分衍生其他的子金鑰。因此,有了子金鑰並不能讓它發現自己的相似子金鑰,除非你已經有了鏈碼。最初的鏈碼種子(在密碼樹的根部)是用隨機資料構成的,隨後鏈碼從各自的母鏈碼中衍生出來。

這三個專案相結合並雜湊可以生成子金鑰,如下。

母公共鑰匙——鏈碼——以及索引號合併在一起並且用HMAC-SHA512方程雜湊之後可以產生512位的雜湊。所得的雜湊可被拆分為兩部分。雜湊右半部分的256位產出可以給子鏈當鏈碼。左半部分256位雜湊以及索引碼被載入在母私鑰上來衍生子私鑰。在圖4-11中,我們看到這種這個說明——索引集被設為0去生產母金鑰的第0個子金鑰(第一個通過索引)。

圖4-11 延長母私鑰去創造子私鑰

改變索引可以讓我們延長母金鑰以及創造序列中的其他子金鑰。比如子0,子1,子2等等。每一個母金鑰可以右20億個子金鑰。

向密碼樹下一層重複這個過程,每個子金鑰可以依次成為母金鑰繼續創造它自己的子金鑰,直到無限代。

使用衍生的子金鑰

子私鑰不能從非確定性(隨機)金鑰中被區分出來。因為衍生方程是單向方程,所以子金鑰不能被用來發現他們的母金鑰。子金鑰也不能用來發現他們的相同層級的姊妹金鑰。如果你有第n個子金鑰,你不能發現它前面的(第n-1)或者後面的子金鑰(n+1)或者在同一順序中的其他子金鑰。只有母金鑰以及鏈碼才能得到所有的子金鑰。沒有子鏈碼的話,子金鑰也不能用來衍生出任何孫金鑰。你需要同時有子金鑰以及對應的鏈碼才能建立一個新的分支來衍生出孫金鑰。

那子私鑰自己可被用做什麼呢?它可以用來做公共鑰匙和比特幣地址。之後它就可以被用那個地址來簽署交易和支付任何東西。


子金鑰、對應的公共鑰匙以及比特幣地址都不能從隨機創造的金鑰和地址中被區分出來。事實是它們所在的序列,在創造他們的HD錢包方程之外是不可見的。一旦被創造出來,它們就和“正常”鑰匙一樣運行了。

擴充套件金鑰

正如我們之前看到的,金鑰衍生方程可以被用來創造鑰匙樹上任何層級的子金鑰。這隻需要三個輸入量:一個金鑰,一個鏈碼以及想要的子金鑰的索引。金鑰以及鏈碼這兩個重要的部分被結合之後,就叫做extended key。術語“extended key”也被認為是“可擴充套件的金鑰”是因為這種金鑰可以用來衍生子金鑰。

擴充套件金鑰可以簡單地被儲存並且表示為簡單的將256位金鑰與256位鏈碼所並聯的512位序列。有兩種擴充套件金鑰。擴充套件的私鑰是私鑰以及鏈碼的結合。它可被用來衍生子私鑰(子私鑰可以衍生子公共金鑰)公共鑰匙以及鏈碼組成擴充套件公共鑰匙。這個鑰匙可以用來擴充套件子公共鑰匙,見“4.1.6 生成公鑰”。

想象一個擴充套件金鑰作為HD錢包中鑰匙樹結構的一個分支的根。你可以衍生出這個分支的剩下所有部分。擴充套件私人鑰匙可以建立一個完整的分支而擴充套件公共鑰匙只能夠創造一個公共鑰匙的分支。


一個擴充套件鑰匙包括一個私鑰(或者公共鑰匙)以及一個鏈碼。一個擴充套件金鑰可以創造出子金鑰並且能創造出在鑰匙樹結構中的整個分支。分享擴充套件鑰匙就可以訪問整個分支。