比特币入门之使用分层确定性密钥

时间:2024-01-24 21:03:53

一、概述

一旦我们开始自己管理密钥与地址,很快就会发现,备份密钥 是一件很痛苦的事情:只要生成一个新的地址,你就需要备份一次。

这是因为我们生成的密钥之间没有什么关联,你不可能从一个 密钥推导出另一个密钥。通常情况下,这不是问题。但是,如果 你的网站每天需要为成千上万的订单生成地址,就是另一回事了。 而分层确定性密钥(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();

        }
    }
}