比特幣學習筆記——————4、金鑰、地址、錢包

FLy_鵬程萬里發表於2018-06-12

無意中發現了一個巨牛巨牛的人工智慧教程,忍不住分享一下給大家。教程不僅是零基礎,通俗易懂,小白也能學,而且非常風趣幽默,還時不時有內涵段子,像看小說一樣,哈哈~我正在學習中,覺得太牛了,所以分享給大家。點這裡可以跳轉到教程!

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 示例:同樣的私鑰,不同的格式

格式 私鑰
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座標,就可以通過解方程y2 mod 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錢包中鑰匙樹結構的一個分支的根。你可以衍生出這個分支的剩下所有部分。擴充套件私人鑰匙可以建立一個完整的分支而擴充套件公共鑰匙只能夠創造一個公共鑰匙的分支。

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

擴充套件金鑰通過Base58Check來編碼,從而能輕易地在不同的BIP0032-相容錢包之間匯入匯出。擴充套件金鑰編碼用的Base58Check使用特殊的版本號,這導致在Base58編碼字元中,出現字首“xprv”和“xpub”。這種字首可以讓編碼更易被識別。因為擴充套件金鑰是512或者513位,所以它比我們之前所看到的Base58Check-encoded串更長一些。

這是一個在Base58Check中編碼的擴充套件私鑰的例子:

xprv9tyUQV64JT5qs3RSTJkXCWKMyUgoQp7F3hA1xzG6ZGu6u6Q9VMNjGr67Lctvy5P8oyaYAL9CAWrUE9i6GoNMKUga5biW6Hx4tws2six3b9c

這是在Base58Check中編碼的對應的擴充套件公共鑰匙:

xpub67xpozcx8pe95XVuZLHXZeG6XWXHpGq6Qv5cmNfi7cS5mtjJ2tgypeQbBs2UAR6KECeeMVKZBPLrtJunSDMstweyLXhRgPxdp14sk9tJPW9

公共子鑰匙推導

正如之前提到的,分層確定性錢包的一個很有用的特點就是可以不通過私鑰而直接從公共母鑰匙派生出公共子鑰匙的能力。這就給了我們兩種去衍生子公共鑰匙的方法:或者通過子私鑰,再或者就是直接通過母公共鑰匙。

因此,擴充套件的公共鑰匙可以再HD錢包結構的分支中,被用來衍生所有的公鑰(且只有公共鑰匙)。

這種快捷方式可以用來創造非常保密的public-key-only配置。在配置中,伺服器或者應用程式不管有沒有私鑰,都可以有擴充套件公共鑰匙的副本。這種配置可以創造出無限數量的公共鑰匙以及比特幣地址。但是不可以花送到這個地址裡的任何比特幣。與此同時,在另一種更保險的伺服器上,擴充套件私鑰可以衍生出所有的對應的可簽署交易以及花錢的私鑰。

這種方案的一個常見的方案是安裝一個擴充套件的公共鑰匙在服務電商公共程式的網路伺服器上。網路伺服器可以使用這個公共鑰匙衍生方程去給每一筆交易(比如客戶的購物車)創造一個新的比特幣地址。但為了避免被偷,網路服務商不會有任何私鑰。沒有HD錢包的話,唯一的方法就是在不同的安全伺服器上創造成千上萬個比特幣地址,之後就提前上傳到電商伺服器上。這種方法比較繁瑣而且要求持續的維護來確保電商伺服器不“用光”公共鑰匙。

這種解決方案的另一種常見的應用是冷藏或者硬體錢包。在這種情況下,擴充套件的私鑰可以被儲存在紙質錢包中或者硬體裝置中(比如 Trezor 硬體錢包)與此同時擴充套件公共鑰匙可以線上儲存。使用者可以根據意願創造“接收”地址而私鑰可以安全地線上下被儲存。為了支付資金,使用者可以使用擴充套件的私鑰離線簽署比特幣客戶或者通過硬體錢包裝置(比如Trezor)簽署交易。圖4-12闡述了擴充套件母公共鑰匙來衍生子公共鑰匙的傳遞機制。

圖4-12 擴充套件母公共鑰匙來創造一個子公共鑰匙

硬化子金鑰的衍生

從擴充套件公共鑰匙衍生一個分支公共鑰匙的能力是很重要的,但牽扯一些風險。訪問擴充套件公共鑰匙並不能得到訪問子私人金鑰的途徑。但是,因為擴充套件公共鑰匙包含有鏈碼,如果子私鑰被知道或者被洩漏的話,鏈碼就可以被用來衍生所有的其他子私鑰。一個簡單地洩露的私鑰以及一個母鏈碼,可以暴露所有的子金鑰。更糟糕的是,子私鑰與母鏈碼可以用來推斷母私鑰。

為了應對這種風險,HD錢包使用一種叫做hardened derivation的替代衍生方程。這就“打破”了母公共鑰匙以及子鏈碼之間的關係。這個硬化衍生方程使用了母私鑰去推到子鏈碼,而不是母公共鑰匙。這就在母/子順序中創造了一道“防火牆”——有鏈碼但並不能夠用來推運算元鏈碼或者姊妹私鑰。強化的衍生方程看起來幾乎與一般的衍生的子私鑰相同,不同的是是母私鑰被用來輸入雜湊方程中而不是母公共鑰匙,如圖4-13所示。

圖4-13 子金鑰的強化衍生;忽略了母公共金鑰

當強化私鑰衍生方程被使用時,得到的子私鑰以及鏈碼與使用一般衍生方程所得到的結果完全不同的。得到的金鑰“分支”可以被用來生產不易被攻擊的擴充套件公共鑰匙,因為它所含的鏈碼不能被用來開發或者暴露任何私鑰。強化的衍生也因此被用來在上一層級,使用擴充套件公共鑰匙的的金鑰樹中創造“間隙”。

簡單地來說,如果你想要利用擴充套件公共鑰匙的便捷來衍生公共鑰匙的分支而不將你自己暴露在洩露擴充套件鏈碼的風險下,你應該從強化母私鑰,而不是一般的母私鑰,來衍生公共鑰匙。最好的方式是,為了避免了推到出主鑰匙,主鑰匙所衍生的第一層級的子鑰匙最好使用強化衍生。

正常衍生和強化衍生的索引號碼

用在衍生方程中的索引號碼是32位的整數。為了區分金鑰是從正常衍生方程中衍生出來還是從強化衍生方程中產出,這個索引號被分為兩個範圍。索引號在0和231–1(0x0 to 0x7FFFFFFF)之間的是隻被用在常規衍生。索引號在231和232–1(0x80000000 to 0xFFFFFFFF)之間的只被用在強化衍生方程。因此,索引號小於231就意味著子金鑰是常規的,而大於或者等於231的子金鑰就是強化型的。

為了讓索引號碼更容易被閱讀和展示,強化子密碼的索引號碼是從0開始展示的,但是右上角有一個小撇號。第一個常規子金鑰因此被表述為0,但是第一個強化子金鑰(索引號為0x80000000)就被表示為0'。第二個強化金鑰依序有了索引號0x80000001,且被顯示為1',以此類推。當你看到HD錢包索引號i',這就意味著 231+i。

HD錢包金鑰識別符(路徑)

HD錢包中的金鑰是用“路徑”命名的,且每個級別之間用斜槓(/)字元來表示(見表4-8)。由主私鑰衍生出的私鑰起始以“m”打頭。因此,第一個母金鑰生成的子私鑰是m/0。第一個公共鑰匙是M/0。第一個子金鑰的子金鑰就是m/0/1,以此類推。

金鑰的“祖先”是從右向左讀,直到你達到了衍生出的它的主金鑰。舉個例子,識別符號m/x/y/z描述的是子金鑰m/x/y的第z個子金鑰。而子金鑰m/x/y又是m/x的第y個子金鑰。m/x又是m的第x個子金鑰。

表4-8 HD錢包路徑的例子

HD path 金鑰描述
m/0 從主私鑰(m)衍生出的第一個(0)子金鑰。
m/0/0 第一個私人子金鑰(m/0)的子金鑰。
m/0'/0 第一個子強化金鑰first hardened child(m/0')的第一個常規子金鑰。
m/1/0 第2個子金鑰(m/1)的第一個常規子金鑰
M/23/17/0/0 主金鑰衍生出的第24個子金鑰所衍生出的第17個子金鑰的第一個子金鑰所衍生出的第一個子金鑰。

HD錢包樹狀結構的導航

HD錢包樹狀結構提供了極大的靈活性。每一個母擴充套件金鑰有40已個子金鑰:20億個常規子金鑰和20億個強化子金鑰。而每個子金鑰又會有40億個子金鑰並且以此類推。只要你願意,這個樹結構可以無限類推到無窮代。但是,又由於有了這個靈活性,對無限的樹狀結構進行導航就變得異常困難。尤其是對於在不同的HD錢包之間進行轉移交易,因為內部組織到內部分支以及亞分支的可能性是無窮的。

兩個比特幣改進建議(BIPs)提供了這個複雜問的解決辦法——通過建立幾個HD錢包樹的提議標準。BIP0043提出使用第一個強化子索引作為特殊的識別符號表示樹狀結構的“purpose”。基於BIP0043,HD錢包應該使用且只用第一層級的樹的分支,而且有索引號碼去識別結構並且有名稱空間來定義剩餘的樹的目的地。舉個例子,HD錢包只使用分支m/i'/是為了表明那個被索引號“i”定義的特殊為目地。

在BIP0043標準下,為了延長的那個特殊規範,BIP0044提議了多賬戶結構作為“purpose”。所有遵循BIP0044的HD錢包依據只使用樹的第一個分支的要求而被定義:m/44'/。

BIP0044指定了包含5個預定義樹狀層級的結構:

m / purpose' / coin_type' / account' / change / address_index

第一層的目的地總是被設定為44'。第二層的“coin_type”特指密碼貨幣硬幣的種類並且允許多元貨幣HD錢包中的貨幣在第二個層級下有自己的亞樹狀結構。目前有三種貨幣被定義:Bitcoin is m/44'/0'、Bitcoin Testnet is m/44'/1',以及Litecoin is m/44'/2'。

樹的第三層級是“account”,這可以允許使用者為了會計或者組織目的,而去再細分他們的錢包到獨立的邏輯性亞賬戶。舉個例子,一個HD錢包可能包含兩個比特幣“賬戶”:m/44'/0'/0' 和 m/44'/0'/1'。每個賬戶都是它自己亞樹的根。

第四層級就是“change”。每一個HD錢包有兩個亞樹,一個是用來接收地址一個是用來創造找零地址。注意無論先前的層級是否使用是否使用強化衍生,這一層級使用的都是常規衍生。這是為了允許這一層級的樹可以在可供不安全環境下,輸出擴充套件的公共鑰匙。被HD錢包衍生的可用的地址是第四層級的子級,就是第五層級的樹的“address_index”。比如,第三個層級的主賬戶收到比特幣支付的地址就是 M/44'/0'/0'/0/2。表4-9展示了更多的例子。

表4-9 BIP0044 HD 錢包結構的例子

HD 路徑 主要描述
M/44'/0'/0'/0/2 第三個收到公共鑰匙的主比特幣賬戶
M/44'/0'/3'/1/14 第十五改變地址公鑰的第四個比特幣賬戶
m/44'/2'/0'/0/1 為了簽署交易的在萊特幣主賬戶的第二個私鑰

使用比特幣瀏覽器實驗比特幣錢包

依據第3章介紹的使用比特幣瀏覽管理器命令工具,你可以試著生產和延伸BIP0032確定性金鑰以及將它們用不同的格式進行展示:

$ sx hd-seed > m # create a new master private key from a seed and store in file "m"
$ cat m # show the master extended private key
96 | Chapter 4: Keys, Addresses, Wallets
xprv9s21ZrQH143K38iQ9Y5p6qoB8C75TE71NfpyQPdfGvzghDt39DHPFpovvtWZaR- gY5uPwV7RpEgHs7cvdgfiSjLjjbuGKGcjRyU7RGGSS8Xa
$ cat m | sx hd-pub 0 # generate the M/0 extended public key xpub67xpozcx8pe95XVuZLHXZeG6XWXHpGq6Qv5cmNfi7cS5mtjJ2tgypeQbBs2UAR6KE- CeeMVKZBPLrtJunSDMstweyLXhRgPxdp14sk9tJPW9
$ cat m | sx hd-priv 0 # generate the m/0 extended private key xprv9tyUQV64JT5qs3RSTJkXCWKMyUgoQp7F3hA1xzG6ZGu6u6Q9VMNjGr67Lctvy5P8oyaYAL9CA- WrUE9i6GoNMKUga5biW6Hx4tws2six3b9c
$ cat m | sx hd-priv 0 | sx hd-to-wif # show the private key of m/0 as a WIF L1pbvV86crAGoDzqmgY85xURkz3c435Z9nirMt52UbnGjYMzKBUN
$ cat m | sx hd-pub 0 | sx hd-to-address # show the bitcoin address of M/0 1CHCnCjgMNb6digimckNQ6TBVcTWBAmPHK
$ cat m | sx hd-priv 0 | sx hd-priv 12 --hard | sx hd-priv 4 # generate m/ 0/12'/4 xprv9yL8ndfdPVeDWJenF18oiHguRUj8jHmVrqqD97YQHeTcR3LCeh53q5PXPkLsy2kRaqgwoS6YZ- BLatRZRyUeAkRPe1kLR1P6Mn7jUrXFquUt

4.5 高階金鑰和地址

在以下部分中,我們將看到高階形式的金鑰和地址,諸如加密私鑰、指令碼和多重簽名地址,靚號地址,和紙錢包。

4.5.1 加密私鑰(BIP0038)

私鑰必須保密。私鑰的機密性需求事實情況是,在實踐中相當難以實現,因為該需求與同樣重要的安全物件可用性相互矛盾。當你需要為了避免私鑰丟失而儲存備份時,會發現維護私鑰私密性是一件相當困難的事情。通過密碼加密內有私鑰的錢包可能要安全一點,但那個錢包也需要備份。有時,例如使用者因為要升級或重灌錢包軟體,而需要把金鑰從一個錢包轉移到另一個。私鑰備份也可能需要儲存在紙張上(參見“4.5.4 紙錢包”一節)或者外部儲存介質裡,比如U盤。但如果一旦備份檔案失竊或丟失呢?這些矛盾的安全目標推進了便攜、方便、可以被眾多不同錢包和比特幣客戶端理解的加密私鑰標準BIP0038的出臺。

BIP0038提出了一個通用標準,使用一個口令加密私鑰並使用Base58Check對加密的私鑰進行編碼,這樣加密的私鑰就可以安全地儲存在備份介質裡,安全地在錢包間傳輸,保持金鑰在任何可能被暴露情況下的安全性。這個加密標準使用了AES,這個標準由NIST建立,並廣泛應用於商業和軍事應用的資料加密。

BIP0038加密方案是:輸入一個比特幣私鑰,通常使用WIF編碼過,base58chek字串的字首“5”。此外BIP0038加密方案需要一個長密碼作為口令,通常由多個單詞或一段複雜的數字字母字串組成。BIP0038加密方案的結果是一個由base58check編碼過的加密私鑰,字首為6P。如果你看到一個6P開頭的的金鑰,這就意味著該金鑰是被加密過,並需要一個口令來轉換(解碼)該金鑰回到可被用在任何錢包WIF格式的私鑰(字首為5)。許多錢包APP現在能夠識別BIP0038加密過的私鑰,會要求使用者提供口令解碼並匯入金鑰。第三方APP,諸如非常好用基於瀏覽器的Bit Address,可以被用來解碼BIP00038的金鑰。

最通常使用BIP0038加密的金鑰用例是紙錢包——一張紙張上備份私鑰。只要使用者選擇了強口令,使用BIP0038加密的私鑰的紙錢包就無比的安全,這也是一種很棒的比特幣離線儲存方式(也被稱作“冷儲存”)。

在bitaddress.org上測試表4-10中加密金鑰,看看如何輸入密碼以得到加密金鑰。

表4-10 BIP0038加密私鑰例子

私鑰(WIF) 5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1Jcn
密碼 MyTestPassphrase
加密私鑰(BIP0038) 6PRTHL6mWa48xSopbU1cKrVjpKbBZxcLRRCdctLJ3z5yxE87MobKoXdTsJ

4.5.2 P2SH (Pay-to-Script Hash)和多重簽名地址

正如我們所知,傳統的比特幣地址從數字1開頭,來源於公鑰,而公鑰來源於私鑰。雖然任何人都可以將比特幣傳送到一個1開頭的地址,但比特幣只能在通過相應的私鑰簽名和公鑰雜湊值後才能消費。

以數字3開頭的比特幣地址是P2SH地址,有時被錯誤的稱謂多重簽名或多重簽名地址。他們指定比特幣交易中受益人作為雜湊的指令碼,而不是公鑰的所有者。這個特性在2012年1月由BIP0016引進,目前因為BIP0016提供了增加功能到地址本身的機會而被廣泛的採納。不同於P2PKH交易傳送資金到傳統1開頭的比特幣地址,資金被髮送到3開頭的地址時,需要的不僅僅是一個公鑰的雜湊值,同時也需要一個私鑰簽名作為所有者證明。在建立地址的時候,這些要求會被定義在指令碼中,所有對地址的輸入都會被這些要求阻隔。

一個P2SH地址從事務指令碼中建立,它定義誰能消耗這個事務輸出。(132頁“P2SH(Pay-to-Script-Hash)”一節對此有詳細的介紹)編碼一個P2SH地址涉及使用一個在建立比特幣地址用到過的雙重雜湊函式,並且只能應用在指令碼而不是公鑰:

script hash = RIPEMD160(SHA256(script))

指令碼雜湊的結果是由Base58Check編碼字首為5的版本、編碼後得到開頭為3的編碼地址。一個P2SH地址例子是32M8ednmuyZ2zVbes4puqe44NZumgG92sM。

P2SH 不一定是多重簽名的交易。雖然P2SH地址通常都是代表多重簽名,但也可能是其他型別的交易指令碼。

4.5.2.1 多重簽名地址和P2SH

目前,P2SH函式最常見的實現是用於多重簽名地址指令碼。顧名思義,底層指令碼需要多個簽名來證明所有權,此後才能消費資金。設計比特幣多重簽名特性是需要從總共N個金鑰中需要M個簽名(也被稱為“閾值”),被稱為M-N多簽名,其中M是等於或小於N。例如,第一章中提到的咖啡店主鮑勃使用多重簽名地址需要1-2簽名,一個是屬於他的金鑰和一個屬於他同伴的金鑰,以確保其中一方可以簽署度過一個事務鎖定輸出到這個地址。這類似於傳統的銀行中的一個“聯合賬戶”,其中任何一方配偶可以憑藉單一簽名消費。或Gopesh, Bob僱傭的網頁設計師創立一個網站,可能為他的業務需要一個2-3的多簽名地址,確保沒有資金會被花費除非至少兩個業務合作伙伴簽署這筆交易。

我們將會在第五章節探索如何使用P2SH地址建立事務用來消費資金。

4.5.3 比特幣靚號地址

靚號地址包含了可讀資訊的有效比特幣地址。例如,1LoveBPzzD72PUXLzCkYAtGFYmK5vYNR33就是包含了Base-58字母love的。靚號地址需要生成並通過數十億的候選私鑰測試,直到一個私鑰能生成具有所需圖案的比特幣地址。雖然有一些優化過的靚號生成演算法,該方法必須涉及隨機上選擇一個私鑰,生成公鑰,再生成比特幣地址,並檢查是否與所要的靚號圖案相匹配,重複數十億次,直到找到一個匹配。

一旦找到一個匹配所要圖案的靚號地址,來自這個靚號地址的私鑰可以和其他地址相同的方式被擁有者消費比特幣。靚號地址不比其他地址具有更多安全性。它們依靠和其他地址相同的ECC和SHA。你無法比任何別的地址更容易的獲得一個靚號圖案開頭的私鑰。

在第一章中,我們介紹了Eugenia,一位在菲律賓工作的兒童慈善總監。我們假設Eugenia組織了一場比特幣募捐活動,並希望使用靚號比特幣地址來宣佈這個募捐活動。Eugenia將會創造一個以1Kids開頭的靚號地址來促進兒童慈善募捐的活動。讓我們看看這個靚號地址如何被建立,這個靚號地址對Eugenia慈善募捐的安全性又意味著什麼。

4.5.3.1 生成靚號地址

我們必須認識到使用來自Base58字母表中簡單符號來代表比特幣地址是非常重要的。搜尋“1kids”開頭的圖案我們會發現從1Kids11111111111111111111111111111到1Kidszzzzzzzzzzzzzzzzzzzzzzzzzzzzz的地址。這些以“1kid”開頭的地址範圍中大約有58的29次方地址。表4-11顯示了這些有“1kids”字首的地址。

表4-11 “1Kids”靚號的範圍

From 1Kids11111111111111111111111111111
To 1Kidszzzzzzzzzzzzzzzzzzzzzzzzzzzzz

我們把“1Kids”這個字首當作數字,我們可以看看比特幣地址中這個字首出現的頻率。如果是一臺普通效能的桌面電腦,沒有任何特殊的硬體,可以每秒發現大約10萬個金鑰。

表4-12 靚號的出現的頻率(1KidsCharity)以及生成所需時間

長度 地址字首 概率 平均生成時間
1 1K 1/58 < 1毫秒
2 1Ki 1/3,364 50毫秒
3 1Kid 1/(195*103) < 2秒
4 1Kids 1/(11*106) 1分鐘
5 1KidsC 1/(656*106) 1小時
6 1KidsCh 1/(38*109) 2天
7 1KidsCha 1/(2.2*1012) 3–4 月
8 1KidsChar 1/(128*1012) 13–18年
9 1KidsChari 1/(7*1015) 800年
10 1KidsCharit 1/(400*1015) 46,000年
11 1KidsCharity 1/(23*1018) 250萬年

正如你所見,Eugenia將不會很快地建立出以“1KidsCharity”開頭的靚號地址,即使她有數千臺的電腦同時進行運算。每增加一個字元就會增加58倍的計算難度。超過七個字元的搜尋模式通常需要專用的硬體才能被找出,譬如使用者定製的具有多圖形處理單元(GPU)的桌面級裝置。那些通常是無法繼續在比特幣挖礦中盈利的鑽機,被重新賦予了尋找靚號地址的任務。用GPU系統搜尋靚號的速度比用通用CPU要快很多個量級。

另一種尋找靚號地址的方法是將工作外包給一個礦池裡的靚號礦工們,如靚號礦池中的礦池。一個礦池是一種允許那些GPU硬體通過為他人尋找靚號地址來獲得比特幣的服務。對小額的賬單,Eugenia可以外包搜尋模式為7個字元靚號地址尋找工作,在幾個小時內就可以得到結果,而不必用一個CPU搜尋上幾個月才得到結果。

生成一個靚號地址是一項通過蠻力的過程:嘗試一個隨機金鑰,檢查結果地址是否和所需的圖案相匹配,重複這個過程直到成功找到為止。例4-8是個靚號礦工的例子,用C++程式來尋找靚號地址。這個例子運用到了我們在56頁“其他替代客戶端、資料庫、工具包”一節介紹過的libbitcoin庫。

例4-8 靚號挖掘程式

#include <bitcoin/bitcoin.hpp>

// The string we are searching for
const std::string search = "1kid";

// Generate a random secret key. A random 32 bytes.
bc::ec_secret random_secret(std::default_random_engine& engine); // Extract the Bitcoin address from an EC secret.
std::string bitcoin_address(const bc::ec_secret& secret);
// Case insensitive comparison with the search string.
bool match_found(const std::string& address);

int main() 
{
    std::random_device random; 
    std::default_random_engine engine(random()); 
    // Loop continuously...
    while (true) 
    {
        // Generate a random secret.
        bc::ec_secret secret = random_secret(engine); 
        // Get the address.
        std::string address = bitcoin_address(secret); 
        // Does it match our search string? (1kid)
        if (match_found(address)) 
        {
            // Success!
            std::cout << "Found vanity address! " << address << std::endl; 
            std::cout << "Secret: " << bc::encode_hex(secret) << std::endl; return 0;
        } 
    }
    // Should never reach here!
    return 0; 
}

bc::ec_secret random_secret(std::default_random_engine& engine)
{
    // Create new secret...
    bc::ec_secret secret;
    // Iterate through every byte setting a random value... for (uint8_t& byte: secret)
        byte = engine() % std::numeric_limits<uint8_t>::max(); 
    // Return result.
    return secret;
}

std::string bitcoin_address(const bc::ec_secret& secret) 
{
    // Convert secret to pubkey...
    bc::ec_point pubkey = bc::secret_to_public_key(secret); 
    // Finally create address.
    bc::payment_address payaddr; bc::set_public_key(payaddr, pubkey);
    // Return encoded form.
    return payaddr.encoded(); 
}

bool match_found(const std::string& address) 
{
    auto addr_it = address.begin();
    // Loop through the search string comparing it to the lower case 
    // character of the supplied address.
    for (auto it = search.begin(); it != search.end(); ++it, ++addr_it)
        if (*it != std::tolower(*addr_it)) 
            return false;
    // Reached end of search string, so address matches.
    return true; 
    }

示例程式需要用C編譯器連結libbitcoin庫(此庫需要提前裝入該系統)進行編譯。直接執行vanity-miner的可執行檔案(不用引數,參見例4-9),它就會嘗試碰撞以“1kid”開頭的比特幣地址。

例4-9 編譯並執行vanity-miner程式示例

$ # Compile the code with g++
$ g++ -o vanity-miner vanity-miner.cpp $(pkg-config --cflags --libs libbitcoin) $ # Run the example
$ ./vanity-miner
Found vanity address! 1KiDzkG4MxmovZryZRj8tK81oQRhbZ46YT
Secret: 57cc268a05f83a23ac9d930bc8565bac4e277055f4794cbd1a39e5e71c038f3f
$ # Run it again for a different result
$ ./vanity-miner
Found vanity address! 1Kidxr3wsmMzzouwXibKfwTYs5Pau8TUFn
Secret: 7f65bbbbe6d8caae74a0c6a0d2d7b5c6663d71b60337299a1a2cf34c04b2a623
# Use "time" to see how long it takes to find a result
$ time ./vanity-miner
Found vanity address! 1KidPWhKgGRQWD5PP5TAnGfDyfWp5yceXM
Secret: 2a802e7a53d8aa237cd059377b616d2bfcfa4b0140bc85fa008f2d3d4b225349

real    0m8.868s
user    0m8.828s
sys     0m0.035s

正如我們執行Unix命令time所測出的執行時間所示,示例程式碼要花幾秒鐘來找出匹配“kid”三個字元模板的結果。讀者們可以在原始碼中改變search這一搜尋模板,看一看如果是四個字元或者五個字元的搜尋模板需要花多久時間!

4.5.3.2 靚號地址安全性

靚號地址既可以增加、也可以削弱安全措施,它們著實是一把雙刃劍。用於改善安全性時,一個獨特的地址使對手難以使用他們自己的地址替代你的地址,以欺騙你的顧客支付他們的賬單。不幸的是,靚號地址也可能使得任何人都能建立一個類似於隨機地址的地址,甚至另一個靚號地址,從而欺騙你的客戶。

Eugenia可以讓捐款人捐款到她宣佈的一個隨機生成地址(例如:1J7mdg5rbQyUHENYdx39WVWK7fsLpEoXZy)。或者她可以生成一個以“1Kids”開頭的靚號地址以顯得更獨特。

在這兩種情況下,使用單一固定地址(而不是每比捐款用一個獨立的動態地址)的風險之一是小偷有可能會黑進你的網站,用他自己的網址取代你的網址,從而將捐贈轉移給自己。如果你在不同的地方公佈了你的捐款地址,你的使用者可以在付款之前直觀地檢查以確保這個地址跟在你的網站、郵件和傳單上看到的地址是同一個。在隨機地址1j7mdg5rbqyuhenydx39wvwk7fslpeoxzy的情況下,普通使用者可能會只檢查頭幾個字元“1j7mdg”,就認為地址匹配。使用靚號地址生成器,那些想通過替換類似地址來盜竊的人可以快速生成與前幾個字元相匹配的地址,如表4-13所示。

表4-13 生成匹配某隨機地址的多個靚號

原版隨機地址 1J7mdg5rbQyUHENYdx39WVWK7fsLpEoXZy
4位字元匹配 1J7md1QqU4LpctBetHS2ZoyLV5d6dShhEy
5位字元匹配 1J7mdgYqyNd4ya3UEcq31Q7sqRMXw2XZ6n
6位字元匹配 1J7mdg5WxGENmwyJP9xuGhG5KRzu99BBCX

那靚號地址會不會增加安全性?如果Eugenia生成1Kids33q44erFfpeXrmDSz7zEqG2FesZEN的靚號地址,使用者可能看到靚號圖案的字母和一些字元在上面,例如在地址部分中註明了1Kids33。這樣就會迫使攻擊者生成至少6個字母相匹配的的靚號地址(比之前多2個字元),就要花費比Eugenia多3364倍的靚號圖案。本質上,Eugenia付出的努力(或者靚號池付出的)迫使攻擊者不得不生成更長的靚號圖案。如果Eugenia花錢請礦池生成8個字元的靚號地址,攻擊者將會被逼迫到10字元的境地,那將是個人電腦,甚至昂貴自定義靚號挖掘機或靚號池也無法生成。對Eugenia來說可承擔的起支出,對攻擊者來說則變成了無法承擔支出,特別是如果欺詐的回報不足以支付生成靚號地址所需的費用。

 4.5.4 紙錢包

紙錢包是列印在紙張上的比特幣私鑰。有時紙錢包為了方便起見也包括對應的比特幣地址,但這並不是必要的,因為地址可以從私鑰中匯出。紙錢包是一個非常有效的建立備份或者線下儲存比特幣(即冷錢包)的方式。作為備份機制,一個紙錢包可以提供安全性,以防在電腦硬碟損壞、失竊或意外刪除的情況下造成金鑰的的丟失。作為一個冷儲存的機制,如果紙錢包金鑰線上下生成並永久不在電腦系統中儲存,他們在應對黑客攻擊,鍵盤記錄器,或其他線上電腦欺騙更有安全性。

紙錢包有許多不同的形狀,大小,和外觀設計,但非常基本的原則是一個金鑰和一個地址列印在紙張上。表4-14展現了紙錢包最基本的形式。

表4-14 比特幣紙錢包的私鑰和公鑰的列印形式

公開地址 1424C2F4bC9JidNjjTUZCbUxv6Sa1Mt62x
私鑰(WIF) 5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1Jcn

通過使用工具,就可以很容易地生成紙錢包,譬如使用bitaddress.org網站上的客戶端Javascript生成器。這個頁面包含所有必要的程式碼,甚至在完全失去網路連線的情況下,也可以生成金鑰和紙錢包。若要使用它,先將HTML頁面儲存在本地磁碟或外部U盤。從Internet網路斷開,從瀏覽器中開啟檔案。更方便的,使用一個原始作業系統啟動電腦,比如一個光碟啟動的Linux系統。任何在離線情況下使用這個工具所生成的金鑰,都可以通過USB線在本地印表機上列印出來,從而製造了金鑰只存在紙張上而從未儲存在線上系統上的紙錢包。將這些紙錢包放置在防火容器內,傳送比特幣到對應的比特幣地址上,從而實現了一個簡單但非常有效的冷儲存解決方案。圖4-14展示了通過bitaddress.org 生成的紙錢包。


圖4-14 通過bitaddress.org 生成的普通紙錢包

這個簡單的紙錢包系統的不足之處是那些被列印下來的金鑰容易被盜竊。一個能夠接近這些紙的小偷只需偷走紙或者用把拍攝紙上的金鑰,就能控制被這些金鑰鎖定的比特幣。一個更復雜的紙錢包儲存系統使用BIP0038加密的私鑰。列印在紙錢包上的這些私鑰被其所有者記住的一個口令保護起來。沒有口令,這些被加密過的金鑰也是毫無用處的。但它們仍舊優於用口令保護,因為這些金鑰從沒有線上過,並且必須從保險箱或者其他物理的安全儲存中匯出。圖4-15展示了通過bitaddress.org 生成的加密紙錢包。

 


圖4-15 通過bitaddress.org 生成的加密紙錢包。密碼是“test”。

雖然你可以多次存款到紙錢包中,但是你最好一次性提款,一次性提取裡面所有的資金。因為如果你提取的金額少於其中的金額的話,會生成一個找零地址。並且,你所用的電腦可能被病毒感染,那麼就有可能洩露私鑰。一次性提款可以減少私鑰洩露的風險,如果你所需的金額比較少,那麼請把餘額找零到另一個紙錢包中。

紙錢包有許多設計和大小,並有許多不同的特性。有些作為禮物送給他人,有季節性的主題,像聖誕節和新年主題。另外一些則是設計儲存在銀行金庫或通過某種方式隱藏私鑰的保險箱內,或者用不透明的刮刮貼,或者摺疊和防篡改的鋁箔膠粘密封。圖4-16至圖4-18展示了幾個不同安全和備份功能的紙錢包的例子。


圖4-16 通過bitcoinpaperwallet.com生成的、私鑰寫在摺疊皮瓣上的紙錢包



圖4-17 通過bitcoinpaperwallet.com 生成的、私鑰被密封住的紙錢包

其他設計有金鑰和地址的額外副本,類似於票根形式的可以拆卸存根,讓你可以儲存多個副本以防火災、洪水或其他自然災害。



圖4-18 在備份“存根”上有多個私鑰副本的紙錢包

 

相關文章