3.3 bytomd守护进程的初始化实现
bytomd守护进程的Cobra流程与bytomcli过程非常相似,所以在此略去,后续主要对bytomd守护进程重要内容进行深入分析。在这里我们看一下bytomd预处理过程中使用到的代码文件结构,命令如下:
$ tree cmd/bytomd/ cmd/bytomd/ ├—— bytomd ├—— commands | ├—— init.go 节点网络初始化相关 | ├—— root.go 节点root目录相关 | ├—— run_node.go 节点守护进程相关 | └—— version.go 节点版本参数相关 ├—— main.go bytomd入口函数
bytomd守护进程启动时,会根据不同的命令行flag参数,初始化不同的模块,最终以守护进程的方式运行。有关bytomd守护进程所有的运行工作都在node.NewNode(config)的具体实现中。下面介绍具体的实现过程。
3.3.1 Node对象
Node对象说明如下。
❏ cmn.BaseService:服务管理。
❏ config:当前节点的全局配置。
❏ syncManager:区块和交易同步管理。
❏ wallet:本地钱包管理。
❏ accessTokens:token管理,用户访问凭证。
❏ api:API Server接口服务。
❏ chain:本地区块链管理对象。
❏ txfeed:当前版本中该功能未使用。
❏ cpuMiner:CPU挖矿管理对象。
❏ miningPool:矿池管理对象。
❏ miningEnable:是否启用挖矿模式。
node.NewNode(config)整个过程是为了创建Node对象,Node对象是整个bytomd所有模块运行的基础。
cmn.BaseService是tendermint框架的一个服务管理模块,在这里我们可以把Node作为一个服务,对该服务进行OnStart/OnStop/IsRunning等操作管理。tendermint框架可以保证这些操作不会被重复执行多次。
3.3.2 配置初始化
在执行node.NewNode(config)之前,config的默认配置就已经定义好了。在深入node. NewNode(config)分析之前,我们需要先了解默认配置都有哪些。
首先,bytomd守护进程声明一个config全局变量,表示整个bytomd守护进程的配置信息。进程启动时config对象被赋予一个默认的配置参数。
默认的配置参数分有6个,每个针对不同的模块。下面对配置进行说明。我们将默认参数归纳为三块:Base基础配置、P2P网络配置、其他配置。
1. Base基础配置
BaseConfig用于配置bytomd节点所需的基础参数,包括数据目录、日志、监听地址等相关参数。
部分参数从配置文件中获取默认值,比如ApiAddress参数,它的tag是api_addr。我们可以从config/toml.go中获取默认值:
2. P2P网络配置
P2PConfig用于配置bytomd P2P通信协议中使用的参数,包括本机监听端口、通信节点超时、地址簿等相关参数。
注意,在比特币中,节点会采用DNS的方式来询问种子节点,进而查询到其他节点的IP地址。而在比原链中,种子节点是IP地址,一般会硬编码到代码里。技术细节我们会在后面的第10章详细讲解。
3.其他配置
WalletConfig用于配置bytomd本地钱包使用的参数,包括是否启用本地钱包和更新等相关参数。
在bytomd守护进程声明config = DefaultConfig()之后,init()函数实现了config对象中各属性的赋值。具体实现代码如下:
在init()函数中定义了很多不同类型的flag参数,并将flag的参数值绑定到config对象上,比如:
runNodeCmd.Flags().Bool("mining", config.Mining, "Enable mining")
这条语句的含义为:
❏ 定义一个Bool类型的flag参数。
❏ 该flag的名称为mining。
❏ 该flag的赋值对象为config.Mining。
❏ 该flag的描述信息为Enable mining。
至此,bytomd守护进程所需要的配置信息初始化完毕,程序运行真正进入初始阶段。下面对此进行深入分析。
3.3.3 创建文件锁
在比原链中,一份数据目录(--root参数指定)只能同时由一个bytomd守护进程读写,因为LevelDB高性能键值数据库是单进程模式,如果多个进程同时读写一份数据,会造成数据不一致的情况。因此,需要使用文件锁可以保证同一时间一个进程读写一份数据目录,代码如下:
bytomd启动时,lockDataDirectory函数使用flock在RootDir目录下创建一个LOCK文件。如果bytomd进程在一个文件的inode上加了锁,那么再次启动bytomd进程则会对errors.New中的内容报错并退出进程。flock的作用是检测进程是否已经存在。
flock主要有3种操作类型。
❏ LOCK_SH:共享锁,多个进程使用同一把锁用于读锁。
❏ LOCK_EX:排他锁,同时只允许一个进程使用,一般用于写锁。
❏ LOCK_UN:释放锁。
如果深入研究flock包的函数,我们可以看到,这里使用了LOCK_EX锁,即同时只允许一个进程使用,代码示例如下:
3.3.4 初始化网络类型
比原链的三种网络模式,分别是mainnet主网、testnet测试网和solonet单机模式。
其中initActiveNetParams函数根据用户传入的chain_id,初始化网络类型。consensus. ActiveNetParams对象保存了当前使用的网络模式。在比原链代码中会经常引用consensus. ActiveNetParams对象,用来识别当前节点连接的网络类型。
ActiveNetParams默认使用主网。MainNetParams中的参数说明如下。
❏ Bech32HRPSegwit:隔离见证,是一种协议升级,我们会在后面第5章讲解。
❏ Checkpoints:检查点,指定一个高度,以及与这个高度相匹配的hash值,用于快速同步时验证区块的正确性。通常在主网升级时,会将历史的块信息硬编码在Checkpoints中。
Checkpoints检查点有两种作用:第一是防止分叉,如果有人试图从检查点之前的区块进行分叉,当前节点不会接受这个分叉;也用于保护网络不受全网51%的算力攻击,因为攻击者不可能逆转检查点之前的交易。第二是用于节点间的快速同步,我们将在第10章中详细讲解。
3.3.5 初始化数据库(持久化存储)
创建一条公链,需要将链上的所有数据(包含块信息、交易信息等)存储在本地键值数据库中。在比原链中使用LevelDB来存储链上数据,代码如下:
dbm使用tendermint框架的db管理库。dbm.NewDB返回一个DB对象,DB对象提供了数据库接口和许多方法实现,包括使用内存映射、文件系统目录结构、GO中LevelDB等的实现。
dbm.NewDB返回一个DB对象,需要传入三个参数:db的名称,db使用的键值数据库(默认为LevelDB), db数据存储的路径。leveldb.NewStore函数返回一个Store对象,即比原链对LevelDB进行了封装,在LevelDB的基础上增加了区块缓存(cache)、区块验证、区块状态、区块查询等功能。
3.3.6 初始化交易池
当交易被广播到网络中并且被矿工接收到时,矿工会将接收到的交易加入到本地的TxPool交易池中,TxPool对象的作用是管理本地交易池。交易池相当于一个缓冲区,它并不是无限大。默认情况下比原链中交易池最大可以存储10000笔交易。如果超出这个阈值,则会返回"transaction pool reach the max number"提示。
protocol.NewTxPool()返回一个TxPool实例对象。此处我们只介绍交易池初始化部分,交易池实现原理的代码将在6.10节中深入剖析。
3.5.7 创建一条本地区块链
当节点第一次启动时,判断本地持久化存储的状态,当状态为初始化时会初始化本地的区块链。区块链的第一个区块(创世区块)会被加入到区块高度为0的地方。代码如下:
protocol.NewChain返回一个Chain对象,NewChain需要接收两个参数:Store区块链的存储对象,TxPool交易池。Chain对象管理着比原链的整个区块链条。代码如下:
NewChain函数的执行可分为下面几个步骤:
1)实例化Chain对象。
2)store.GetStoreStatus获取本地区块链的存储状态,如果状态为nil则说明区块链未被初始化。执行initChainStatus初始化本地区块链,该函数初始化创世区块(第一个区块)并添加到本地链上。
3)store.LoadBlockIndex加载块索引,从数据库中读取所有Block Header信息并缓存在内存中,目的是加速访问区块头信息。
4)c.index.SetMainChain,设置当前节点已同步的最新区块。
5)go c.blockProcesser(),启动一个go rutine,用于更新本地区块链上的区块信息。
3.3.8 初始化本地钱包
默认情况下比原链节点会启用本地钱包功能。代码实例如下:
在比原链的节点启动时,上述代码流程主要逻辑为:
1)创建加密机hsm对象,hsm对象管理keystore文件,该文件是存储私钥的一种格式(JSON)。keystore是一串代码,本质上是加密后的私钥,需配合钱包的密码来使用。
2)创建钱包数据库。
3)创建账户管理对象。
4)创建资产管理对象。
5)实例化Wallet对象。
6)RescanBlocks扫描本地所有区块,触发钱包更新操作。
3.3.9 初始化网络同步管理
P2P通信模块主要由SyncManager管理,SyncManager负责节点业务层信息的同步工作,即区块和交易信息的同步。代码如下:
主要参数说明如下:
❏ newBlockCh:通道用于新挖掘出的区块进行快速广播给其他节点。通道大小为1024。
❏ netsync.NewSyncManager:实例化syncManager同步管理对象,它管理节点与节点之间的区块、交易信息同步。
❏ newPoolTxListenner:启动一个go routine,监听交易池中的交易,将交易发送给syncManager同步管理对象或本地钱包。
详细实现机制将在第10章进行讲解。
3.3.10 初始化Pprof性能分析工具
Pprof是GO语言标准库中自带的性能分析工具。用于内存分析、CPU分析、代码追踪等,还可以生成性能分析图表。(详细参考https://golang.org/pkg/net/ http/pprof/)。在比原链中默认不启用该功能,可以使用--prof_laddr参数启动代码性能分析功能,代码示例如下:
3.3.11 初始化CPU挖矿功能
在比原链节点源码中,只提供了CPU设备的挖矿功能,以目前全网的算力来看,CPU设备挖矿几乎挖不到BTM币了。目前主流的挖矿设备,有比特大陆定制的挖矿芯片或各大矿池使用GPU设备挖矿。挖矿和矿池细节将在第13章中详细解读。代码实例如下:
其中,simd参数用于Tenaority CPU指令的优化。