版权声明:本文为博主原创文章,未经博主允许不得转载。
其实接入比特币网络是非常简单的,我说了你一定不信,启动比特币客户端即可:
在命令行终端输入启动命令:./src/bitcoind -testnet
输入之后会有一个和网络同步数据的过程,你会看到:
这个过程需要一点时间,同步数据完成后,即接入了比特币网络。
虽然说一句命令即搞定,但是,这个背后代码运行的逻辑可就不简单咯~
来,我给大家分析一下
当在命令行终端输入启动命令:./src/bitcoind -testnet
后,操作系统就会找到这个文件中的 main 函数,开始比特币客户端的启动。
对于所有的c++代码,整个程序都是从main函数开始执行的,bitcoind 的main函数位于 src/bitcoind.cpp
,代码拉到最后就找到了我们的 main 函数。
main 函数本身没有太多东西,主要是调用3个函数来执行,它们的主要作用是设置环境变量、设置信号处理和启动系统。
具体代码如下:
int main(int argc, char* argv[])
{
SetupEnvironment();
// Connect bitcoind signal handlers
noui_connect();
return (AppInit(argc, argv) ? EXIT_SUCCESS : EXIT_FAILURE);
}
这段代码简单说明如下:
SetupEnvironment
函数,主要用来设置系统的环境变量,包括:malloc
分配内存的行为、Locale、文件路径的本地化设置等。noui_connect
函数,设置连接到 bitcoind 的信号的处理。AppInit
函数,进行系统启动。下面我们重点讲下 AppInit
函数的执行
调用 SetupServerArgs
函数,设置系统可接受的所有命令行参数。然后开始解析命令行传递的各种参数。
系统执行的重要一步就是设置可以接收的参数并解析用户启动时传递的各种参数,SetupServerArgs
函数就是完成这个目的。下面来看这个函数的执行流程。
首先,调用 CreateBaseChainParams
函数,生成默认的基本参数,包括:使用的数据目录和监听的端口。根据不同的网络类型,主网络使用 8332 端口和指定目录下的当前目录,测试网络使用 18332 端口和指定目录下的 testnet3 子目录,回归测试网络 使用 18443 端口和指定目录下的 regtest 子目录。
然后,调用 CreateChainParams
函数,生成默认的区块链参数。这个方法也会区分不同的网络。
如果是主网络,则生成 CMainParams
对象进行初始化。在构造函数中,进行如下的设置:
main
;Consensus::Params
)的各个值:nSubsidyHalvingInterval
)后续比特币的奖励会减半,值为 210000。根据创世区块奖励的数量(50),根据等比数列求和公式:
BIP34Height
)为 227931。BIP34Hash
)为 0x000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8
。BIP65Height
)为 388381。BIP66Height
)为 363725。powLimit
)为一个大整数。nPowTargetTimespan
)为 2周。nPowTargetSpacing
)为10分钟。nRuleChangeActivationThreshold
)为 1916,即 2016 的 95%。nMinerConfirmationWindow
)为 2016,等于难度改变周期除以平均出块时间。DEPLOYMENT_TESTDUMMY
)、CSV 软分叉相关的(涉及到 BIP68、BIP112、BIP113)和隔离见证相关的(涉及到 BIP141、BIP143、BIP147)。nDefaultPort
)为 8333。nPruneAfterHeight
),当前值为 100000。CreateGenesisBlock
方法,生成创世区块。这个方法的参数是固定的,指定了创世区块的时间、随机数、难度值、版本号、奖励等。在方法内部,生成创世区块的输出脚本和输入脚本,中本聪那句著名的评论就出现在创世区块的第一个交易的签名中,他写道:The Times 03/Jan/2009 Chancellor on brink of second bailout for banks。vSeeds
集合包含的 DNS 种子有:seed.bitcoin.sipa.be
,dnsseed.bluematt.me
,dnsseed.bitcoin.dashjr.org
,seed.bitcoinstats.com
,seed.bitcoin.jonasschnelli.ch
,seed.btc.petertodd.org
,seed.bitcoin.sprovoost.nl
等,通过解析 DNS 种子节点,比特币节点启动时可以找到更多的对等节点来进行连接。如果是测试网络,则生成 CTestNetParams
对象进行初始化。(供开发完成后测试使用。)
如果是回归测试网络,则生成 CRegTestParams
对象进行初始化。(供开发时连接使用。)
对于这两种测试网络,处理基本和主网络相同,只是某些参数不一样。
上面提到的3个对象 CMainParams
CTestNetParams
CRegTestParams
的定义都在 chainparams.cpp
文件中。感兴趣同学的可以对照源代码进一步探究。
接下来,设置系统可接收的所有参数。
部分参数解释如下:
pruneblockchain
RPC 来删除特定块,并且如果提供目标大小,则启用对旧块的自动修剪。 此模式与 -txindex
和 -rescan
不兼容。blk*.dat
文件重建区块链状态和区块的索引。getrawtransaction
RPC 命令调用。-addnode
、-seednode
、-connect
总是使用 DNS 查找。checkblocks
验证区块的程度。上面是一些常用的参数,通过这些参数可以影响比特币核心的命令。应用开发者比较关注的是 RPC 相关的设置,通过 RPC 接口,我们调用比特币核心提供的多种服务。这些命令通常会在配置文件中进行设置,不用在命令行指定。
接下来,检查用户指定命令参数是否正确。
if (!gArgs.ParseParameters(argc, argv, error)) {
fprintf(stderr, "Error parsing command line arguments: %s\n", error.c_str());
return false;
}
如果传递的是帮助和版本参数,则显示帮助或版本信息,然后退出。
检查数据目录(可指定或默认)是否是存在。如果不存在,则打印错误信息,然后退出。
if (!fs::is_directory(GetDataDir(false)))
{
fprintf(stderr, "Error: Specified data directory \"%s\" does not exist.\n", gArgs.GetArg("-datadir", "").c_str());
return false;
}
在 GetDataDir
方法中,根据用户是否在命令行提供 datadir
参数来确定使用默认的数据目录还是用户指定的数据目录。
读取并解析配置文件,同时检查指定数据目录是否存在。如果任何一个步骤出错,都打印错误信息,然后退出。
if (!gArgs.ReadConfigFiles(error, true)) {
fprintf(stderr, "Error reading configuration file: %s\n", error.c_str());
return false;
}
其中 ReadConfigFiles
方法具体处理如下:
GetArg
方法,获取配置文件名称,默认为 bitcoin.conf
。GetConfigFile
方法获取配置文件的绝对路径(方法内部会委托 AbsPathForConfigVal
方法进行处理,后者决定根据用户指定的路径或使用默认路径来生成配置文件的绝对路径)。在得到配置文件的绝对路径之后,构造文件输入流,从而读取配置文件 fs::ifstream stream(GetConfigFile(confPath))
。ReadConfigStream
方法开始读取配置文件的内容。方法内部按行读取配置文件,并以键值对的形式保存在 m_config_args
集合中。调用 SelectParams(gArgs.GetChainName())
函数,生成全局的区块链参数,并设置系统的网络类型。如果有错误,则打印错误,然后退出。
gArgs.GetChainName()
方法会返回当前使用的网络。针对主网络,返回字符串 main
;测试网络,返回字符串 test
;回归测试网络,返回字符串 regtest
。
SelectParams
方法的实现如下所示:
void SelectParams(const std::string& network)
{
SelectBaseParams(network);
globalChainParams = CreateChainParams(network);
}
SelectBaseParams
方法会根据指定的网络参数生成 CBaseChainParams
对象,并保存在 globalChainBaseParams
变量中,并在指定 gArgs
对象中保存网络类型(m_network
属性)。CBaseChainParams
对象中仅保存系统的数据目录和运行的端口,所以称之为基本区块链参数对象。
CreateChainParams
方法会根据不同的网络参数生成 CChainParams
类的子对象,可能为以下三种:CMainParams、CTestNetParams、CRegTestParams。CChainParams
对象包含了区块链对象的所有重要信息,比如:共识规则、部署状态、检查点、创世区块等。
检查所有命令行参数,如果有错误,则打印错误,并退出。
设置参数 -server
默认为真。
bitcoind 守护进程默认 server
为真。
调用 InitLogging
函数,初始化系统所用日志,并打印系统的版本信息。
具体代码如下,根据是否指定 debuglogfile
、printtoconsole
等确定日志打印到文件或是控制台。
void InitLogging()
{
g_logger->m_print_to_file = !gArgs.IsArgNegated("-debuglogfile");
g_logger->m_file_path = AbsPathForConfigVal(gArgs.GetArg("-debuglogfile", DEFAULT_DEBUGLOGFILE));
LogPrintf("\n\n\n\n\n");
g_logger->m_print_to_console = gArgs.GetBoolArg("-printtoconsole", !gArgs.GetBoolArg("-daemon", false));
g_logger->m_log_timestamps = gArgs.GetBoolArg("-logtimestamps", DEFAULT_LOGTIMESTAMPS);
g_logger->m_log_time_micros = gArgs.GetBoolArg("-logtimemicros", DEFAULT_LOGTIMEMICROS);
fLogIPs = gArgs.GetBoolArg("-logips", DEFAULT_LOGIPS);
std::string version_string = FormatFullVersion();
LogPrintf(PACKAGE_NAME " version %s\n", version_string);
}
调用 InitParameterInteraction
函数,根据参数间的关系,检查所有的交互参数。
调用 AppInitBasicSetup
函数,进行基本的设置。如果有错误,则打印错误,然后退出。
经过前面漫长的检查与设置,终于开始了应用基本的设置。具体解读见第二部分。
调用 AppInitSanityChecks
函数,处理底层加密函数相关内容。
具体解读见第二部分。
调用 AppInitLockDataDirectory
函数,检查并锁定数据目录。
具体解读见第二部分。
调用 AppInitMain
函数,比特币主要的启动过程。
具体解读见第二部分。
如果应用初始化主函数出错,则调用 Interrupt
函数进行中止,否则调用 WaitForShutdown
函数等待系统结束。
WaitForShutdown
函数是一个无限循环函数。
以下为系统启动过程中重要的步骤。
src/bitcoind.cpp
)AppInitBasicSetup
函数进行基本的设置。
调用 SetupNetworking
函数,进行网络设置。
主要是针对 Win32 系统处理套接字,别的系统直接返回真。
如果不是 WIN32 系统,进行下面的处理:
sysperms
参数为真,调用 umask
函数,设置位码为 077。registerSignalHandler
函数,设置 SIGTERM
信息处理器为 HandleSIGTERM
;SIGINT
为 HandleSIGTERM
;SIGHUP
为 HandleSIGHUP
。src/bitcoind.cpp
)AppInitParameterInteraction
函数前半部分。
首先,调用 Params
方法,获取前面初始化的 globalChainParams
区块链对象。
检查指定的区块目录是否存。如果不存在,则返回初始化错误。
如果同时指定了 prune
、txindex
,则抛出初始化错误。
如果指定了区块修剪 prune
,就要禁止交易索引 txindex
,两者不兼容,只能其一。
如果同时指定了 bind
或 whitebind
就不能同时指定 listen
。
确保有足够的文件符可用。因为在类 Unix 系统中,每个套接字都是一个文件,都需要一个文件描述符。所以要检查指定的最大连接数 maxconnections
是否超过系统可用限制。
src/bitcoind.cpp
)AppInitParameterInteraction
函数后半部分。
debug
、debugexclude
、debugnet
等参数。socks
,则提示使用 SOCKS5tor
,则提示使用 onion
。benchmark
,则提示使用 -debug=bench
。whitelistalwaysrelay
,则提示使用 whitelistrelay
,或whitelistforcerelay
。blockminsize
,则提示使用 blockminsize
。regtest
下,Checkmempool
和 checkblockindex
默认为真。assumevalid
参数。minimumchainwork
,计算最小区块链工作量。maxmempool
、limitdescendantsize
incrementalrelayfee
,则进行相关处理。par
参数。prune
。timeout
。minrelaytxfee
参数。blockmintxfee
参数。dustrelayfee
参数。acceptnonstdtxn
参数。bytespersigop
参数。ParameterInteraction
方法,初始钱包相关的参数。本方法在 wallet/init.cpp
文件中。 disablewallet
。wallet
在没有指定情况默认为空字符串。blocksonly
、walletbroadcast
参数。salvagewallet
、rescan
参数。zapwallettxes
、persistmempool
参数。upgradewallet
参数。maxtxfee
参数。permitbaremultisig
、datacarrier
、datacarriersize
等参数的值。SetMockTime
方法,设置模拟时间。peerbloomfilters
参数,设置本地支持的服务。rpcserialversion
参数是否小于0,是否大于1。maxtipage
参数值,表示区块链顶端值存活时间。mempoolreplacement
参数。vbparams
参数。src/bitcoind.cpp
)AppInitSanityChecks
函数初始相关的加密曲线与函数。
同时,调用 LockDataDirectory
函数,锁定数据目录,确保只有 Bitcoind 在运行。
src/init.cpp::AppInitMain()
)AppInitMain
函数是应用初始化的主体,包括本步骤在内的以下步骤的主体都是在这个函数内部执行。
调用 Params
函数,获取 chainparams
。
方法定义在 src/chainparams.cpp
文件中。这个变量主要是包含一些共识的参数,自身是根据选择不同的网络 main
、testnet
或者 regtest
来生成不同的参数。
如果是非 Windows 系统,则调用 CreatePidFile
函数,创建进程的PID文件。
pid 文件简介如下:
pid文件的内容
pid文件为文本文件,内容只有一行, 记录了该进程的ID。 用cat命令可以看到。
pid文件的作用
防止进程启动多个副本。只有获得pid文件(固定路径固定文件名)写入权限(F_WRLCK)的进程才能正常启动并把自身的PID写入该文件中。其它同一个程序的多余进程则自动退出。
如果命令行指定了 shrinkdebugfile
参数或默认的调试文件,则调用日志对象的 ShrinkDebugFile
方法,处理 debug.log
文件。
如果日志长度小于11MB,那么就不做处理;否则读取文件的最后 RECENT_DEBUG_HISTORY_SIZE
10M 内容,重新保存到debug.log文件中。
调用日志对象的 OpenDebugLog
方法,打开日志文件。如果不能打开则抛出异常。
调用 InitSignatureCache
函数,设置签名缓冲区大小。
调用 InitScriptExecutionCache
函数,设置脚本执行缓存区大小。
根据 nScriptCheckThreads
变量的值,循环调用 threadGroup.create_thread
方法,创建指定数量的线程,并放入线程组。
nScriptCheckThreads
变量在前面根据命令行参数 par
进行设置。
线程内部调用 ThreadScriptCheck
函数进行执行。 ThreadScriptCheck
函数过程如下:
首先调用 RenameThread
函数(内部调用 pthread_setname_np
函数)将当前线程重命名为 bitcoin-scriptch
。
然后调用 CCheckQueue
队列对象的 Thread
方法,开启内部循环。
Thread
方法又调用内部私有方法 Loop
方法,生成一个脚本验证工作者,然后进行无限循环,在循环内部调用工作者的 wait(lock)
方法,从而线程进入阻塞,直到有新的任务被加到队列中中时,才会被唤醒执行任务。
调用 boost::bind
方法,生成 CScheduler
对象 serviceQueue
方法的替代方法。然后调用 threadGroup.create_thread
方法,创建一个线程。
线程执行的方法是 boost::bind
返回的替代方法,bind
方法的第一个参数为 TraceThread
函数,第二个参数为线程的名字,第三个参数为serviceQueue
方法的替代方法。
TraceThread
函数内部调用 RenameThread
方法修改线程名字,此处线程名字修改为 bitcoin-scheduler
;然后执行传入的可调用对象,此处为前面的替代方法,即 CScheduler
对象 serviceQueue
方法。
serviceQueue
方法主体是一个无限循环方法,如果队列为空,则进程进入阻塞,直到队列有任务,则醒来执行任务,并把任务从队列中移除。
调用 GetMainSignals().RegisterBackgroundSignalScheduler
方法,注册后台信号调度器。
调用 GetMainSignals().RegisterWithMempoolSignals
方法,注册内存池信号处理器。
调用内联函数 RegisterAllCoreRPCCommands
,注册所有核心的 RPC 命令。
第一步,调用 RegisterBlockchainRPCCommands
方法,注册所有关于区块链的 RPC 命令。
第二步,调用 RegisterNetRPCCommands
方法,注册所有关于网络相关的 RPC 命令。
第三步,调用 RegisterMiscRPCCommands
方法,注册所有的杂项 RPC 命令。
第四步,调用 RegisterMiningRPCCommands
方法,注册所有关于挖矿相关的 RPC 命令。
第五步,调用 RegisterRawTransactionRPCCommands
方法,注册所有关于原始交易的 RPC 命令。
调用钱包接口的 RegisterRPC
方法,注册钱包接口的 RPC 命令。
实现类为 wallet/init.cpp
,方法内部调用 RegisterWalletRPCCommands
进行注册,后者又调用 wallet/rpcwallet.cpp
文件中的 RegisterWalletRPCCommands
方法,完成注册钱包的 RPC 命令。
如果命令参数指定 server
,则调用 AppInitServers
方法,注册服务器。
方法内处理流程如下:
调用 RPCServer::OnStarted
方法,设置 RPC 服务器启动时的处理方法。
调用 RPCServer::OnStopped
方法,设置 RPC 服务器关闭时的处理方法。
调用 InitHTTPServer
方法,初始化 HTTP 服务器。
调用 StartRPC
方法,启动 RPC 信号监听。
调用 StartHTTPRPC
方法,启动 HTTP RPC 服务器。
方法内部调用 RegisterHTTPHandler
方法,注册 /
请求处理方法为 HTTPReq_JSONRPC
。调用 RegisterHTTPHandler
方法,注册 /wallet/
请求处理方法为 HTTPReq_JSONRPC
。
如果命令参数指定 rest
,调用 StartREST
方法,设置 /rest/xxx
一系列 HTTP 请求的处理器。
调用 StartHTTPServer
方法,启动 HTTP 服务器。
src/init.cpp::AppInitMain()
)调用钱包接口的 Verify
方法,验证钱包数据库。实现类为 wallet/init.cpp
,内部处理流程如下:
disablewallet
,如果禁止,则直接返回。walletdir
,则检查钱包数据库目录是否存在,是否为目录、且是否为常规的的路径。CWallet::Verify
方法进行验证)。src/init.cpp::AppInitMain()
)uacomment
,处理用户代理。version
消息指定的消息最大长度。onlynet
参数,则设置仅可以连接的节点。proxy
,且不等于 0,则:根据代理参数、dns
查找等,调用 Lookup
方法,查找/设置代理服务器;调用 SetProxy
方法,设置 IPv4、IPv6、Tor 网络的代理;调用 SetNameProxy
方法,设置命名(域名)代理;调用 SetLimited
方法,设置不自动连接到 Tor 网络。onion
参数,则处理洋葱网络的相关设置。externalip
参数设置的外部 IP,调用 Lookup
方法查找外部地址,如果成功则调用 AddLocal
方法,保存新的地址。maxuploadtarget
参数,则设置最大出站限制。src/init.cpp::AppInitMain()
)首先,计算缓存的大小。包括:区块索引数据库、区块状态数据库、内存中 UTXO 集。
然后,以下开始循环处理。
UnloadBlockIndex
方法,卸载区块相关的索引。LoadBlockIndex
方法,加载区块索引。LookupBlockIndex
方法,加载区块索引,并检查是否包含创世区块。如果出错,则返回异常。LoadGenesisBlock
加载创世区块失败,则退出循环。src/init.cpp::AppInitMain()
)如果指定了 txindex
参数,则调用 MakeUnique
函数,生成交易索引对象,然后调用其 Start
方法,开始建立索引。
src/init.cpp::AppInitMain()
)调用钱包接口对象的 Open
方法,开始加载钱包。
src/init.cpp::AppInitMain()
)如果当前为修剪模式,本地服务去掉 NODE_NETWORK
,然后如果不需要索引则调用 PruneAndFlush
函数,修剪并刷新。
src/init.cpp::AppInitMain()
)调用 CheckDiskSpace
函数,检查硬盘空间是否足够。
如果没有足够的硬盘空间,则退出。
检查最佳区块链顶端指示指针。
如果顶端打针为空,UI界面进行通知。如果不空,则设置有创世区块,即 fHaveGenesis
设为真。
如果指定了 blocknotify
参数,设置界面通知为 BlockNotifyCallback
。
遍历参数 loadblock
指定要加载的区块文件,放进向量变量 vImportFiles
中。然后调用 threadGroup.create_thread
方法,创建一个线程。线程执行的函数为 ThreadImport
,参数为要加载的区块文件。
获取 cs_GenesisWait
锁,等待创世区块被处理完成。
src/init.cpp::AppInitMain()
)如果指定了监听洋葱网络 listenonion
,调用 StartTorControl
函数,开始 Tor 控制。
调用 Discover
函数,开始发现外部节点。
如果指定了 upnp
参数,则调用 StartMapPort
函数,开始进行端口映射。
生成选项对象,并进行初始化。
如果指定了 bind
参数,则对所有的绑定地址,调用 Lookup
方法,查找并进行绑定,然后放入选项对象的 vBinds
属性中。
如果指定了 whitebind
参数,则对所有的绑定地址,调用 Lookup
方法,查找并进行绑定,然后放入选项对象的 vWhiteBinds
属性中。
whitebind
参数指定的地址需要带有端口号。
如果指定了 whitelist
参数,遍历列表,调用 LookupSubNet
方法,生成对应的子网,然后放入选项对象的 vWhitelistedRange
属性中。
取得参数 seednode
指定的值,放入选项对象的 vSeedNodes
属性中。
调用 CConnman
对象的 Start
方法,初始所有的出站连接。
本方法非常非常重要,因为它启动了一个重要的流程,即底层的 P2P 网络建立和消息处理流动。
src/init.cpp::AppInitMain()
)Start
方法,开始进行钱包相关的处理,并定时刷新钱包数据到数据库中。我是区小白,区块链开发者,区块链技术爱好者,深入研究比特币,以太坊,EOS Dash,Rsk,Java, Nodejs,PHP,Python,C++ 现为Ulord全球社区联盟(优得社区)核心开发者。
我希望能聚集更多区块链开发者,一起学习共同进步。
敬请期待下一篇文章:如何启动比特币系统并加入比特币网络