经过一段时间的积累,终于来到了比特币源码阅读的环节。还是按照之前的节奏,我们就比对着精通比特币一书的进度,进行源码的阅读。
对于此文,只需你对比特币系统中私钥-公钥-地址的产生及关系有最基本的了解
因此你可以放心的直接阅读,如果遇到疑惑,可以返回来阅读以下资料,填补一些基本概念即可:
下面进入正题,本文将对比特币源码中的私钥相关部分进行梳理。
在阅读代码前,先明确一个概念:私钥是如何产生的?
比特币的私钥就是一个256位二进制数字,就这么简单。
但是有一个条件,这个256位二进制数要小于一个非常大的质数n
n = 0xffffffff ffffffff ffffffff fffffffe baaedce6 af48a03b bfd25e8c d0364141
这是由于比特币使用的椭圆曲线secp256k1
的方程所对应的循环子群的秩为n。
这一点了解即可,如果你想了解为什么,建议仔细阅读椭圆曲线加密算法教程
也就是说,你可以用硬币、铅笔和纸来随机生成你的私钥:
掷硬币256次,用纸和笔记录正反面并转换为0和1,随机得到的256位二进制数字可作为比特币钱包的私钥,只要其小于n。
当然更普遍的方法是使用代码生成,但是一定要注意:在你不够了解随机数产生器前,不要自己写代码或使用你的编程语言提供的简易随机数生成器来获得一个随机数作为私钥。应使用密码学安全的伪随机数生成器(CSPRNG),并且需要有一个来自具有足够熵值的源的种子。(这段警告来自于《精通比特币》,我目前还不知晓怎样才算一个足够安全的伪随机数发生器,也望大家告知交流)
明确了私钥的定义,我们来阅读源码。
首先,我们进入src
目录下
使用ls
和grep
命令,试图找到私钥-公钥
相关源文件的位置
一番探索后确定,头文件key.h
中的CKey
类便是私钥的定义。
下面是key.h
的源码,我个人的理解直接放在注释中了
// Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2009-2017 The Bitcoin Core developers
// Copyright (c) 2017 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_KEY_H
#define BITCOIN_KEY_H
#include <pubkey.h> // 显然这是定义公钥的代码
#include <serialize.h>
#include <support/allocators/secure.h>
#include <uint256.h>
#include <stdexcept>
#include <vector>
/** * CPrivKey本质就是一个vector<unsigned char> * 其allocator在support/allocators/secure.h中进行了重载(具体原因暂不清楚) * 这个CPrivKey并不是我们理解的256bit的随机数私钥 * 而是一个编码后的一个私钥,编码的私钥长度为PRIVATE_KEY_SIZE或COMPRESSED_PRIVATE_KEY_SIZE。 * 从代码来看,这个私钥应该是DER编码的,具体什么是DER,为什么这样编码我还不太清楚。 */
typedef std::vector<unsigned char, secure_allocator<unsigned char> > CPrivKey;
/** 封装的私钥类. */
class CKey
{
public:
/** * 定义两个静态的大小用来表示普通私钥的长度和压缩后私钥的长度 * */
static const unsigned int PRIVATE_KEY_SIZE = 279;
static const unsigned int COMPRESSED_PRIVATE_KEY_SIZE = 214;
/** * 压缩后的私钥必须要比压缩前小,这是合理的要求了,使用static_assert在编译时进行检查 */
static_assert(
PRIVATE_KEY_SIZE >= COMPRESSED_PRIVATE_KEY_SIZE,
"COMPRESSED_PRIVATE_KEY_SIZE is larger than PRIVATE_KEY_SIZE");
private:
// 用于表示私钥是否有效
// 因为每次key被修改的时候都会做正确性判断,所以fValid应该和真实的状态保持一致。
bool fValid;
// 表示对应私钥的公钥是否被压缩
bool fCompressed;
//! 实际的私钥数据。
//! 这里存储的是我们所熟悉的256bit私钥(32字节)
std::vector<unsigned char, secure_allocator<unsigned char> > keydata;
//! 判断vch指向的32字节数据是否是有效的私钥数据
bool static Check(const unsigned char* vch);
public:
// 构造函数,初始化fValid和fCompressed,设置keydata的长度为32
CKey() : fValid(false), fCompressed(false)
{
keydata.resize(32);
}
// 重载Ckey的==运算符,只要密钥数据一致,是否压缩也一直,就表示两个CKey数据相同
friend bool operator==(const CKey& a, const CKey& b)
{
return a.fCompressed == b.fCompressed &&
a.size() == b.size() &&
memcmp(a.keydata.data(), b.keydata.data(), a.size()) == 0;
}
// 设置密钥的内容,并通过check判断是否为有效密钥
template <typename T>
void Set(const T pbegin, const T pend, bool fCompressedIn)
{
if (size_t(pend - pbegin) != keydata.size()) {
fValid = false;
} else if (Check(&pbegin[0])) {
memcpy(keydata.data(), (unsigned char*)&pbegin[0], keydata.size());
fValid = true;
fCompressed = fCompressedIn;
} else {
fValid = false;
}
}
// 这块是简单的加了几个方法,能让CKey的函数能够更方便的使用存储私钥的成员keydata
unsigned int size() const { return (fValid ? keydata.size() : 0); }
const unsigned char* begin() const { return keydata.data(); }
const unsigned char* end() const { return keydata.data() + size(); }
// 返回私钥是否有效
bool IsValid() const { return fValid; }
// 返回私钥(对应的公钥)是否是压缩格式的
bool IsCompressed() const { return fCompressed; }
//! 使用随机的方式创建一个新的密钥.
void MakeNewKey(bool fCompressed);
//! 获得私钥
//! 返回的私钥是CPrivKey类型的,也就是编码后的私钥
CPrivKey GetPrivKey() const;
//! 获得公钥
//! 通过私钥计算出公钥并返回
CPubKey GetPubKey() const;
/** * 签名,返回DER序列化的数字签名 * @param[in] hash 要进行签名的哈希值 * @param[out] 签名结果 * @param[test_case] 我也没搞清楚这是干啥的,貌似是传一个和随机数有关的任意数 */
bool Sign(const uint256& hash, std::vector<unsigned char>& vchSig, uint32_t test_case = 0) const;
// Create a compact signature (65 bytes)
bool SignCompact(const uint256& hash, std::vector<unsigned char>& vchSig) const;
// Derive BIP32 child key.
bool Derive(CKey& keyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const;
// 验证私钥和公钥是否匹配。
// 使用的机制并不是使用私钥再次生成公钥并比对
bool VerifyPubKey(const CPubKey& vchPubKey) const;
// 加载一个私钥,顺便判断下与公钥是否匹配
bool Load(const CPrivKey& privkey, const CPubKey& vchPubKey, bool fSkipCheck);
};
/** * 这也是一个私钥类型,应该是和HD钱包相关的 * CKey, CPubKey, CExtKey, CExtPubKey 是bitcoin core中的四种密钥实现 * 其中CKey, CPubKey是普通的私钥和公钥类型 * 而如果想使用 HD Wallet,必须使用CExtKey, CExtPubKey * 由于还不了解HD钱包,这个类我也没有详细阅读 * 或许可以看看这个https://medium.com/codechain/hd-wallet-observed-through-bitcoin-core-source-code-ce38f9eab371 */
struct CExtKey {
unsigned char nDepth;
unsigned char vchFingerprint[4];
unsigned int nChild;
ChainCode chaincode;
CKey key;
friend bool operator==(const CExtKey& a, const CExtKey& b)
{
return a.nDepth == b.nDepth &&
memcmp(&a.vchFingerprint[0], &b.vchFingerprint[0], sizeof(vchFingerprint)) == 0 &&
a.nChild == b.nChild &&
a.chaincode == b.chaincode &&
a.key == b.key;
}
void Encode(unsigned char code[BIP32_EXTKEY_SIZE]) const;
void Decode(const unsigned char code[BIP32_EXTKEY_SIZE]);
bool Derive(CExtKey& out, unsigned int nChild) const;
CExtPubKey Neuter() const;
void SetMaster(const unsigned char* seed, unsigned int nSeedLen);
template <typename Stream>
void Serialize(Stream& s) const
{
unsigned int len = BIP32_EXTKEY_SIZE;
::WriteCompactSize(s, len);
unsigned char code[BIP32_EXTKEY_SIZE];
Encode(code);
s.write((const char *)&code[0], len);
}
template <typename Stream>
void Unserialize(Stream& s)
{
unsigned int len = ::ReadCompactSize(s);
unsigned char code[BIP32_EXTKEY_SIZE];
if (len != BIP32_EXTKEY_SIZE)
throw std::runtime_error("Invalid extended key size\n");
s.read((char *)&code[0], len);
Decode(code);
}
};
// 使用椭圆算法加密前必须调用该程序启用上下文
void ECC_Start(void);
// 使用椭圆加密后使用该函数销毁加密上下文
void ECC_Stop(void);
// 获取运行时椭圆曲线需要的支持是否满足
bool ECC_InitSanityCheck(void);
#endif // BITCOIN_KEY_H
下面简要的阅读源文件key.cpp
生成一个新的私钥的方法如下:
bool CKey::Check(const unsigned char *vch) {
return secp256k1_ec_seckey_verify(secp256k1_context_sign, vch);
}
void CKey::MakeNewKey(bool fCompressedIn) {
do {
GetStrongRandBytes(keydata.data(), keydata.size());
} while (!Check(keydata.data()));
fValid = true;
fCompressed = fCompressedIn;
}
如前所述,比特币私钥其实就是一个256bit随机数(32字节长)
可以看到,代码中使用GetStrongRandBytes()函数生成强随机性的私钥
使用grep命令搜索该方法
$ grep -rlw "GetStrongRandBytes" *
key.cpp
random.cpp
random.h
wallet/wallet.cpp
最终确定,该方法位于random.h
中,可在其中探寻详细代码
可以看到MakeNewKey()
方法,通过不停调用GetStrongRandBytes()
,直到找到能够满足要求的随机数作为私钥
Check()
方法调用libsecp256k1
加密库中的函数secp256k1_ec_seckey_verify
进行验证
获取私钥的方法如下
CPrivKey CKey::GetPrivKey() const {
assert(fValid);
CPrivKey privkey;
int ret;
size_t privkeylen;
privkey.resize(PRIVATE_KEY_SIZE);
privkeylen = PRIVATE_KEY_SIZE;
ret = ec_privkey_export_der(secp256k1_context_sign, privkey.data(), &privkeylen, begin(), fCompressed ? SECP256K1_EC_COMPRESSED : SECP256K1_EC_UNCOMPRESSED);
assert(ret);
privkey.resize(privkeylen);
return privkey;
}
该函数核心是向ec_privkey_export_der()
提供私钥数据keydata(通过begin()传递),进行DER序列化,并返回CPrivKey类型的私钥
至于比特币系统中为什么不传递原始的私钥,而是传递DER编码的私钥,相信随着不断学习,会得到答案。
获取公钥的方法如下
CPubKey CKey::GetPubKey() const {
assert(fValid);
secp256k1_pubkey pubkey;
size_t clen = CPubKey::PUBLIC_KEY_SIZE;
CPubKey result;
int ret = secp256k1_ec_pubkey_create(secp256k1_context_sign, &pubkey, begin());
assert(ret);
secp256k1_ec_pubkey_serialize(secp256k1_context_sign, (unsigned char*)result.begin(), &clen, &pubkey, fCompressed ? SECP256K1_EC_COMPRESSED : SECP256K1_EC_UNCOMPRESSED);
assert(result.size() == clen);
assert(result.IsValid());
return result;
}
可以看到,公钥的获取过程首先调用了libsecp256k1
加密库中的函数secp256k1_ec_pubkey_create()
生成了secp256k1_pubkey类型的公钥pubkey
然后又调用libsecp256k1
加密库中的函数secp256k1_ec_pubkey_serialize
将secp256k1_pubkey类型的公钥序列化为Bitcoin Core中的自定义公钥类型CPubKey
关于私钥的源码阅读就到这里。
由于刚刚窥探Bitcoin Core源码的冰山一角,难免出现理解错误的地方,本文中也列出了我目前还不理解的地方,还望各位能够指出~