钱包,参考书籍《精通比特币(第二版)》第五章钱包
钱包的一些标准有:
助记码,基于 BIP-39
HD 钱包,基于 BIP-32
多用途 HD 钱包结构,基于 BIP-43
多币种和多帐户钱包,基于 BIP-44
比特币钱包只含有密钥而不是钱币,每个用户有包含多个密钥的钱包。有两种主要类型非确定性钱包(其中每个密钥都是从随 机数独立生成的)和确定性钱包(其中所有的密钥都是从一个主 密钥派生出来,这个主密钥即为种子(seed))。
1.非确定性钱包
2.确定性(种子)钱包
3.分层确定性钱包(HD Wallets (BIP-32/BIP-44))
确定性钱包的最高级形式是通过 BIP0032 标准定义的 HD 钱包。
结合代码部分来看这个标准
https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
比特币改进协议32 https://blog.csdn.net/pony_maggie/article/details/76178228
The specification consists of two parts. In a first part, a system for deriving a tree of keypairs from a single seed is presented. The second part demonstrates how to build a wallet structure on top of such a tree.
该规范由两部分组成。 在第一部分中,提出了一种用于从单个种子导出密钥对树的系统。 第二部分演示了如何在这样的树顶部构建钱包结构。
在下面,会定义一个从父密钥派生多个子密钥的函数,为了防止这些子密钥仅依赖密钥本省,我们首先使用额外的256位熵扩展私钥和公钥,这个被称为链码的扩展对于相应的私钥和公钥是相同的,由32个字节组成。
我们将扩展私钥表示为(k,c),k表示普通私钥,c表示链码;扩展公钥表示为(K,c)K表示point(k)
point§: returns the coordinate pair resulting from EC point multiplication (repeated application of the EC group operation) of the secp256k1 base point with the integer p.
每个扩展密钥有231个普通子密钥和231个硬化子密钥,这些子密钥都有一个索引。普通子密钥使用索引0-231,硬化子密钥使用231-232-1,为简化硬化子密钥的索引,用数字iH代表i+231
链码是用来给这个过程引入确定性随机数据的,使得索引不能充分衍生其他的子密钥。因此,有了子密钥并不能让它发现自己的姊妹密钥,除非
你已经有了链码。
来看这个函数
给定父扩展密钥和索引,可以计算相应的子密钥。这个算法的实现取决于子密钥是否是一个硬化的密钥(或者,等价与i>=231),以及我们是否在讨论私钥或公钥。
Child key derivation (CKD) functions
Given a parent extended key and an index i, it is possible to compute the corresponding child extended key. The algorithm to do so depends on whether the child is a hardened key or not (or, equivalently, whether i ≥ 231), and whether we’re talking about private or public keys.
Private parent key → private child key
//父私钥到子私钥
The function CKDpriv((kpar, cpar), i) → (ki, ci) computes a child extended private key from the parent extended private key:
1.Check whether i ≥ 231 (whether the child is a hardened key).
If so (hardened child): let I = HMAC-SHA512(Key = cpar, Data = 0x00 || ser256(kpar) || ser32(i)). (Note: The 0x00 pads the private key to make it 33 bytes long.)
If not (normal child): let I = HMAC-SHA512(Key = cpar, Data = serP(point(kpar)) || ser32(i)).
2.Split I into two 32-byte sequences, IL and IR.
The returned child key ki is parse256(IL) + kpar (mod n).
The returned chain code ci is IR.
3.In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, and one should proceed with the next value for i. (Note: this has probability lower than 1 in 2127.)
The HMAC-SHA512 function is specified in RFC 4231.
Public parent key → public child key
//父公钥->子公钥
The function CKDpub((Kpar, cpar), i) → (Ki, ci) computes a child extended public key from the parent extended public key. It is only defined for non-hardened child keys.//只针对未硬化的子密钥定义
Check whether i ≥ 231 (whether the child is a hardened key).
If so (hardened child): return failure
If not (normal child): let I = HMAC-SHA512(Key = cpar, Data = serP(Kpar) || ser32(i)).
Split I into two 32-byte sequences, IL and IR.
The returned child key Ki is point(parse256(IL)) + Kpar.
The returned chain code ci is IR.
In case parse256(IL) ≥ n or Ki is the point at infinity, the resulting key is invalid, and one should proceed with the next value for i.
Private parent key → public child key
//父私钥->子公钥
The function N((k, c)) → (K, c) computes the extended public key corresponding to an extended private key (the “neutered” version, as it removes the ability to sign transactions).
The returned key K is point(k).
The returned chain code c is just the passed chain code.
To compute the public child key of a parent private key:
N(CKDpriv((kpar, cpar), i)) (works always).
CKDpub(N(kpar, cpar), i) (works only for non-hardened child keys).
Public parent key → private child key
//不存在
子密钥的派生函数,这段可以结合源码来看,对应Derive函数
seed->masterkey,主密钥生成,seed一般使用BIP39标准
根种子输入到 HMAC-SHA512 算法中就可以得到一个可用来创造主私钥(m) (master private key(m) )和主链代码(a master chain code)的哈希。主私钥(m) 之后可以通过使用我们在本章先前看到的那个普通椭圆曲线 m * G 过程生来成相 对应的主公钥(M)。 链代码用于从母密钥中创造子密钥的那个函数中引入熵。
Generate a seed byte sequence S of a chosen length (between 128 and 512 bits; 256 bits is advised) from a §RNG.
Calculate I = HMAC-SHA512(Key = “Bitcoin seed”, Data = S)
Split I into two 32-byte sequences, IL and IR.
Use parse256(IL) as master secret key, and IR as master chain code
void CExtKey::SetMaster(const unsigned char *seed, unsigned int nSeedLen) {
static const unsigned char hashkey[] = {'B','i','t','c','o','i','n',' ','s','e','e','d'};
unsigned char out[64];
LockObject(out);
CHMAC_SHA512(hashkey, sizeof(hashkey)).Write(seed, nSeedLen).Finalize(out);
key.Set(&out[0], &out[32], true);
memcpy(chaincode.begin(), &out[32], 32);
UnlockObject(out);
nDepth = 0;//表示master node
nChild = 0;//还没有子密钥
memset(vchFingerprint, 0, sizeof(vchFingerprint));//指纹初始化为0
}
memcpy
:memcpy指的是c和c++使用的内存拷贝函数,memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。
void *memcpy(void *dest, const void *src, size_t n)
memset
:memset是计算机中C/C++语言初始化函数。将s所指向的某一块内存中的后n个 字节的内容全部设置为ch指定的ASCII值, 第一个值为指定的内存地址,块的大小由第三个参数指定,这个函数通常为新申请的内存做初始化工作, 其返回值为s。
首先调用CHMAC_SHA512,
HMAC-SHA-512 is the realization of the HMAC message authentication code using the SHA-512 hash function.
HMAC-SHA-512是使用SHA-512哈希函数实现HMAC消息认证码。
CHMAC_SHA512::CHMAC_SHA512(const unsigned char* key, size_t keylen)
{
unsigned char rkey[128];
if (keylen <= 128) {
memcpy(rkey, key, keylen);
memset(rkey + keylen, 0, 128 - keylen);//其余位初始化为0
} else {
CSHA512().Write(key, keylen).Finalize(rkey);
memset(rkey + 64, 0, 64);
}
//每个字符做异或操作
for (int n = 0; n < 128; n++)
rkey[n] ^= 0x5c;
outer.Write(rkey, 128);//CSHA512 outer
for (int n = 0; n < 128; n++)
rkey[n] ^= 0x5c ^ 0x36;//抵消0x5c,等于原 rkey[n]^0x36
inner.Write(rkey, 128);//CSHA512 inner
}
这里调用的write是CSHA512::Write,具体步骤不介绍了
//src/crypto/sha512.cpp
CSHA512& CSHA512::Write(const unsigned char* data, size_t len)
{
const unsigned char* end = data + len;
size_t bufsize = bytes % 128;
if (bufsize && bufsize + len >= 128) {
// Fill the buffer, and process it.
memcpy(buf + bufsize, data, 128 - bufsize);
bytes += 128 - bufsize;
data += 128 - bufsize;
sha512::Transform(s, buf);
bufsize = 0;
}
while (end >= data + 128) {
// Process full chunks directly from the source.
sha512::Transform(s, data);
data += 128;
bytes += 128;
}
if (end > data) {
// Fill the buffer with what remains.
memcpy(buf + bufsize, data, end - data);
bytes += end - data;
}
return *this;
}
Key identifiers
Extended keys can be identified by the Hash160 (RIPEMD160 after SHA256) of the serialized ECDSA public key K, ignoring the chain code. This corresponds exactly to the data used in traditional Bitcoin addresses. It is not advised to represent this data in base58 format though, as it may be interpreted as an address that way (and wallet software is not required to accept payment to the chain key itself).
The first 32 bits of the identifier are called the key fingerprint.
//key.cpp
bool CExtKey::Derive(CExtKey &out, unsigned int nChild) const {
out.nDepth = nDepth + 1;//深度加一
CKeyID id = key.GetPubKey().GetID();//密钥标识符,GetID函数就是取Hash160
memcpy(&out.vchFingerprint[0], &id, 4);//标识符的前32位称为密钥指纹
out.nChild = nChild;
return key.Derive(out.key, out.chaincode, nChild, chaincode);
}
bool CKey::Derive(CKey& keyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const {
assert(IsValid());
assert(IsCompressed());
unsigned char out[64];
LockObject(out);
//1.Check whether i ≥ 231 (whether the child is a hardened key).
if ((nChild >> 31) == 0) {
CPubKey pubkey = GetPubKey();
assert(pubkey.begin() + 33 == pubkey.end());
BIP32Hash(cc, nChild, *pubkey.begin(), pubkey.begin()+1, out);
} else {
assert(begin() + 32 == end());
BIP32Hash(cc, nChild, 0, begin(), out);
}
//2. Split I into two 32-byte sequences, IL and IR.
memcpy(ccChild.begin(), out+32, 32);
memcpy((unsigned char*)keyChild.begin(), begin(), 32);
//3.
bool ret = secp256k1_ec_privkey_tweak_add(secp256k1_context_sign, (unsigned char*)keyChild.begin(), out);
UnlockObject(out);
keyChild.fCompressed = true;
keyChild.fValid = ret;
return ret;
}
密钥衍生函数可以被用来创造密钥树上任何层级的子密钥。 这只需要三个输入量:一个密钥,一个链码以及想要的子密钥的索引。密钥以及 链码这两个重要的部分被结合之后,就叫做扩展密钥(extended key)。术语 “extended key”也被认为是“可扩展的密钥”,因为这种密钥可以用来衍生子密钥。
扩展的私钥
//key.cpp
struct CExtKey {
unsigned char nDepth;//深度
unsigned char vchFingerprint[4];//父密钥的指纹
unsigned int nChild;
ChainCode chaincode;//链码
CKey key;//私钥
...
4 byte: version bytes (mainnet: 0x0488B21E public, 0x0488ADE4 private; testnet: 0x043587CF public, 0x04358394 private)
1 byte: depth: 0x00 for master nodes, 0x01 for level-1 derived keys, …
4 bytes: the fingerprint of the parent’s key (0x00000000 if master key)
4 bytes: child number. This is ser32(i) for i in xi = xpar/i, with xi the key being serialized. (0x00000000 if master key)
32 bytes: the chain code
33 bytes: the public key or private key data (serP(K) for public keys, 0x00 || ser256(k) for private keys)
这种78字节结构可以像Base58中的其他比特币数据一样进行编码,首先添加32个校验和位(从双SHA-256校验和中导出),然后转换为Base58表示。 这导致Base58编码的字符串最多112个字符。 由于版本字节的选择,Base58表示将在主网上以“xprv”或“xpub”开头,在testnet上以“tprv”或“tpub”开头。
扩展的公钥结构也是类似的。
分层确定性钱包的一个很有用的特点就是可以 不通过私钥而直接从公共母密钥派生出公共子密钥的能 力。这就给了我们两种衍 生子公钥的方法:或者通过子私钥,再或者就是直接通过母公钥。
这种快捷方式可以用来创造非常保密的只有公钥配置。在配置中,服务器或者应用程序不管有没有私钥,都可以有扩展公钥的副本。这种配置可以创造出无限数量的公钥以及比特币地址。但是发送到这个地址里的任何比特币都不能使用。与此同时,在另一种更保险的服务器上,扩展私钥可以衍生出所有的对应的可签署交易以及花钱的私钥。
bool CPubKey::Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const {
assert(IsValid());
assert((nChild >> 31) == 0);
assert(begin() + 33 == end());
unsigned char out[64];
BIP32Hash(cc, nChild, *begin(), begin()+1, out);
memcpy(ccChild.begin(), out+32, 32);
secp256k1_pubkey pubkey;
if (!secp256k1_ec_pubkey_parse(secp256k1_context_verify, &pubkey, &(*this)[0], size())) {
return false;
}
if (!secp256k1_ec_pubkey_tweak_add(secp256k1_context_verify, &pubkey, out)) {
return false;
}
unsigned char pub[33];
size_t publen = 33;
secp256k1_ec_pubkey_serialize(secp256k1_context_verify, pub, &publen, &pubkey, SECP256K1_EC_COMPRESSED);
pubkeyChild.Set(pub, pub + publen);
return true;
}
关于硬化
前面在派生的子密钥的时候有对密钥是否硬化做的判断,这里介绍下硬化
从扩展公钥衍生一个分支公钥的能力是很重要的,但牵扯一些风险。访问扩展公钥并不能得到访问子私钥的途径。但是,因为扩展公钥包含有链码,如果子私钥被知道或者被泄漏的话,链码就可以被用来衍生所有的其他子私钥。简单地泄露的私钥以及一个母链码,可以暴露所有的子密钥。更糟糕的是,子私钥与母链码可以用来推断母私钥。
为了应对这种风险,HD 钱包使用一种叫做硬化衍生(hardened derivation)的替代 衍生函数。这就“打破”了母公钥以及子链码之间的关系。这个硬化衍生函数使用 了母私钥去推导子链码,而不是母公钥。这就在母/子顺序中创造了一道“防火 墙”——有链码但并不能够用来推算子链码或者姊妹私钥。强化衍生函数看起来几 乎与一般的衍生的子私钥相同,不同的是母私钥被用来输入散列函数中而不是母公钥。
简单地来说,如果你想要利用扩展公钥的便捷来衍生公钥的分支而不将你自己暴 露在泄露扩展链码的风险下, 你应该从强化母私钥衍生公钥,而不是从一般的母 私钥来衍生。最好的方式是,为了避免了推到出主密钥,主密钥所衍生的第一层 级的子密钥最好使用强化衍生。
客户端要求具备兼容性,即使不支持所有功能。
HDW(分层确定性钱包)被组织为几个“帐户”。 帐户已编号,默认帐户(“”)为数字0.客户端不需要支持多个帐户 - 如果不支持,则仅使用默认帐户。
由主私钥衍生出的私钥起始以“m”打头。由主公钥衍生的公钥起始以 “M“打头。因此,母密钥生成的第一个子私钥是 m/0。第一个公钥是 M/0。第一 个子密钥的子密钥就是 m/0/1,以此类推。
每个帐户由两个密钥对链组成:内部和外部链。 外部钥匙串用于生成新的公共地址,而内部钥匙串用于所有其他操作(更改地址,生成地址,…,任何不需要通信的内容)。 不支持单独的钥匙串的客户端应该使用外部钥匙链。
m / iH / 0 / k对应于从主m导出的HDW的帐号i的外链的第k个密钥对。
m / iH/ 1 / k对应于从主m导出的HDW的帐号i的内部链的第k个密钥对。
为了符合此标准,客户端必须至少能够导入扩展的公钥或私钥,以便将其直接后代作为钱包密钥进行访问。 在规范的第二部分中提供的钱包结构(主/帐户/链/子链)仅供参考,但建议作为最小结构以便于兼容 - 即使没有单独的帐户或内部链和外部链之间的区别。 但是,实现可能会因特定需求而偏离它; 更复杂的应用程序可能需要更复杂的树结构。
文档https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
该BIP基于BIP-0032(从现在开始的BIP32)和BIP-0043(从现在开始的BIP43)中描述的目的方案中描述的算法定义确定性钱包的逻辑层级。
HD 钱包树状结构提供了极大的灵活性。每一个母扩展密钥有 40 亿个子密钥:20 亿个常规子密钥和 20 亿个强化子密钥。而每个子密钥又会有 40 亿个子密钥并 且以此类推。只要你愿意,这个树结构可以无限类推到无穷代。但是,又由于有了这个灵活性,对无限的树状结构进行导航就变得异常困难。尤其是对于在不同的HD钱包之间进行转移交易,因为内部组织到内部分支以及亚分支的可能性是无穷的
BIP-43 提出使用第一个强化子索引作为特殊的标识符表示 树状结构的“purpose”。基于 BIP-43,HD 钱包应该使用且只用第一层级的树的分 支,而且有索引号码去识别结构并且有命名空间来定义剩余的树的目的地。举个 例子,HD 钱包只使用分支 m/i’/是 为了表明那个被索引号“i”定义的特殊为目地。
在 BIP-43 标准下,为了延长的那个特殊规范,BIP-44 提议了多账户结构作为 “purpose”。所有遵循 BIP-44 的 HD 钱包依据只使用树的第一个分支的要求而被定 义:m/44’/。 BIP-44 指定了包含 5 个预定义树状层级的结构:
第一层的 purpose 总是被设定为 44’。
第二层的“coin_type”特指币种并且允许多元货币 HD 钱包中的货币在第二个层级下有自己的亚树状结构。目前有三种货币被定义:Bitcoin is m/44’/0’、Bitcoin Testnet is m/44’/1’,以及 Litecoin is m/44’/2’。
完整注册硬币类型列表https://github.com/satoshilabs/slips/blob/master/slip-0044.md
第三层级是“account”,这可以允许使用者为了会计或者组织目的,而去再细 分他们的钱包到独立的逻辑性亚账户。 举个例子,一个 HD 钱包可能包含两个比 特币“账户”:m/44’/0’/0’ 和 m/44’/0’/1’。每个账户都是它自己亚树的根。
第四层级就是“change”。每一个 HD 钱包有两个亚树,一个是用来接收地址一个是用来创造找零地址。注意无论先前的层级是否使用强化衍生,这一层级使用的都 是常规衍生。这是为了允许这一层级的树可以在不安全环境下,输出扩展公钥。
被 HD 钱包衍生的可用的地址是第四层级的子级,就是第五层级的树的 “address_index”。比如,第三个层级的主账户收到比特币支付的地址就是 M/44’/0’/0’/0/2。