比特币源码研读之一
一、源码下载
本文比特币源码下载地址为:https://github.com/bitcoin/bitcoin,下载的版本为github中的最新版本,即0.14版。其源码目录结构如下。
图中红色矩形框选中的src文件夹为比特币源码所在目录,因此我的比特币源码之旅将从这个文件夹开始。
二、找到入口函数
众所周知,任何事物都有其起始位置,就像我们走进一栋房子应该先找到大门一样。软件程序也不例外,每个软件程序都有其入口函数,那么要研读比特币源码,首先需要从其入口函数看起,这样才能逐步理解其执行顺序与逻辑结构。
因此,在进入src目录后,我的第一要事就是找到初始化函数的具体位置。由于刚开始看比特币源码,所以对src中的所有代码都很感兴趣,每个都想点开看看每个文件中都说了什么,而且我自己也确实这么做的,挨个看了一遍之后,发现比特币源码确实有点复杂,着实佩服《精通比特币》的作者,不愧是牛人!他能在把源码看完之后,根据自己对源码的深刻理解写了一本让我们能看懂比特币运行原理的书,以便于我们这些后来者可以很好地理解比特币及其源码。
想到此内心开始有点小激动,因为我看到我们研读班的同学们在经过一段时间的学习之后,编程能力将得到很大的提升,同时,还能有很多的输出,为后来者提供方便!
话不多说,回到正题!前面说到首先要找到比特币源码的入口函数,想到比特币源码是基于C/C++编写的,所以,首当其冲是要找到main函数。想到比特币源码编译完成后,其后台服务进程名为bitcoind,所以,我就想是否有bitcoind.cpp或类似名字的实现文件呢?经过查看,在src文件夹中果然发现了bitcoind.cpp,打开这个文件后,搜索main函数,在189行还真就搜到了main函数,函数中包含的有效代码只有3行,如下所示:
int main(int argc, char* argv[])
{
SetupEnvironment();
// Connectbitcoind signal handlers
noui_connect();
return(AppInit(argc, argv) ? EXIT_SUCCESS : EXIT_FAILURE);
}
对于C/C++程序员来说,这是多么熟悉的函数!说真的,找到这个main函数还真不容易啊,因为这是经过了大量的源码文件浏览才找到的!虽然在找main函数时,花费了不少时间看其他的源码文件,但正如笑来老师在其公众号中说的:“世界上没有白走的路,每一步都算数……”,这个过程对于我熟悉比特币核心源码的整体结构还是有帮助的,让我可以知道钱包实现代码、区块实现代码、区块链实现代码以及挖矿实现代码的具体位置,为后续的源码研读提供了很好的帮助!
三、初始化过程解析
main函数的运行过程如图所示。
具体运行过程为:
第一步:设置运行环境;
第二步:连接bitcoind信号处理对象;
第三步:应用程序初始化操作;
第四步:控制台命令传入参数解析;
第五步:解析后参数的处理;
第六步:初始化日志打印;
第七步:初始化参数设置;
第八步:初始化应用程序基本上下文环境;
第九步:应用程序参数设置;
第十步:应用程序完整性检查;
第十一步:应用程序运行主函数;
第十二部:循环等待关闭消息;
第十三步:程序关闭。
以上就是比特币源码中后台进程bitcoind的运行过程,本文只是列举出了其运行所执行的具体过程,其实每一步都包含了相应的子分支,包含了很多实现代码,我将在后续的文章将对其每一步进行详细说明,敬请期待!
比特币源码研读之二
前一篇文章中已经完成了main函数运行过程的梳理,并且也绘制了其运行流程图,为了更清晰地记录每个过程的详细执行内容,我将从本文开始对每个过程进行说明。
在做代码分析之前,我将先把本文主要涉及的代码文件列举出来,大家可以准备好相应文件,更好地理解源码:
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/util.h、src/util.cpp、src/noui.h、src/noui.cpp、ui_interface.h、ui_interface.cpp
一、设置运行环境SetupEnviroment
main函数执行的第一步是通过SetupEnviroment函数实现比特币后台进程运行环境的设置,SetupEnviroment实现位于src/util.cpp中。在研读SetupEnviroment函数之前,我们有必要先说明下其所在的实现文件util.cpp的主要功能,因为该实现文件将在后续的研读中经常出现。
util.cpp其位于src目录中。util源码文件主要功能是什么呢?我们可以从其头文件util.h的最开始处找到相应的注释说明:
/**
* Server/client environment: argumenthandling, config file parsing,
* logging, thread wrappers
*/
其大意为该源文件实现了服务器/客户端运行环境的设置,包括参数处理、配置文件解析、日志打印以及线程封装等的初始化与属性设置。也可以将util看做是对比特币核心源码的通用功能的统一封装实现文件。
再次回到SetupEnviroment函数,该函数由3部分组成:
(1)内存分配区设置
此处内存分配区设置的目的是为了防止32位操作系统中虚拟地址空间过渡使用,即程序中的控制内存分配。通过sizeof(void*)==4判断当前系统是否为32位,如果是,则通过mallopt设置只有1个内存分配区,即表示系统按CPU进行自动设置。
(2)本地化设置
C/C++程序中,locale(即系统区域设置,即国家或地区设置)将决定程序所使用的当前语言编码、日期格式、数字格式及其它与区域有关的设置,locale设置的正确与否将影响到程序中字符串处理(wchar_t如何输出、strftime()的格式等)。因此,对于每一个程序,都应该慎重处理locale设置。
具体可参见:http://blog.csdn.net/haiross/article/details/45074355
(3)本地化文件路径设置
第三部分代码主要通过boost::filesystem::path::imbue实现文件系统的本地化设置。
二、信号处理noui_connect
noui_connect函数位于noui.cpp中,从文件名noui(no ui,即无UI界面的消息连接)我们可以看出该文件实现是无操作界面的,noui_connect就是无界面情况下的信息连接!
通过分析,发现noui_connect通过信号/槽的通信方式实现子线程与主线程的消息处理功能。其处理的消息包括三类,分别是:消息弹出框信息提示消息、对用户问题询问的交互提示消息以及程序初始化过程的消息。
voidnoui_connect()
{
// Connect bitcoind signal handlers
uiInterface.ThreadSafeMessageBox.connect(noui_ThreadSafeMessageBox);
uiInterface.ThreadSafeQuestion.connect(noui_ThreadSafeQuestion);
uiInterface.InitMessage.connect(noui_InitMessage);
}
上述代码中的uiInterfaceui_为全局变量,该变量在ui_interface.h的最后面进行了声明:
extern CClientUIInterface uiInterface;
在ui_interface.cpp文件的上方实现了其定义:
CClientUIInterface uiInterface;
我们可以看到noui_connect()实现的3行代码中,uiInterface分别调用了3个变量:
ThreadSafeMessageBox、ThreadSafeQuestion以及InitMessage
每个变量都通过connect方法调用了noui.cpp中定义的3个静态函数:
noui_ThreadSafeMessageBox、noui_ThreadSafeQuestion以及noui_InitMessage
我们来分析下上述代码的实现原理。首先来看看uiInterface调用的3个变量的类型与定义。
这3个变量位于ui_interface.h中:
/**Show message box. */
boost::signals2::signal > ThreadSafeMessageBox;
/**If possible, ask the user a question. If not, falls back toThreadSafeMessageBox(noninteractive_message, caption, style) and returns false.*/
boost::signals2::signal > ThreadSafeQuestion;
/**Progress message during initialization. */
boost::signals2::signal InitMessage;
我们可以看到这3个变量的类型均为boost::signals2::signal信号类型,同时,在signal中通过<>方式包含了程序接收到该信号时的处理方法,也就是信号接收槽,整个机制也叫信号/槽机制。Boost中的信号/槽机制在设计模式中对应的是观察者模式,其具体解释如下:
signals2基于Boost的另一个库signals,实现了线程安全的观察者模式。在signals2库中,观察者模式被称为信号/插槽(signals and slots),他是一种函数回调机制,一个信号关联了多个插槽,当信号发出时,所有关联它的插槽都会被调用。
许多成熟的软件系统都用到了这种信号/插槽机制(另一个常用的名称是事件处理机制:event/event handler),它可以很好地解耦一组互相协作的类,有的语言设置直接内建了对它的支持(如c#),signals2以库的形式为c++增加了这个重要的功能。
signal最重要的操作函数是插槽管理connect()函数,它吧插槽连接到信号上,相当于为信号(事件)增加了一个处理的handler。
插槽可以是任意的可调用对象,包括函数指针、函数对象、以及它们的bind表达式和function对象,signal内部使用function作为容器来保存这些可调用对象。连接时可以指定组号也可以不指定组号,当信号发生时将依据组号的排序准则依次调用插槽函数。
如果连接成功connect()将返回一个connection,表示了信号与插槽之间的连接关系,它是一个轻量级的对象,可以处理两者间的连接,如断开、重连接、或者测试连接状态。
成员函数disconnect()可以断开插槽与信号的连接,它有两种形式:传递组号将断开该组的所有插槽,传递一个插槽对象将仅断开该插槽。函数disconnect_all_slots()可以一次性断开信号的所有插槽连接。
我们再来看这3个信号/槽对象的定义,以ThreadSafeMessageBox为例:
boost::signals2::signal
(const std::string& message, const std::string& caption, unsigned int
style), boost::signals2::last_value > ThreadSafeMessageBox;
从定义中我们可以看到signal<>中包含了1个函数定义和1个类型定义:
函数定义:
bool (const std::string&message, const std::string& caption, unsigned int style)
返回类型定义:
boost::signals2::last_value
那么这个函数定义与类型定义的作用是什么呢?其实这个函数定义就是信号对应的槽,类型就是这个槽函数的返回值类型。也就是说,程序在实现该接收槽时,其参数数量、类型、返回值必须要与上述定义一致,才能正确处理对应信号!
有了这个解释,我们再来看我们本节要说明的noui_connect()函数,我们以第一行代码为例来进行代码的解读:
uiInterface.ThreadSafeMessageBox.connect(noui_ThreadSafeMessageBox);
在第一行代码中,我们通过ThreadSafeMessageBox.connect方法确定了槽函数noui_ThreadSafeMessageBox,而这个槽函数的实现就在noui.cpp中,其函数定义如下:
static bool noui_ThreadSafeMessageBox(conststd::string& message, const std::string& caption, unsigned int style)
通过对比我们可以发现noui_ThreadSafeMessageBox其定义完全符合ThreadSafeMessageBox定义所要求的槽函数定义,在该函数中完成了对应信号的实现。通过其代码我们会发现其主要通过日志打印和文件记录方式实现相应信号的处理,以作为程序运行过程的记录。
比特币源码研读之三
自从发表了两篇比特币源码研读总结系列之后,很多朋友都表示对编程和研读源码产生了兴趣,有些朋友提出想加入我们区块链研习社的源码研读班,参与比特币源码的研读、分析与讨论。我和社长看到大家这么积极都很开心,但鉴于第一期名额已满暂不对外开放,我们后续会开放一定名额,让更多人参与进来的,请大家关注我们的通知。
本文将继续沿着我们比特币核心后台进程的运行过程进行研读分析,话不多说,继续我们的征程!
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/util.h、src/util.cpp、src/syn.h、src/syn.cpp、src/scheduler.h、src/ scheduler.cpp
一、应用程序初始化AppInit
AppInit函数是main函数执行的最后一个过程,我们通过该函数的注释(Start)以及AppInit函数的参数为main函数中的参数可知,其代表的是整个比特币后台进程真正开始运行的入口。
//////////////////////////////////////////////////////////////////////////////
//
// Start
//
bool AppInit(int argc, char* argv[])
在AppInit中包含了图中其后所有过程从上至下的调用。首先来看该函数在最开始定义了2个变量。
boost::thread_group threadGroup;
CScheduler scheduler;
boost::thread_group从其字面意思我们可以看出其为线程组,可以实现对多个线程统一管理。
CScheduler为比特币源码中定义的线程调度类,其定义头文件为src/Scheduler.h,实现文件为src/Scheduler.cpp。我们首先来看其头文件中对CSheduler类的解释与使用方法。
// Simple class for backgroundtasks that should be run
// periodically or once “aftera while”
//
// Usage:
//
// CScheduler* s = newCScheduler();
//s->scheduleFromNow(doSomething, 11); // Assuming a: void doSomething() { }
//s->scheduleFromNow(boost::bind(Class::func, this, argument), 3);
//boost::thread* t = newboost::thread(boost::bind(CScheduler::serviceQueue, s));
// … then at program shutdown,clean up the thread running serviceQueue:
// t->interrupt();
// t->join();
// delete t;
// delete s; // Must be done afterthread is interrupted/joined.
本节先分析其注释的含义,该类主要用于对后台任务的管理,其管理对象为那些需要周期执行或在某时刻一次性运行的任务(可理解为线程)。
在CSChedule类的示例使用代码中,我们可以发现其包含了scheduleFromNow(现在执行某任务)、
boost::bind(CScheduler::serviceQueue, s));(线程与任务调度队列绑定, bind是绑定函数和函数的参数)等方法,所以可以看出,其主要是对比特币程序中的任务执行顺序进行设置,并针对执行顺序进行调度,具体使用方法将在后续实际使用时详细说明。
二、参数解析ParseParmeters
AppInit函数中执行的第一个函数为ParsePameters,通过其字面意思我们可以看出其主要功能为解析外部传入的参数。其实现代码位于src/util.cpp中。
(1)互斥锁
该函数的第一行代码为LOCK(cs_args);LOCK和cs_args都没有在该函数中定义,因此我们就需要查找其定义所在,以便我们理解其真正含义。通过查找cs_args定义位于src/util.cpp的上方,其定义为:
CCriticalSection cs_args;
CCriticalSection又为何物,有过线程编程经验的应该知道Critical Section为线程中的访问临界资源,多个线程必须互斥地对它进行访问,即保证在该代码后面的全局变量在程序运行过程中不会被其他线程对其后的变量进行篡改。那么CCriticalSection类在哪定义呢?通过查找发现其在src/sync.h中实现了定义,从定义可以看出该类继承自boost::recursive_mutex互斥类。
class CCriticalSection : publicAnnotatedMixin
{
public:
~CCriticalSection() {
DeleteLock((void*)this);
}
};
了解了cs_args为互斥类对象,我们再来看LOCK函数,其又为何物呢?我们再次来到src/sync.h,可以找到其定义如下。
#define LOCK(cs) CCriticalBlock PASTE2(criticalblock,__COUNTER__)(cs, #cs, __FILE__, __LINE__)
通过其定义,我们可以看出LOCK并不是一个单独的函数,而是一个宏定义,与前面的CCriticalSection对象结合实现对包含代码在各线程中进行互斥加锁处理,防止后续代码中涉及的全局变量被不同线程抢夺。
(2)参数解析
在互斥锁后,程序使用两个map对象实现了传入参数与其参数值的key-value式的映射存储。这两个对象分别为:
map<string, string> mapArgs;
static map<string, vector<string>> _mapMultiArgs;
mapArgs实现对单个参数及其对应值的映射存储,mapMultiArgs实现单个参数及其对应多个值的映射存储。
在使用这两个变量时,程序对其使用clear方法进行了清空操作。
mapArgs.clear();
_mapMultiArgs.clear();
随后通过for循环实现对所有参数进行逐个解析,获取参数及其值,对于存在多个值的参数mapArgs只存储最后的参数值,mapMultiArgs则将参数对应的所有的值通过vector变量进行存储。
至此完成了应用程序初始化的第一步:参数解析任务。在完成参数解析后,程序将完成相应参数的处理工作。具体处理过程,我将在下一篇源码分析文章中给出!
比特币源码研读之四
比特币源码研读系列已经更新了3篇文章了,每篇文章都得到了很多朋友的关注和讨论。也有很多朋友在看了我的研读系列之后加入我们的区块链研习社精英群、比特币源码研读班小密圈以及区块链研习社币圈交流小密圈的,因为他们也看到了我们区块链研习社是重视价值与技术学习的,不是简单地带着大家炒币,而是通过区块链知识的传递,让大家更好地了解区块链领域,正确地分析自己关注的区块链产品,让我们真正拥有站在投资对象背后的资格,从而让我们可以真正做到处乱不惊,长期持有。
照惯例,文将继续沿着我们比特币核心后台进程的运行过程进行研读分析,话不多说,继续我们的征程!
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/util.h、src/util.cpp、src/init.h、src/init.cpp、src/utilstrencodings.、src/utilstrencodings.cpp、src/clientversion.h、src/ clientversion.cpp、config/bitcoin-config.h
本文主要讲述参数处理中的版本信息与帮助信息打印实现代码。
一、版本信息
在完成了上一步的参数解析之后,程序开始进入“参数处理”阶段。首先我们来看ParseParameters函数后面的这段代码。
//Process help and version before taking care about datadir
if (IsArgSet(“-?”) || IsArgSet(“-h”) ||IsArgSet(“-help”) ||IsArgSet(“-version”))
{
std::string strUsage =strprintf(_(“%s Daemon”), _(PACKAGE_NAME)) + ” ” +_(“version”) + ” ” + FormatFullVersion() + “\n”;
if (IsArgSet(“-version”))
{
strUsage +=FormatParagraph(LicenseInfo());
}
else
{
strUsage += “\n” +_(“Usage:”) + “\n” +
“bitcoind [options]” +strprintf(_(“Start %s Daemon”), _(PACKAGE_NAME)) + “\n”;
strUsage += “\n” +HelpMessage(HMM_BITCOIND);
}
fprintf(stdout,”%s”, strUsage.c_str());
return true;
}
(1)代码注释。这段代码的注释的含义为:在处理数据目录操作前,先完成版本与帮助命令的处理。所以,通过这段代码,比特币后台进程将可根据用户输入相应参数给出对应的程序版本与帮助信息。
(2)条件判断。在注释之后的if判断语句中判断bitcoind后台进程参数中是否包含“-?”、“-h”、“-help”或者“-version”,如果包含则执行If中包含的代码,执行完成后返回true,程序运行结束。否则不执行其包含的内容,跳出If语句包含内容,执行其后语句。
此处判断是否包含这几个参数的方法为IsArgSet,该函数的实现位于我们熟悉的src/util.cpp文件中,其代码实现为:
bool IsArgSet(const std::string& strArg)
{
LOCK(cs_args);
returnmapArgs.count(strArg);
}
我们应该还记得上一篇文章中的mapArgs变量,该变量中存储了用户输入的所有参数及其值。所有此处通过mapArgs查找是否包含“-?”、“-h”、“-help”或者“-version”,如果查找到了则返回true,反之为false。还需要说明的是程序通过map类型的变量实现对参数的存储,由于其采用的是键值对存储方式,对于参数信息的快速查找相比于使用数组或队列方式优势很明显。
(3)版本信息。在If语句成立时,其第一行代码的含义为通过strUsage字符串变量存储包含比特币后台进程名称与版本信息内容。
std::string strUsage = strprintf(_(“%sDaemon”), _(PACKAGE_NAME)) + ” ” + _(“version”) +” ” + FormatFullVersion() + “\n”;
strprintf函数为字符串格式化命令,主要功能是把格式化的数据写入某个字符串中。此处是将PACKAGE_NAME写入_(“%s Daemon”)中,生成PACKAGE_NAME Daemon形式的字符串内容。PACKAGE_NAME的定义位于src/config/bitcoin-config.h中,其定义为:
该文件在我们下载的源码中一开始是不存在的,需经过对源码进行./configure命令后才能生成。源码的./configure过程可参见我的《聊聊比特币(Bitcoin)客户端源码编译那些事》一文。
FormatFullVersion函数的功能是输出比特币核心的完整版本信息。该函数的实现位于src/clientversion.cpp中,其实现代码如下:
std::string FormatFullVersion()
{
return CLIENT_BUILD;
}
函数中直接调用了CLIENT_BUILD函数,该函数的定义也在当前文件
const std::string CLIENT_BUILD(BUILD_DESC CLIENT_VERSION_SUFFIX);
再来看BUILD_DESC,其定义就在当前文件:
#ifndef BUILD_DESC
#ifdef BUILD_SUFFIX
#define BUILD_DESCBUILD_DESC_WITH_SUFFIX(CLIENT_VERSION_MAJOR, CLIENT_VERSION_MINOR,CLIENT_VERSION_REVISION, CLIENT_VERSION_BUILD, BUILD_SUFFIX)
#elif defined(GIT_COMMIT_ID)
#define BUILD_DESCBUILD_DESC_FROM_COMMIT(CLIENT_VERSION_MAJOR, CLIENT_VERSION_MINOR,CLIENT_VERSION_REVISION, CLIENT_VERSION_BUILD, GIT_COMMIT_ID)
#else
#define BUILD_DESC BUILD_DESC_FROM_UNKNOWN(CLIENT_VERSION_MAJOR,CLIENT_VERSION_MINOR, CLIENT_VERSION_REVISION, CLIENT_VERSION_BUILD)
#endif
#endif
通过分析#ifndef BUILD_DESC,我们可以判断BUILD_DESC将在BUILD_DESC_FROM_UNKNOWN函数中执行,该函数采用的是预编译实现方式,这样的好处是对于小型、通用性函数采用预编译方式可以提高程序的执行效率。
#define BUILD_DESC_FROM_UNKNOWN(maj, min,rev, build) \
“v” DO_STRINGIZE(maj) “.” DO_STRINGIZE(min)”.” DO_STRINGIZE(rev) “.” DO_STRINGIZE(build)”-unk”
在函数实现中调用了DO_STRINGIZE函数,该函数的实现位于src/clientversion.h中
/**
* Converts theparameter X to a string after macro replacement on X has been performed.
* Don’t mergethese into one macro!
*/
#define STRINGIZE(X) DO_STRINGIZE(X)
#define DO_STRINGIZE(X) #X
通过其注释,我们可以知道该函数的作用是将宏定义的参数X转变为字符串。那问题来了,BUILD_DESC_FROM_UNKNOWN函数中调用的4次DO_STRINGIZE函数包含的变量是宏定义变量吗?答案肯定是的,不然程序就出错了。那包含的maj, min, rev, build在哪定义呢?那我们就需要看BUILD_DESC_FROM_UNKNOWN调用位置传入的四个变量:
CLIENT_VERSION_MAJOR
CLIENT_VERSION_MINOR
CLIENT_VERSION_REVISION
CLIENT_VERSION_BUILD
他们的定义位于src/clientversion.h中,通过定义我们可知其为宏定义,因此传入DO_STRINGIZE函数中是没问题的。
#define CLIENT_VERSION_MAJOR 0
#define CLIENT_VERSION_MINOR 14
#define CLIENT_VERSION_REVISION 2
#define CLIENT_VERSION_BUILD 0
再来看BUILD_DESC_FROM_UNKNOWN的实现,其功能是将版本信息的主要、次要、修正以及建立4个值进行拼接,从而输出完整的版本号信息。
(4)版权许可信息
如果参数中包含”-version”,则将执行以下语句:
if (IsArgSet(“-version”))
{
strUsage +=FormatParagraph(LicenseInfo());
}
我们首先看到strUsage字符串将FormatParagraph(LicenseInfo());返回的内容进行拼接,组成完整的输出信息。
下面来看FormatParagraph函数,其定义位于src/utilstrencodings.h中,实现位于src/utilstrencodings.cpp中,该函数在头文件中的注释内容如下:
/**
* Format aparagraph of text to a fixed width, adding spaces for
* indentationto any added line.
*/
std::string FormatParagraph(const std::string& in, size_t width = 79, size_t indent = 0);
其含义是对成段落的文本信息进行处理,形成固定宽度,为缩进排版的代码行添加空格等格式化处理功能。
FormatParagraph在该处主要是对比特币核心的版权许可信息进行格式化处理。版权许可信息的内容位于src/init.cpp中的LicenseInfo()函数中,
该函数主要实现比特币版权相关信息的输出,函数中使用的CopyrightHolders函数在src/util.cpp中有具体的实现。其主要作用为补全版本信息。
至此,我们完成了版本信息输出实现源码,本文以ubuntu为例,我们在其终端中输入“bitcoind -version”命令来验证,我们对代码理解的正确性。其运行结果如图所示。
图中1为输入bitcoind –version命令,2为输出的比特币版本信息,3为比特币版权信息。通过比较我们可以发现与我们分析的内容基本一致。
二、帮助信息
帮助信息的输出位于版权信息输出之后,当输入的参数为“-?”” -h”、” -help”时,程序将会输出帮助信息。在有了查看版本信息命令的经验后,我们在终端中输入“bitcoind -?”、”bitcoind -h”或” bitcoind -help”将获取相同的帮助内容,均为比特币后台进程包含参数使用方法的帮助信息。具体效果如图所示。
而此帮助信息均在HelpMessage(HMM_BITCOIND);函数中给出,该函数的定义与实现位于src/init.h、src/init.cpp中。该函数的参数类型为枚举变量,该枚举变量的定义如下(注:中文为我自己添加的):
/** The help message mode determines what helpmessage to show */
/**帮助信息模式定义了将显示的帮助信息内容*/
enum HelpMessageMode {
HMM_BITCOIND, //比特币后台进程帮助信息
HMM_BITCOIN_QT //比特币前端界面程序帮助信息
};
此处我们研读的代码为后台进程bitcoind程序,所以参数为HMM_BITCOIND。在HelpMessage函数中,将会根据具体的类型输出相应的帮助信息内容,其帮助内容主要为后台进程涉及参数的使用方法说明。所以,大家后续在使用后台进程时,如果遇到不会的命令,可以通过“bitcoind -?”、”bitcoind -h”或” bitcoind -help”得到帮助信息。
最后程序通过fprintf(stdout, “%s”,strUsage.c_str());实现版本或帮助信息的输出。这里要说明的是stdout为控制台对象,在linux中对应的是终端,windows中对应的是cmd窗口。
至此,我们完成了参数处理中的版本与帮助信息参数处理源码与流程的分析,后续我们将继续完成参数处理的其他部分,并逐步深入到比特币源码的核心代码,让我们更清晰地理解比特币是如何实现钱包、挖矿、交易以及交易脚本等功能的。
比特币源码研读之五
本文将继续参数处理其他部分源码的研读。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/util.h、src/util.cpp、src/init.h、src/init.cpp、src/chainparamsbase.h、src/chainparamsbase.cpp
一、数据目录
此处的数据目录为比特币中的区块、区块链、交易、交易池、钱包以及P2P网络等数据文件所在目录,该目录涉及我们的比特币核心程序是否可以正确运行,因此其正确设置将是至关重要的。数据目录存储文件信息如图所示。
其实现代码如下:
if(!boost::filesystem::is_directory(GetDataDir(false)))
{
fprintf(stderr, “Error: Specified datadirectory \”%s\” does not exist.\n”, GetArg(“-datadir”,””).c_str());
return false;
}
我们首先看第一行的if语句,在该语句中判断了GetDataDir(false)函数返回的数据路径是否为目录名称,如果不是,则打印指定目录不存在的错误提示信息,并且因为数据目录不正确,而导致比特币核心程序无法正常运行,所以返回false,程序退出。
所以,数据目录是否正确的关键在于GetDataDir(false)函数获取的目录信息的正确性。那我们来看GetDataDir函数的具体实现。该函数的实现可以在src/util.cpp中找到。
实现文件src/util.cpp
const boost::filesystem::path &GetDataDir(bool fNetSpecific)
该函数的具体实现流程如图所示。
我们先来看流程图中的第2步和第3步,程序中对GetDataDir(false)函数传入的参数为false,即使用本地文件目录,如果未设置“-datadir”参数,程序将执行流程图中的GetDefaultDataDir函数,该函数的实现也位于src/util.cpp中。
在该函数中我们可以获得比特币后台进程在Windows、Mac以及unix等操作系统下的默认数据目录。具体如下:
// Windows < Vista: C:\Documents andSettings\Username\Application Data\Bitcoin
// Windows >= Vista:C:\Users\Username\AppData\Roaming\Bitcoin
// Mac: ~/Library/Application Support/Bitcoin
// Unix: ~/.bitcoin
第4步中,程序判断是否为网络目录,如果是在执行第5步,在第5步中我们将获得Path中的BaseParams.DataDir()目录,该目录的定义在chainparamsbase.h中有具体实现。
第6步通过fs::create_directories(path);创建数据目录。
第7步返回创建的数据目录,此时程序通过GetDataDir(false)函数获得了数据目录路径,如果路径信息正确且存在,程序将继续运行,否则前文所述,程序将停止运行,返回false。
二、读取配置文件
完成数据目录的创建后,程序将进入配置文件读取部分,其实现代码如下:
ReadConfigFile(GetArg(“-conf”,
BITCOIN_CONF_FILENAME));
我们首先来看BITCOIN_CONF_FILENAME宏定义,比特币后台进程的配置文件名为bitcoin.conf,且位于数据目录中。该文件中包含的信息如图所示。
因此BITCOIN_CONF_FILENAME应该定义为“bitcoin.conf”,通过查找发现其定义位于src/util.cpp中,如图所示。
再来看GetArg函数,在其实现中首先判断是否存在”-conf”参数,如果存在,则使用我们在“比特币源码研读之三”中参数解析结果中保存的参数值作为配置文件,否则使用默认的“bitcoin.conf”。
在获得配置文件名后,我们可以来分析ReadConfigFile函数实现。在该函数实现了配置文件中参数与参数值的读取操作,并将读取的参数信息存入“比特币源码研读之三”mapArgs与_mapMultiArgs中。
函数最后是为防止配置文件中设置了数据目录参数datadir,通过ClearDatadirCache()函数将数据文件路径参数设置为空目录,这样下次进入GetDataDir()时,我们将会根据新的datadir创建数据目录。
至此,我们完成了参数处理中的数据目录创建与配置文件读取源码与流程的分析,后续我们将继续前行,敬请期待!
比特币源码研读之六
本文将继续参数处理其他部分源码的研读。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/util.h、src/util.cpp、src/init.h、src/init.cpp、src/chainparamsbase.h、src/chainparamsbase.cpp、src/chainparams.h、src/chainparams.cpp
一、比特币网络
比特币网络分为主网、测试网以及私有网三种网络,其中主网就是我们现在使用的正式运行的可进行实际交易的网络,在其上我们可以实现物品的交易与服务;测试网顾名思义即为公共测试网络,因为其实测试网,所以,其上的信息是可以重设的;私有网的难度很低,很容易产生块,所以开发者一般在私有网中开展应用的开发与自测试。三个网络的的英文名分别为:
主网:Main network
测试网:Testnet (v3)
私有网:Regression test
二、选择比特币网络
介绍完比特币网络的三种类型后,我们来看如下代码:
//Check for -testnet or -regtest parameter (Params() calls are only valid afterthis clause)
try{
SelectParams(ChainNameFromCommandLine());
} catch (const std::exception&e) {
fprintf(stderr,”Error: %s\n”, e.what());
returnfalse;
}
首先我们来看代码注释,其注释的含义为检测-testnet或者-regtest参数。那么这两个参数是什么意思呢?它们分别是我们前面介绍的测试网和私有网。
知道了后面代码的具体功能后,我们就继续对代码进行深入剖析。注释后面的代码通过try catch异常捕捉机制实现比特币网络设置工作。在try代码块中,SelectParams函数以ChainNameFromCommandLine()返回值作为参数,SelectParams函数的声明位于src/chainparams.h中,其参数为字符串类型。那ChainNameFromCommandLine()的返回值是什么呢?
(1)获取网络名称
要知道ChainNameFromCommandLine()的返回值需进入src/chainparamsbase.cpp一看究竟。其函数实现如下:
std::string ChainNameFromCommandLine()
{
boolfRegTest = GetBoolArg(“-regtest”, false);
boolfTestNet = GetBoolArg(“-testnet”, false);
if(fTestNet && fRegTest)
throwstd::runtime_error(“Invalid combination of -regtest and -testnet.”);
if(fRegTest)
returnCBaseChainParams::REGTEST;
if(fTestNet)
returnCBaseChainParams::TESTNET;
returnCBaseChainParams::MAIN;
}
ChainNameFromCommandLine()函数从其名称可以看出,该函数将从命令行中获取链路的名称。其实现流程如图所示。
1)该函数首先获取”-regtest”与”-testnet”参数设置情况;
2)如果两个参数都设置了,因为一个比特币程序不可能同时存在两个网络,所以,程序将异常退出,同时抛出异常错误,由之前的try catch模块处理,打印异常错误提示信息;
3)如果只设置了回归测试,则返回CBaseChainParams::REGTEST,REGTEST为静态字符串常量,代表的是回归测试,与其并行的另两个网络描述字符串也为静态字符串常量,他们均在src/chainparamsbase.h中声明,也同时在src/chainparamsbase.cpp中定义。其声明与定义如下:
声明:
定义:
const std::string CBaseChainParams::MAIN = “main”; //主网
const std::string CBaseChainParams::TESTNET = “test”; //测试网
const std::string CBaseChainParams::REGTEST = “regtest”; //私有网
4)如果只设置了测试网络,,则返回CBaseChainParams::TESTNET;
5)如果都没有设置,则返回,则返回CBaseChainParams::MAIN。
(2)网络基本参数设置
在获得网络名称后,我们将其传给SelectParams函数,该函数的实现位于chainparams.cpp中,其函数实现如下:
void SelectParams(conststd::string& network)
{
SelectBaseParams(network);
pCurrentParams = &Params(network);
}
在该函数中首先调用SelectBaseParams函数,该函数的实现位于chainparamsbase.cpp中,其函数实现内容如下:
void SelectBaseParams(conststd::string& chain)
{
pCurrentBaseParams = &BaseParams(chain);
}
在该函数中,实现了对链参数对象pCurrentBaseParams的赋值,pCurrentBaseParams的类型为CBaseChainParams指针,其定义位于src/chainparams.cpp中,从定义可以看出pCurrentBaseParams为静态全局变量。
static CBaseChainParams* pCurrentBaseParams = 0;
CBaseChainParams类为前面提到的3种区块链基本参数的基类,3种区块链基本参数设置类分别为:CBaseMainParams、CBaseTestNetParams、CBaseRegTestParams,其定义位于src/chainparamsbase.cpp中,具体定义代码如下:
/**
*Main network主网
*/
class CBaseMainParams : publicCBaseChainParams
{
public:
CBaseMainParams()
{
nRPCPort = 8332;
}
};
static CBaseMainParams mainParams;
/**
*Testnet (v3)测试网
*/
class CBaseTestNetParams : publicCBaseChainParams
{
public:
CBaseTestNetParams()
{
nRPCPort = 18332;
strDataDir = “testnet3”;
}
};
static CBaseTestNetParams testNetParams;
/*
*Regression test私有链
*/
class CBaseRegTestParams : publicCBaseChainParams
{
public:
CBaseRegTestParams()
{
nRPCPort = 18332;
strDataDir = “regtest”;
}
};
static CBaseRegTestParams regTestParams;
从上述定义我们可以看到每个类的构造函数中定义了主链、测试链以及私有链使用的端口与数据目录,其端口分别为8332、18332以及18332。
在完成3种链的定义后,我们再来看BaseParams函数的实现就很容易明白其返回值的意义了:
CBaseChainParams& BaseParams(conststd::string& chain)
{
if (chain == CBaseChainParams::MAIN)
return mainParams;
else if (chain == CBaseChainParams::TESTNET)
return testNetParams;
else if (chain == CBaseChainParams::REGTEST)
return regTestParams;
else
throw std::runtime_error(strprintf(“%s: Unknown chain %s.”,__func__, chain));
}
BaseParams将返回对应链的基本参数对象,并赋值给pCurrentBaseParams。
(3)主要参数设置
我们最后再来看SelectParams中的pCurrentParams = &Params(network);代码。其实现与我们刚看到的BaseParams有点类似,只不过其少了Base单词而已,我们可以这样理解,在执行完链的基本参数设置后,比特币程序将设置相应链的主要参数了。从Params函数实现我们可以看到,其实现与BaseParams是一样的,都是根据链名称获取相应的链参数对象,只不过此处的链路参数类中包含的参数信息更详细些。主链、测试链以及私有链对应的类分别为CMainParams、CTestNetParams、CRegTestParamsstatic,这3个类的定义位于src/chainparams.cpp中,它们均继承了CChainParams类,通过CChainParams可知链参数类主要实现共识参数、CDNSSeedData种子数据、默认端口、创世块信息以及链交易数据等参数的设置。
这里要着重讲解的是共识参数与创世块信息参数,它们分别为:
Consensus::Params consensus;
CBlock genesis;
因为我们主要使用主网,因此以主网中的参数内容来说明我们经常听到的区块奖励减半、出块时间、创世块奖励等参数是如何设置的。
区块奖励减半间隔:consensus.nSubsidyHalvingInterval = 210000;
算力极限值:consensus.powLimit =uint256S(“00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff”);
算力修改间隔:consensus.nPowTargetSpacing = 10 * 60;即10分钟
创世块genesis = CreateGenesisBlock(1231006505, 2083236893,0x1d00ffff, 1, 50 * COIN);第一个块的奖励为50个比特币
以上这些参数都是我们经常听到的名词,如果我们想创建自己命名的数字货币,只需简单修改这些参数即可,比如把区块奖励减半间隔修改为420000或其他数,创世块中比特币奖励的50该为100或其他数。所以要创建一个自己的数字货币并不难,关键在于看其是否有应用价值。
程序在每个类的定义之后,程序也定义了对应的静态链参数对象。
static CMainParamsmainParams;
staticCTestNetParams testNetParams;
staticCRegTestParams regTestParams;
Params将根据用户设置的链参数名称,将对应的链参数返回给pCurrentParams,从而完成链基本参数与主要参数的实现任务。
至此,程序根据用户输入的网络类型参数完成了比特币运行网络的设置。在这段代码中,我知道了私有网络,以前听得最多的是主网和测试网,而私有网或私有链基本很少听到,在这段代码中我知道了私有链是开发团队在开发时使用的网络,因为其挖矿难度很低,很容易进行程序的调试与功能试验。进而让我明白了一些区块链项目为什么会说在XX时刻要进入测试网阶段,然后再是最终的主网运行阶段。因此区块链开发过程应该是这样的:
以上就是本文的源码研读过程,通过源码的研读确实让我更好地理解区块链网络的运转过程,也让我清晰地明白市场上每个区块链的具体进程,这样可以让我更好地判断每个区块链产品的价值,对我投资区块链资产也有很大的帮助。
比特币源码研读之七
源码研读系列从第4期开始到现在已有3期在介绍参数处理部分,可见这部分内容对于比特币后台进程来说是很重要的,因为其运行的状态是根据用户输入的参数来决定的,所以这部分的介绍相对会多些,我将在本期完成该部分源码的研读。话不多说,我们继续参数处理剩余部分源码的研读。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/util.h、src/util.cpp、src/init.h、src/init.cpp
一、RPC命令行判断
RPC为远程过程调用,其在后台进程中RPC调用方式的参数正确设置形式如下:
bitcoind -daemon //后台运行
bitcoind -stop//停止daemon进程
以下为RPC命令行参数设置正确性判断过程:
// Command-line RPC
bool fCommandLine = false;
for (int i = 1; i < argc; i++)
if (!IsSwitchChar(argv[i][0]) &&!boost::algorithm::istarts_with(argv[i], “bitcoin:”))
fCommandLine= true;
if(fCommandLine)
{
fprintf(stderr, “Error: There is no RPC client functionality inbitcoind anymore. Use the bitcoin-cli utility instead.\n”);
exit(EXIT_FAILURE);
}
上述代码对输入的参数逐个判断,首先通过IsSwitchCahr函数(src/util.h)判断参数是否有’-‘或’/’,并且不包含’bitcoin:’,bitcoin:URI是用于转账的,所以应该排除这种情况:bitcoin://1F2EUzKR1XsLRCtEnsnpDQZ13XJgS6P3ZK?amount=0.001&message=donation,如果出现上述情况则在终端中输出错误提示信息,并且退出程序。其显示效果如图所示。
通过其输出信息我们可以知道,对于不含’-‘或’/’的参数和bitcoin:URI只能在bitcoin-cli客户端工具中使用,bitcoind中是无法处理的,会导致程序异常退出。
参考文章:http://blog.sina.com.cn/s/blog_14ed0b9990102wp7p.html
二、服务参数设置
服务参数设置代码实现是通过SoftSetBoolArg(“-server”, true);完成的。其注释如下:
// -server defaults to true for bitcoindbut not for the GUI so do this here
通过该注释我们可以知道bitcoind默认是服务端,其参数-server是true,但是对于GUI(即图形界面程序)则不然,需要显示设置,因此在此处添加该行代码。该函数的实现代码以及其调用的函数均在src/util.cpp中,其代码实现也比较简单,具体如下:
bool SoftSetBoolArg(const std::string&strArg, bool fValue)
{
if (fValue)
return SoftSetArg(strArg, std::string(“1”));
else
return SoftSetArg(strArg, std::string(“0”));
}
bool SoftSetArg(const std::string&strArg, const std::string& strValue)
{
LOCK(cs_args);
if (mapArgs.count(strArg))
return false;
mapArgs[strArg] = strValue;
return true;
}
其实现思路为:首先判断mapArgs(该参数的解释在第4篇研读分析中)中是否存在server参数,如果存在则无需设置,不存在则根据SoftSetBoolArg传入的fValue进行设置相应值。具体服务参数设置就这么简单,大家只要看了之前的源码研读分析文章,该函数的解读很容易理解。
三、初始化日志
接下来我们将进入日志打印内容基础框架设置部分,在该部分主要对日志打印的内容根据参数进行解析,确定后续运行过程中需打印的信息,其具体实现代码也很简单,具体如下:
Src/init.cpp
//日志打印
void InitLogging()
{
fPrintToConsole = GetBoolArg(“-printtoconsole”, false);
fLogTimestamps =GetBoolArg(“-logtimestamps”, DEFAULT_LOGTIMESTAMPS);
fLogTimeMicros = GetBoolArg(“-logtimemicros”,DEFAULT_LOGTIMEMICROS);
fLogIPs = GetBoolArg(“-logips”, DEFAULT_LOGIPS);
LogPrintf(“\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n”);
LogPrintf(“Bitcoin version %s\n”, FormatFullVersion());
}
日志文件的默认位置为我们设置的数据目录中,其名称为debug.log,在ubuntu系统中其默认目录位置为~/.bitcoin。
我们接下来逐个分析其参数含义:
(1)printtoconsole
日志信息是否发送跟踪/调试信息到控制台而不是debug.log文件。我们看到其默认为false,即不打印至控制台或终端上,反之则打印。
(2)logtimestamps
该参数的含义为在日志中打印时间戳,该参数的默认值定义为静态常量DEFAULT_LOGTIMESTAMPS,该常量在src/util.h中定义,与该常量一起定义的还有本函数中使用到的另两个常量,这3个常量分别给作为函数中3个参数的默认值,其分别是:
static const bool DEFAULT_LOGTIMEMICROS = false;//按微秒格式打印日志时间
static const bool DEFAULT_LOGIPS= false; //日志打印中包含IP地址
static const bool DEFAULT_LOGTIMESTAMPS = true;//打印日志时间戳
通过名字我们可以很明显看出它们的对应关系。分别为:
Logtimestamps ——-> DEFAULT_LOGTIMESTAMPS
fLogTimeMicros ——-> DEFAULT_LOGTIMEMICROS
DEFAULT_LOGIPS——-> DEFAULT_LOGIPS
从DEFAULT_LOGTIMESTAMPS的定义可以看出,日志打印中是默认打印时间戳的。
(3)logtimemicros
从DEFAULT_LOGTIMEMICROS的定义可以看出,日志打印中日志时间是按微秒格式打印的。
(4)DEFAULT_LOGIPS
从DEFAULT_LOGIPS的定义可以看出,日志打印中是默认不打印IP地址的。
在ubuntu系统中打开~/.bitcoin下的日志文件debug.log,我们可以看到其具体的日志打印输出内容如图所示。
到此我们完成了AppInit函数中集中处理传入参数的源码,在这个参数解析过程中,我知道了区块链的3个链,那就是第6篇中讲到的私有链、测试链以及公有链;知道了比特币帮助信息输出的代码实现方法;知道了参数存储变量mapArgs与_mapMultiArgs。当然还有很多小的知识点,这里就不一一列举了,相信通过后续的源码研读我将会收获更多,也欢迎更多的朋友一起加入源码研读行动,更多地了解比特币!
比特币源码研读之八
时间过得真快,转眼间比特币源码研读系列已经发表了7篇了,在这过程中,陆续不少朋友关注我的简书、向我打赏、喜欢我的文章,这些都是对我最好的鼓励与支持,也正是因为大家的支持我将继续开展源码研读工作,从技术上深入理解比特币与区块链!
本文将开始对InitParameterInteraction函数进行研读,通过其名称我们可以直观的理解该函数的功能是对传入参数的交互处理,也就是根据参数的信息做出相应的操作或执行相应的任务,这对于我们理解后续比特币程序运行的理解将会很关键,因为这些参数决定了其具体运行方式,所以,让我们一起认真地完成该函数的研读。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/util.h、src/util.cpp、src/init.h、src/init.cpp、src/net.h、src/torcontrol.h、src/torcontrol.cpp
InitParameterInteraction函数的运行主要分为以下7部分:
(1)绑定并监听地址
我们首先来看该部分的实现源码:
// when specifying an explicitbinding address, you want to listen on it
// even when -connect or -proxy isspecified
if (IsArgSet(“-bind”)) {
if (SoftSetBoolArg(“-listen”, true))
LogPrintf(“%s:parameter interaction: -bind set -> setting -listen=1\n”, __func__);
}
if (IsArgSet(“-whitebind”)) {
if (SoftSetBoolArg(“-listen”, true))
LogPrintf(“%s: parameter interaction: -whitebind set -> setting-listen=1\n”, __func__);
}
其注释的含义为:当显示指定了绑定地址后,即使指定了-connect和-proxy参数信息,程序将会接受来自外部的连接,并监听该地址。
绑定地址的方式有两种参数,分别是“-bind”和“-whitebind”,程序对这两种参数的处理方式是一致的,均通过SoftSetBoolArg函数实现“-listen”参数的设置,并将其值设置为true,表示要监听设置的外部连接IP地址。
这里要特别说明的是LogPrintf函数,因为该该函数在后续的代码中将会频繁出现,所以有必要对其进行说明。它在src/util.h中以预编译方式实现的定义,其本身不实现日志打印功能,而是通过调用LogPrintStr函数实现,该函数在src/util.cpp中进行了实现,其实现流程如图所示:
(2)连接可信节点
对连接可信节点参数的处理比较简单,其代码实现如下:
if (mapMultiArgs.count(“-connect”)&& mapMultiArgs.at(“-connect”).size() > 0) {
// whenonly connecting to trusted nodes, do not seed via DNS, or listen by default
if(SoftSetBoolArg(“-dnsseed”, false))
LogPrintf(“%s: parameter interaction: -connect set -> setting-dnsseed=0\n”, __func__);
if(SoftSetBoolArg(“-listen”, false))
LogPrintf(“%s: parameter interaction: -connect set -> setting-listen=0\n”, __func__);
}
其通过mapMultiArgs判断是否包含“-connect”参数,如果包括则将“-dnsseed(使用DNS查找节点)”和“-listen(即接受来自外部的连接,并对其进行监听)”设置为true。并进行日志打印,日志打印函数仍为LogPrintf。
这里需要注意的是,前面(1)中提到如果设置了”-bind”和”-whitebind”参数,程序将会监听指定的IP地址,即使指定了-connect和-proxy参数信息,程序将会接受来自外部的连接,并监听该地址。所以,此处代码的有效执行是在为设置”-bind”和”-whitebind”参数的情况下进行的。
(3)代理模式
设置代理参数的目的是为了保护隐私,所以此处将”-listen”、”-upnp”以及”-discover”均设置为false,也就是说比特币后台进程只使用代理提供的监听地址与端口,并且不去查找默认的监听地址。这里的upnp代表的意思是使用全局即插即用(UPNP)映射监听端口,默认不使用。
但与(2)中的说明一样,如果(1)中设置了”-bind”和”-whitebind”参数,程序将会监听指定的IP地址,即使指定了-connect和-proxy参数信息,程序将会接受来自外部的连接,并监听该地址。所以,此处代码的有效执行是在为设置”-bind”和”-whitebind”参数的情况下进行的。
(4)监听设置处理
监听设置处理代码在if (!GetBoolArg(“-listen”, DEFAULT_LISTEN)){}块中实现。
如果监听参数设置为false,即不实施监听则upnp(端口)、discover(自动发现默认地址)以及listenonion(匿名地址监听)均设置为false。
if (!GetBoolArg(“-listen”, DEFAULT_LISTEN))语句中的DEFAULT_LISTEN在src/net.h中定义。其定义默认为true,具体定义如下:
/** -listen default */
static const bool DEFAULT_LISTEN = true;
此处需要说明的是listenonion(匿名地址监听),此处设计一个通信机制的一个概念:第二代洋葱路由(onion routing),其解释如下:
Tor(The Onion Router)是第二代洋葱路由(onion routing)的一种实现,用户通过Tor可以在因特网上进行匿名交流。Tor专门防范流量过滤、嗅探分析,让用户免受其害。最初该项目由美国海军研究实验室赞助。2004年后期,Tor成为电子前哨基金会的一个项目。2005年后期,EFF不再赞助Tor项目,但他们继续维持Tor的官方网站。
比特币程序中使用src/torcontrol.h、src/torcontrol.cpp实现了Tor的控制,这个类的实现我们将在后续文章说明。
(5)外部IP参数处理
外部IP参数处理代码在if (IsArgSet(“-externalip”)) {}块中实现。
如果显示指定了公共IP地址,那么bitcoind就不需要查找其他监听地址了。
(6)区块模式参数设置
在区块模式参数设置代码在if (GetBoolArg(“-blocksonly”,DEFAULT_BLOCKSONLY)) { }块中实现。
DEFAULT_BLOCKSONLY在Src/net.h中定义,默认值为false,具体定义如下:
/** Default for blocks only*/
static const bool DEFAULT_BLOCKSONLY = false;
如果-blocksonly设置为false,那么在参数中将GetBoolArg设置为true,那么whitelistrelay参数将设置为false,意味着在区块模式下白名单列表将失效。
(7)强制白名单节点连接参数处理
强制白名单节点连接参数处理意味着比特币网络中的信息将优先在白名单节点间传递。
到此我们又完成了一个函数(InitParameterInteraction)源码的研读,在这个函数源码研读的过程中,我们梳理了bitcoind中日志打印的实现逻辑、时间戳显示方式以及输出途径(终端打印还是日志文件打印),还梳理了bitcoind对网络中IP地址的监听设置方法,知道了白名单列表,最后还知道了Tor(The Onion Router)。所以说,源码研读虽然比较晦涩、枯燥,但当我们看懂了其实现逻辑,学到了新知识,我们反而会觉得这是件很有意思的事情!
比特币源码研读之九
本文将继续接着上一篇文章继续开展源码研读之旅,本文将完成初始化基本环境构建源码部分(AppInitBasicSetup)的讲解。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/util.h、src/util.cpp、src/init.h、src/init.cpp
一、警告消息处理
警告消息处理函数实现代码很简单,只有5行代码
#ifdef _MSC_VER
// Turnoff Microsoft heap dump noise
_CrtSetReportMode(_CRT_WARN,_CRTDBG_MODE_FILE);
_CrtSetReportFile(_CRT_WARN,CreateFileA(“NUL”, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, 0));
#endif
通过#ifdef _MSC_VER,我们可以知道上面这段代码是针对微软的VS开发环境而设置的,在其他编译环境下是这段代码是不会执行的。上述这段代码中调用了2个开发环境相关的函数_CrtSetReportMode和_CrtSetReportFile。
(1)_CrtSetReportMode函数定义如下:
int _CrtSetReportMode(
intreportType,
intreportMode
);
其包含的两个参数分别为报告类型和报告输出模式:
报告类型包含:
l_CRT_WARN:警告、消息和不需要立即关注的信息。
l_CRT_ERROR:错误、不可恢复的问题和需要立即关注的问题。
l_CRT_ASSERT:断言失败(断言表达式的计算结果为FALSE)。。
报告模式包含:
l_CRTDBG_MODE_DEBUG:将消息写入调试器的输出窗口。
l_CRTDBG_MODE_FILE:将消息写入用户提供的文件句柄。_CrtSetReportFile应调用以定义要用作目标流的特定文件。
l_CRTDBG_MODE_WNDW:创建一个消息框,以显示该消息以及Abort,Retry,和Ignore按钮。
l_CRTDBG_REPORT_MODE:返回reportMode指定reportType:1 _CRTDBG_MODE_FILE、2_CRTDBG_MODE_DEBUG、4_CRTDBG_MODE_WNDW
由上述分析可以知道当前代码中设置的报告类型为警告,报告输出方式为文件输出。
(2)_CrtSetReportFile函数定义如下:
_HFILE _CrtSetReportFile(
int reportType,
_HFILE reportFile
);
参数reportType与_CrtSetReportMode中的参数一致,都是_CRT_WARN, _CRT_ERROR, and _CRT_ASSERT3种类型,这里就不需要赘述了。
参数reportFile为文件句柄,其对应的文件作为reportType相应消息的输出对象。
在当前代码中使用CreateFileA(“NUL”, GENERIC_WRITE, 0,NULL, OPEN_EXISTING, 0, 0)实现了输出文件的创建。但是其第一个参数值(文件名)为“NULL”,则说明这个文件为空,警告消息虽然说是输出至文件中,但当前为空文件,那么警告消息的输出可理解为被关闭了。这点可从代码注释找到依据:// Turn off Microsoft heap dump noise,即关闭微软内存堆快照的“噪音”,这里的噪音应该就是指此处的警告消息了。
二、abort函数调用处理
很多软件通过设置自己的异常捕获函数,捕获未处理的异常,生成报告或者日志(例如生成mini-dump文件),达到Release版本下追踪Bug的目的。但是,到了VS2005(即VC8),Microsoft对CRT(C运行时库)的一些与安全相关的代码做了些改动,典型的,例如增加了对缓冲溢出的检查。新CRT版本在出现错误时强制把异常抛给默认的调试器(如果没有配置的话,默认是Dr.Watson),而不再通知应用程序设置的异常捕获函数,这种行为主要在以下3种情况出现。
(1)调用abort函数,并且设置了_CALL_REPORTFAULT选项(这个选项在Release版本是默认设置的);
(2)启用了运行时安全检查选项,并且在软件运行时检查出安全性错误,例如出现缓存溢出。(安全检查选项/GS默认也是打开的);
(3)遇到_invalid_parameter错误,而应用程序又没有主动调用_set_invalid_parameter_handler设置错误捕获函数。
所以结论是,使用VS2005(VC8,代码中的宏定义为_MSC_VER >= 1400)编译的程序,许多错误都不能在SetUnhandledExceptionFilter捕获到。这是CRT相对于前面版本的一个比较大的改变,但是很遗憾,Microsoft却没有在相应的文档明确指出。
我们可以通过_set_abort_behavior(0, _WRITE_ABORT_MSG |_CALL_REPORTFAULT)解决(1),也就是abort函数异常错误捕获问题。
详细解释参考:http://blog.csdn.net/yuzhiyuxia/article/details/16889155
三、数据执行保护(DEP)功能处理
数据执行保护(DEP)的目的是为了防止病毒或其他安全威胁造成损害,Windows XP SP2、WinXP SP3, WinVista >= SP1, Win Server
2008使用了数据执行保护(DEP)功能,而GCCs winbase.h将该功能限制在_WIN32_WINNT >= 0x0601 (Windows 7)才能使用,所以在代码中强制定义了宏定义
#ifndef PROCESS_DEP_ENABLE
#define PROCESS_DEP_ENABLE 0x00000001
#endif
并且通过函数指针获取Kernel32.dll中的SetProcessDEPPolicy函数对象,实现DEP功能的开启。
typedef BOOL (WINAPI*PSETPROCDEPPOL)(DWORD);
PSETPROCDEPPOL setProcDEPPol =(PSETPROCDEPPOL)GetProcAddress(GetModuleHandleA(“Kernel32.dll”),”SetProcessDEPPolicy”);
if (setProcDEPPol != NULL)
setProcDEPPol(PROCESS_DEP_ENABLE);
四、初始化网络连接
SetupNetworking函数在src/util.cpp中实现,其主要实现Winsock服务的初始化,初始化的工作通过WSAStartup函数完成。这一步的执行是必须在初始化中完成的,具体原因如下:
为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。使用Socket的程序在使用Socket之前必须调用WSAStartup函数。该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本;操作系统利用第二个参数返回请求的Socket的版本信息。当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。
总得来说,Socket连接涉及的API调用,都必须在WSAStartup函数执行之后才有效,否则将无法执行网络连接操作。
五、信号处理设置
通过宏定义判断#ifndef WIN32,我们可以知道信号处理设置是针对非Windows系统的。信号处理设置代码可分为4部分来分析:
(1)文件创建权限
if (!GetBoolArg(“-sysperms”,false)) {
umask(077);
}
在该代码中,程序首先判断是否设置了sysperms参数,该参数的的含义为:
Create new files with system default permissions,instead of umask 077 (only effective with disabled wallet functionality)
在创建新文件时,文件权限为系统默认权限,以此来代替umask 077命令(因为umask 077只在钱包功能被禁止时才其作用)
如果设置了则返回其状态值,如果为false,则需执行umask(077)命令。
umask用于设置文件与文件夹使用权限,此处077代表—rwxrwx,表示owner没有任何权限,group和other有完全的操作权限。
(2)进程终止信号处理
进程终止信号处理代码如下所示:
// Clean shutdown on SIGTERM
struct sigaction sa; //信号处理对象
sa.sa_handler = HandleSIGTERM; //进程终止信号处理句柄
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGTERM, &sa, NULL); //终止信号处理
sigaction(SIGINT, &sa, NULL); //中断信号处理
从代码可以看到,程序首先定义了信号处理对象sa,然后对其句柄、标志和掩码赋值,最后将该信号对象传递给终止与中断信号处理函数。
这里要说明的是句柄函数HandleSIGTERM,该函数在src/init.cpp中实现,其实现代码为:
void HandleSIGTERM(int)
{
fRequestShutdown = true;
}
很简单的一个函数,将全局变量fRequestShutdown设置为true,所有正在运行的线程将根据一定的规则停止运行。
(3)挂起信号处理
进程挂起信号处理代码如下所示:
// Reopen debug.log on SIGHUP
struct sigaction sa_hup;
sa_hup.sa_handler = HandleSIGHUP; //挂起信号处理句柄函数
sigemptyset(&sa_hup.sa_mask);
sa_hup.sa_flags = 0;
sigaction(SIGHUP, &sa_hup, NULL); //挂起信号处理
从代码可以看出挂起信号处理过程与终止信号处理过程是一样的。这里要说明的是句柄函数HandleSIGHUP,该函数在src/init.cpp中实现,其实现代码为:
void HandleSIGHUP(int)
{
fReopenDebugLog = true;
}
很简单的一个函数,将全局变量fReopenDebugLog设置为true,src/util.cpp中的LogPrintStr将重新打开调试日志打印文件。
(4)管道错误处理
管道错误处理只有一行代码:
signal(SIGPIPE, SIG_IGN);
signal为信号函数,第一个参数表示需要处理的信号值(SIGPIPE,管道错误),第二个参数为处理函数或者是一个表示,此处SIG_IGN表示忽略SIGPIPE那个注册的信号。
此处需设置忽略SIGPIPE管道错误,否则客户端异常关闭时会将守护进程连带着也给关闭,影响守护进程的正常运行。
六、内存分配失败处理
内存分配失败处理函数主要由set_new_handler函数完成,该函数说明如下:
- set_new_handler函数的作用是设置new_p指向的函数为new操作或new[]操作失败时调用的处理函数。
2.设置的处理函数可以尝试使更多空间变为可分配状态,这样新一次的new操作就可能成功。当且仅当该函数成功获得更多可用空间它才会返回;否则它将抛出bad_alloc异常(或者继承该异常的子类)或者终止程序(例如调用abort或exit)。
3.如果设置的处理函数返回了(例如,该函数成功获得了更多的可用空间),它可能将被反复调用,直到内存分配成功,或者它不再返回,或者被其它函数所替代。
4.在尚未用set_new_handler设置处理函数,或者设置的处理函数为空时,将调用默认的处理函数,该函数在内存分配失败时抛出bad_alloc异常。
从上述说明我们可以看出,set_new_handler需要一个内存分配失败后的处理函数,源码中通过new_handler_terminate()函数完成了该功能,通过new_handler_terminate函数中的注释我们可以知道其为了防止影响区块链被破坏,通过执行terminate命令,直接终止程序的方式解决内存分配失败导致的错误,并且进行日志打印。
比特币源码研读之十
本文将继续开展源码研读之旅,本文将开展应用程序参数交互源码部分(AppInitParameterInteraction)的研读与分析。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/util.h、src/util.cpp、src/init.h、src/init.cpp、src/net.h、src/compat.h、src/validation.h
在研读AppInitParameterInteraction的过程中,发现代码中包含了Step 2: parameter interactions和Step 3: parameter-to-internal-flags两部分,既然有Setp 2和Setp 3,那肯定有Step 1,在哪呢?按常理应该从这个函数的前面去查找Step 1。发现Step 1就在我们前一篇文章AppInitBasicSetup中包含了。从此可以很清楚地看出AppInit系列的执行顺序,中本聪对代码注释做得还是很不错的,让读者可以清晰地研读源码。既然这样,我们就跟随Step的步伐前行吧!本文主要开展Step 2: parameter interactions,应用程序参数交互源码的解读。
一、区块修剪参数处理
区块修剪参数处理额代码很简单,3行代码搞定,如图所示。
其修剪参数为prune,从代码可以出该参数与txindex是有冲突的,这点虽然可以从上述代码看出一些端倪,但是我们并不知道二者为什么会存在不兼容性。我们来看看prune参数与txindex参数都有什么作用!
(1)prune参数
刚开始看到prune参数,还有这段代码上面的注释我还以为是直接把区块修剪掉,但仔细一想,这肯定不对的啊,把区块都删除了那还能叫区块链吗?区块链不完整了,那我们常说的账本还能全吗?当然不行,所以带着这个疑惑,我去网上搜索了一番!功夫不负有心人,经过努力让我找到了一篇好帖子,让我对prune参数有了很好的理解。
来源:https://bitcoin.stackexchange.com/questions/36100/pruning-the-branches-in-merkle-tree/
从该页面的提问部分我知道了修剪是针对默克尔树(Merkle Tree)来说的,所以修剪一词用得很贴切,可以直观地看出prune是对树上的节点进行修剪。其修剪的对象又是谁呢?从回答中我们可以看出其修剪对象有2种:
一种是所有输出都被花费的叶子节点(交易);
另一种是节点包含的所有子节点均已被修剪。
根据以上分析我们可以看出修剪的目的是为了节省存储空间。但回答者在后面补充到比特币核心未实现此修剪功能,比特币核心针对修剪功能只以两种模式进行:
不修剪(默认选项)
不保留区块和默克尔树,只跟踪未花费输出(UTXO)和公钥脚本,但该模式不允许你帮助新节点进行同步操作。
我们从代码GetArg(“-prune”, 0)可以看出prune的默认值确实为不修剪,除非是在参数中设置为1,否则不进行修剪操作。该帖子是在2015年2月18日发出来的,帖子的最后说到:
Bitcoin core doesn’t implement this yet. Pruning is likely to be implemented in 0.11(the next version).
0.11版之后的版本开始实现Prune功能了。下面我们再来看看txindex参数。
(2)txindex参数
该参数的注释我们可以从Src/init.cpp的HelpMessage(HelpMessageMode mode)函数中可以找到,当然我们也可以从bitcoind的命令行中获得其帮助信息,该帮助信息其实就是这段源码实现的,所以咱们看源码就可以了。
strUsage += HelpMessageOpt(“-txindex”,strprintf(_(“Maintain a full transaction index, used by thegetrawtransaction rpc call (default: %u)”), DEFAULT_TXINDEX));
从注释我们可以看出txindex参数的作用是维护一个全交易索引,所以其与prune存在不兼容性问题就很明白了。如果两个都设置了,出现一个要剪、一个要全保留的,程序是会乱的,所以此时程序如果发现二者都设置了,将了会报错(”Prune mode is incompatible with-txindex.”),并退出程序。
二、文件描述符处理
Step 2的第二部分是对比特币核心中使用到的文件描述符进行处理,主要是处理用于连接的文件描述符数量,将其控制在一个合理的范围内。那么何为文件描述符呢?
在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号码,因此,在网络通信过程中稍不注意就有可能造成串话。
在编写文件操作的或者网络通信的软件时,初学者一般可能会遇到“Too many open files”的问题。这主要是因为文件描述符是系统的一个重要资源,虽然说系统内存有多少就可以打开多少的文件描述符,但是在实际实现过程中内核是会做相应的处理的,一般最大打开文件数会是系统内存的10%(以KB来计算)(称之为系统级限制),查看系统级别的最大打开文件数可以使用sysctl -a| grep fs.file-max命令查看。与此同时,内核为了不让某一个进程消耗掉所有的文件资源,其也会对单个进程最大打开文件数做默认值处理(称之为用户级限制),默认值一般是1024,使用ulimit -n命令可以查看。
详细描述见:http://blog.csdn.net/cywosp/article/details/38965239
通过对代码的研读可以发现其主要是确保程序中有足够多的文件描述符,用于程序所需文件,套接字的操作。此处的代码中设置了几个重要的宏定义变量,它们分别是:
DEFAULT_MAX_PEER_CONNECTIONS:定义于src/net.h中,代表了最大可维护的节点连接数,默认为125个
static const unsigned intDEFAULT_MAX_PEER_CONNECTIONS = 125;
FD_SETSIZE:定义于src/compat.h,代表可包含的最大文件描述符的个数,默认为1024
#ifdef FD_SETSIZE
#undef FD_SETSIZE// prevent redefinition compiler warning
#endif
#define FD_SETSIZE1024 // max number of fds in fd_set
MIN_CORE_FILEDESCRIPTORS:定义于src/init.cpp中,代表了最小核心文件描述符个数,window下为0,linux下为150
#ifdef WIN32
// Win32 LevelDBdoesn’t use filedescriptors, and the ones used for
// accessing blockfiles don’t count towards the fd_set size limit
// anyway.
#defineMIN_CORE_FILEDESCRIPTORS 0
#else
#defineMIN_CORE_FILEDESCRIPTORS 150
#endif
MAX_ADDNODE_CONNECTIONS:定义于src/net.h中,代表了最大增加节点连接数,默认为8
/** Maximum numberof addnode outgoing nodes */
static const intMAX_ADDNODE_CONNECTIONS = 8;
至此,我们一起完成了AppInitParameterInteraction中的第一部分Step 2,区块修剪与文件描述符处理,代码虽然不多,但包含的内容其实是挺丰富的,所以单独用一篇文件详细对其进行了描述,对于我们理解后续的代码将会很有帮助的。
比特币源码研读之十一
比特币源码研读系列已经发表了十篇了,通过这十篇源码研读系列让我对比特币源码及比特币运行原理有了进一步的理解,也让我不论是在技术层面,还是读者人气都有了很多的收获。这些都是鼓励我继续前行的动力。其实在开始准备本篇文章初期,我在继续看比特币源码时,发现难度开始有点提升了,有些参数和实现开始变得不好理解了,心中确实出现了点退却的心理,因为担心自己的理解出错,误导了读者!但笑来老师这周的文章刚好是关于执行力的,从他的文章让我知道,不会的去学就好了,学会了再来做,直到做成为止,就这么简单,一定不能让自己成为不了了之之人!于是我好像又有能量了,开始沉下心来继续研读源码。相信在后面的研读过程中也将遇到很多问题,我不断提升执行力,继续努力突破困难,让自己成长起来,将我自己的研读成果分享给大家,与大家共同进步!
本文将继续开展应用程序参数交互源码部分(AppInitParameterInteraction)的研读与分析。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/util.h、src/util.cpp、src/init.h、src/init.cpp、src/net.h、src/validation.h、src/txmempool.h
一、不支持内部标志参数提示
在Step 3: 内部标志(flags)参数的最开始部分,比特币核心源码主要是对标志参数的有效性进行判断,并作出警告或错误退出处理。具体处理的参数、处理顺序以及处理方法见流程图。
(1)debug标志参数:主要是标识程序是运行过程中是否输出调试信息,如果-debug=0或者设置了-nodebug参数,则关闭调试信息,并且fDebug=false;(fDebug在src/util.h中声明,在util.cpp中定义);反之如果-debug=1则输出调试信息;
(2)debugnet标志参数:从InitWarning(_(“Unsupported argument -debugnet ignored, use -debug=net.”));语句我们可以看出比特币程序目前已不支持debugnet参数,需用-debug=net代替;
(3)socks标志参数:从InitError(_(“Unsupported argument -socks found. Setting SOCKS version isn’t possible anymore, only SOCKS5 proxies are supported.”));语句我们可以看出比特币程序目前已不支持socks参数,对于socket通讯目前只支持SOCKS5代理协议。SOCKS5代理协议又是什么呢?
百度百科的解释如下:
SOCKS5是一个代理协议,它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色,使得内部网中的前端机器变得能够访问Internet网中的服务器,或者使通讯更加安全。SOCKS5服务器通过将前端发来的请求转发给真正的目标服务器,模拟了一个前端的行为。在这里,前端和SOCKS5之间也是通过TCP/IP协议进行通讯,前端将原本要发送给真正服务器的请求发送给SOCKS5服务器,然后SOCKS5服务器将请求转发给真正的服务器。
SOCKS像一堵墙被夹在Internal服务器和客户端之间,对于出入企业网络的资讯提供流量和安全的管理。
从以上解释我们可以看出使用SOCKS代理协议的主要目的是提高网络通信的安全性。
(4)tor标志参数:tor的英文全称为The Onion Router,即第二代洋葱路由(onion routing),用于匿名通信。关于tor我们在《比特币源码研读之八》中已给出了其具体解释,有兴趣的读者可以前往了解。通过其提示语句InitError(_(“Unsupported argument -tor found, use -onion.”));,我们可以发现目前已不支持-tor参数,使用-onion参数代替之;
(5)benchmark标志参数:通过其提示语句“InitWarning(_(“Unsupported argument -benchmark ignored, use -debug=bench.”));”,我们可以知道benchmark已被忽略,使用debug=bench代替之;
(6)whitelistalwaysrelay标志参数:通过其提示语句“InitWarning(_(“Unsupported argument -whitelistalwaysrelay ignored, use -whitelistrelay and/or -whitelistforcerelay.”));”,我们可以知道whitelistalwaysrelay也已被废弃,转而使用“-whitelistrelay、-whitelistforcerelay”两个参数之一或共同使用来代替之;whitelistrelay参数的意义是节点间的通信优先在白名单节点之接力实现;
(7)blockminsize标志参数:通过其提示语句“InitWarning(“Unsupported argument -blockminsize ignored.”);”,我们可以知道blockminsize参数也已被废弃,讲不能通过blockminsize参数设置区块中包含信息量的最小值。
二、交易池与区块索引检测参数
通过注释“// Checkmempool and checkblockindex default to true in regtest mode(检测交易池和区块索引,这两个参数在私有网模式(也可理解为开发模式)下默认为true,即默认执行检测)”以及后面的代码,我们可以发现checkmempool与checkblockindex在开发模式下是肯定会执行的,其目的是为了比特币在正式运行前确保交易状态与区块索引的正确性。作为开发人员,我们可以很容易理解到,检测程序来说是存在资源消耗的,从而会影响程序的运行效率,那么在测试网模式与主网模式下,这两个参数是默认为false的。
(1)交易池参数检测
我们首先来看交易池参数检测处理代码如下:
int ratio = std::min(std::max(GetArg(“-checkmempool”,chainparams.DefaultConsistencyChecks() ? 1 : 0), 0),1000000);
if (ratio != 0) {
mempool.setSanityCheck(1.0 / ratio);
}
上述源码中,我们首先来看mempool对象,定义于src/validation.h中,其类型为CTxMemPool类,该类在src/txmempool.h中定义,其主要用途是存储主链中发生的有效交易,这些交易也将会被打包至后续的区块中。但如下条件下的交易是不会加入到交易池中的:
1)交易所给出的交易费未达到最小交易费;
2)存在“双花”的交易,即矿池中已包含该交易;
3)非标准交易。
正因为有了上述3个条件,所以CTxMemPool会对交易进行完整性检测,检测的频率通过setSanityCheck函数来设置,该函数的实现内容如下:
void setSanityCheck(double dFrequency = 1.0) { nCheckFrequency = dFrequency * 4294967295.0; }
其中,4294967295=2^32次方-1,此处的nCheckFrequency代表交易池中所有交易的检测频率,即每nCheckFrequency隔多少个交易检测一次。此处以4294967295为基数设置检测频率。
那我们在来看传入的dFrequency值是如何计算的。在init.cpp中,dFrequency的值是1.0/ratio,ratio的计算是在如下代码实现的:
int ratio = std::min(std::max(GetArg(“-checkmempool”,chainparams.DefaultConsistencyChecks()? 1 : 0), 0), 1000000);
这里需要关注的是chainparams.DefaultConsistencyChecks(),chainparams是在const CChainParams& chainparams = Params();中获取,其具体实现的解读可在《比特币源码研读之六》中详细了解。这里要说的是chainparams根据传入的命令参数(默认为主网),其有可能是主网、测试网或私有网3种对象之一。而这3种模式下DefaultConsistencyChecks获取的值是不一样的,在主网、测试网中其返回值为false(见Chainparams.cpp的CMainParams与CTestNetParams类中fDefaultConsistencyChecks=false),私有网的返回值为true(见Chainparams.cpp的CRegTestParams类中fDefaultConsistencyChecks=true)。即只有在私有网中才默认需要进行一致性检测。
而ratio值则是根据-checkmempool传入的参数值与1000000之间的最小值而定的,它们的比较取值用的是std::min,即ratio的值取二者中最小者,我们也可以看出其值不会超过1000000。
(2)区块索引检测
区块索引参数处理也很简单,其实现代码如下:
fCheckBlockIndex =GetBoolArg(“-checkblockindex”,chainparams.DefaultConsistencyChecks());
从其代码我们可以看出,只有在私有网模式下才会进行区块索引的检测,其他两个网默认是不检测的。
fCheckBlockIndex参数定义于src/validation.h中,该变量为全局变量,所以可在init.cpp中根据“checkblockindex”参数进行修改。进而在validation.cpp中的CheckBlockIndex函数中使用该变量,在这个函数中如果checkblockindex为true,将实现了区块索引信息的验证,具体验证内容我们将在后续的研读文章中详细记录。
以上就是本次研读的内容,在这篇研读文章中我们知道了SOCKS5代理协议、用于匿名通信的第二代洋葱路由(onion routing)、交易池以及区块索引等概念,让我们对比特币源码的内在实现有了进一步的理解!我相信后续会有更多值得我们去探索与理解的新概念,敬请期待吧!
比特币源码研读之十二
在我发表了第十一篇源码研读分享之后,社友大硕给我打赏了,而且还说了一句倒出了我心声的话:“菜菜子,你的工作成果能让后来者节省很多时间,很有价值,谢谢分享!”。是的,我在简书中的Sia教程、比特币源码研读系列在我最初编写的时候,只要有2个目的:一是为了保留自己的学习记录,以便后面可以随时回顾自己所学知识,减去重复学习的时间;二是可以让后来者少走弯路,提升学习效率。就是基于这2个原因,我才会坚持写比特币源码研读系列(在这期间我还会穿插),让自己和更多的人节省时间,提高效率,说大点是可以促进比特币和区块链的快速发展!希望我的文章真正能帮到大家,同时,也欢迎大家给我提出宝贵意见,这也能对我成长提供帮助。
本文将继续开展应用程序参数交互源码部分(AppInitParameterInteraction)的研读与分析。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/init.h、src/init.cpp、src/util.h、src/util.cpp、src/validation.h、src/uint256.h、src/uint256.cpp、consensus/Params.h
一、检测点参数提示
检测点参数信息获取代码如下:
fCheckpointsEnabled =GetBoolArg(“-checkpoints”, DEFAULT_CHECKPOINTS_ENABLED);
检测点设置参数checkpoints,在程序中其默认值为DEFAULT_CHECKPOINTS_ENABLED,该常量在src/validation.h中定义,其默认值为true,即需要进行检测点检查。那何为checkpoints,它的作用是什么呢?带着这两个疑问我在网上搜索了一番,找了一篇解释得比较全面的英文帖子(https://bitcoin.stackexchange.com/questions/1797/what-are-checkpoints):
首先是提问者的给出的疑问(Question):
What are checkpoints?
I often read that checkpoints protect the network from a 51% attack because an attacker cannot reverse transactions madebefore the last checkpoint.
How exactly does this checkpoint mechanismwork? And who creates the checkpoints?
从其提问中我们可以得知checkpoints,即检测点,是用于保护网络不受全网51%算力攻击,因为攻击者是不能逆转最后一个检测点前发生的那些交易的。
同时,提问者又提出问题:检测点的工作机制是怎么样的呢?又是谁创建了检测点?
回答是这样的:
The checkpoints are hard coded into the standard client. The concept is, that the standard client will accept all transactions up to the checkpoint as valid and irreversible. If anyone tries to fork the blockchain starting from a block before the checkpoint, the client will not accept the fork. This makes those blocks “set in stone”.
这段话的含义是这样的:检测点是通过硬编码方式写入标准客户端的。意味着标准客户端将在检测点之前将接受所有有效交易,这些交易将是不可逆的。如果任何人试图在检测点前从一个区块对区块链进行分叉,客户端将不会接受这个分叉,这使得那些区块一成不变(set in stone)。
从上述提问与回答我们可以看出checkpoint的重要性,通过checkpoint可以防范区块链被恶意分叉,保护比特币网络的正常运行。
我们再来看看fCheckpointsEnabled的使用情况,该变量为全局变量,声明于src/validation.h,定义于src/validation.cpp中,并且其默认为true。主要在AcceptBlockHeader与TestBlockValidity中使用,这两个函数的具体解释将在后续实际使用时进行详细说明,但我们通过其函数名也可以看出检测点主要是对区块相关内容的检测。
二、哈希假定有效参数
我们首先来看哈希假定有效参数的处理代码:
hashAssumeValid= uint256S(GetArg(“-assumevalid”, chainparams.GetConsensus().defaultAssumeValid.GetHex()));
if (!hashAssumeValid.IsNull())
LogPrintf(“Assumingancestors of block %s have valid signatures.\n”,hashAssumeValid.GetHex());
else
LogPrintf(“Validating signatures for all blocks.\n”);
在该源码中,我们可以看到assumevalid参数的默认值可以通过chainparams.GetConsensus().defaultAssumeValid.GetHex()获得,这个默认值的获得过程包括3个步骤:
(1)获得链上共识参数:通过chainparams.GetConsensus()获得链上共识参数(chainparams在前面的文章中已多次提起,此处就不再赘述),返回类型为Consensus::Params,其详细定义我们可以在consensus/Params.h中找到,Params为一个结构体,该结构体主要定义了影响链上共识的重要参数,比如:创世块(hashGenesisBlock)、奖励减半时间间隔(nSubsidyHalvingInterval)、各种BIP启动时的区块高度、工作量证明参数(powLimit、fPowAllowMinDifficultyBlocks等)、难度调整间隔计算函数(DifficultyAdjustmentInterval)以及默认假定有效对象(defaultAssumeValid);
(2)默认假定有效对象:默认假定有效对象在Consensus::Params中定义uint256 defaultAssumeValid,其类型uint256是一个类,定义于src/uint256.h中,其基类为模板类base_blob,主要用于存储固定大小不透明二进制数值模板,在其后实现了uint160与uint256两个子类,分别实现了160位与256位二进制数值存储类,所以在此处我们可以看到默认假定有效对象主要是需要存储二进制值,而通过base_blob与uint256可以发现该二进制数为区块的哈希值;
(3)获取哈希值:最后一步就是获取哈希值的十六进值,其获取方式通过GetHex()实现。
assumevalid参数的值也可以由用户在命令行中输入,输入形式为包含64位数的十六进制值,其样式如下:
0x00000000000a4d0a398161ffc163c503763b1f4360639393e0e4c8e300e0caec
在获得十六进制值后,我们通过uint256S函数将其转换为uint256对象,该函数定义于src/unit256.h中。其函数实现如下:
inline uint256 uint256S(const char *str)
{
uint256 rv;
rv.SetHex(str);
return rv;
}
最后判断hashAssumeValid的有效性,如果不为空,则假定该哈希值对应区块的所有父区块都具备有效的签名,否则需要验证所有区块的签名,并将判断信息输入在日志中。hashAssumeValid主要在src/Validation.cpp的ConnectBlock函数中使用。
三、交易池大小限定参数
交易池大小限定参数设置代码比较简单,主要是设定交易池最大与最小容量,其代码如下:
// mempool limits
int64_t nMempoolSizeMax =GetArg(“-maxmempool”, DEFAULT_MAX_MEMPOOL_SIZE) * 1000000;
int64_t nMempoolSizeMin =GetArg(“-limitdescendantsize”, DEFAULT_DESCENDANT_SIZE_LIMIT) * 1000* 40;
if (nMempoolSizeMax < 0 ||nMempoolSizeMax < nMempoolSizeMin)
return InitError(strprintf(_(“-maxmempool must be at least %dMB”), std::ceil(nMempoolSizeMin / 1000000.0)));
在上述代码中DEFAULT_MAX_MEMPOOL_SIZE常量定义于src/policy.h中,其代表的是交易池最大存储容量(MB)数值,通过其定义我们可以看到其最大容量为300MB。
/** Default for -maxmempool,maximum megabytes of mempool memory usage */
static const unsigned int
DEFAULT_MAX_MEMPOOL_SIZE = 300;
而交易池最小存储容量对应的默认值为DEFAULT_DESCENDANT_SIZE_LIMIT常量,定义于src/validation.h,其单位为KB,通过其定义我们可以看到其最小存储容量为101KB。
/** Default for-limitdescendantsize, maximum kilobytes of in-mempool descendants */
static const unsigned int
DEFAULT_DESCENDANT_SIZE_LIMIT = 101;
以上就是本次源码研读的内容,在这篇研读文章中我们知道了检测点、哈希假定有效、共识信息参数以及交易池大小范围等内容,可以说我们离比特币源码的核心越来越近了!后面的难度也会随之增加,但只要坚持研读就好,一遍不行就看两遍,不会就的就去学,只要通过努力我们一定能把比特币源码读懂读透的,因为《精通比特币》的作者就是这么过来的,他可是技术大牛啊,连他都愿意花了几个月的时间专注比特币源码的分析,进而掌握比特币的运行原理,我们还有什么理由不跟随着他的脚步前行呢!
比特币源码研读之十三
比特币源码研读系列已完成十二篇了,在前面的十二篇中我们大部分时间是在对传入的参数、定义的全局变量以及代码结构进行了解析,虽然比较繁琐,但是这些内容的研读着实让我对比特币源码中的一些细节有了更深入的理解,掌握了其主要参数所在位置。掌握了这些信息后不管是快速理解后续的源码,还是后续开发自己的区块链产品都可奠定很好的基础。所以,我仍然选择耐着性子稳步对源码进行仔细研读,只有这样才能让自己真正掌握比特币源码,不至于以后还要反复回来重新阅读源码,耗费不必要的时间与精力。同时,也希望大家在看我文章时能提出宝贵意见,一起讨论一起进步。
本文将继续开展应用程序参数交互源码部分(AppInitParameterInteraction)的研读与分析。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/init.h、src/init.cpp、src/util.h、src/util.cpp、src/validation.h、src/uint256.h、src/policy.h、src/policy.cpp、src/amount.h、src/amount.cpp
一、交易费增长量
交易费增长量(incrementalRelayFee)的默认值在src/policy.h中定义,该值为静态全局变量,其默认值为1000聪,具体定义如下:
/**Default for -incrementalrelayfee, which sets the minimum feerate increase formempool limiting or BIP 125 replacement **/
static const unsigned int DEFAULT_INCREMENTAL_RELAY_FEE = 1000;
incrementalRelayFee为全局变量,其在src/policy.h中声明,并在src/policy.cpp中定义,具体定义如下:
CFeeRate incrementalRelayFee = CFeeRate(DEFAULT_INCREMENTAL_RELAY_FEE);
同时init.cpp中针对incrementalRelayFee的注释如下:
// incremental relay fee sets the minimimum feerateincrease necessary for BIP 125 replacement in the mempool
// and the amount the mempool min fee increases above the feerate of txs evicted due to mempool limiting.
通过其注释我们可以看到incrementalRelayFee的功能是设置最小费率增长量,通过设置交易费增长量与交易最小费的目的考虑交易池的容量限制,排除一些交易费过低的交易,即将其交易退回。
此处还需对incrementalRelayFee做进一步解释,该值可理解为最小交易费用设置的最低值,因为交易池中交易费的增量是以incrementalRelayFee为基础的,所以每笔交易费必须大于等于incrementalRelayFee,也就是说最小交易费也必须大于等于该值。
我们再来看针对incrementalrelayfee参数的处理代码,程序首先通过IsArgSet判断是否设置了incrementalrelayfee参数,如果设置了,则通过ParseMoney函数将输入的以字符串表达的交易增长费转换为数字型的增长交易费,ParseMoney与其反向求解的FormatMoney函数均定义与src/utilmoneystr.h,这两个函数一个是将数字转换为字符串,一个是将字符串转换为数字。
如果传入的金额无效则退出程序,反之为incrementalRelayFee赋值,为其费率值赋予传入的数值:
incrementalRelayFee = CFeeRate(n);
通过CFeeRate(src/amount.h中定义与注释)我们可以知道传入的n的单位为每千字节需要n聪的金额。
二、验证脚本线程数
验证脚本线程数可通过-par参数设置,其线程数获取代码如下:
nScriptCheckThreads = GetArg(“-par”,DEFAULT_SCRIPTCHECK_THREADS);
通过其注释我们可以看到-par=0时意味着程序自动根据机器情况自动检测线程数,同时如果nScriptCheckThreads==0意味着将不按并发方式实现脚本验证,即脚本验证线程数为0。
默认线程数DEFAULT_SCRIPTCHECK_THREADS在src/validation.h中定义,其默认值为0,意味默认选择自动检测验证线程数。
/**-par default (number of script-checking threads, 0 = auto) */
static const int DEFAULT_SCRIPTCHECK_THREADS = 0;
同时与该宏定义一起定义的还有最大脚本验证线程数常量MAX_SCRIPTCHECK_THREADS,其默认值为16,即程序中最多启动16个线程用于脚本验证操作,其定义如下:
/** Maximum number of script-checking threads allowed*/
static const int MAX_SCRIPTCHECK_THREADS = 16;
我们再来看后面对nScriptCheckThreads的逻辑判断,当nScriptCheckThreads输入值为0或负数时,程序将通过GetNumCores()函数获取程序运行机器能提供的线程数,然后nScriptCheckThreads加上获取的线程数获得脚本验证的线程数。最后是判断nScriptCheckThreads的值:
(1)如果为nScriptCheckThreads<= 1,则nScriptCheckThreads=0;
(2)如果nScriptCheckThreads >MAX_SCRIPTCHECK_THREADS,即验证线程数大于16时,则nScriptCheckThreads赋值为MAX_SCRIPTCHECK_THREADS。
nScriptCheckThreads在src/validation.cpp以及src/init.cpp中使用,src/validation.cpp中主要用于判断是否需要进行脚本验证,使用之处包括
(1)CCheckQueueControl control(fScriptChecks &&nScriptCheckThreads ? &scriptcheckqueue : NULL);
(2)CheckInputs(tx, state, view, fScriptChecks, flags, fCacheResults,txdata[i], nScriptCheckThreads ? &vChecks : NULL)。
而src/init.cpp中则在后面将会研读到的AppInitMain函数中,在该函数中将根据nScriptCheckThreads启动相应数量的线程,其实现代码如下:
LogPrintf(“Using %u threads for scriptverification\n”, nScriptCheckThreads);
if (nScriptCheckThreads) {
for(int i=0; i
threadGroup.create_thread(&ThreadScriptCheck);
}
通过上面代码我们可以看出,通过线程组创建nScriptCheckThreads个脚本验证线程,线程处理函数为ThreadScriptCheck,其定义于src/validation.h中,实现于src/validation.cpp中,在该函数中通过脚本验证队列管理脚本验证线程,其具体运行方式我们将AppInitMain函数的研读中详细说明。
以上就是本篇文章的研读记录,这篇文章涉及的交易费增长量和验证脚本线程都与比特币中的交易相关,正如《精通比特币》作者在其交易章节说到的“比特币交易是比特币系统中最重要的部分。”,所以,我在写本文的研读记录时对其中的概念进行了多方斟酌、考量与验证,当然也有可能存在理解不到位的情况,大家如果有好的建议可以在留言中给出,我们可以一起讨论,完善我们的研读系列!
比特币源码研读之十四
由于近期比较忙,所以源码研读系列更新较之前有点慢,但不管怎么样源码研读系列将会继续写下去的,保证每周至少有一篇,这样才能源码研读的持续性。
自从开始对比特币源码进行研读之后,在看比特币相关的技术文章时,让我可以很清晰地理解文章中提到的一些专业术语,比如前几天看到的segwit、setwit2x以及比特币实行setwit后要修改的DNSSeed,这些术语我都能联想到其所在源码位置以及其底层实现。有种内行看门道的感觉,所以,更加坚定了我继续坚持进行源码研读的信心。
话不多说,让我们继续源码研读的征程。同时再次希望大家在看我文章时能提出宝贵意见,一起讨论一起进步。
本文将继续开展应用程序参数交互源码部分(AppInitParameterInteraction)的研读与分析。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/init.h、src/init.cpp、src/util.h、src/util.cpp、src/validation.h、src/RPC/register.h、src/rpc/blockchain.cpp、src/rpc/net.cpp、src/rpc/misc.cpp、src/rpc/mining.cpp、src/rpc/ rawtransaction.cpp、src/wallet/rpcwallet.cpp
一、区块裁剪
区块裁剪处理的标识参数为“prune”,该参数我们已在《比特币源码研读之十》的“区块修剪参数处理”中进行了详细描述,其主要是针对默克尔树(Merkle Tree)来说的,所以整个修剪用得很贴切,可以直观地看出prune是对树上的节点进行修剪。其修剪的对象又是谁呢?从回答中我们可以看出其修剪对象有2种:
(1)一种是所有输出都被花费的叶子节点(交易);
(2)另一种是节点包含的所有子节点均已被修剪。
根据以上分析我们可以看出修剪的目的是为了节省存储空间。
在《比特币源码研读之十》中程序只是解析prune参数,并与txindex参数进行冲突判断,因为二者不能同时设置,具体内容可参见第十篇文章。
我们通过其注释可以知道区块裁剪是针对预先设定的存储容量来进行的,即根据客户端所在计算机中的存储情况进行设定,如果超过了设定值,将进行区块裁剪,以防超过设定值,并且该设定值为MiB单位。其注释如下:
block pruning; get the amount of disk space (in MiB) to allot for block & undo files
区块裁剪操作根据其prune参数的值进行处理的流程如图所示。
(1)首先是获取prune参数值并赋值给nPruneArg,通过GetArg函数我们可以知道其默认值为0;
(2)判断nPruneArg是否为小于0,此时程序将出错,并退出,因为我们知道如果nPruneArg小于0,表示不会为区块提供存储空间了,程序将无法正常工作,这显然是不合理的,所以程序直接退出是合理的;
(3)如果nPruneArg大于0,则计算该值所对应的字节数,其计算公式为
nPruneTarget = (uint64_t) nPruneArg * 1024 * 1024;
(4)此处判断prune是否为1,如果为1则转入第5步中,反之则进入第6步;
(5)此处为prune== 1的情况,在该情况下程序不会自动对区块进行裁剪,需要我们通过RPC命令pruneblockchain对相应区块进行裁剪,同时因为是手动裁剪,也就没有裁剪限定值一说了,所以程序将其设置为最大值,并且设置裁剪模式为true,即fPruneMode=true,其中fPruneMode为全局变量,其在src/validation.h中声明,并在src/validation.cpp中定义,其默认值为false,即不进行裁剪;
(6)此处为prune不等于1的情况,在该情况下首先判断设定的裁剪值是否小于程序默认设置的用于存储区块的最小硬盘存储空间MIN_DISK_SPACE_FOR_BLOCK_FILES,该值在src/validation.h中定义,其定义如下:
static const uint64_tMIN_DISK_SPACE_FOR_BLOCK_FILES = 550 * 1024 * 1024;
从其定义我们可以看出,为区块设定的最小存储空间为550MiB,所以我们设置的-prune参数的值必须大于等于550才能让程序正常运行,除非像(5)中设置为1。如果nPruneTarget>=550,程序将继续正常运行,并且PruneMode将设置为true。
以上就是区块裁剪参数的具体处理过程,在此处完成裁剪信息的设置后,程序将在后面根据这些信息进行有效地开展区块裁剪操作。
二、RPC命令注册
比特币核心程序在src/RPC/register.h文件中实现了对RegisterAllCoreRPCCommands的定义与实现,在该函数中实现了区块链、P2P网络、挖矿、交易以及其他工具等模块的RPC命令的注册。该函数中包含的这些命令注册函数分别为:
static inline voidRegisterAllCoreRPCCommands(CRPCTable &t)
{
RegisterBlockchainRPCCommands(t);—区块链RPC命令注册
RegisterNetRPCCommands(t);—P2P网络RPC命令注册
RegisterMiscRPCCommands(t);—其他工具RPC命令注册
RegisterMiningRPCCommands(t);—挖矿RPC命令注册
RegisterRawTransactionRPCCommands(t);—交易PRC命令注册
}
下面我们分别来看每个RPC命令注册实现的具体位置:
(1)区块链RPC命令:在命令在src/rpc/blockchain.cpp中实现,其包含的RPC命令通过命令常量数组commands进行存储,具体包含的命令如下:
该常量数组中的name项即为RPC命令,程序在RegisterBlockchainRPCCommands中通过对commands的遍历实现这些命令的添加,即为注册:
voidRegisterBlockchainRPCCommands(CRPCTable &t)
{
for (unsigned int vcidx = 0; vcidx
t.appendCommand(commands[vcidx].name,&commands[vcidx]);
}
(2)P2P网络RPC命令:在命令在src/rpc/net.cpp中实现,其包含的RPC命令也是通过命令常量数组commands进行存储,具体包含的命令如下:
P2P网络RPC命令的注册方法与区块链的一致,其通过RegisterNetRPCCommands实现,具体实现如下:
void RegisterNetRPCCommands(CRPCTable &t)
{
for(unsigned int vcidx = 0; vcidx < ARRAYLEN(commands); vcidx++)
t.appendCommand(commands[vcidx].name, &commands[vcidx]);
}
(3)其他工具RPC命令:在命令在src/rpc/misc.cpp中实现,其包含的RPC命令通过命令常量数组commands进行存储,具体包含的命令如下:
其他工具RPC命令的注册方法与区块链的一致,其通过RegisterMiscRPCCommands实现,具体实现代码与区块链的一致;
(4)挖矿RPC命令:在命令在src/rpc/mining.cpp中实现,其包含的RPC命令也是通过命令常量数组commands进行存储,具体包含的命令如下:
挖矿RPC命令的注册方法与区块链的一致,其通过RegisterMiningRPCCommands实现,具体实现代码与区块链的一致;
(5)交易PRC命令:在命令在src/rpc/ rawtransaction.cpp中实现,其包含的RPC命令通过命令常量数组commands进行存储,具体包含的命令如下:
交易RPC命令的注册方法与区块链的一致,其通过RegisterRawTransactionRPCCommands实现,具体实现代码与区块链的一致。
完成这些命令的注册后,后面是针对钱包RPC命令的注册,钱包命令是否注册是根据程序编译时是否包含钱包模块而定的,其判断条件为
#ifdef ENABLE_WALLET
即如果钱包功能打开,则需进行钱包RPC命令的注册,反之则不需要。钱包RPC命令注册在src/wallet/rpcwallet.cpp中实现,其实现方法与区块链是一样的,通过commands数组进行存储,然后在RegisterWalletRPCCommands函数中进行命令的注册。
说了这么多RPC命令的注册,那RPC命令是怎么用的呢?在哪可以用这些命令呢?这些命令是在比特币核心客户端中使用的,具体来说其在比特币核心的“Help(帮助)”菜单下的“Debug Window(调试窗口)”中使用,调试窗口界面如下:
我们可在该调试窗口的命令框中输入相应的RPC命令,然后回车即可执行相应命令,调试窗口中将显示其执行结果。具体命令的运行大家可根据实际情况输入。如果临时不记得命令,可在输入框中输入help获取命令提示帮助。
以上即为本次研读内容,在本文我们分析了区块裁剪的处理以及比特币中各模块包含的RPC命令、命令注册以及命令的运行。希望对大家理解这块源码有帮助,也欢迎大家留言讨论。
比特币源码研读之十五
比特币昨日已成功突破3万大关,这个价格是上个月还处于无法想象的,作为普罗大众的我们是根本不敢想的。不过这点还是要佩服笑来老师,他在硬币资本的分享会上就说了:“现在价格处于下跌阶段,正是买入好时机。”。所以,还真是要像李老师那样深刻理解比特币的内涵与原理才能做到长期持有,才能拿得住!而投资的世界与我们平时的世界是反的,在投资的世界里我们越频繁地操作就越容易犯错,我们要做的就是在投资之前花时间认真研究投资标的,研究后认可其价值则长期持有之,坚决不能频繁关注价格!我们要做的就是去学习知识、学习区块链知识、学习比特币源码,进而深刻理解其内涵!在掌握了比特币的实现原理之后,我们再去研究其他的区块链资产肯定可以得心应手了。所以现在让我们继续在比特币源码研读之路中前行。开启我们的第十五篇源码研读之旅。
本文将继续开展应用程序参数交互源码部分(AppInitParameterInteraction)的研读与分析。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/init.h、src/init.cpp、src/util.h、src/util.cpp、src/netbase.h、src/policy/policy.h、src/policy/policy.cpp
一、节点超时参数
比特币网络中新加入的节点都会去寻找节点,加入比特币P2P网络中,与其他节点完成同步操作。但是在网络中寻找节点,并建立连接是有时间限制的,即会出现连接超时的问题。这个超时时间通过src/netbase.h中的全局变量DEFAULT_CONNECT_TIMEOUT定义,其默认值为5000毫秒,最小值为1毫秒,这个1毫秒的最小值是通过-timeout参数的帮助信息得知的。该帮助信息可在src/init.cpp中的HelpMessage函数中获取,或者在比特币程序的help命令进行查询-timeout参数的详细信息。
默认值(src/netbase.h):
//! -timeout default
static const int DEFAULT_CONNECT_TIMEOUT = 5000;
最小值(src/init.h):
strUsage +=HelpMessageOpt(“-timeout=”, strprintf(_(“Specifyconnection timeout in milliseconds (minimum: 1, default: %d)”), DEFAULT_CONNECT_TIMEOUT));
程序中的帮助信息
我们通过timeout参数解析代码可以明确地知道连接超时时间不能设置为负数,如果为负数则将设置为默认值。
二、最小交易费
此处讨论的最小交易费涉及四方面内容:minrelaytxfee、incrementalRelayFee、blockmintxfee以及dustrelayfee。
(1)minrelaytxfee为最小交易费率。通过代码与注释我们可以了解到该费率为每千字节所需的最小费率。该费率值的设置对于矿工来说很重要,需谨慎设置,切忌设置为为0,因为如果设置为0时,每个被挖出的区块中都将会被塞满1聪交易费的交易,这将会使得矿工入不敷出。所以最低交易费必须高于处理交易所需成本。其默认值为DEFAULT_MIN_RELAY_TX_FEE=1000,定义于src/policy/policy.h中。最小交易费用通过全局变量::minRelayTxFee进行存储,其类型为CFeeRate。如果我们在程序没有设置minrelaytxfee参数,minRelayTxFee必须大于等于incrementalRelayFee(其含义见《比特币源码研读之十三》);
(2)incrementalRelayFee该变量我们已经在《比特币源码研读之十三》中介绍了,具体内容可前往第十三篇阅读;
(3)blockmintxfee为区块中打包交易的最小费用值信息,我们可以通过其帮助信息了解到其最低费用通过src/policy.h中的DEFAULT_BLOCK_MIN_TX_FEE全局变量进行定义。
/** Default for -blockmintxfee, whichsets the minimum feerate for a transaction in blocks created by mining code **/
static const unsigned intDEFAULT_BLOCK_MIN_TX_FEE = 1000;
从上可以看出,通过挖矿发现的区块打包交易的最低费率为1000聪。
(4)dustrelayfee为全局变量,其在src/policy/policy.h中声明,在policy.cpp中实现定义:
CFeeRate dustRelayFee = CFeeRate(DUST_RELAY_TX_FEE);
其默认值为DUST_RELAY_TX_FEE定义于policy.h,其默认值与minrelayfee一致,均为1000聪。
针对dustrelayfee的含义,我们可通过其具体使用情况来分析,在src/qt/paymentserver.cpp中使用dustRelayFee代码如下:
// Extract and check amounts
CTxOut txOut(sendingTo.second, sendingTo.first);
if (txOut.IsDust(dustRelayFee)) {
Q_EMITmessage(tr(“Payment request error”), tr(“Requested paymentamount of %1 is too small (considered dust).”)
.arg(BitcoinUnits::formatWithUnit(optionsModel->getDisplayUnit(),sendingTo.second)),
CClientUIInterface::MSG_ERROR);
returnfalse;
}
通过消息输出内容为“Requested payment amount of %1 is too small(considered dust).”我们可以得知dustrelayfee为那些交易费用很低的交易,可以形象得理解为灰尘、忽略不计的费用。而为了防止出现非标准交易,源码中设置了默认的灰尘交易判断标准,同时针对用户传入的灰尘交易费进行了逻辑判断,保证其大于0。
// Feerate used to define dust.Shouldn’t be changed lightly as old
// implementations may inadvertently createnon-standard transactions
if (IsArgSet(“-dustrelayfee”))
{
CAmount n =0;
if(!ParseMoney(GetArg(“-dustrelayfee”, “”), n) || 0 == n)
returnInitError(AmountErrMsg(“dustrelayfee”,GetArg(“-dustrelayfee”, “”)));
dustRelayFee= CFeeRate(n);
}
三、非标准交易
Acceptnonstdtxn参数的含义是比特币网络中是否需要非标准交易,是否接受标准交易主要看当前运行的是什么网络(主网、测试网、私有网),这3种网络对是否需要标准交易是有默认要求的。该状态是通过fRequireStandard布尔变量记录的,该变量可在chainparams.h中找到,在chainparams.cpp中我们可以看到三种网络对fRequireStandard的赋值。主网中fRequireStandard=true,其他二者为false。即主网只接受标准交易,测试网与私有网可以接受非标准交易。这也好理解,主网是真正上线运行的网络,交易必须是标准的,不然整个网络种的交易就会出错。而另外两者只是开发与测试时使用,测试交易不同状态下的运行情况,保证主网上线后的正常运行。
四、签名操作字节数
我们再来看看签操作字节数参数bytespersigop,其处理代码如下:
nBytesPerSigOp = GetArg(“-bytespersigop”,nBytesPerSigOp);
nBytesPerSigOp在src/policy/policy.h中声明为全局变量,并在其cpp中实现定义,并对其设置了默认值DEFAULT_BYTES_PER_SIGOP,该默认值也在src/policy/policy.h中进行了定义,默认值为20:
/** Default for -bytespersigop */
static const unsigned int DEFAULT_BYTES_PER_SIGOP =20;
以上即为本次研读内容,在本文我们分析了节点超时参数、最小交易费设置参数、非标准交易参数以及签名操作字节数参数,可以了解到比特币网络中对于交易费是有最低限额要求的,如果太少将影响我们的交易,所以大家在转账时如果想保证交易的成功,交易费这块一定不能太少哦!希望对大家理解这块源码有帮助,也欢迎大家留言讨论。
比特币源码研读之十六
距离上一篇源码研读记录已有1个多月了,实乃惭愧,本打算每周保持1篇研读记录的节奏的,无奈近期公司业务繁忙,加班甚多,让自己没有保持好研读记录的发表任务。但继续进行源码研读的想法一直在内心中萦绕,时刻提醒自己不能停,一定要抽时间把下一篇研读记录写出来!我不能只看着源码研读班的同学们发表记录,自己却没有文章输出!所以今天重新坐在电脑前重启我的源码研读之旅,开启我的第十六篇源码研读之旅。
本文将继续开展应用程序参数交互源码部分(AppInitParameterInteraction)的研读与分析。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/init.h、src/init.cpp、src/util.h、src/wallet/wallet.h、src/wallet/wallet.cpp、src/net、src/validation.h、src/validation.cpp
本文主要对钱包交互参数源码的分析,这部分代码是否执行要看比特币程序是否启用了钱包功能,其是否启动依赖于ENABLE_WALLET宏定义变量,其判断代码如下:
#ifdef ENABLE_WALLET
if (!CWallet::ParameterInteraction())
return false;
#endif
其中ENABLE_WALLET宏定义开关在configure.ac文件中,也就是说,我们可以在源码编译的时候就可以控制该宏定义的开关。其具体设置如下图所示:
从图中我们可以看出默认是打开钱包功能的,我们也可以在运行比特币客户端时通过—disablewallet参数关闭钱包功能。
再回到刚才的代码,如果比特币程序启用了钱包功能,那么与钱包相关的交互参数必须设置正确,否则程序将终止运行。其涉及的参数较多,我们在本文将对ParameterInteraction静态函数中涉及的参数进行逐个分析。
一、禁止钱包功能参数
ParameterInteraction函数的第一个判断的参数为-disablewallet,其判断代码如下:
if (GetBoolArg(“-disablewallet”,DEFAULT_DISABLE_WALLET))
return true;
DEFAULT_DISABLE_WALLET宏定义在src/wallet/wallet.h中定义,其默认值为false,也就是说默认是打开钱包功能的,但如果我们在运行时设置了-disablewallet参数,我们的钱包功能将被关闭,将不会加载钱包,同时禁用钱包的RPC调用,程序也将返回,并停止运行。
二、blocksonly参数
blocksonly参数为比特币客户端以调试状态启动时才会使用的,这点我们可以从src/init.cp的HelpMessage函数中获取到其帮助信息:
if (showDebug)
strUsage += HelpMessageOpt(“-blocksonly”,strprintf(_(“Whether to operate in a blocks only mode (default:%u)”), DEFAULT_BLOCKSONLY));
此处的默认参数DEFAULT_BLOCKSONLY在src/net.h中定义,具体定义如下:
/** Default for blocks only*/
static const bool DEFAULT_BLOCKSONLY =false;
我们可以看到其默认条件下为false,即默认不会只以区块模式运行,因为如果在区块模式下运行,全网的交易将不会被打包,钱包的交易广播功能将失效,也就是我们看到的walletbroadcast参数此时需要设置为false,否则将会互斥。
三、salvagewallet参数
salvagewallet参数的功能为试图在比特币客户端启动时从损坏的钱包中恢复私钥。通过日志打印代码:
LogPrintf(“%s: parameter interaction:-salvagewallet=1 -> setting -rescan=1\n”, __func__);
我们可以看出该参数只有用-rescan参数启动客户端的情况下才能生效。
四、zapwallettxes参数
zapwallettxes参数用于删除钱包的所有交易记录,且只有用-rescan参数启动客户端才能重新取回交易记录,且只有用-rescan参数启动客户端才能重新取回交易记录(mode=1时保留tx meta data ,如account owner和payment request information, mode=1时不保留tx meta data)。
五、sysperms参数
该参数已经在《比特币源码研读之九》中进行了解释,其含义此处不再赘述。我们通过其后面的错误提示信息:
-sysperms is not allowed in combinationwith enabled wallet functionality
可以看出该参数在具备钱包功能时是不能用-sysperms参数的,否则二者将会冲突,程序自动退出。
六、prune与rescan参数
该参数已经在《比特币源码研读之九》中进行了解释,其含义此处不再赘述。我们通过其后面的错误提示信息:
Rescans are not possible in pruned mode.You will need to use -reindex which will download the whole blockchain again.
可以看出该参数在比特币客户端使用rescan参数启动时,是不能用prune参数的,否则二者将会冲突,程序自动退出。
以上6类参数均为考虑在启动钱包功能时,一些参数之间的冲突处理,后面我们将涉及交易与交易费用相关的参数处理。
七、minRelayTxFee
minRelayTxFee为全局变量,其在src/validation.h中声明,并且在src/validation.cpp中完成定义,我们已经在《比特币源码研读之十五》中对其进行了详细分析,此处不再赘述。我们通过代码来看minRelayTxFee在此处的用途:
if (::minRelayTxFee.GetFeePerK()> HIGH_TX_FEE_PER_KB)
InitWarning(AmountHighWarn(“-minrelaytxfee”)+ ” ” +
_(“The wallet will avoid paying less than the minimum relay fee.”));
我们首先来看这段代码中代表最高手续费的宏定义HIGH_TX_FEE_PER_KB,该宏在src/validation.h中定义:
//! Discourageusers to set fees higher than this amount (in satoshis) per kB
static constCAmount HIGH_TX_FEE_PER_KB = 0.01 * COIN;
其中COIN在src/amount.h中定义,具体定义如下:
staticconst CAmount COIN = 100000000;
从定义我们可以看出COIN为1亿,即代码1个比特币的数量。进而我们可以知道HIGH_TX_FEE_PER_KB代表手续费最高为0.01个比特币,超过这个值系统将会提示出错。
而比特币转账是需要手续费的,而且可以由用户自定义设置,但其设置的手续费是不能低于最低手续费,也不能高于最高手续费的。从这我们就可以理解此处的if判断语句的用途了,其目的是防止用户设置的最低手续费高于比特币程序中设置的最高手续费,导致手续费过高,影响比特币网络的正常运转。
八、mintxfee
此处的mintxfee参数判断与minRelayTxFee参数的处理基本一致,最低手续费不应高于最高手续费,否则程序将给出警告提示。
九、fallbackfee
fallbackfee的具体用途我们通过当前的代码应该是不清楚的,但我们可以在wallet.cpp文件的多个地方查找关于这个参数的注释:
(1)wallet.cpp的初始位置
/**
* Iffee estimation does not have enough data to provide estimates, use this feeinstead.
*Has no effect if not using fee estimation
*Override with -fallbackfee
*/
CFeeRate CWallet::fallbackFee =CFeeRate(DEFAULT_FALLBACK_FEE);
(2)GetWalletHelpString函数
strUsage +=HelpMessageOpt(“-fallbackfee=”, strprintf(_(“A feerate (in %s/kB) that will be used when fee estimation has insufficient data (default:%s)”),CURRENCY_UNIT,FormatMoney(DEFAULT_FALLBACK_FEE)));
在这个函数中我们可以看到fallbackfee参数的帮助信息,其翻译过来的意思是当没有足够的数据供程序估算费用的时候,将默认使用fallbackfee,而这个参数的默认值为,该参数的默认值DEFAULT_FALLBACK_FEE在src/wallet/wallet.h中定义:
//! -fallbackfee default
static const CAmount DEFAULT_FALLBACK_FEE =20000;
估算费用存储在数据目录的fee_estimates.dat文件中,如图所示:
(3)GetMinimumFee函数佐证
// … unless we don’t have enough mempooldata for estimatefee, then use fallbackFee
if(nFeeNeeded == 0)
nFeeNeeded= fallbackFee.GetFee(nTxBytes);
通过其注释我们也可以看出当交易池中没有足够数据支撑手续费的估算时,就使用fallbackFee作为较低最低收费费。
十、paytxfee与maxtxfee
paytxfee与maxtxfee代表的分别是支付交易手续费与最高交易手续费,此处主要判断二者是否在最高低手续费与最高手续费之间,如果最低手续费则程序退出,高于最高手续费将给出警告,提示您给多了,超出了最高手续费。
十一、txconfirmtarget
相信大家应该都知道比特币的每一笔交易都需要经过6次区块确认才能算真正的交易成功了,且不能回退,而这个确认值是怎么来的呢?经过我们长途跋涉的源码研读,今天终于看到了,就是我们现在要介绍的txconfirmtarget参数。我们看到该参数的默认参数为DEFAULT_TX_CONFIRM_TARGET,该宏定义在src/wallet/wallet.h中,具体如下:
//! -txconfirmtarget default
static const unsigned intDEFAULT_TX_CONFIRM_TARGET = 6;
我们看到了它的值默认为6!其实我们也可以在启动客户端时通过txconfirmtarget参数进行修改其确认数为其他值,但为了全网一致,大家很少做这方面的修改。
十二、spendzeroconfchange
spendzeroconfchange参数的作用是表示比特币客户端是否可以花费0确认的费用,在0.14版其默认是允许的,因为其默认参数在src/wallet/wallet.h中定义:
static const boolDEFAULT_SPEND_ZEROCONF_CHANGE = true;
即其默认为true,允许花费0确认的费用。
十三、sendfreetransactions
sendfreetransactions参数的含义是是否发送0手续费的交易,默认为不可以发送0手续费的交易,其默认参数为DEFAULT_SEND_FREE_TRANSACTIONS,其在src/wallet/wallet.h中定义,其定义如下:
//! Default for -sendfreetransactions
static const bool DEFAULT_SEND_FREE_TRANSACTIONS = false;
我们看到其默认值为false,也就是说不能发送0手续费的交易。
十四、walletrbf
Bitcoin Core 0.14.0开启了一项可选的功能(默认为禁用):
src/wallet/wallet.h中的定义
static const bool DEFAULT_WALLET_RBF = false;
它可以为钱包产生的所有新交易添加交易费,具体是指BIP125可选费用替代法(RBF)。
想要启用该功能,可以在Bitcoin Core客户端用-walletrbf启用,这一功能可为先前未确认的交易添加手续费,以加大交易被确认的机会。支持opt-in RBF或者full RBF功能的矿工通常会在他们的交易处理队列中放入更高费用的交易,而更高的交易费将鼓励矿工更快地挖取新版本的交易。
十五、limitfreerelay
limitfreerelay参数的注释我们可以从src/init.cpp中的HelpMessage函数中获取,其具体解释如下:
strUsage+=HelpMessageOpt(“-limitfreerelay=”,strprintf(“Continuously rate-limit free transactions to *1000
bytes per minute (default: %u)”, DEFAULT_LIMITFREERELAY));
通过其注释我们可以看到limitfreerelay参数表示的含义是其每分钟可连续广播的免费交易数,其大小为默认的DEFAULT_LIMITFREERELAY个千字节量。
此处代码为:
if(fSendFreeTransactions&&GetArg(“-limitfreerelay”,DEFAULT_LIMITFREERELAY) <= 0)
return InitError(“Creation of free transactions with their relay disabled is notsupported.”);
即如果允许发送免费交易,那么limitfreerelay不能为0,如果为0则表示不能广播免费交易,程序给出错误提示。
至此我们完成了CWallet::ParameterInteraction()函数的解析,即完成了钱包交互参数的解析。我们对钱包相关的参数有了更进一步的理解,在运行比特币客户端时,我们也能按照我们自己的需求设置相应的参数值,玩转比特币客户端钱包功能,当然在玩转之前我们需要先有可运行的客户端,客户端得到呢?我们可以自己编译,如何编译?推荐大家参考我们研习社比特币编程系列中的《比特币源码编译》课程,跟着课程我们可以完成客户端的编译,并运行客户端,设置我们个性化的钱包,还等什么呢?行动起来吧!
比特币源码研读之十七
今天我将继续第十七篇源码研读,这篇文章我们将结束AppInitParameterInteraction函数的解析。该函数的解析从第十篇源码研读开始,到此已有8篇文章在分析其源码了,足以说明该函数信息量之大,大家有兴趣可以详细看看该函数的功能实现及其处理的参数。
本文将继续开展应用程序参数交互源码部分(AppInitParameterInteraction)的研读与分析。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/init.h、src/init.cpp、src/util.h、src/net、src/validation.h、src/validation.cpp、src/utiltime.h、src/utiltime.cpp、src/protocol.h、src/net.h、src/rpc/server.h、src/script/standard.h、src/script/standard.cpp、src/ policy /policy.h、src/ policy /policy.cpp
本文的分析按以下七部分进行:
一、交易相关参数
此处涉及的交易与挖矿相关的三个参数分别是permitbaremultisig、datacarrier以及datacarriersize。
(1)permitbaremultisig
permitbaremultisig参数的处理代码如下:
fIsBareMultisigStd =GetBoolArg(“-permitbaremultisig”, DEFAULT_PERMIT_BAREMULTISIG);
通过搜索和代码分析了解到-permitbaremultisig代表的含义是允许发送非P2SH脚本多重签名(baremultisig)。其默认参数DEFAULT_PERMIT_BAREMULTISIG定义在src/validation.h中,默认值为true,具体情况如下:
/** Default for -permitbaremultisig */
static const boolDEFAULT_PERMIT_BAREMULTISIG = true;
也就是默认允许非P2SH多重签名的交易在全网传播。
此处的fIsBareMultisigStd变量为全局变量,其在src/validation.h中声明,在src/validation.cpp中定义,其默认值为DEFAULT_PERMIT_BAREMULTISIG。
fIsBareMultisigStd参数在src/policy/policy.cpp中使用,使用的函数为IsStandardTx,在其中参与判断交易是否为标准交易,如果该参数为false,则该函数返回baremultisig为非标准交易,并且给出的原因是当前交易为“bare-multisig”。
if ((whichType == TX_MULTISIG) &&(!fIsBareMultisigStd))
{
reason = “bare-multisig”;
return false;
}
(2)datacarrier与datacarriersize
这两个参数放可以放在一起分析,因为它们均为从0.10.0版本开始加入到比特币客户端的命令参数,其作用是允许交易OP_RETURN交易是否可以包含除交易信息之外的其他数据信息,datacarrier参数表示是否可以传播和挖矿是否包含交易意外的数据内容,其默认值为true,即是允许的。在datacarrier为true的情况下,我们再来看datacarriersize参数,其表示包含数据的交易大小默认值,其默认值为83字节。83字节的信息我们可以从源码的src/script/standard.h中找到:
static const boolDEFAULT_ACCEPT_DATACARRIER = true;
static const unsigned intMAX_OP_RETURN_RELAY = 83; //!< bytes (+1 for OP_RETURN, +2 for the pushdataopcodes)
extern bool fAcceptDatacarrier;
extern unsigned nMaxDatacarrierBytes;
我们可以看到fAcceptDatacarrier与nMaxDatacarrierBytes都是全局变量,都在该头文件中声明,并在standard.cpp中定义:
bool fAcceptDatacarrier =DEFAULT_ACCEPT_DATACARRIER;
unsigned nMaxDatacarrierBytes =MAX_OP_RETURN_RELAY;
我们可以再来看下fAcceptDatacarrier与nMaxDatacarrierBytes使用之处,在policy.cpp中的IsStandard函数中,程序通过分析二者的值判断交易是否为标准交易,防止DoS攻击:
二、单元测试参数处理
此处的mocktime为用于测试网络起始时间,通过SetMockTime设置测试网络起始时间。在分析该函数之前,我们先来了解下mock测试,在百度百科中其解释为:
mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。
其包含的内容有:
通过上面的注释我们可以很好地理解mockTime的作用与意义。我们再来看setmocktime函数,该函数在src/utiltime.h中定义:
src/utiltime.h
void SetMockTime(int64_t nMockTimeIn);
utiltime.cpp
static int64_t nMockTime = 0; //!< For unit testin
void SetMockTime(int64_t nMockTimeIn)
{
nMockTime =nMockTimeIn;
}
我们可以通过nMockTime赋值语句可以看到其主要是用于单元测试。通过其默认值为0,在0的情况下将为no-op,即无操作的设置,因为我们在平时运行时并不是测试状态,所以如果为测试模式,将需要设置nMockTime的值。
三、Bloom过滤参数处理
此处代码主要用于判断当前节点是否支持针对区块和交易的bloom过滤,我们可以通过src/init.cpp中的HelpMessage函数确认peerblommfilters参数的具体含义,其具体帮助信息如下:
strUsage +=HelpMessageOpt(“-peerbloomfilters”, strprintf(_(“Supportfiltering of blocks and transaction with bloom filters (default: %u)”),DEFAULT_PEERBLOOMFILTERS));
我们可以看到其默认值为DEFAULT_PEERBLOOMFILTERS,该值在src/validation.h中定义:
src/validation.h
static const bool DEFAULT_PEERBLOOMFILTERS = true;
也就是说默认是支持bloom过滤器的。在支持该过滤器的前提下,程序中设置了当前运行节点的服务模式:
nLocalServices =ServiceFlags(nLocalServices | NODE_BLOOM);
此处的nLocalServices在src/init.cpp中定义,具体如下:
namespace { // Variables internal to initializationprocess only
ServiceFlags nRelevantServices = NODE_NETWORK;
int nMaxConnections;
int nUserMaxConnections;
int nFD;
ServiceFlags nLocalServices = NODE_NETWORK;
}
nLocalServices的数据类型为ServiceFlags,该类型在src/protocol.h中定义,具体如图所示:
我们可以看到ServiceFlags是枚举变量,在当前代码中我们对nLocalServices赋值为NODE_NETWORK与NODE_BLOOM,即具备全节点信息存储与bloom过滤器功能,需要说明的是二者对于所有客户端来说也是默认具备的。
四、rpcserialversion参数处理
通过HelpMessage函数确认rpcserialversion参数的具体含义,其具体帮助信息如下:
strUsage +=HelpMessageOpt(“-rpcserialversion”, strprintf(_(“Sets theserialization of raw transaction or block hex returned in non-verbose mode,non-segwit(0) or segwit(1) (default: %d)”),DEFAULT_RPC_SERIALIZE_VERSION));
我们可以看到其默认值为DEFAULT_RPC_SERIALIZE_VERSION,该值在src/rpc/server.h中定义:
src/rpc/server.h
static const unsigned intDEFAULT_RPC_SERIALIZE_VERSION = 1;
也就是说默认情况,在非冗长模式、非隔离见证模式(0)或隔离见证(1)模式下原始交易或区块以十六进制序列化方式呈现。
通过其代码我们可以了解到rpcserialversion不能为负数,也不能大于1,而其类型又为unsigned int,所以其值只能为0或1。
if (GetArg(“-rpcserialversion”,DEFAULT_RPC_SERIALIZE_VERSION) < 0)
returnInitError(“rpcserialversion must be non-negative.”);
if (GetArg(“-rpcserialversion”,DEFAULT_RPC_SERIALIZE_VERSION) > 1)
returnInitError(“unknown rpcserialversion requested.”);
五、maxtipage参数处理
此处我们要分析的是maxtipage参数,其赋值代码如下:
nMaxTipAge = GetArg(“-maxtipage”,DEFAULT_MAX_TIP_AGE);
maxtipage参数的作用我们可以通过HelpMessage函数确认maxtipage参数的具体含义,其具体帮助信息如下:
strUsage +=HelpMessageOpt(“-maxtipage=”, strprintf(“Maximum tipage in seconds to consider node in initial block download (default: %u)”, DEFAULT_MAX_TIP_AGE));
其中,nMaxTipAge变量在src/validation.h中声明,在src/validation.cpp中定义,其默认值为DEFAULT_MAX_TIP_AGE,DEFAULT_MAX_TIP_AGE也定义于src/validation.h中。
src/validation.h
static const int64_t DEFAULT_MAX_TIP_AGE =24 * 60 * 60;
通过其帮助提示信息我们可以看到该参数的用途是当我们运行的节点包含的区块信息落后于主网最长几点24小时后,我们的比特币客户端将进行Initial block download(IBD)操作,进行区块同步下载。我们也可以看下比特币官网中对IBD的解释(https://bitcoin.org/en/developer-guide#connecting-to-peers):
从其解释我们可以看出,这里说的Initial并不是说只在刚启动时执行IBD操作,而是当我们的节点信息比全网最长链落后了24小时或者144个块时就会执行IBD操作。当然程序默认为24小时,我们可以根据具体情况修改该值。
六、mempoolreplacement参数处理
此处的mempoolreplacement参数的详细含义我们可以从bitcoinwiki上找到,其对应的解释为Transaction replacement,在该维基解释为:
Transaction replaceability occurs when a full node allows one or more of the transactions in its memory pool (mempool) to be replaced with a different transaction that spends some or all of the same inputs. Transaction replaceability was enabled in the first version of Bitcoin but was disabled in the 0.3.12 release with the comment, “Disable replacement feature for now”.Since then, there have been various attempts to make transaction replaceability widely available again.
通过以上注释我们可以看到其作用是可以在拥有全节点的客户端替换交易池中的交易,即针对同一输入,可以用花费了该输入的一部分或全部金额的交易替换交易池中的交易。这功能在比特币的第一版就有了,后来在0.3.12中被禁止了,但后来的版本中又广泛使用起来。
综上所述,交易池中的交易是可以被替换的,但前提是替换的交易产生于同一输入。
此处交易替换参数处理代码为:
fEnableReplacement =GetBoolArg(“-mempoolreplacement”, DEFAULT_ENABLE_REPLACEMENT);
if ((!fEnableReplacement) &&IsArgSet(“-mempoolreplacement”)) {
// Minimal effort at forwards compatibility
std::string strReplacementModeList = GetArg(“-mempoolreplacement”,””);// default is impossible
std::vector vstrReplacementModes;
boost::split(vstrReplacementModes, strReplacementModeList,boost::is_any_of(“,”));
fEnableReplacement = (std::find(vstrReplacementModes.begin(),vstrReplacementModes.end(), “fee”) != vstrReplacementModes.end());
}
代码首先处理了交易池参数设置状态,我们可以看到其默认值为DEFAULT_ENABLE_REPLACEMENT,其定义于src/validation.h中,默认为true:
/** Default for -mempoolreplacement */
static const boolDEFAULT_ENABLE_REPLACEMENT = true;
fEnableReplacement为全局变量,声明于src/validation.h中,定义在src/validation.cpp中,具体定义如下:
bool fEnableReplacement =DEFAULT_ENABLE_REPLACEMENT;
从分析中我们可以看出fEnableReplacement默认为true,即交易池中的交易按照既定规则是可以被替换的。
在后面的if ((!fEnableReplacement) &&IsArgSet(“-mempoolreplacement”))处理中,其条件是当fEnableReplacement设置我false,并且客户端是添加了mempoolreplacement参数的情况下,程序会获取交易池替换模式内容,判断其是否包含“fee”模式,如果包含,则将fEnableReplacement设置为true,即可执行交易替换操作。
七、bip9params参数处理
我们首选在HelpMessage函数中看bip9params参数的作用:
strUsage += HelpMessageOpt(“-bip9params=deployment:start:end”,”Use given start/end times for specified BIP9 deployment(regtest-only)”);
从上可以看出bip9params为比特币程序在私有网络测试时使用的参数,其作用为执行部署、执行部署开始和部署结束时间。而这个部署一般是指什么部署呢?我们来看下BIP9这个改进协议的作用:
新的软分叉升级规范BIP9
比特币的软/硬分叉升级一直采用块的version字段来完成。由于是一个分布式系统,必然需要采用灰度发布模式。
传统的升级过程
在实施BIP9前是这样升级的,当前块版本为version,那么新块版本是version + 1,当近1000个块中的版本超过95%都是新版本时,则触发启用新特性,同时不再接收旧版本号的块。由于中间存在1000个块的窗口期,大约一周,所以给出足够的时间给当前网络中的节点实施升级。
上图是来自BTC.COM统计的比特币历史上几个升级过程,最近的v3升级v4的过程大约花费了一个半月左右。前面几次的升级时间更长。
这种依次递增版本号的方法,有一个明显的弊端:每次仅能进行一个特性升级。当需要同时进行多个升级时,则无法完成。BIP9的诞生就是为了解决这个问题的,同时把向下兼容性升级过程制定了规范。
(以上摘自http://blog.biqu.io/2016/04/21/BIP9/)
我们再来看整个参数的处理代码,可以了解到其主要是针对bip9params参数的值进行私有网络测试部署,以测试软分叉后软件是否正常运行。
以上就是本篇研读记录的全部内容,也是AppInitParameterInteraction处理函数的最后一部分,后面我们将根据我们的研读流程图继续前行,也就是即将进入AppInitSanityChecks函数,进行完整性检测部分。
比特币源码研读之十八_SanityChecks
比特币源码研读记录已有一段时间没有更新了,最近除了工作比较忙意外,还有就是最近一直在我们研习社的千聊平台中开讲《比特币编程》系列课程,能开讲这课程源于之前的源码研读以及《精通比特币》的研读工作,让我能有一定的知识储备和心得体会,将自己学到的东西分享出来,在分享的同时也能更深入地理解之前学到的东西,可以说是做到了教与学的相互促进了。
今天我将继续第十八篇源码研读,这篇文章我们将开始AppInitSanityChecks函数源码的研读与解析。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/init.h、src/init.cpp、src/util.h、src/key.h、src/key.cpp、src/random.h、src/random.cpp、src/pubkey.h、src/pubkey.cpp、src/secp256k1、src/crypto/sha512.h、src/crypto/sha512.cpp、src/compat/sanity.h、src/compat/glibc_sanity.cpp、src/compat/glibcxx_sanity.cpp
我们可以看到在InitSanityCheck函数中只有短短的8行代码,其注释是该函数是用于比特币核心的一个可用性测试,初步验证比特币核心的功能完整性,验证运行环境中包含了所需依赖库。
下图是可用性验证的一个具体流程:
其中,ECC_InitSanityCheck为椭圆曲线加密结果的完整性验证,后面的glibc_sanity_test与glibcxx_sanity_test验证当前运行环境是否支持C/C++运行环境,具体验证过程,我们将在本文的后续章节中详细描述。
本文的也将按照流程图中的3个部分进行逐一分析:
一、椭圆曲线加密结果验证(ECCSanityCheck)
ECC_InitSanityCheck函数执行过程如下:
通过源码研读我们会发现ECC_InitSanityCheck函数作为全局函数定义于key.h中,并在key.cpp中得以实现。其在key.cpp中的实现代码也很简短,具体如下:
bool ECC_InitSanityCheck() {
CKey key;
key.MakeNewKey(true);
CPubKeypubkey = key.GetPubKey();
returnkey.VerifyPubKey(pubkey);
}
在key.h的定义中包含了ECC_InitSanityCheck的功能注释:
/** Check that required EC support is available atruntime. */
验证椭圆曲线算法的计算功能是否实时有效,从其代码我们可以看出其主要是验证是否能正确计算公钥。
(1)定义私钥对象(CKey key)
在ECC_InitSanityCheck函数中首先定义了Ckey类型的私钥对象key。Ckey类在key.h中定义,在其类中使用keydata参数存储私钥对象,其定义如下:
//! The actual byte data
std::vector<char, secure_allocator<unsigned char> > keydata;
我们可以看到其为无符号字符串类型的数组,并且在CKey的构造函数中对该参数进行了初始化,定义其字节大小为32字节,并且是必须为32字节:
//! Construct an invalid private key.
CKey() : fValid(false), fCompressed(false)
{
//Important: vch must be 32 bytes in length to not break serialization
keydata.resize(32);
}
大家应该知道私钥长度为256位,256除以8的结果是32,所以,其字节为32字节是正确的。
同时在Ckey类中还定义了2个参数fValid和fCompressed,它们的详细定义分别是:
//! Whether this private key is valid. We check forcorrectness when modifying the key data, so fValid should always correspond tothe actual state.
bool fValid;
//! Whether the public key corresponding to thisprivate key is (to be) compressed.
boolfCompressed;
其中,
fValid:参数用于表示私钥是否有效,该参数是在私钥值发生变化时进行相应修改,即私钥值有效时,其为true,反之则为false。
fCompressed:参数代表的是公钥是否为压缩公钥,true为压缩公钥,false为非压缩公钥。
(2)创建私钥(MakeNewKey)
上面是对Ckey类的一个解析,我们接着ECC_InitSanityCheck函数代码的分析,在定义了key参数之后,执行了其MakeNewKey()函数,执行代码为:
key.MakeNewKey(true);
我们先来看key.h中MakeNewKey的函数定义:
//! Generate a new private key using a cryptographicPRNG.
void MakeNewKey(bool fCompressed);
通过其注释我们可以看到该函数通过使用加密PRNG(伪随机数)生成私钥。
在MakeNewKey函数中通过GetStrongRandBytes函数循环获取私钥,直到获取的私钥满足Check函数验证条件时才停止。
我们首先来看GetStrongRandBytes函数,该函数定义于src/random.h中,并在random.cpp中实现:
/**
* Function togather random data from multiple sources, failing whenever any of those sourcefail to provide a result.
*/
void GetStrongRandBytes(unsigned char* buf, int num);
通过其注释我们可以知道,随机数是通过多渠道共同作用获得的,但获得的私钥在某个渠道出错时,生成的随机数将无效。该函数的实现过程如图所示:
在通过GetStrongRandBytes函数获取随机数后,程序将把随机数放入Check中进行验证,其验证代码为:
bool CKey::Check(const unsigned char *vch) {
returnsecp256k1_ec_seckey_verify(secp256k1_context_sign, vch);
}
该函数实现很简单,其主要是通过调用libsecp256k1库实现随机数的验证的。libsecp256k1库的源码已经包含至比特币源码中,其源码位于src/secp256k1文件夹中。其README.md文件中的描述如下:
通过该描述信息我们可以知道libsecp256k1为标准C动态库,其主要功能是通过ECDSA(椭圆曲线加密算法)实现签名验证、公私钥生成、公私钥添加等能力。
此处的验证函数为secp256k1_ec_seckey_verify,该函数定义于src\secp256k1\include\secp256k1.h中,其定义如下:
我们通过其注释与参数说明可以看到该函数的功能是验证基于椭圆曲线创建的密钥。其返回值如果为1,密钥有效;如果为0则无效。传入的两个参数ctx与seckey均不能为NULL,都需要有值。
在找到了正确的私钥后,程序此时将私钥有效性标志fValid设置为true,同时将传入的fCompressedIn值赋值给fCompressed,用于标识是否使用压缩公钥。
(1)私钥创建公钥(GetPubKey)
在完成了私钥的创建后,我们可以通过私钥创建公钥了。
其获取代码为CPubKey pubkey = key.GetPubKey();
其中CPubKey类定义于src/pubkey.h中,实现与src/pubkey.cpp中,其包含了一个参数vch[65],该参数主要用于存储公钥值,该值为序列化的十六进制数,通过vch的注释我们可以通过vch[0]获得公钥的长度,也就是通过该值判断其值为2和3,还是4,,6,7,如果为2和3则为压缩公钥,长度为33,反之则为非压缩公钥,长度为65。其长度获取代码就在CPubKey类的GetLen函数中,实现代码如下:
//! Compute the length of a pubkey with a given firstbyte.
unsigned int static GetLen(unsigned char chHeader)
{
if (chHeader== 2 || chHeader == 3)
return33;
if (chHeader ==4 || chHeader == 6 || chHeader == 7)
return65;
return 0;
}
同时,我们可以看到,如果vch[0]如果不是2,3, 4,,6,7中的任何一个值的话,其长度为空,也可说明该公钥值无效。
在完成了CPubKey的说明后,我们再来看下我们本小节关注的GetPubKey函数。通过其源代码我们发现该函数通过secp256pk1库提供的函数实现了压缩或非压缩公钥的计算。其首先通过secp256k1_ec_pubkey_create函数创建公钥值,随后通过secp256k1_ec_pubkey_serialize函数实现压缩或非压缩公钥序列值的计算。
(2)验证公钥(VerifyPubKey)
验证公钥代码在CKey的VerifyPubKey函数中实现。该函数的实现流程为:
1、通过OpenSSL的GetRandBytes函数实现随机数的生成;
2、根据”Bitcoin key verification\n”字符串与刚生成的随机数共同作用计算随机哈希值;
3、在Sign函数通过该随机哈希值基于ECDSA算法实现签名值的计算;
4、利用该签名信息验证获取的公钥的有效性,验证函数位于CPubKey的Verify函数中。
二、C与C++运行环境验证
本节我们将分析glibc_sanity_test与glibcxx_sanity_test两个函数验证的相关内容。glibc_sanity_test与glibcxx_sanity_test在src\compat\sanity.h中定义,分别在src\compat\glibc_sanity.cpp与src\compat\glibcxx_sanity.cpp中实现。通过这两个函数的实现代码我们可以了解到它们主要是为了验证运行环境中的C/C++运行库的有效性,即比特币核心软件能否在当前环境中正常运行,其所需的运行库是否能够支撑比特币核心的正常运行。
以上就是InitSanityCheck函数的研读记录,本文主要对其验证过程与部分细节进行了分析,对于secp256k1库使用的函数未进行详细描述,对该库的详细分析与研读将在后续研读记录中进行,敬请期待!
三、小结
通过对InitSanityCheck函数的研读,让我对私钥、公钥、压缩或非压缩公钥、椭圆曲线加密算法有了更深入的理解,这也是我坚持读源码的原因,可以让自己从技术角度更好地理解比特币,更好地理解区块链。所以,鼓励有编程基础的,对比特币源码感兴趣的朋友一起来参加我们的比特币源码研读。参与方式见《比特币源码研读班第三期招募令》 一文。
比特币源码研读之十九
近期一直在忙我们区块链研习社代币QYB的事,从开始测试到发布已经过去2周了,一切运行正常,大家普遍反映通过使用QYB进行挖矿和转账体验,真实地感受到了比特币挖矿和比特币相关知识,比自己看文章和书籍更直观,这也是我们研习社的初衷,就是通过不同的方式让大家学习区块链知识,让大家真正通过学习来提升自己的认知,提升自己对区块链的理解。
还有我仍然坚持在我们研习社的千聊平台中开讲《比特币编程》系列课程,比特币相关的课程已达8讲,收听人数也在逐步增加,社长和听众也给了很多正面反馈,让我更有信心继续讲下去。
虽然目前事很多,但源码研读这事我是不会停的,我会继续坚持写下去,因为近期有很多朋友找到我,希望我能继续写,把源码解读完,我自己也很想继续写下去,所以,只要我有时间,一定会坚持写下去的,请各位看官监督!
下面我们正式开始第十九篇源码研读,这篇文章我们将开始AppInitMain函数源码的研读与解析。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/init.h、src/init.cpp、src/util.h、src/util.cpp
一、daemon参数解析
在解析AppInitMain函数之前,我们先来看对daemon参数,该参数应用于比特币核心的bitcond.exe控制台程序,该程序为比特币核心的后台服务程序。daemon参数的解析代码:
if (GetBoolArg(“-daemon”, false))
{
#if HAVE_DECL_DAEMON
fprintf(stdout,”Bitcoin server starting\n”);
//Daemonize
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”);
returnfalse;
#endif // HAVE_DECL_DAEMON
}
在该代码块中,程序首先通过GetBoolArg函数判断判断是否在启动时设置了守护进程daemon参数,如果设置了则继续执行该参数相关的代码,否则将在控制台中输出当前系统不支持守护进程的错误提示。
设置信息后其具体执行内容为:
(1)判断是否定包含了HAVE_DECL_DAEMON宏定义,该宏定义在未经编译的源码中是不包含的,需经过./configure配置后才会出现,经过configure后,其放置于src/config/bitcoin-config.h文件中,一般来说HAVE_DECL_DAEMON是默认定义的,其定义如下:
通过定义的注释我们可以看到,如果该宏定义表示的数值为1,则定义了daemon,否则不定义。一般来说该值为1。
(2)如果为1则执行fprintf(stdout, “Bitcoin server starting\n”);,其表示在控制台中输出”Bitcoin server starting\n”信息,表明比特币后台守护进程在运行;
(3)通过执行daemon(1, 0)开启守护进程操作,我们看到在daemon函数中设置了两个参数,这两个参数的含义是什么呢?我们首先来看看daemon函数的定义:
#include
int daemon(int nochdir, int noclose);
参数:
当nochdir为零时,当前目录变为根目录,否则不变;
当noclose为零时,标准输入、标准输出和错误输出重导向为/dev/null,也就是不输出任何信息,否则照样输出。
返回值:
deamon()调用了fork(),如果fork成功,那么父进程就调用_exit(2)退出,所以看到的错误信息全部是子进程产生的。如果成功函数返回0,否则返回-1并设置errno。
从该函数的定义和参数解释我们可以看出,该函数为守护进程根据输入的参数启动该进程。我们当前设置的参数为1和0,根据其注释我们可以得知,该守护进程将当前目录设为根目录,即程序中的相对目录是从该目录开始的,第二个参数设置为0则表示不输出任何信息。
最后如果daemon()函数返回为0时则正常运行,为-1时则输出errorno对应的错误提示,并返回false,程序退出,同时我们在运行时可根据该错误进行比特币守护进程的配置。
二、AppInitMain与WaitForShutdown
此处先分析AppInitMain与WaitForShutDown的关系。我们可以看到AppInitMain返回值直接影响了WaitForShutdown的运行情况,其执行流程如图所示:
(1)首先执行AppInitMain函数,执行相应的初始化过程;
(2)如果初始化过程运行正常,fRet则为true,否则为false;
(3)如果为true,则程序执行WaitForShutdown函数,进入程序关闭请求等待状态,我们来看下bitcoind.cpp中的WaitForShutdown函数:
void WaitForShutdown(boost::thread_group* threadGroup)
{
bool fShutdown = ShutdownRequested();
// Tell the main threads to shutdown.
while (!fShutdown)
{
MilliSleep(200);
fShutdown = ShutdownRequested();
}
if (threadGroup)
{
Interrupt(*threadGroup);
threadGroup->join_all();
}
}
如果fShutdown为false,则每隔200毫秒循环执行关闭请求函数ShutdownRequested(Init.cpp)的获取,直到该返回值为true时,则通知主线程执行关闭程序,其执行过程与fRet为false的代码一致,见(4);
(4)如果为false,则表明初始化失败,程序将关闭,此时将执行Interrupt函数,该函数定义于init.h,实现与init.cpp中,具体代码如下:
void Interrupt(boost::thread_group& threadGroup)
{
InterruptHTTPServer();
InterruptHTTPRPC();
InterruptRPC();
InterruptREST();
InterruptTorControl();
if (g_connman)
g_connman->Interrupt();
threadGroup.interrupt_all();
}
我们看到该代码实现HTTPServer、HTTPRPC、RPC、REST以及TorControl、connman等功能线程的中断,最后通过完成线程组的中断操作;
(5)随后通过threadGroup.join_all();实现所有中断线程的收回操作,相当于释放线程所占用的空间;
(6)最后是调用Shutdown();操作,实现程序中所有正在运行模块的关闭与停止操作。
三、小结
本研读记录主要分析了daemon参数、AppInitMain与WaitForShutDown执行过程的分析,后续将深入剖析AppInitMain的内容,因为该函数中包含了比特币核心中各模块的初始化工作,对我们了解各模块的运行过程至关重要,敬请大家期待。
比特币源码研读之二十
今天是2017年12月31日,即2017年的最后一天了,本应好好总结,对今年做个总结的,但想着自己身为源码研读班班长,许久没有带头发表研读记录了,因此,我今天先把研读记录写出来,同时今天这篇是第二十篇研读记录,也就是说我个人的研读记录已经到达2“”字头的数量了,趁着2017年最后一天,将这篇文章写完,来年更加努力,实现源码的全覆盖!
我们今天将深入AppInitMain函数,该函数将带领我们进入比特币核心程序的初始化过程,下面我们一起来对其进行详细分析。
本文主要涉及的源码文件包括:
src/bitcond.cpp、src/init.h、src/init.cpp、src/util.h、src/util.cpp、src/script/scriptcache.h、src/script/scriptcache.cpp、src/cuckoocache.h
AppInitMain函数中包含的内容较多,涉及了应用程序初始化、钱包数据库完整性、网络初始化、区块链加载、钱包加载、数据目录以及区块加载等内容,其具体执行流程如图所示。
在开始应用程序初始化之前,程序首先执行运行网络的选择代码:
const CChainParams& chainparams = Params();
关于运行网络的分析我在第六篇文章中已详细说明,此处不再赘述,一般来说,如果我们在启动比特币核心程序时,没有设置相应网络参数,则默认运行主链,否则将根据输入的参数启动相应网络。
在完成链参数设置后,程序将进入应用程序初始化模块,具体初始化内容如下。
一、应用程序初始化
(1).lock文件。首先LockDataDirectory实现对数据目录的锁定,锁定的原因是保证数据目录在同一台机器中仅被一个比特币核心核心程序所使用,否则如果多个比特币核心程序同时使用同一数据目录,将会造成该程序数据内容产生不一致的情况。
在LockDataDirectory中,首先获取数据目录值,然后打开数据目录下的.lock文件,判断其是否存在。该文件我们可以在比特币数据目录中找到,其形式如图所示:
其文件名就是.lock,而且其内容为空。该文件的作用我们可以通过后面的static boost::interprocess::file_lock来确定,其实现如下:
try {
static boost::interprocess::file_locklock(pathLockFile.string().c_str());
if (!lock.try_lock()) {
return InitError(strprintf(_(“Cannot obtain a lock on datadirectory %s. %s is probably already running.”), strDataDir,_(PACKAGE_NAME)));
}
if (probeOnly) {
lock.unlock();
}
}catch(const boost::interprocess::interprocess_exception& e) {
return InitError(strprintf(_(“Cannot obtain a lock on datadirectory %s. %s is probably already running.”) + ” %s.”,strDataDir, _(PACKAGE_NAME), e.what()));
}
通过上述代码,我们可以理解到.lock文件将通过lock.try_lock()被锁定,但是如果已被其他先期启动的比特币程序锁定了的话,本次锁定将失效,同时将提示错误信息,返回false,整个程序将退出。
因为该处为程序的正式初始化,所以LockDataDirectory函数中传入的probeOnly为false,意思是当前已不是用于试验、检测锁定文件了,如果仅是检测,将会释放锁定文件,而当前是正式的锁定,所以,一旦锁定将不会解锁,除非程序退出。
(2)pid文件。完成了数据目录的锁定之后,如果是在非windows中的比特币核心程序,程序将通过CreatePidFile函数创建进程编号记录文件,进程编号记录文件名在src/util.h和src/util.cpp中进行了声明与定义,其定义如下:
const char * const BITCOIN_PID_FILENAME = “bitcoind.pid”;
我们通过QYB的程序可以直观看到,Ubuntu下的数据目录中存在qybcoind.pid(同bitcoind.pid),且其文件中记录了比特币核心的进程号。而在QYB的Windows版的数据目录中并不存在该文件。
在非windows中记录比特币核心程序的pid,其目的也是为了与前面的锁定文件一样,防止出现多个比特币核心程序,文件中始终记录第一个启动的程序进程。
pid的详细作用大家可以参考:http://blog.csdn.net/yinqingwang/article/details/52841744
(3)调试日志输出。完成了进程ID存储文件的处理后,我们再来看下后面的调试日志文件的处理代码。此处出现了shrinkdebugfile这个参数,该参数的含义,我们可在init.cpp的HelpMessage函数中看到其解释信息:
strUsage +=HelpMessageOpt(“-shrinkdebugfile”, _(“Shrink debug.log file onclient startup (default: 1 when no -debug)”));
其含义为当客户端启动时,对debug.log文件进行压缩处理,默认在不进行调试时会进行压缩操作,这个也好理解,因为我们不进行调试,所以该文件中的内容没必要保留那么多。同时,if
(GetBoolArg(“-shrinkdebugfile”, !fDebug))语句默认来说是为true的,因为fDebug默认是false,我们在启动时很少会使用shrinkdebugfile参数,所以将会执行ShrinkDebugFile函数,该函数中包含了具体的压缩处理过程。
我们来看具体的压缩处理过程,具体见ShrinkDebugFile函数,该函数位于src/util.cpp中,通过该函数的代码我们可以知道debug.log文件的大小限定在RECENT_DEBUG_HISTORY_SIZE,即10 * 1000000=10MB。如果debug.log文件大小超过限定大小的10%时,则对文件进行裁剪处理,使其限制在RECENT_DEBUG_HISTORY_SIZE范围内。
对debug.log文件进行压缩处理后,程序将正式打开debug.log文件,实现程序运行过程的记录,以便调试。我们首先来看定义于src/util.cpp中的fPrintToDebugLog变量默认为true,然后程序将执行OpenDebugLog函数,该函数定义于src/util.cpp中,在该函数中完成了debug.log文件的打开,打开方式是增加内容“a”模式,即启动后程序将在上一次日志信息的基础上继续添加本次运行日志,在打开日志文件后,程序将实现vMsgsBeforeOpenLog包含内容的打印输出。vMsgsBeforeOpenLog为日志文件未打开之前,预先存储的一些打印输出信息。该信息的存储位于src/util.cpp中的LogPrintStr函数中:
从图中代码可以看出,fileout为空时,vMsgsBeforeOpenLog将预先存储将打印至日志文件的内容,待日志文件打开后,进行写入,写入代码位于OpenDebugLog函数中:
至此完成了日志文件的打开操作,并完成了预先存储日志信息的输出。
随后是日志时间戳信息在日志文件中的输出,但我们看fLogTimestamps其默认值为DEFAULT_LOGTIMESTAMPS,而DEFAULT_LOGTIMESTAMPS在src/util.h中定义为true,意味着日志的每一行都会带有时间戳信息,输入内容加时间戳的函数位于src/util.cpp中的LogTimestampStr函数中,大家有兴趣可以详细看看该函数实现。正因为每一行都带有时间戳,因此,此处不单独输出时间信息。
再下来就是实现基本配置信息的输出了,这些内容包括数据默认目录、当前实际数据目录、比特币配置文件目录以及最大连接数等信息,我们可以看看实际的日志文件debug.log来对比下代码实现,这样就可以更清晰的明确代码实现内容:
从上图我们可以清晰地看到日志文件中包含了预输出内容,还有后面的日志启动输出内容与我们现在的代码内容是一致的。
(4)签名缓存。我们再来看下签名缓存的初始化代码,其代码在src/script/sigcache.h的InitSignatureCache()函数中实现,其包含的代码很简单:
// To be calledonce in AppInit2/TestingSetup to initialize the signatureCache
voidInitSignatureCache()
{
// nMaxCacheSize is unsigned. If-maxsigcachesize is set to zero,
// setup_bytes creates the minimum possiblecache (2 elements).
size_t nMaxCacheSize =std::min(std::max((int64_t)0, GetArg(“-maxsigcachesize”,DEFAULT_MAX_SIG_CACHE_SIZE)), MAX_MAX_SIG_CACHE_SIZE) * ((size_t) 1 <<20);
size_t nElems = signatureCache.setup_bytes(nMaxCacheSize);
LogPrintf(“Using %zu MiB out of %zurequested for signature cache, able to store %zu elements\n”,
(nElems*sizeof(uint256))>>20, nMaxCacheSize>>20, nElems);
}
通过其代码我们可以知道签名的默认缓存大小默认为DEFAULT_MAX_SIG_CACHE_SIZE,如果设置了-maxsigcachesize,并且大于DEFAULT_MAX_SIG_CACHE_SIZE,签名大小将是-maxsigcachesize或者MAX_MAX_SIG_CACHE_SIZE之间的最小者。
其中,DEFAULT_MAX_SIG_CACHE_SIZE、MAX_MAX_SIG_CACHE_SIZE的定义在src/script/sigcache.h中:
// DoS prevention:limit cache size to 32MB (over 1000000 entries on 64-bit
// systems). Dueto how we count cache size, actual memory usage is slightly
// more (~32.25MB)
static const unsigned intDEFAULT_MAX_SIG_CACHE_SIZE = 32;
// Maximum sigcache size allowed
static const int64_t MAX_MAX_SIG_CACHE_SIZE= 16384;
在获得了最大签名缓存大小后,程序将计算在当前大小的缓存下能存储多少的签名数,即nElems的值是多少。nElems的计算要直接从src/cuckoocache.h中来看:
很简单的一个计算公式,就是根据传入的字节数除以每个元素的字节数,即可得到相应的元素数量。最后就是签名缓存信息内容的日志输出了。
二、小结
本研读记录主要分析了AppInitMain中初始化部分,属于刚刚开始,后面包括的内容较多,也很重要,我将会继续带领大家一起详细解读、剖析后续内容,敬请期待。
比特币源码研读之二十一
0 引子
今天是大年三十,除夕之夜,明天就是狗年了,预祝大家新年快乐,狗年旺起来!
自从进入区块链行业之后就发现根本停不下来,放假在家也把电脑带回来了,每天都要学习区块链知识,看区块链新闻,因为区块链的世界实在是太快了,不想落后,所以今天就算是过大年也没休息,坚持学习。作为区块链程序猿,技术的学习是必不可少的,尤其是比特币的源码,我觉得是必经之路,因为基本上所有的区块链项目都是在比特币的架构基础上发展和改进的。所以我也一直在研读比特币的源码,尽自己所能覆盖比特币的所有源码。
我们今天将继续深入AppInitMain函数,本文将主要分析两个与线程相关的内容:脚本验证线程与任务调度线程内容。本文主要涉及的源码文件包括:
src/bitcond.cpp、src/init.h、src/init.cpp、src/util.h、src/util.cpp、src/validation.h、src/validation.cpp、src/scheduler.h、src/scheduler.cpp、src/interpreter.h、src/interpreter.cpp
一、脚本验证线程
此处的脚本验证线程代码如下:
LogPrintf(“Using %u threads for scriptverification\n”, nScriptCheckThreads);
if(nScriptCheckThreads) {
for(int i=0; i
threadGroup.create_thread(&ThreadScriptCheck);
}
此处代码中的nScriptCheckThreads我们已在《比特币源码研读之十三》中的“二、验证脚本线程数”进行了详细说明,而后面的线程组threadGroup创建脚本验证线程的代码也有相应的简要说明。具体如下:
通过线程组创建nScriptCheckThreads个数量的脚本验证线程,线程处理函数为ThreadScriptCheck,其定义于src/validation.h中,实现于src/validation.cpp中,在该函数中通过脚本验证队列管理脚本验证线程,其具体运行方式我们将AppInitMain函数的研读中详细说明。
我们现在来看下src/validation.h中的ThreadScriptCheck函数,其在src/validation.h中的定义如下:
/** Run an instance of the script checking thread */
void ThreadScriptCheck();
通过其注释我们可以看出该函数为运行一个脚本验证线程的实例,其实现代码位于src/validation.cpp中,代码如下;
void ThreadScriptCheck() {
RenameThread(“bitcoin-scriptch”);
scriptcheckqueue.Thread();
}
在该函数中首先通过RenameThread函数定义了运行脚本验证线程的名字:bitcoin-scriptch。然后通过脚本验证队列对象scriptcheckqueue启动脚本验证线程。scriptcheckqueue在ThreadScriptCheck函数之上进行了定义:
staticCCheckQueue scriptcheckqueue(128);
第一次看到这个定义,大家有可能会误以为这是一个数组,然而并不是的。因为CCheckQueue是个验证队列模板类,其定义位于src/checkqueue.h中,template classCCheckQueue,其注释如下;
/**
* Queue forverifications that have to be performed.
* Theverifications are represented by a type T, which must provide an
* operator(),returning a bool.
*
* One thread(the master) is assumed to push batches of verifications
* onto thequeue, where they are processed by N-1 worker threads. When
* the masteris done adding work, it temporarily joins the worker pool
* as an N’thworker, until all jobs are done.
*/
通过以上注释我们可以知道该队列是用于执行验证的队列,验证类为typename T,类T必须实现返回bool值的operator()函数。我们待会再看下CScriptCheck是否提供了operator()函数。同时,该队列的实现过程是:
(1)主线程(Master)会将验证批量地分配到队列中,然后这些队列的任务由N-1个工作线程处理;
(2)当主线程完成了任务添加后,它也暂时作为第N个工人加入到工人池队列中,知道所有任务都完成才退出。
staticCCheckQueue scriptcheckqueue(128);中的128是给CCheckQueue模板中的nBatchSizeIn赋值的,用于限制每批的处理任务数,即脚本验证个数。
我们再来看下CScriptCheck类,其定义于validation.h中,其定义如下;
从该类的注释我们可以看出其为脚本验证的封装,并且包含了输出交易的信息,也就是说主要是对输出交易的脚本验证。
分析完模板类和脚本验证类后,我们具体来看下线程的执行代码,即从scriptcheckqueue.Thread();调用开始,Thread()函数位于CCheckQueue模板中,该函数的代码很简单,运行了私有函数Loop();,从Loop函数的注释我们可以看出脚本验证就是在此处运行的,此函数中大部分代码是在进行脚本验证任务的筛选与判断,而真正进行脚本验证则是在Loop函数的最下面代码运行的,如图所示:
该部分代码中,循环遍历验证脚本,并运行check()函数,check为CScriptCheck类的实例对象,check()函数实际运行的是CScriptCheck::operator()()函数,该函数代码如下:
bool CScriptCheck::operator()() {
constCScript &scriptSig = ptxTo->vin[nIn].scriptSig;
constCScriptWitness *witness = &ptxTo->vin[nIn].scriptWitness;
if(!VerifyScript(scriptSig, scriptPubKey, witness, nFlags,CachingTransactionSignatureChecker(ptxTo, nIn, amount, cacheStore, *txdata),&error)) {
returnfalse;
}
returntrue;
}
在脚本验证函数中,我们可以看到验证函数为VerifyScript函数,该函数在src\script\interpreter.h、src\script\interpreter.cpp中声明与定义,在该函数中对交易脚本进行了验证,验证过程中根据不同脚本类型进行了验证,因为脚本对于比特币交易来说至关重要,所以具体过程我们将在后面专门写几篇文章详细描述脚本概念、脚本定义、脚本执行以及脚本验证的源码分析文章。
此处即完成了脚本验证线程创建与启动线程代码的分析。
二、任务调度线程
下面我们将进入轻量级任务调度线程的初始化代码部分,代码如下:
// Start the lightweight task scheduler thread
CScheduler::Function serviceLoop =boost::bind(&CScheduler::serviceQueue, &scheduler);threadGroup.create_thread(boost::bind(&TraceThread,”scheduler”, serviceLoop));
关于任务调度,我们首先来看任务调度类CScheduler,该类在src/scheduler中定义,在该类文件的最上方有其注释以及使用示例:
其注释说明了任务调度类的作用是用于后台运行任务,这些任务是周期性或随后的某个时刻一次性运行的。
通过其使用示例可以看出我们可以在任务调度函数中定义某个任务从当前时刻后的某个执行时间,完成任务定义后我们可以将该任务放入线程中等待执行,而这些任务的调度是通过serviceQueue函数来处理的。
三、结论
从上面分析我们可以很清楚地明白我们现在分析的这两行任务分析代码的含义,即启动任务调度队列,并将其放入线程中,然后启动线程等待任务的加入,随后根据任务的运行时间,正确调度并执行任务。
以上就是今天的比特币源码研读记录,主要对脚本验证线程和任务调度线程进行了分析,下一篇文章我们将继续分析后面的代码,将这条主线延续下去。