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

一、概述

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

這是因爲我們生成的密鑰之間沒有什麼關聯,你不可能從一個 密鑰推導出另一個密鑰。通常情況下,這不是問題。但是,如果 你的網站每天需要爲成千上萬的訂單生成地址,就是另一回事了。 而分層確定性密鑰(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();

        }
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章