(十一)继续看bitcoind.cpp中的142-146行
if (!AppInitSanityChecks())
{
//InitError将被调用,并有详细的错误,最终将在控制台结束
exit(EXIT_FAILURE);
}
这个部分最重要的是AppInitSanityChecks()
函数。对这个函数名的定义中我找出了几个关键词:“appinit”,“sanity”和“checks”,我猜测这个函数是关于初始化健全性检查的。那么就来分析这个函数。
AppInitSanityChecks()
函数的声明在init.h的第45行:
/**
*初始化健全性检查:ecc初始化、健全性检查、目录锁检查。
*注意:这可以在daemonization之前完成。如果此函数失败,请不要调用Shutdown()。
*前提:参数解析和配置文件应该已经被读过,AppInitParameterInteraction()函数应该已经被调用过。
*/
bool AppInitSanityChecks();
该函数的实现在init.cpp中1190-1209,其中的有效代码只有8行:
bool AppInitSanityChecks()
{
// *************第四步:健全性检查
// 初始化椭圆曲线密码(ECC)
std::string sha256_algo = SHA256AutoDetect();
LogPrintf("Using the '%s' SHA256 implementation\n", sha256_algo);
RandomInit();
ECC_Start();
globalVerifyHandle.reset(new ECCVerifyHandle());
// 健全性检查
if (!InitSanityCheck())
return InitError(strprintf(_("Initialization sanity check failed. %s is shutting down."), _(PACKAGE_NAME)));
// 如果可能,探测数据目录锁以提供早期错误消息。
// 我们不能将数据目录锁定在这里,因为deamon()尚未调用forking指令,
// 并且fork指令将会对它产生一个奇怪的行为。
return LockDataDirectory(true);
}
由此可以看出,这个函数完成的是初始化的第四步,在该函数中主要完成了三个内容:
- 初始化椭圆曲线密码(ECC);
- 健全性检查;
- 目录锁检查。
下面对这三个内容分别说明:
1. 初始化椭圆曲线密码(ECC)
对于椭圆曲线密码的概念想必对比特币的加密知识了解的都很清楚:比特币使用椭圆曲线算法生成公钥和私钥,选择的是secp256k1曲线。所以在此也再强调一点:比特币中的椭圆曲线算法只在生成公钥和私钥的时候才用到,其他大部分的加密都是用SHA256算法。椭圆曲线算法牵涉到一些密码学知识和许多数学原理,感兴趣的可以参考:
那么比特币中应用的是现有的SHA256算法和椭圆曲线加密算法,那么我们对这两种算法在源码中的定义和实现逻辑不详细分析,感兴趣的可以在网络上自行更深入的了解,我们只是对应用它们之后的效果进行分析。
回到AppInitSanityChecks()
函数的第一部分:
std::string sha256_algo = SHA256AutoDetect();
LogPrintf("Using the '%s' SHA256 implementation\n", sha256_algo);
RandomInit();
ECC_Start();
globalVerifyHandle.reset(new ECCVerifyHandle());
这个部分调用了5个函数:
(1)SHA256AutoDetect()
:该函数是SHA256算法相关的函数,函数声明在sha256.h中32行,函数实现在sha256.cpp的179-192行。由声明可以知道该函数的功能是:
//自动选择最佳有效的SHA256实现。
//返回实现的名称。
即这个函数选择了最合适的实现SHA256的方式,并在日志中打印该消息。
(2)RandomInit()
:该函数声明在random.h的第144行,实现在random.cpp中的464-467行。由声明知道该函数是:
初始化RNG
那么RNG是什么呢?通过查找资料知道RNG(The Random Number Generator)是随机数发生器,它是被嵌入到计算机硬件中的随机数生成器,生成的是伪随机数。对于普通用户使用的笔记本和台式机来说是本身就含有的一部分,和有没有比特币客户端无关,这里可以理解成比特币客户端需要用到这个随机数生成器,给他在客户端初始化了,方便软件的使用。我们读过《精通比特币》这本书都知道:在生成私钥的时候,我们是需要有一个256位的随机数的,而比特币软件使用操作系统底层的随机数生成器来产生256位的熵(随机性)。那么就明白这个函数对私钥生成的重要作用了。
(3)ECC_Start()
:该函数的声明在key.h的183行,实现在key.cpp中291-306行。由它的声明知道该函数的功能:
初始化椭圆曲线支持。如果在调用了第一次之后不首先调用ECC_Stop,就不能第二次调用这个函数。
由该函数的所在位置(key.cpp)可以知道该函数和私钥与公钥的操作有关。而且它的声明文件说的很清楚,是初始化椭圆曲线支持,该函数定义中也多次出现了secp256k1,说明这个函数是开启椭圆曲线算法功能。
(4)ECCVerifyHandle()
:该函数是ECCVerifyHandle类的一个方法,声明在pubkey.h中的241-248行,实现在pubkey.cpp中282-290行。由ECCVerifyHandle类的声明:
此模块的用户必须持有ECCVerifyHandle。不过,它们的构造函数和析构函数不允许并行运行。
说明该函数是实例化ECC的核实处理,这个函数在后面用来验证基于椭圆曲线创建的密钥。
(5)globalVerifyHandle.reset()
:这个主要是reset()
函数,这个是重置函数,主要清除上面的核实处理内存。
总之,这个部分是对椭圆曲线密码(ECC)的初始化。选择SHA256准备、随机数生成器准备、椭圆曲线功能开启、验证椭圆曲线开启和重置内存,都是为ECC的正常运行提供的条件。
2. 健全性检查
这个部分只有两行代码:
if (!InitSanityCheck())
return InitError(strprintf(_("Initialization sanity check failed. %s is shutting down."), _(PACKAGE_NAME)));
主要是判断InitSanityCheck()
函数。
该函数实现位于init.cpp的707-723行:
/** 健全性检查
* 确保比特币在一个可用的环境中运行,并提供所有必要的库支持。
*/
bool InitSanityCheck(void)
{
if(!ECC_InitSanityCheck()) {
InitError("Elliptic curve cryptography sanity check failure. Aborting.");
return false;
}
if (!glibc_sanity_test() || !glibcxx_sanity_test())
return false;
if (!Random_SanityCheck()) {
InitError("OS cryptographic RNG sanity check failure. Aborting.");
return false;
}
return true;
}
该部分又有三个判断函数,ECC_InitSanityCheck()
为椭圆曲线加密结果的完整性验证,glibc_sanity_test()
与glibcxx_sanity_test()
验证当前运行环境是否支持C/C++运行环境,Random_SanityCheck()
验证系统的随机数生成器是否可用。
(1)ECC_InitSanityCheck()
:椭圆曲线加密结果验证。
这个函数声明在key.h中189行:
/** 验证椭圆曲线算法的计算功能是否实时有效 */
bool ECC_InitSanityCheck(void);
它的实现在key.cpp中的284-289行:
bool ECC_InitSanityCheck() {
CKey key;
key.MakeNewKey(true);
CPubKey pubkey = key.GetPubKey();
return key.VerifyPubKey(pubkey);
}
现在我们对这段代码进行分析:
①CKey
类
这个类在key.h中第35行定义,对这个类解释为:一个封装的私钥。则可以知道key
为私钥对象,它是一个无符号字符串类型的数组,在CKey的构造函数中对该参数进行了初始化,定义其字节大小为32字节,并且是必须为32字节。
这个类中还有两个变量:fValid
和fCompressed
fValid
:参数用于表示私钥是否有效,该参数是在私钥值发生变化时进行相应修改,即私钥值有效时,其为true,反之则为false。
fCompressed
:参数代表的是公钥是否为压缩公钥,true为压缩公钥,false为非压缩公钥。
②MakeNewKey()
函数:
该函数在key.h中98行声明:
//使用加密PRNG(伪随机数)来生成私钥
void MakeNewKey(bool fCompressed);
该函数的实现在key.cpp的126-132行:
void CKey::MakeNewKey(bool fCompressedIn) {
do {
GetStrongRandBytes(keydata.data(), keydata.size());
} while (!Check(keydata.data()));
fValid = true;
fCompressed = fCompressedIn;
}
可以看出,在MakeNewKey()
函数中是通过GetStrongRandBytes()
函数循环获取私钥,直到获取的私钥满足Check()
函数验证条件时才停止。
其实MakeNewKey()
函数的作用可以这样理解:我们在前面知道了私钥是机器内部的随机数生成器生成的,但是也不是任意生成的随机数就能满足成为私钥的条件,还是要规定能成为私钥的条件的,MakeNewKey()
函数就是找到符合要求的私钥的。其中GetStrongRandBytes()
函数是生成一个未验证的私钥的,而这个过程也不简单:
a. 首先会通过Open SSL的RNG获取随机数;
b. 然后通过操作系统的RNG获取随机数;
c. 再然后获得私钥的哈希值;
d. 最后清除随机数的内存。
Check()
函数就是检查该随机数是否满足要求的。其实在Check()
函数的有个函数secp256k1_ec_seckey_verify()
就是通过调用libsecp256k1库实现随机数的验证的:如果返回值如果为1,密钥有效;如果为0则无效。传入的两个参数ctx与seckey均不能为NULL,都需要有值。
获得私钥并检查符合作为私钥的条件后,将私钥有效性标志fValid设置为true,同时将传入的fCompressedIn值赋值给fCompressed,用于标识是否使用压缩公钥。
③CPubKey
类:
和CKey
类相似,这个就是公钥类,pubkey
就是公钥对象。chHeader值为2或3,为压缩公钥,长度为33;chHeader值为4或6或7,为非压缩公钥,长度为65;都不是时其长度为空,也可说明该公钥值无效。
④key.GetPubKey()
函数:
那么由私钥怎么怎么到公钥呢?其实就是用这个函数来实现。这个函数在key.cpp的147-158行,其实包括很多加密相关函数的调用,主要调用了secp256k1_ec_pubkey_create()
函数创建公钥值,然后调用secp256k1_ec_pubkey_serialize()
函数实现压缩或非压缩公钥序列值的计算。
注意
我们都知道椭圆曲线这种非对称加密方式,使比特币中的私钥得到公钥是很简单的,但是要想从公钥反推出私钥基本是不可能的,在这里就是用这两个椭圆曲线算法函数来实现这个特点。
⑤VerifyPubKey()
函数:
这个是验证公钥的函数,这个函数在key.h的134行声明,在key.cpp中的175-187行实现。由它的声明中的注释可以知道,这个函数是用来彻底检查私钥和公钥的匹配,这个过程是用另一个完全不同的机制来完成的。我们知道椭圆曲线加密方式的不可逆性,我们得到公钥的机制就用了这个加密算法,那么验证时就不能再根据这个公钥反推出私钥和私钥比较来验证,那么在比特币中是用什么机制来验证的呢?其实可以把它的验证机制看作是模拟交易的签名检验机制:
a. 通过OpenSSL的
GetRandBytes()
函数实现随机数的生成;
b. 根据"Bitcoin key verification\n"字符串与刚生成的随机数共同作用计算随机哈希值;
c. 在Sign函数通过该随机哈希值基于ECDSA算法实现签名值的计算;
d. 利用该签名信息验证获取的公钥的有效性,验证函数位于CPubKey的Verify()
函数中。
(2)glibc_sanity_test()
与glibcxx_sanity_test()
:C与C++运行环境验证。
这两个函数主要是为了验证运行环境中的C/C++运行库的有效性,即比特币核心软件能否在当前环境中正常运行,其所需的运行库是否能够支撑比特币核心的正常运行。
(3)Random_SanityCheck()
函数:验证系统的随机数生成器。
该函数的声明在random.h的第141行:
/**检查操作系统的随机性是否可用,
* 并返回所请求的字节数。
*/
bool Random_SanityCheck();
该函数的实现在random.cpp的411-453行,由实现函数代码中的注释我们可以知道:
这并不能度量随机性的质量,但它确实测试了OSRandom()在给定最大尝试次数的情况下覆盖了输出的所有32个字节。
????????????????????????
对于这部分的源码我有了点困惑:
首先我知道了RNG是随机数生成器,猜测这个函数是检测生成的随机数是否可用的。那么如果是这样的话为什么不把它放在生成私钥之前,即ECC_InitSanityCheck()
函数的前面?像源码中那样把该验证放在公钥都已经生成了之后,那么如果检测到的结果不符合条件,那么按理说私钥和公钥都有问题,那样私钥和公钥生成和检测的工作算是白做了,何不把它放在私钥和公钥生成之前呢?
然后我就怀疑我对这个函数的理解不对,但注释中对它的解释不够全面,代码有些又看不太明白,就先把这个问题留着,等询问大牛和在网上查找相关问题后再补上!
????????????????????????
3. 目录锁检查
这个是AppInitSanityChecks()
函数的最后一步,通过调用LockDataDirectory()
函数来完成,其中LockDataDirectory()
函数实现位于init.cpp的1167-1188行。该函数主要是确保只有一个比特币进程正在使用数据目录,因为要证数据目录在同一台机器中仅被一个比特币核心核心程序所使用,否则如果多个比特币核心程序同时使用同一数据目录,将会造成该程序数据内容产生不一致的情况。
在LockDataDirectory中,首先获取数据目录值,然后打开数据目录下的.lock文件,判断其是否存在。该文件在ubuntu中的$HOME/.bitcoin/文件夹下是存在的,其内容为空。.lock文件的作用是:该文件将通过lock.try_lock()被锁定,但是如果已被其它先启动的比特币程序锁定了的话,本次锁定将失效,同时提示错误信息,并返回false,整个程序将退出。
********************************************
第四步总结:
这一步在函数AppInitSanityChecks()
中实现,主要分为三个部分:首先是初始化椭圆曲线密码(ECC),为一系列的准备工作——选择SHA256准备、随机数生成器准备、椭圆曲线功能开启、验证椭圆曲线开启和重置内存,都是为了下一步做准备工作;然后是InitSanityCheck()
函数进行的健全性检查——包括椭圆曲线加密结果的完整性验证、验证当前运行环境是否支持C/C++运行环境和验证系统的随机数生成器是否可用,是此步骤的核心;最后是AppInitSanityChecks()
函数控制的目录锁检查——确保只有一个比特币进程正在使用数据目录,也是确保只有一个bitcoind运行。
********************************************
(十二)继续看bitcoind.cpp中的147-161行
if (gArgs.GetBoolArg("-daemon", false))
{
#if HAVE_DECL_DAEMON
fprintf(stdout, "Bitcoin server starting\n");
// 后台运行
if (daemon(1, 0)) { // don't chdir (1), do close FDs (0)
fprintf(stderr, "Error: daemon() failed: %s\n", strerror(errno));
return false;
}
#else
fprintf(stderr, "Error: -daemon is not supported on this operating system\n");
return false;
#endif // HAVE_DECL_DAEMON
}
大概可以知道这一部分主要是-deamon
参数的解析,该参数应用于比特币核心的bitcond.exe控制台程序,该程序为比特币核心的后台服务程序。
这段代码的实现流程是:
①首先if条件语句判断是否在启动时设置了守护进程
-daemon
参数,如果设置了则该参数使用设置的代码,并且继续下面的代码;否则返回false。
②用条件编译判断是否包含HAVE_DECL_DAEMON
宏定义,包含则继续下面的代码;不包含则将在控制台中输出当前系统不支持守护进程的错误提示。
HAVE_DECL_DAEMON
宏定义在未经编译的源码中是不包含的,需经过./configure配置后才会出现,该定义位于config/bitcoin-config.h的106行,默认为1,表示定义了daemon;如果为0则为不定义daemon。③如果包含
HAVE_DECL_DAEMON
宏定义,且该值为1,则在控制台中输出"Bitcoin server starting"信息,表明比特币后台守护进程在运行。
④然后再判断daemon(1, 0)
函数的返回值,如果返回值为非零,则输出errorno对应的错误提示,并返回false,程序退出;如果daemon()函数返回为0时则正常运行。
daemon()
函数是成功时返回0,否则返回-1并设置errno。daemon()
函数的用法可以参考:http://www.cppblog.com/cjz/archive/2011/12/29/163123.html由
daemon(1, 0)
可知:当前设置的参数为1和0,根据其注释我们可以得知,该守护进程将当前目录设为根目录,即程序中的相对目录是从该目录开始的,第二个参数设置为0则表示不输出任何信息。
********************************************
-deamon
参数设置总结:
该参数应用于比特币核心的bitcond.exe控制台程序,该程序为比特币核心的后台服务程序。一般在启动时会设置此参数,表示启动bitcoin的后台守护进程,但是如果判断没有在启动时设置此参数会报错,提示当前系统不支持守护进程的错误消息。并且就算是启动了该参数,如果设置的参数不对,也会报错。总之就是检测在程序启动时是否有-deamon
参数并是否是正确设置的值,保证程序正常启动后台服务。
********************************************
(十三)继续看bitcoind.cpp中的163-167行
// 在守护进程后锁定数据目录。
if (!AppInitLockDataDirectory())
{
// 如果锁定数据目录失败,立即退出
exit(EXIT_FAILURE);
}
对于这部分我们可以在注释中了解它主要是完成锁定数据目录的。这部分主要调用了AppInitLockDataDirectory()
函数,该函数的声明在init.h的51行:
/**
* 锁定比特币核心数据目录。
* 注意: 这只能在守护进程之后完成。 如果此功能失败,请勿调用Shutdown()。
* 前提: 应该解析参数并读取配置文件,应该调用过AppInitSanityChecks。
*/
bool AppInitLockDataDirectory();
从该注释中我们知道这个步骤只能在守护进程完成之后,而且要是调用过AppInitSanityChecks()
函数之后。AppInitSanityChecks()
函数我们前面已经学习过了,这个是目标锁检查函数,其中它主要是调用LockDataDirectory()
函数来完成的。我们再看看它的实现文件,在init.cpp中的1211-1221行:
bool AppInitLockDataDirectory()
{
// 在守护进程之后,再次获取数据目录锁定并保持它直到退出;
// 这为竞争条件创造了一个小小的窗口,但是这个条件是无害的:它最多会让我们退出而不打印消息给控制台。
if (!LockDataDirectory(false)) {
// 在LockDataDirectory内部打印详细的错误
return false;
}
return true;
}
不出所料,该函数的实现真的调用了LockDataDirectory()
函数,该函数实现位于init.cpp的1167-1188行,它是目录锁检查的主要实现函数,主要是确保只有一个bitcoind运行。这里是再次获取数据目录锁定并一直保持它目录锁锁定状态,直到程序的退出。