一、概述
一旦我们开始自己管理密钥与地址,很快就会发现,备份密钥 是一件很痛苦的事情:只要生成一个新的地址,你就需要备份一次。
这是因为我们生成的密钥之间没有什么关联,你不可能从一个 密钥推导出另一个密钥。通常情况下,这不是问题。但是,如果 你的网站每天需要为成千上万的订单生成地址,就是另一回事了。 而分层确定性密钥(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(); } } }