本文所有源代码均来自Bitcoin Core 0.11
1.比特币的私钥
以下是《精通比特币》一书中关于私钥生成部分的描述:
生成密钥的第一步也是最重要的一步,是要找到足够安全的熵源,即随机性来源。生成一个比特币私钥在本质上与“在1到2^256之间选一个数字”无异。只要选取的结果是不可预测或不可重复的,那么选取数字的具体方法并不重要。比特币软件使用操作系统底层的随机数生成器来产生256位的熵(随机性)。通常情况下,操作系统随机数生成器由人工的随机源进行初始化,也可能需要通过几秒钟内不停晃动鼠标等方式进行初始化。对于真正的偏执狂,可以使用掷骰子的方法,并用铅笔和纸记录。
更准确地说,私钥可以是1和n-1之间的任何数字,其中n是一个常数(n=1.158 * 10^77 ,略小于2^256),并由比特币所使用的椭圆曲线的阶所定义(见4.1.5 椭圆曲线密码学解释)。要生成这样的一个私钥,我们随机选择一个256位的数字,并检查它是否小于n-1。从编程的角度来看,一般是通过在一个密码学安全的随机源中取出一长串随机字节,对其使用SHA256哈希算法进行运算,这样就可以方便地产生一个256位的数字。如果运算结果小于n-1,我们就有了一个合适的私钥。否则,我们就用另一个随机数再重复一次。
根据描述,我们找到比特币关于私钥生成部分的源代码:
void CKey::MakeNewKey(bool fCompressedIn) {
RandAddSeedPerfmon();
do {
GetRandBytes(vch, sizeof(vch));
} while (!Check(vch));
fValid = true;
fCompressed = fCompressedIn;
}
函数首先调用了RandAddSeedPerfmon()
函数,让我们看一下这个函数:
void RandAddSeed()
{
// Seed with CPU performance counter
int64_t nCounter = GetPerformanceCounter();
RAND_add(&nCounter, sizeof(nCounter), 1.5);
memory_cleanse((void*)&nCounter, sizeof(nCounter));
}
void RandAddSeedPerfmon()
{
RandAddSeed();
#ifdef WIN32
// Don't need this on Linux, OpenSSL automatically uses /dev/urandom
// Seed with the entire set of perfmon data
// This can take up to 2 seconds, so only do it every 10 minutes
static int64_t nLastPerfmon;
if (GetTime() < nLastPerfmon + 10 * 60)
return;
nLastPerfmon = GetTime();
std::vector<unsigned char> vData(250000, 0);
long ret = 0;
unsigned long nSize = 0;
const size_t nMaxSize = 10000000; // Bail out at more than 10MB of performance data
while (true) {
nSize = vData.size();
ret = RegQueryValueExA(HKEY_PERFORMANCE_DATA, "Global", NULL, NULL, begin_ptr(vData), &nSize);
if (ret != ERROR_MORE_DATA || vData.size() >= nMaxSize)
break;
vData.resize(std::max((vData.size() * 3) / 2, nMaxSize)); // Grow size of buffer exponentially
}
RegCloseKey(HKEY_PERFORMANCE_DATA);
if (ret == ERROR_SUCCESS) {
RAND_add(begin_ptr(vData), nSize, nSize / 100.0);
memory_cleanse(begin_ptr(vData), nSize);
LogPrint("rand", "%s: %lu bytes\n", __func__, nSize);
} else {
static bool warned = false; // Warn only once
if (!warned) {
LogPrintf("%s: Warning: RegQueryValueExA(HKEY_PERFORMANCE_DATA) failed with code %i\n", __func__, ret);
warned = true;
}
}
#endif
}
在非windows平台直接调用RandAddSeed()函数,如上图。这里使用了GetPerformanceCounter()
函数,其源代码:
static inline int64_t GetPerformanceCounter()
{
int64_t nCounter = 0;
#ifdef WIN32
QueryPerformanceCounter((LARGE_INTEGER*)&nCounter);
#else
timeval t;
gettimeofday(&t, NULL);
nCounter = (int64_t)(t.tv_sec * 1000000 + t.tv_usec);
#endif
return nCounter;
}
QueryPerformanceCounter()
查询性能计数器,此函数用于获取精确的性能计数器数值,如果存在则返回当前计数器值的地址 。gettimeofday()
为Linux下获取精确时间的函数。可以看到这里均为了从系统中获取足够安全的熵源,即随机性来源,并将值赋给nCounter。随后使用RAND_add函数。RAND_add(&nCounter, sizeof(nCounter), 1.5);
关于这个函数,详细解释如下:
void RAND_add(const void *buf,int num,double entropy)
增加随机数生成的不可预知性,将buf数组中num个数据加入PRNG中,entropy是对buf中数据的随机性估计值,如果entropy 和num相等,那么RAND_add函数与Rand_seed函数相同;
buf中的数据,一般采用系统中随机性的事件,比如一些交互性数据,用户敲击的键盘值,鼠标滑过的位置等等。
由此我们便利用系统生成了一个随机数种子,并将其加入PRNG(伪随机数生成器)中。
然后循环调用GetRandBytes()
void GetRandBytes(unsigned char* buf, int num)
{
if (RAND_bytes(buf, num) != 1) {
LogPrintf("%s: OpenSSL RAND_bytes() failed with error: %s\n", __func__, ERR_error_string(ERR_get_error(), NULL));
assert(false);
}
}
即计算RAND_bytes()
int RAND_bytes(unsigned char *buf,int num);
根据加密算法生成随机数,其实也是一个伪随机数,但是,如果在调用此函数之前,设定好随机种子,那么生成的随机数是不能被预先计算出来的。
buf:输出,生产的随机数存储的数组;
num: 输入,生产的随机数个数;
返回值:1成功,0失败;
直到满足eccrypto::Check(vch)
函数要求停止,同时将fValid置为true。至此便生成了一个私钥。
2.比特币的公钥
通过椭圆曲线算法可以从私钥计算得到公钥,这是不可逆转的过程:K = k * G 。其中k是私钥,G是被称为生成点的常数点,而K是所得公钥。其反向运算,被称为“寻找离散对数”——已知公钥K来求出私钥k——是非常困难的,就像去试验所有可能的k值,即暴力搜索。
关于生成公钥的函数如下:
CPubKey CKey::GetPubKey() const {
assert(fValid);
CPubKey result;
int clen = 65;
int ret = secp256k1_ec_pubkey_create(secp256k1_context, (unsigned char*)result.begin(), &clen, begin(), fCompressed);
assert((int)result.size() == clen);
assert(ret);
assert(result.IsValid());
return result;
}
核心便是利用secp256k1_ec_pubkey_create()
函数来根据之前生成的私钥产生对应的公钥。
3.比特币的地址
在交易中,比特币地址通常以收款方出现。如果把比特币交易比作一张支票,比特币地址就是收款人,也就是我们要写入收款人一栏的内容。一张支票的收款人可能是某个银行账户,也可能是某个公司、机构,甚至是现金支票。支票不需要指定一个特定的账户,而是用一个普通的名字作为收款人,这使它成为一种相当灵活的支付工具。与此类似,比特币地址的使用也使比特币交易变得很灵活。比特币地址可以代表一对公钥和私钥的所有者,也可以代表其它东西,比如脚本。现在,让我们来看一个简单的例子,由公钥生成比特币地址。
比特币地址可由公钥经过单向的加密哈希算法得到。哈希算法是一种单向函数,接收任意长度的输入产生指纹摘要。加密哈希函数在比特币中被广泛使用:比特币地址、脚本地址以及在挖矿中的工作量证明算法。由公钥生成比特币地址时使用的算法是Secure Hash Algorithm (SHA)和the RACE Integrity Primitives Evaluation Message Digest (RIPEMD),特别是SHA256和RIPEMD160。
以公钥 K 为输入,计算其SHA256哈希值,并以此结果计算RIPEMD160 哈希值,得到一个长度为160比特(20字节)的数字:
A = RIPEMD160(SHA256(K))
不考虑隔离见证,比特币有两种主要的交易类型:
- P2PKH(pay to public key hash)
该类型的脚本可将bitcoin锁定到某一公钥hash上。- P2SH(pay to script hash)
该类型的脚本可将bitcoin锁定到某一脚本的hash值上。
分别对应两种不同的地址:
- 公钥hash
锁定到该种地址上的bitcoin,可用密钥签名即可解锁使用。- 脚本hash
锁定到该种地址上的bitcoin,可用密钥签名+补偿脚本解锁使用。
3.1 对于P2PKH类型交易,地址生成函数如下:
//! Get the KeyID of this public key (hash of its serialization)
CKeyID GetID() const
{
return CKeyID(Hash160(vch, vch + size()));
}
其中Hash160分为SHA256和RIPEMD160两步
/** Compute the 160-bit hash an object. */
template<typename T1>
inline uint160 Hash160(const T1 pbegin, const T1 pend)
{
static unsigned char pblank[1] = {};
uint160 result;
CHash160().Write(pbegin == pend ? pblank : (const unsigned char*)&pbegin[0], (pend - pbegin) * sizeof(pbegin[0]))
.Finalize((unsigned char*)&result);
return result;
}
3.2 对于P2SH类型交易,地址生成函数如下:
/** A reference to a CScript: the Hash160 of its serialization (see script.h) */
class CScriptID : public uint160
{
public:
CScriptID() : uint160() {}
CScriptID(const CScript& in);
CScriptID(const uint160& in) : uint160(in) {}
};
此时便根据公钥生成了160比特(20字节)的地址。
4.总结
比特币由公钥以及地址的生成流程大致如下图: