區塊鏈的第一應用---比特幣,之 (密鑰、地址、錢包)


區塊鏈的第一應用---比特幣,之 (密鑰、地址、錢包)


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


每筆比特幣交易都需要一個有效的簽名才會被存儲在區塊鏈。只有有效的數字密鑰才能產生有效的數字簽名,因此擁有比特幣的密鑰副本就擁有了該帳戶的比特幣控制權。密鑰是成對出現的,由一個私鑰和一個公鑰所組成。公鑰就像銀行的帳號,而私鑰就像控制賬戶的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位):


<code>1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD/<code>


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


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


<code>$ bitcoind getnewaddress1J7mdg5rbQyUHENYdx39WVWK7fsLpEoXZy$ bitcoind dumpprivkey 1J7mdg5rbQyUHENYdx39WVWK7fsLpEoXZyKxFC1jmwwCoACiCAWZ3eXa96mBM6tb3TYzGmf6YwgdGWZgawvrtJ/<code>

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



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


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


<code>$ sx newkey5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1Jcn/<code>

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對其檢驗:


<code>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 darwinType "help", "copyright", "credits" or "license" for more information.>>> p = 115792089237316195423570985008687907853269984665640564039457584007908834671663>>> x = 55066263022277343669578718895168534326250603453777594175500187360389116729240>>> y = 32670510020758816978083085130507043184471273380659243275938904335757337482424>>> (x ** 3 + 7 - y**2) % p0/<code>

在橢圓曲線的數學原理中,有一個點被稱為“無窮遠點”,這大致對應於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標準的一部分,比特幣密鑰的生成點都是相同的:


<code>K = 1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD * G/<code>

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



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


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


<code>K = 1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD * G/<code>

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


<code>K = (x, y)其中,x = F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341Ay = 07CF33DA18BD734C600B96A72BBC4749D5141C90EC8AC328AE52DDFE2E505BDB/<code> 

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


區塊鏈的第一應用---比特幣,之 (密鑰、地址、錢包)


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



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


4.2 比特幣地址


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


<code>1J7mdg5rbQyUHENYdx39WVWK7fsLpEoXZy/<code>

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


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


以公鑰 K 為輸入,計算其SHA256哈希值,並以此結果計算RIPEMD160 哈希值,得到一個長度為160比特(20字節)的數字:


<code>A = RIPEMD160(SHA256(K))/<code>

公式中,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字母表


<code>123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz/<code>

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


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


接下來,我們計算“雙哈希”校驗碼,意味著要對之前的結果(前綴和數據)運行兩次SHA256哈希算法:


<code>checksum = SHA256(SHA256(prefix+data))/<code>

在產生的長32個字節的哈希值(兩次哈希運算)中,我們只取前4個字節。這4個字節就作為校驗碼。校驗碼會添加到數據之後。


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


區塊鏈的第一應用---比特幣,之 (密鑰、地址、錢包)



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


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


表4-1 Base58Check版本前綴和編碼後的結果


種類版本前綴 (hex)Base58格式Bitcoin Address0x001Pay-to-Script-Hash Address0x053Bitcoin Testnet Address0x6Fm or nPrivate Key WIF0x805, K or LBIP38 Encrypted Private Key0x01426PBIP32 Extended Public Key0x0488B21Expub


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


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


<code>#include 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; }/<code>

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


例4-3 編譯並運行addr代碼


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

4.2.2 密鑰的格式


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


私鑰的格式


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


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


種類版本描述HexNone64 hexadecimal digitsWIF5Base58Check encoding: Base58 with version prefix of 128 and 32-bit checksumWIF-compressedK or LAs above, with added suffix 0x01 before encoding


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


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


格式私鑰Hex1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDDWIF5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1JcnWIF-compressedKxFC1jmwwCoACiCAWZ3eXa96mBM6tb3TYzGmf6YwgdGWZgawvrtJ


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


將Base58Check編碼解碼為十六進制


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


我們使用的命令是base58check-decode:


<code>$ sx base58check-decode 5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1Jcn1e99423a4ed27608a15a2616a2b0e9e52ced330ac530edcc32c8ffc6a526aedd 128/<code> 

所得結果是十六進制的密鑰,緊接著是錢包導入格式(Wallet Import Format,WIF)的版本前綴128。


將十六進制轉換為Base58Check編碼


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


<code>$sx base58check-encode 1e99423a4ed27608a15a2616a2b0e9e52ced330ac530edcc32c8ffc6a526aedd 128 5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1Jcn/<code>

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


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


<code>K = 04F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341A07CF33DA18BD734C600B96A72BBC4749D5141C90EC8AC328AE52DDFE2E505BDB/<code>

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


公鑰的格式


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


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


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


<code>x = F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341Ay = 07CF33DA18BD734C600B96A72BBC4749D5141C90EC8AC328AE52DDFE2E505BDB/<code>

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


<code>K = 03F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341A/<code>

壓縮格式公鑰


引入壓縮格式公鑰是為了減少比特幣交易的字節數,從而可以節省那些運行區塊鏈數據庫的節點磁盤空間。大部分比特幣交易包含了公鑰,用於驗證用戶的憑據和支付比特幣。每個公鑰有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座標是一個奇數:


<code>K = 03F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341A/<code>

這個壓縮格式公鑰對應著同樣的一個私鑰,這意味它是由同樣的私鑰所生成。但是壓縮格式公鑰和非壓縮格式公鑰差別很大。更重要的是,如果我們使用雙哈希函數(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 示例:同樣的私鑰,不同的格式


格式私鑰Hex1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDDWIF5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1JcnHex-compressed1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD01WIF-compressedKxFC1jmwwCoACiCAWZ3eXa96mBM6tb3TYzGmf6YwgdGWZgawvrtJ



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


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


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


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


<code>import bitcoin# Generate a random private keyvalid_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.Nprint "Private Key (hex) is: ", private_keyprint "Private Key (decimal) is: ", decoded_private_key# Convert private key to WIF formatwif_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 keycompressed_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 pointpublic_key = bitcoin.base10_multiply(bitcoin.G, decoded_private_key) print "Public Key (x,y) coordinates is:", public_key# Encode as hex, prefix 04hex_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 keyprint "Bitcoin Address (b58check) is:", bitcoin.pubkey_to_address(public_key)# Generate compressed bitcoin address from compressed public keyprint "Compressed Bitcoin Address (b58check) is:", \\             bitcoin.pubkey_to_address(hex_compressed_public_key)/<code>

例4-5顯示了上段代碼運行結果。


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


<code>$ python key-to-address-ecc-example.pyPrivate Key (hex) is: 3aba4162c7251c891207b747840551a71939b0de081f85c4e44cf7c13e41daa6Private Key (decimal) is: 26563230048437957592232553826663696440606756685920117476832299673293013768870Private Key (WIF) is: 5JG9hT3beGTJuUAmCQEmNaxAuMacCTfXuw1R3FCXig23RQHMr4KPrivate Key Compressed (hex) is: 3aba4162c7251c891207b747840551a71939b0de081f85c4e44cf7c13e41daa601Private Key (WIF-Compressed) is: KyBsPXxTuVD82av65KZkrGrWi5qLMah5SdNq6uftawDbgKa2wv6SPublic Key (x,y) coordinates is: (41637322786646325214887832269588396900663353932545912953362782457239403430124L, 16388935128781238405526710466724741593761085120864331449066658622400339362166L)Public Key (hex) is: 045c0de3b9c8ab18dd04e3511243ec2952002dbfadc864b9628910169d9b9b00ec↵243bcefdd4347074d44bd7356d6a53c495737dd96295e2a9374bf5f02ebfc176Compressed Public Key (hex) is: 025c0de3b9c8ab18dd04e3511243ec2952002dbfadc864b9628910169d9b9b00ecBitcoin Address (b58check) is: 1thMirt546nngXqyPEz532S8fLwbozud8Compressed Bitcoin Address (b58check) is: 14cxpo3MBCYYWCgF74SWTdcmxipnGUsPw3/<code>

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


例4-6 使用在比特幣密鑰中的橢圓曲線算法的腳本


<code>import ecdsaimport randomfrom 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 = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8Lcurve_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 = _rcurve = curve_secp256k1generator = generator_secp256k1def 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:", pointprint "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/<code>

例4-7顯示了運行腳本的結果。


例4-7 安裝Python ECDSA庫,運行ec_math.py腳本


<code>running the ec_math.py>

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)熵+校驗符字段長128413212160516515192619818224723121256826424


助記碼錶示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中編碼的擴展私鑰的例子:


<code>xprv9tyUQV64JT5qs3RSTJkXCWKMyUgoQp7F3hA1xzG6ZGu6u6Q9VMNjGr67Lctvy5P8oyaYAL9CAWrUE9i6GoNMKUga5biW6Hx4tws2six3b9c/<code>

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


<code>xpub67xpozcx8pe95XVuZLHXZeG6XWXHpGq6Qv5cmNfi7cS5mtjJ2tgypeQbBs2UAR6KECeeMVKZBPLrtJunSDMstweyLXhRgPxdp14sk9tJPW9/<code>

公共子鑰匙推導


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


因此,擴展的公共鑰匙可以再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個預定義樹狀層級的結構:


<code>m / purpose' / coin_type' / account' / change / address_index/<code>

第一層的目的地總是被設定為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確定性密鑰以及將它們用不同的格式進行展示:


<code>$ 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 key96 | Chapter 4: Keys, Addresses, Walletsxprv9s21ZrQH143K38iQ9Y5p6qoB8C75TE71NfpyQPdfGvzghDt39DHPFpovvtWZaR- 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/<code>


分享到:


相關文章: