创建数字钱包(二)HD Wallet

BIP 全称是 Bitcoin Improvement Proposals,相当于互联网中RFC (Request for Comments),它是用来记录草案或者标准的。

BIP32解释

定义

BIP32定义了Hierarchical deterministic wallets (HD Wallets),HD指出了这类钱包的两大特征。

第一点特征是层级结构,钱包一般会存储一组key-pair对,这组key-pair对是链状存储,但是HD钱包是树状存储,也就是说它的结构中有根节点,根节点会派生出子节点,子节点又可以派生出子节点。这样做的优势是它可以有选择的把某个层级的一组key-pair对分配出去,这样就可以和组织结构匹配,比如:总部保留根密钥,其它分部用总部派生的密钥;也可以和用途匹配,比如:花钱的和收钱的地址可以分开。

第二点特征是确定性,因为所有的key-pair对都是从同一个根派生出来的,所以只要妥善保管好根(主密钥)就可以在其它的系统中快速地恢复钱包。

层级结构和确定性如下图示:


HD Wallets

主要概念

  1. Master key
  2. Chain Code
  3. Extended key

主密钥及其生成

主密钥是从一串长度在128到256位的比特序列(种子)中生成的,然后使用HMAC-SHA512计算出64字节序列(称为I),左边32字节(称为IL)作为主私钥,右边32字节(称为IR)作为主Chain Code。

大致步骤如下:

  1. 生成熵为128 - 256bit 的种子
  2. I = HMAC-SHA512(key="Bitcoin seed", data = seed)
  3. <> = I
  4. MasterSecretKey = IL & MasterChainCode = IR

这里有个问题值得探讨,这样生成的 MasterSecretKey 是符合 secp256k1 定义的利用ECDSA算法生成的私钥吗?我们可以利用secp256k1.privateKeyVerify(...)方法验证,结果是正确的。

额外的熵 Chain Code

因为每个父密钥都可以派生出很多子密钥,所以为了避免子密钥直接依赖父密钥,需要引入额外的熵(chain code)去增强父密钥,这个额外的熵,或者说,随机的256位的比特序列就是 Chain Code。

扩展密钥 Extended Key

根据定义,父密钥和Chain Code的组合 (k, c) 就是扩展私钥,而扩展公钥则是 (K, c),其中的 K 是通过 secp256k1 计算私钥 k 得到的。

Extended Key 在序列化的地方也值得关注,具体的规则,可以细读BIP32。

举个例子:

// publicKey
03795fd38bbffdddb24b72af417cf3fa540db8f60783dd32f61f0ca5af464fd492
// ExtendedPublicKey
xpub6GmbjntbdLF4JNhBxwoRBrdw2BBujvJ514tRHFMQaoFA5eSRaWwr6CQSGq1HtirLGSTT8SHqMGWQk4rbZLJsVFA4NLZZYUR25ZEdhnGJ7R1

序列化之后的publicKey的首部4比特是版本号,比如此处的xpub就是mainnet的意思。

每个扩展密钥都有个普通子密钥和个Hardened子密钥,一般会用i+表示Hardened子密钥,记为。

代码解释

这里,我们使用hdkey[1]进行代码解释。

主密钥及其生成

var MASTER_SECRET = Buffer.from('Bitcoin seed', 'utf8')

HDKey.fromMasterSeed = function (seedBuffer, versions) {
  var I = crypto.createHmac('sha512', MASTER_SECRET).update(seedBuffer).digest()
  var IL = I.slice(0, 32)
  var IR = I.slice(32)

  var hdkey = new HDKey(versions)
  hdkey.chainCode = IR
  hdkey.privateKey = IL

  return hdkey
}

seedBuffer是128-256bit的随机序列作为种子,然后利用HMAC-SHA512生成I值,分割出的IL和IR分别赋值给privateKey和chainCode。

Chain code

Chain code 会在派生子密钥的时候起作用,derive(path) -> deriveChild(index) 是派生子密钥的过程:

var data = Buffer.concat([this.publicKey, indexBuffer])
var I = crypto.createHmac('sha512', this.chainCode).update(data).digest()
var IL = I.slice(0, 32)
var IR = I.slice(32)

var hd = new HDKey(this.versions)
// Private parent key -> private child key
// ki = parse256(IL) + kpar (mod n)
hd.privateKey = secp256k1.privateKeyTweakAdd(this.privateKey, IL)

从上面的代码可以看到,chainCode用来作为HMAC-SHA512的密钥对data进行了哈希处理。最终子密钥privateKey通过secp256k1.privateKeyTweakAdd(...)生成,这个函数来自secp256k1[2]库,主要功能是拼接,如下:

exports.privateKeyTweakAdd = function (privateKey, tweak) {
  var bn = BN.fromBuffer(tweak)
  if (bn.isOverflow()) throw new Error(messages.EC_PRIVATE_KEY_TWEAK_ADD_FAIL)

  bn.iadd(BN.fromBuffer(privateKey))
  if (bn.isOverflow()) bn.isub(BN.n)
  if (bn.isZero()) throw new Error(messages.EC_PRIVATE_KEY_TWEAK_ADD_FAIL)

  return bn.toBuffer()
}

值得一提的是,在derive(path)函数中,我们会看到Hardened判断的条件是是否带有单引号,例如:44'

var hardened = (c.length > 1) && (c[c.length - 1] === "'")
var childIndex = parseInt(c, 10) // & (HARDENED_OFFSET - 1)
assert(childIndex < HARDENED_OFFSET, 'Invalid index')
if (hardened) childIndex += HARDENED_OFFSET

hdkey = hdkey.deriveChild(childIndex)

在后续介绍BIP44的过程中,我们会明白这样处理的含义,Path为m/44'/60'/0'/0/0在BIP44中有特定的含义,这种表示法和BIP32的结合点就在这里。

Extended key

Extended key 可以分为 privateExtendedKey 和 publicExtendedKey,这里以 privateExtendedKey 为例:

cs.encode(serialize(this, this.versions.private, Buffer.concat([Buffer.alloc(1, 0), this.privateKey])))

其中versionls.private,默认值是0x0488ADE4,encode操作可以忽略,具体的序列化逻辑发生在serialize中。

function serialize (hdkey, version, key) {
  // => version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33)
  var buffer = Buffer.allocUnsafe(LEN)

  buffer.writeUInt32BE(version, 0)
  buffer.writeUInt8(hdkey.depth, 4)

  var fingerprint = hdkey.depth ? hdkey.parentFingerprint : 0x00000000
  buffer.writeUInt32BE(fingerprint, 5)
  buffer.writeUInt32BE(hdkey.index, 9)

  hdkey.chainCode.copy(buffer, 13)
  key.copy(buffer, 45)

  return buffer
}

常量LEN为78,这就是序列化的结构大小。需要注意的点是,按照定义这里的字节序都是大端(Big Endian,也成为网络字节序)。

BIP44解释

定义

BIP44 定义了逻辑上的层级结构,所谓逻辑,就是人为赋予意义。BIP44综合了BIP32的HD Wallet设计和BIP43[3]的Purpose约定,使得HD Wallet能够表达多币种,多账号,账号的外部或内部key-pair对构成的组,外部指的是地址对外可见,专门用来接收或发送数字货币的地址;而内部则是对外不可见,多用来表达找零 (change) 的概念。

主要概念

BIP44在BIP32的路径中定义了5个层级:

m / purpose' / coin_type' / account' / change / address_index
  • Purpose: 44'or 0x8000002C 这表明后面的子密钥都遵从BIP44的约定
  • Coin type: 0' 代表比特币,60' 代表以太币
  • Account: 代表不同的用户身份,比如:储蓄或者收款账户,以及各种开支账户
  • Change: 0 表示外部key-pair组;1 代表内部key-pair组,比如专门用来找零的地址
  • Address_index: 根据BIP32,地址会生成多个,可以从0开始索引

Purpose, Coin type以及Account都有单引号,意味着它们都是Hardened密钥,而Change和Address_index则是Normal的密钥。这样做是为了安全,BIP32中提到了一个事实,如果知道了父级的ExtendedPublicKey及其派生出来的Non-hardened private key,就等于知道了父级的ExtendedPrivateKey,这就是Hardened密钥存在的理由。引文如下:

One weakness that may not be immediately obvious, is that knowledge of a parent extended public key plus any non-hardened private key descending from it is equivalent to knowing the parent extended private key (and thus every private and public key descending from it). This means that extended public keys must be treated more carefully than regular public keys. It is also the reason for the existence of hardened keys, and why they are used for the account level in the tree. This way, a leak of account-specific (or below) private key never risks compromising the master or other accounts.

代码解释

继续使用hdkey[1]来解释

let hdWallet = hdkey.fromMasterSeed(seed)
let key = hdWallet.derivePath("m/44'/60'/0'/0/0")
console.log("publicKey:", key1._hdkey._publicKey.toString("hex"), "\nextendPublicKey:", key1.publicExtendedKey())
->
publicKey: 03795fd38bbffdddb24b72af417cf3fa540db8f60783dd32f61f0ca5af464fd492 
extendPublicKey: xpub6GmbjntbdLF4JNhBxwoRBrdw2BBujvJ514tRHFMQaoFA5eSRaWwr6CQSGq1HtirLGSTT8SHqMGWQk4rbZLJsVFA4NLZZYUR25ZEdhnGJ7R1

依据前面提到的定义,通过路径m/44'/60'/0'/0/0派生出了以太坊某个外部账户下的第一个地址。


  1. NodeJS - hdkey ↩ ↩

  2. NodeJS - secp256k1 ↩

  3. BIP43 - Purpose scheme ↩

你可能感兴趣的:(创建数字钱包(二)HD Wallet)