比特幣入門之使用分層確定性金鑰

社會主義接班人發表於2019-06-23

一、概述

一旦我們開始自己管理金鑰與地址,很快就會發現,備份金鑰 是一件很痛苦的事情:只要生成一個新的地址,你就需要備份一次。

這是因為我們生成的金鑰之間沒有什麼關聯,你不可能從一個 金鑰推匯出另一個金鑰。通常情況下,這不是問題。但是,如果 你的網站每天需要為成千上萬的訂單生成地址,就是另一回事了。 而分層確定性金鑰(Hierarchical Deterministic Key)就是 為解決這一金鑰管理問題而提出的解決方案:

分層(Hierarchichal)指的是金鑰之間存在層級關係,從父金鑰可以 生成子金鑰。例如在上圖中,從主金鑰m可以生成第一層子金鑰m/0m/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();

        }
    }
}
View Code

建立一個隨機主金鑰
從主金鑰派生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();
        }
    }
}
View Code

六、路徑表示法

在使用層級確定性金鑰時,使用路徑表示法可以方便地定位到一個遠離 若干層的後代金鑰。例如,在下面的圖中分別表示了金鑰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();

        }
    }
}

 

相關文章