一、概述
一旦我們開始自己管理金鑰與地址,很快就會發現,備份金鑰 是一件很痛苦的事情:只要生成一個新的地址,你就需要備份一次。
這是因為我們生成的金鑰之間沒有什麼關聯,你不可能從一個 金鑰推匯出另一個金鑰。通常情況下,這不是問題。但是,如果 你的網站每天需要為成千上萬的訂單生成地址,就是另一回事了。 而分層確定性金鑰(Hierarchical Deterministic Key)就是 為解決這一金鑰管理問題而提出的解決方案:
分層(Hierarchichal
)指的是金鑰之間存在層級關係,從父金鑰可以 生成子金鑰。例如在上圖中,從主金鑰m可以生成第一層子金鑰m/0
、m/1
... 而從第一層的金鑰又可以繼續生成第二層的金鑰金鑰,例如m/1/0
、 m/1/1
...如此不斷延伸,就構成了以主金鑰為根節點的一顆分層金鑰樹了。
確定性(Deterministic
)指的是,根據金鑰在層級中的編號,就可以從 父金鑰確定性地推匯出該金鑰的具體內容。例如在上圖中,我們可以從主金鑰 m推匯出任何一個指定編號的後代金鑰,例如m/1/1/3。層級金鑰的確定性使得我們 只需要備份主金鑰並記錄後代金鑰的編號就可以了。
二、生成主金鑰
使用分層確定型金鑰樹的第一步,是首先生成層級金鑰樹的主金鑰,下圖展示 了主金鑰生成的主要流程。和普通的金鑰生成類似,種子資料(熵)用來增加 金鑰的不可預測性:
熵經過HAMC雜湊變換後,得到的512位資料拆分為兩部分:主鏈碼和主私鑰。主私鑰 可以繼續推匯出主公鑰,而主鏈碼則可以作為子金鑰生成的熵,提高子金鑰的 不可預測性。
在NBitcoin中,使用ExtKey類來表徵層級確定金鑰:
可以利用種子或者傳入一個Key例項來生成層級主金鑰物件,例如:
Key key = new Key(); ExtKey masterKey = new ExtKey(key); string cc = Encoders.Hex.EncodeData(masterKey.ChainCode); //鏈碼 string prv = Encoders.Hex.EncodeData(masterKey.PrivateKey); //私鑰 string pub = masterKey.PrivateKey.PubKey.ToHex(); //公鑰
不過為了便於備份層級金鑰樹,通常我們會選擇使用助記詞來生成種子, 進而推匯出主金鑰。例如,下面的程式碼將生成助記詞,最後將 助記詞轉換為層級主金鑰:
//生成並儲存助記詞 Mnemonic mc = new Mnemonic(Wordlist.Englisht); File.WriteAllText("./mnemonic.txt",mc.ToString()); //載入助記詞,生成主金鑰 string words = File.ReadAllText("./mnemonic.txt"); Mnemonic mc2 = new Mnemonic(words,Wordlist.Englisth); ExtKey masterKey = mc2.DeriveExtKey("whoami"/*password*/);
using NBitcoin; using NBitcoin.DataEncoders; using System; using System.IO; namespace Newmnemonic { class Program { static void Main(string[] args) { Mnemonic mnemonic = new Mnemonic(Wordlist.English); Console.WriteLine("mnemonic => {0}", mnemonic); byte[] seed = mnemonic.DeriveSeed("whoami"); Console.WriteLine("seed => {0}", Encoders.Hex.EncodeData(seed)); File.WriteAllText("../mnemonic.txt", mnemonic.ToString()); string sentence = File.ReadAllText("../mnemonic.txt"); mnemonic = new Mnemonic(sentence, Wordlist.English); Console.WriteLine("mnenomic => {0}", mnemonic); ExtKey xkey = mnemonic.DeriveExtKey("whoami"); Console.WriteLine("master private key => {0}", Encoders.Hex.EncodeData(xkey.PrivateKey.ToBytes())); Console.WriteLine("master public key => {0}", xkey.PrivateKey.PubKey.ToHex()); Console.WriteLine("master chaincode => {0}", Encoders.Hex.EncodeData(xkey.ChainCode)); Console.ReadLine(); } } }
三、派生子金鑰
在層級金鑰樹中,使用父金鑰(Parent Key)和父鏈碼(Parent Chaincode), 就可以推匯出指定序號的子金鑰:
在上圖中參與單向雜湊運算的三個資訊:父公鑰、父鏈碼和子金鑰 序號一起決定了HMAC雜湊的512位輸出,而這512位輸出的一半將作為子金鑰的鏈碼, 另一半則分別用於生成子公鑰和子私鑰。
在NBitcoin中,使用ExtKey例項的Derive()
方法, 就可以生成指定指定編號的子金鑰及鏈碼了:
例如,下面的程式碼生成主金鑰的7878#子金鑰並顯示其鏈碼、私鑰WIF和公鑰:
ExtKey key7878 = masterKey.Derive(7878); Console.WriteLine("hd-key 7878 chaincode => {0}",key7878.ChainCode); //鏈碼 Console.WriteLine("hd-key 7878 private key => {0}", key7878.PrivateKey); //私鑰 Console.WriteLine("hd-key 7878 public key => {0}",key7878.PrivateKey.PubKey); //公鑰
無私鑰派生
值得指出的是,只需要父公鑰和父鏈碼就可以推匯出指定編號的子公鑰和 子鏈碼,這意味著可以在不洩露主私鑰的情況下動態生成子公鑰(以及地址), 當你為網站增加比特幣支付功能時,這一特性非常有意義:
ExtPubKey masterPub = masterKey.Neuter(); //剔除私鑰 ExtPubKey pub7878 = masterPub.Derive(7878); Console.WriteLine("pub key 7878 public only derivation => {0}",pub7878.PubKey); //僅公鑰推導
using NBitcoin; using NBitcoin.DataEncoders; using System; namespace DeriveChildkey { class Program { static void Main(string[] args) { ExtKey xkeyMaster = new ExtKey(); ExtKey xkey_38 = xkeyMaster.Derive(38); Console.WriteLine("child#38 prv key => {0}", Encoders.Hex.EncodeData(xkey_38.PrivateKey.ToBytes())); Console.WriteLine("child#38 pub key => {0}", xkey_38.PrivateKey.PubKey.ToHex()); ExtPubKey xpkey_38 = xkey_38.Neuter(); ExtPubKey xpkey_38_6 = xpkey_38.Derive(6); Console.WriteLine("child#38#6 pub key => {0}", xpkey_38_6.PubKey.ToHex()); Console.ReadLine(); } } }
四、使用擴充套件金鑰
在生成子金鑰的過程中,最重要的兩個引數,就是金鑰和鏈碼了。因此 如果在父金鑰的表示當中包含這兩部分資訊,就可以直接使用父金鑰來 生成子金鑰了 —— 這就是擴充套件金鑰/Extended Key的直觀含義:
我們可以使用層級金鑰物件的serializePubB58()
或serializePrivB58()
方法將 其轉換為擴充套件金鑰形式,也可以使用層級金鑰類的靜態方法deserializeB58()
將一個擴充套件 金鑰恢復為層級金鑰:
例如,下面的程式碼建立一個隨機主金鑰,派生7878#子金鑰,然後分別 生成其擴充套件私鑰和擴充套件公鑰:
ExtKey masterKey = new ExtKey(); //隨機生成層級主金鑰 ExtKey key7878 = masterKey.Derive(7878); BitcoinExtKey bxk7878 = new BitcoinExtKey(key7878,Network.RegTest); //擴充套件私鑰 BitcoinExtPubKey bxpk7878 = bxk7878.Neuter(); //擴充套件公鑰
需要指出的是,擴充套件金鑰使用字首區分不同的網路,因此我們也需要在生成擴充套件金鑰 時,傳入特定的網路物件:
也可以從從擴充套件金鑰恢復出對應的層級金鑰,例如
String xprv = "tprv...."; BitcoinExtKey bxk = new BitcoinExtKey(xprv,Network.RegTest); //匯入擴充套件金鑰 ExtKey key = bxk.ExtKey; //獲得層級金鑰
using NBitcoin; using NBitcoin.DataEncoders; using System; namespace Extendedkey { class Program { static void Main(string[] args) { ExtKey xkMaster = new ExtKey(); ExtKey xk_78 = xkMaster.Derive(78); Console.WriteLine("child#78 prv key => {0}", Encoders.Hex.EncodeData(xk_78.PrivateKey.ToBytes())); BitcoinExtKey bxk_78 = new BitcoinExtKey(xk_78, Network.RegTest); Console.WriteLine("child#78 extended prv key => {0}", bxk_78); Console.WriteLine("child#78 extended pub key => {0}", bxk_78.Neuter()); string bxkText = bxk_78.ToString(); BitcoinExtKey bxkRestored = new BitcoinExtKey(bxkText, Network.RegTest); Console.WriteLine("restored child#78 prv key => {0}", Encoders.Hex.EncodeData(bxkRestored.ExtKey.PrivateKey.ToBytes())); Console.ReadLine(); } } }
建立一個隨機主金鑰
從主金鑰派生78號子金鑰,匯出其擴充套件公鑰和擴充套件私鑰並存入檔案
從檔案中讀取擴充套件私鑰,並將其轉化為對應的層級金鑰
五、使用強化派生金鑰
擴充套件金鑰同時包含了鏈碼和金鑰資訊,這對於繼續派生子金鑰很方便, 但同時也帶來了安全上的隱患。下圖中展示了第N層鏈碼和公鑰及其某個 後代私鑰洩漏的情況下,受影響的公鑰和私鑰:
解決的辦法是改變金鑰派生的演算法,使用父私鑰而不是父公鑰來生成子鏈碼 及子金鑰,這樣得到的子金鑰被稱為強化金鑰(hardened key
):
比特幣根據子金鑰序號來區分派生普通金鑰還是強化金鑰:當序號 小於0x80000000
時,生成普通子金鑰,否則生成強化子金鑰。
例如,下面的程式碼分別生成普通子金鑰和強化子金鑰:
int id = 123; ExtKey normalKey = masterKey.Derive(id); ExtKey hardenedKey = masterKey.Derive(id,true);
顯然,你需要從一個包含私鑰的層級金鑰才能派生強化子金鑰
using NBitcoin; using System; namespace Hardenedkey { class Program { static void Main(string[] args) { ExtKey xkey = new ExtKey(); ExtKey normalChild = xkey.Derive(12); Console.WriteLine("normal child key => {0}", normalChild.IsHardened); ExtKey hardenedChild = xkey.Derive(12, true); Console.WriteLine("hardened child key => {0}", hardenedChild.IsHardened); Console.ReadLine(); } } }
六、路徑表示法
在使用層級確定性金鑰時,使用路徑表示法可以方便地定位到一個遠離 若干層的後代金鑰。例如,在下面的圖中分別表示了金鑰m/1'/1'
和 M/2/3
在整個層級金鑰樹中的親緣關係:
路徑的各層之間使用/
符號隔開,M
表示主公鑰,金鑰序號之後使用H
則表示 這是一個強化派生金鑰,否則就是一個普通派生金鑰。
在NBitcoin中首先使用KeyPath類靜態方法ParsePath()
將指定的路徑字串 解析為KeyPath例項,然後再呼叫Derive()
方法建立金鑰。例如:
KeyPath path = KeyPath.Parse("M/1H/2H/3"); ExtKey descentKey = masterKey.Derive(path);
BIP44 給出了一種五層路徑劃分的建議,可用於多個幣種:
你可以根據自己的需求決定是否採用這一建議方案。
using NBitcoin; using NBitcoin.DataEncoders; using System; namespace DeriveChildkeyPath { class Program { static void Main(string[] args) { ExtKey master = new ExtKey(); KeyPath path = KeyPath.Parse("m/44'/0'/0'/0/123"); ExtKey derived = master.Derive(path); Console.WriteLine("descent prv key => {0}", Encoders.Hex.EncodeData(derived.PrivateKey.ToBytes())); Console.WriteLine("descent pub key => {0}", derived.PrivateKey.PubKey.ToHex()); Console.ReadLine(); } } }