八、初始化和启动模块(7)

4. 第六步:网络初始化

这一步包含的代码在init.cpp的AppInitMain()函数中的1286-1391行中。
先看看对这个步骤的解释文字:

// ********************第六步: 网络初始化
// 请注意,不到最后面("start node"的时候)我们绝对不能打开任何实际的连接,因为UTXO/block状态还没有设置好,
//并且如果我们在后面需要更新索引,最终可能会使UTXO/block状态设置两次。

由这段的注释可以知道,这一步还是进行一些网络参数的设置,并不是真正的网络连接,实际真正的网络连接只有在最后的"Start node"的时候才能进行。所以这部分放在初始化的第六步是合理的。
(1)在代码开始时用了一个断言语句来确保g_connman变量为空,它为空值后又创建了一个CConnman对象,用于设置连接的参数,并把该值赋值给g_connman变量。接着又创建了一个PeerLogicValidation类型的变量,这个类实现的功能是:在产生一个新的区块时,节点是如何处理的。最后用RegisterValidationInterface()函数来注册节点之间的消息处理信号。
(2)接下来将会是给用户代理添加注释的一段代码:

// 根据BIP-0014清理注释,格式化用户代理并检查总大小
    std::vector uacomments;
    for (const std::string& cmt : gArgs.GetArgs("-uacomment")) {
        if (cmt != SanitizeString(cmt, SAFE_CHARS_UA_COMMENT))
            return InitError(strprintf(_("User Agent comment (%s) contains unsafe characters."), cmt));
        uacomments.push_back(cmt);
    }
    strSubVersion = FormatSubVersion(CLIENT_NAME, CLIENT_VERSION, uacomments);
    if (strSubVersion.size() > MAX_SUBVERSION_LENGTH) {
        return InitError(strprintf(_("Total length of network version string (%i) exceeds maximum length (%i). Reduce the number or size of uacomments."),
            strSubVersion.size(), MAX_SUBVERSION_LENGTH));
    }

其中的for循环的判断体中有个命令参数-uacomment,在帮助文件中对该命令的注释为:将注释附加到用户代理字符串中。
这段代码的实现逻辑为:首先将用户对代理的注释信息保存到uacomments中,将CLIENT_NAMECLIENT_VERSIONuacomments按照/CLIENT_NAME:CLIENT_VERSION(comments1;comments2;...)/的格式连接起来,最后判断格式化后的字符串是否超过了最大长度限制256,如果超过了报错;没超过继续下面的内容。
(3)接下来是关于设定网络范围的代码:

    if (gArgs.IsArgSet("-onlynet")) {
        std::set nets;
        for (const std::string& snet : gArgs.GetArgs("-onlynet")) {
            enum Network net = ParseNetwork(snet);
            if (net == NET_UNROUTABLE)
                return InitError(strprintf(_("Unknown network specified in -onlynet: '%s'"), snet));
            nets.insert(net);
        }
        for (int n = 0; n < NET_MAX; n++) {
            enum Network net = (enum Network)n;
            if (!nets.count(net))
                SetLimited(net);
        }
    }

首先if语句判断是否有-onlynet参数,该参数在帮助文件中的解释为:只连接特定网络中的节点,取值有ipv4ipv6onion三种。
(4)然后是关于代理设置的一段代码:

// 在解析任何网络相关参数之前检查主机查找。
    fNameLookup = gArgs.GetBoolArg("-dns", DEFAULT_NAME_LOOKUP);

    bool proxyRandomize = gArgs.GetBoolArg("-proxyrandomize", DEFAULT_PROXYRANDOMIZE);
    // -proxy为所有出站网络流量设置一个代理。
    // -noproxy (or -proxy=0) 以及空字符串可以用来不设置代理,这是默认值。
    std::string proxyArg = gArgs.GetArg("-proxy", "");
    SetLimited(NET_TOR);
    if (proxyArg != "" && proxyArg != "0") {
        CService proxyAddr;
        if (!Lookup(proxyArg.c_str(), proxyAddr, 9050, fNameLookup)) {
            return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxyArg));
        }

        proxyType addrProxy = proxyType(proxyAddr, proxyRandomize);
        if (!addrProxy.IsValid())
            return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxyArg));

        SetProxy(NET_IPV4, addrProxy);
        SetProxy(NET_IPV6, addrProxy);
        SetProxy(NET_TOR, addrProxy);
        SetNameProxy(addrProxy);
        SetLimited(NET_TOR, false); //默认情况下,-proxy将洋葱设置为可访问,除非稍后使用-noonion
    }

这里面涉及到三个参数:
-dns:允许进行dns解析,默认为1;
-proxyrandomize:为每个代理连接都随机颁发一个证书,默认为1;
-proxy:为网络所有的通信设置一个代理,默认为空。
在这部分,首先检查上面的三个参数,然后会通过SetLimited(NET_TOR);来禁用洋葱路由。然后检查,如果-proxy不为空且值不为0,那么根据代理域名进行dns查询,查到相应的ip并检查代理的合法性之后,再为IPV4IPV6以及TOR设置代理。最后禁用TOR
(5)接下来是一段关于设置洋葱路由的代码:

    //-onion可以用来设置.onion的代理,或者覆盖.onion地址的普通代理
    // -noonion (或者-onion=0)是完全禁止连接到.onion
    // 一个空字符串用于不覆盖洋葱代理(在这种情况下,它默认-proxy为设置上面,或不设置)。
    std::string onionArg = gArgs.GetArg("-onion", "");
    if (onionArg != "") {
        if (onionArg == "0") { // 当-noonion/-onion=0的情况
            SetLimited(NET_TOR); // 禁用洋葱路由
        } else {
            CService onionProxy;
            if (!Lookup(onionArg.c_str(), onionProxy, 9050, fNameLookup)) {
                return InitError(strprintf(_("Invalid -onion address or hostname: '%s'"), onionArg));
            }
            proxyType addrOnion = proxyType(onionProxy, proxyRandomize);
            if (!addrOnion.IsValid())
                return InitError(strprintf(_("Invalid -onion address or hostname: '%s'"), onionArg));
            SetProxy(NET_TOR, addrOnion);
            SetLimited(NET_TOR, false);
        }
    }

这段代码主要是:如果-onion 参数不为空,且值不为0,则会解析该代理域名,然后启动洋葱路由。
(6)然后会看到一段关于三个参数值赋值给内部变量语句:

    // 有关这些的更多信息,请参阅第二步:参数交互
    fListen = gArgs.GetBoolArg("-listen", DEFAULT_LISTEN);
    fDiscover = gArgs.GetBoolArg("-discover", true);
    fRelayTxes = !gArgs.GetBoolArg("-blocksonly", DEFAULT_BLOCKSONLY);

这里面有三个参数:
-listen:这个参数在帮助文件中的解释为:接受来自外部的连接(如果没有设置-proxy-connect,默认:1)。该参数在前面第五章提到过,当时使用这个参数的目的是:当-listen参数为0时-bind-whitebind不能被设置。现在这个参数在此处使用主要是:把该参数的值赋值给fListen变量。
-discover:这个参数在帮助文件中的解释为:发现自己的IP地址(侦听打开(-listen = true)并且没有设置-externalip-proxy时默认为1)。
-blocksonly:这个参数在帮助文件中的解释为:是否只以区块模式运行?默认为false。这个参数在前面第五章也提到过,当时使用这个参数的目的是:在钱包参数交互中,这个参数和-walletbroadcast参数是不能同时设置为true的。此处用此参数主要是:把该参数的相反值赋值给fRelayTxes变量。
可以知道这三个赋值语句主要是把一些和网络连接有关的外部参数赋值给内部变量,方便后面真正的网络连接使用的。
(7)下面是一个for循环语句,这个语句是关于把输入ip指定为公有的ip地址的:

    for (const std::string& strAddr : gArgs.GetArgs("-externalip")) {
        CService addrLocal;
        if (Lookup(strAddr.c_str(), addrLocal, GetListenPort(), fNameLookup) && addrLocal.IsValid())
            AddLocal(addrLocal, LOCAL_MANUAL);
        else
            return InitError(ResolveErrMsg("externalip", strAddr));
    }

①该段代码中涉及到一个参数:-externalip,这个参数在帮助文件中的解释为:指定您自己的公共地址,参数后面跟着的是IP地址。
②此处会有个难懂的for循环语句:

for (const std::string& strAddr : gArgs.GetArgs("-externalip"))

这种for循环的用法是c++ 11的新特性,冒号后面的是前面的容器;容器可以是数组,向量等。在这里容器是-externalip传来的字符串数组,假设该数组为externalip = {"A","B"},那么该for语句可以翻译为:

for(i = 0; i < 2; i++)
{
  strAddr = externalip[i];
  ...
}
//这里for循环得到的结果就是A和B,也就是对externalip数组进行遍历。

那么这段for语句主要功能是:得到-externalip参数的IP地址的值(可以是域名,也可以是将字符串ip转换成的CService),并查询其对应的ip,然后通过AddLocal()函数将指定的ip添加到mapLocalHost中,由mapLocalHost维护所有的本地ip。
(8)接下来会有一段条件编译,这是关于是否启用ZMQ的:

#if ENABLE_ZMQ
    pzmqNotificationInterface = CZMQNotificationInterface::Create();

    if (pzmqNotificationInterface) {
        RegisterValidationInterface(pzmqNotificationInterface);
    }
#endif

在此条件编译中判断的是ENABLE_ZMQ宏定义,这个宏定义来表示是否启用ZMQ。
ZMQ封装了网络通信、消息队列、线程调度等功能,向上层提供简洁的API,应用程序通过加载库文件,调用API函数来实现高性能网络通信。ZMQ的详细讲解可参考:

https://www.cnblogs.com/rainbowzc/p/3357594.html

如果有该宏定义,就会用CZMQNotificationInterface::Create()函数开启ZMQ,并用RegisterValidationInterface()函数注册区块处理的信号。
(9)最后会有一段关于设置最大上传速度的代码:

    uint64_t nMaxOutboundLimit = 0; //除非设置了-maxuploadtarget,要不是就无限制。
    uint64_t nMaxOutboundTimeframe = MAX_UPLOAD_TIMEFRAME;

    if (gArgs.IsArgSet("-maxuploadtarget")) {
        nMaxOutboundLimit = gArgs.GetArg("-maxuploadtarget", DEFAULT_MAX_UPLOAD_TARGET)*1024*1024;
    }

这里面有个参数:-maxuploadtarget,该参数在帮助文件中的解释为:保持上传流量在给定的值之下,值为0表示无限制,默认为0。可以知道这个参数是用来设置最大的上传速度的,默认值时该上传速度是无限制的。

********************************************
第六步总结:
这一步主要是对网络的初始化,是进行一些网络参数的设置,而不是真正的网络连接。
在这一步中包括了给用户代理添加注释、设定网络范围、代理设置、设置洋葱路由、把一些必要的外部参数赋值给内部变量来为以后的网络服务准备、把输入ip指定为公有的ip地址、是否启用ZMQ、设置最大上传速度等操作。
********************************************


5. 第七步:加载区块链数据

这一步包含的代码紧跟着第六步之后,在init.cpp的AppInitMain()函数中的1395-1593行中。根据这个注释可以知道这部分的代码和比特币区块链的区块数据的检测、加载、设置等有关。
(1)第七步的刚开始是关于计算缓存大小的,涉及到1395-1413行的代码:

    fReindex = gArgs.GetBoolArg("-reindex", false);
    bool fReindexChainState = gArgs.GetBoolArg("-reindex-chainstate", false);

    // 缓存大小计算
    int64_t nTotalCache = (gArgs.GetArg("-dbcache", nDefaultDbCache) << 20);
    nTotalCache = std::max(nTotalCache, nMinDbCache << 20); // 总缓存不能小于nMinDbCache
    nTotalCache = std::min(nTotalCache, nMaxDbCache << 20); // 总缓存不能大于nMaxDbcache
    int64_t nBlockTreeDBCache = nTotalCache / 8;
    nBlockTreeDBCache = std::min(nBlockTreeDBCache, (gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX) ? nMaxBlockDBAndTxIndexCache : nMaxBlockDBCache) << 20);
    nTotalCache -= nBlockTreeDBCache;
    int64_t nCoinDBCache = std::min(nTotalCache / 2, (nTotalCache / 4) + (1 << 23)); // 剩余的25%-50%用于磁盘缓存
    nCoinDBCache = std::min(nCoinDBCache, nMaxCoinsDBCache << 20); // 总计比特币数据库缓存
    nTotalCache -= nCoinDBCache;
    nCoinCacheUsage = nTotalCache; //其余的则进入内存缓存
    int64_t nMempoolSizeMax = gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000;
    LogPrintf("Cache configuration:\n");
    LogPrintf("* Using %.1fMiB for block index database\n", nBlockTreeDBCache * (1.0 / 1024 / 1024));
    LogPrintf("* Using %.1fMiB for chain state database\n", nCoinDBCache * (1.0 / 1024 / 1024));
    LogPrintf("* Using %.1fMiB for in-memory UTXO set (plus up to %.1fMiB of unused mempool space)\n", nCoinCacheUsage * (1.0 / 1024 / 1024), nMempoolSizeMax * (1.0 / 1024 / 1024));

由这部分的注释也可以知道,这段代码与缓存大小的计算有关。由注释内容还知道一些具体信息:

① 4MB≤总缓存≤16384MB。
② 块索引数据库缓存大小与-txindex参数和-dbcache参数有关。
③ 链状态数据库大小与-dbcache参数有关。
④ 交易内存池缓存大小设置与-dbcache参数和-maxmempool参数有关。

那么下面分别介绍一下这部分的参数:
-reindex:该参数在帮助文件中的解释为:从磁盘上的blk * .dat文件重建链状态和块索引。此处该参数的作用是:如果该参数没有设置成truefReindex 变量值为false
-reindex-chainstate:该参数在帮助文件中的解释为:从当前索引块重建链状态。处该参数的作用是:如果该参数没有设置成truefReindexChainState 变量值为false
-dbcache:该参数在帮助文件中的解释为:以兆字节为单位设置数据库缓存大小。默认大小为450.
-txindex:该参数在帮助文件中的解释为:维护完整的交易索引,主要是被getrawtransaction这个rpc调用来使用,默认不启用。
-maxmempool:该参数在帮助文件中的解释为:设置交易内存池的最大大小,单位为MB,默认值为300。
总结这部分的实现逻辑为:
首先从命令行中获取两个参数,-reindex-reindex-chainstate,这两个重索引默认都是不启用。接下来开始计算缓存的大小,首先是总的缓存大小用nTotalCache表示,通过-dbcache参数设置,然后这个值要取在nMinDbCachenMaxDbCache之间。接下来计算nBlockTreeDBCachenCoinDBCache以及nCoinCacheUsage,并且nTotalCache = nBlockTreeDBCache + nCoinDBCache + nCoinCacheUsage
(2)第七步接下来是一段关于加载区块索引的代码,涉及到1415-1450行的代码:

bool fLoaded = false;
while (!fLoaded && !fRequestShutdown) {
    bool fReset = fReindex;
    std::string strLoadError;

    uiInterface.InitMessage(_("Loading block index..."));

    nStart = GetTimeMillis();
    do {
       try {
            UnloadBlockIndex();
            delete pcoinsTip;
            delete pcoinsdbview;
            delete pcoinscatcher;
            delete pblocktree;

            pblocktree = new CBlockTreeDB(nBlockTreeDBCache, false, fReset);

            if (fReset) {
                pblocktree->WriteReindexing(true);
       //如果我们在修剪模式下重新索引,那么擦除不可用的块文件和所有的撤销数据文件
            if (fPruneMode)
                  CleanupBlockRevFiles();
              }

            if (fRequestShutdown) break;

       //LoadBlockIndex首先将从数据库中加载fTxIndex变量,如果是在进行重索引那么就从命令行读取fTxIndex的值。
       //另外如果我们之前删除过区块文件,那么这里还会加载fHavePruned变量。
       // 同时还会根据磁盘上的标记来设置fReindex变量,
       // 并且从此往后fReindex和fReset就表示不同的含义。
          if (!LoadBlockIndex(chainparams)) {
                 strLoadError = _("Error loading block database");
                  break;
              }

①首先设置了一个标记变量fLoaded表示索引加载是否成功,如果执行完循环体发现此变量还是false并且没有请求关闭程序的话,那么就再执行一遍。
②在循环体中首先出现了一个UnloadBlockIndex()函数。由于此循环体可能不止执行一遍,所以先调用UnloadBlockIndex()来清除上次循环可能设置的一些变量,这个函数的实现在validation.cpp的第3914行。
③然后在1431-1440行是修剪模式重索引时擦除不可用的块文件和所有的撤销数据文件:创建一个CBlockTreeDB类,这个类是用来向/blocks/index/*下面的文件进行读写操作。然后判断fReset是否为true,这个变量也就是-reindex参数用来设定是否重新创建所有的索引,如果为true,那么就调用CBlockTreeDB中的WriteReindexing()函数向数据库中写入数据。接下来出现的fPruneMode变量在五、初始化和启动模块(4)中也提到过,它是由-prune区块裁剪参数决定的一个变量;它是用来修剪已确认的区块的。在这里主要是如果裁剪模式fPruneModetrue,则需要对区块重新索引,此时会调用CleanupBlockRevFiles()函数擦除不可用的块文件和所有的撤销数据文件。
CleanupBlockRevFiles()函数在init.cpp的596行实现。对它的注释为:如果我们将-reindex-prune一起用,那么就将重索引时不考虑的一些区块文件直接删除。因为重索引是从0号区块一直连续的读取,直到某一个区块信息缺失就停止读取,缺失的区块之后所有的区块都会被直接删除。同时还需要删除rev文件,因为这些文件在重索引时会重新生成。根据注释的内容来看,这个函数要做的就是删除某个缺失的区块之后所有的区块数据,以及rev开头的文件。接下来先将所有的文件和对应的路径保存到一个map中,然后用一个变量nContigCounter从0开始计数,直到遇到第一个不一致的文件名,就从这个开始删除。
④然后是关于LoadBlockIndex() 函数的一个判断语句,其中LoadBlockIndex() 函数在validation.cpp中的3941行实现的,这个函数的实现逻辑是:函数的输入参数chainparams是根据三个不同网络之一的对应的不同的写好了的参数;然后检查fReindex变量,如果设置了这个变量,那么之后会进行重新索引,这里也就没有必要先加载索引了;如果没有设置fReindex,那么这里就是首次加载也是唯一的加载索引的地方。所谓加载索引,就是将/blocks/index/*中的文件加载到内存,实现时就是通过LoadBlockIndexDB()函数并将结果保存在变量mapBlockIndex中,如果加载成功,那么mapBlockIndex就不为空,needs_init也就为false
(3)第七步接下来是一段关于区块合法性检测的代码,涉及到1454-1477行的代码:

// 如果加载的链中有个错误的创世块,那么立即修复它
// (我们可能使用测试网络的数据目录,或者周围的其他的路径).
if (!mapBlockIndex.empty() && mapBlockIndex.count(chainparams.GetConsensus().hashGenesisBlock) == 0)
     return InitError(_("Incorrect or no genesis block found. Wrong datadir for network?"));

// 检查-txindex状态
if (fTxIndex != gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX)) {
      strLoadError = _("You need to rebuild the database using -reindex to change -txindex");
           break;
      }

// 检查-prune的状态。因为用户可能会手动删除一些文件,然后
// 现在又想在未剪裁模式下运行
if (fHavePruned && !fPruneMode) {
     strLoadError = _("You need to rebuild the database using -reindex to go back to unpruned mode.  This will redownload the entire blockchain");
           break;
      }

//此时,blocktree上的参数和磁盘上的一致。
//如果我们不是mid-reindex(基于在磁盘和参数),那么在磁盘上添加一个创世区块(否则,我们使用已经在磁盘上的参数)。
// 在重新索引完成后,这将在ThreadImport中再次调用。
if (!fReindex && !LoadGenesisBlock(chainparams)) {
       strLoadError = _("Error initializing block database");
            break;
      }

①第一个if语句的作用是:判断如果mapBlockIndex中没有加载创世区块,或者存在错误的创世区块,则会提示:不正确或者没有创世区块。是网络的数据目录错误了吗?
②第二个if语句的作用是: 检查-txindex参数的值是否和fTxIndex 变量相等,如果不等,则提示:“您需要使用-reindex重建数据库以更改-txindex”,并跳出循环。
③第三个if语句的作用是:如果用户已经手动修剪了区块文件,然后又想开启不修剪区块模式,则会提示:“您需要使用-reindex重建数据库以回到未修剪模式。 这将重新下载整个区块链”,并跳出循环。
④第四个if语句中涉及到一个函数LoadGenesisBlock(),这个函数的声明在validation.h的263行:

/** 确保区块树中有一个创世区块,如果有可能的话,写入一个到磁盘中。*/
bool LoadGenesisBlock(const CChainParams& chainparams);

该函数的实现在validation.cpp中的3966-3995行:

bool LoadGenesisBlock(const CChainParams& chainparams)
{
    LOCK(cs_main);

    // 通过检查mapBlockIndex中的起源来检查我们是否已经初始化。
    // 需要注意的是,我们不能在这里使用chainActive,
    // 因为它是基于比特币数据库,而不是在此处加载的唯一的区块索引数据库设置的。
    if (mapBlockIndex.count(chainparams.GenesisBlock().GetHash()))
        return true;

    try {
        CBlock &block = const_cast(chainparams.GenesisBlock());
        // 开始新的区块文件
        unsigned int nBlockSize = ::GetSerializeSize(block, SER_DISK, CLIENT_VERSION);
        CDiskBlockPos blockPos;
        CValidationState state;
        if (!FindBlockPos(state, blockPos, nBlockSize+8, 0, block.GetBlockTime()))
            return error("%s: FindBlockPos failed", __func__);
        if (!WriteBlockToDisk(block, blockPos, chainparams.MessageStart()))
            return error("%s: writing genesis block to disk failed", __func__);
        CBlockIndex *pindex = AddToBlockIndex(block);
        if (!ReceivedBlockTransactions(block, state, pindex, blockPos, chainparams.GetConsensus()))
            return error("%s: genesis block not accepted", __func__);
    } catch (const std::runtime_error& e) {
        return error("%s: failed to write genesis block: %s", __func__, e.what());
    }

    return true;
}

可以知道LoadGenesisBlock()函数是检测网络类型等后在唯一一个创世区块基础上写入新的区块文件。
在这第四个if语句中的作用是:当没有从磁盘上的blk * .dat文件重建链状态和块索引时,也没有在唯一一个创世区块基础上写入新的区块文件,那么会提示:“初始化块数据库时出错”,并跳出循环体。
(4)然后会有一段关于区块数据的操作的代码,涉及到1482-1546行的代码:

// 此时,我们要么重新索引,要么加载一个有用的区块树到mapBlockIndex中!
pcoinsdbview = new CCoinsViewDB(nCoinDBCache, false, fReset || fReindexChainState);
pcoinscatcher = new CCoinsViewErrorCatcher(pcoinsdbview);

// 如有必要,从旧数据库格式升级。
// 如果我们用-reindex或-reindex-chainstate清除了coinviewdb,升级是一个无操作。
if (!pcoinsdbview->Upgrade()) {
       strLoadError = _("Error upgrading chainstate database");
       break;
       }

// 如果我们用-reindex或-reindex-chainstate清除了coinviewdb,ReplayBlocks是一个无操作。
if (!ReplayBlocks(chainparams, pcoinsdbview)) {
      strLoadError = _("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.");
      break;
      }

// 磁盘上的coinsdb现在处于良好的状态,那么就创建缓存。
pcoinsTip = new CCoinsViewCache(pcoinscatcher);

bool is_coinsview_empty = fReset || fReindexChainState || pcoinsTip->GetBestBlock().IsNull();
if (!is_coinsview_empty) {
// LoadChainTip根据pcoinsTip的最佳模块设置chainActive。
  if (!LoadChainTip(chainparams)) {
     strLoadError = _("Error initializing block database");
     break;
          }
     assert(chainActive.Tip() != nullptr);
     }

if (!fReset) {
// 请注意,即使我们即将关注-reindex-chainstate,RewindBlockIndex也必须运行。
// 它根据chainActive断开区块,并根据缺少可见的数据将区块数据放到mapBlockIndex中。
uiInterface.InitMessage(_("Rewinding blocks..."));
if (!RewindBlockIndex(chainparams)) {
     strLoadError = _("Unable to rewind the database to a pre-fork state. You will need to redownload the blockchain");
       break;
            }
       }

if (!is_coinsview_empty) {
     uiInterface.InitMessage(_("Verifying blocks..."));
     if (fHavePruned && gArgs.GetArg("-checkblocks", DEFAULT_CHECKBLOCKS) > MIN_BLOCKS_TO_KEEP) {
      LogPrintf("Prune: pruned datadir may not have more than %d blocks; only checking available blocks",
      MIN_BLOCKS_TO_KEEP);
      }

      {
        LOCK(cs_main);
        CBlockIndex* tip = chainActive.Tip();
        RPCNotifyBlockChange(true, tip);
        if (tip && tip->nTime > GetAdjustedTime() + 2 * 60 * 60) {
              strLoadError = _("The block database contains a block which appears to be from the future. "
                    "This may be due to your computer's date and time being set incorrectly. "
                    "Only rebuild the block database if you are sure that your computer's date and time are correct");
             break;
            }
       }

 if (!CVerifyDB().VerifyDB(chainparams, pcoinsdbview, gArgs.GetArg("-checklevel", DEFAULT_CHECKLEVEL),
            gArgs.GetArg("-checkblocks", DEFAULT_CHECKBLOCKS))) {
      strLoadError = _("Corrupted block database detected");
      break;
   }
}

①开始会有两个变量:pcoinsdbviewpcoinscatcherpcoinsdbview 变量主要是初始化一个CoinsViewDB,它配备了从LevelDB中加载比特币的方法。pcoinscatcher变量是一个错误捕捉器,它是一个可以忽略的小程序。
②然后是判断调用的Upgrade()函数,这个函数的作用是:尝试从较旧的数据库格式进行更新。 返回是否发生错误的bool值。
③接着是判断调用的ReplayBlocks()函数,这个函数的作用是:没有完全应用于数据库的重放块。
④接着会再出现一个变量:pcoinsTip。这个变量是关于区块链缓存的,它是代表活动链状态的高速缓存,并由数据库视图支持。
⑤然后是赋值一个变量:is_coinsview_empty,然后判断该变量。这里主要是如果fResetfReindexChainStateGetBestBlock().IsNull()都为false时,会出现一个断言语句;如果不仅如此,LoadChainTip()函数返回的值也为false,那么会提示:初始化区块数据库错误。其中LoadChainTip()函数的作用是:根据数据库信息更新链末端。
⑥然后是倒退链状态的操作:
首先判断fReset变量,如果为false,则调用RewindBlockIndex()函数开始回退数据库到预分叉状态,如果回退失败,则报错,并跳出循环。RewindBlockIndex()函数主要功能是:当数据缺失的活动链中存在块时,倒退链状态并将其从块索引中移除。
⑦然后是验证区块的操作:
判断还是is_coinsview_empty变量,如果为false,则开始验证区块:包括对修剪后的数据块长度的验证、区块时间的验证和区块的健全性检查。
(5)接下来while语句的最后面会出现一个个判断语句:

if (!fLoaded && !fRequestShutdown) {
   // 首先建议重新索引
   if (!fReset) {
      bool fRet = uiInterface.ThreadSafeQuestion(
      strLoadError + ".\n\n" + _("Do you want to rebuild the block database now?"),
      strLoadError + ".\nPlease restart with -reindex or -reindex-chainstate to recover.",
      "", CClientUIInterface::MSG_ERROR | CClientUIInterface::BTN_ABORT);
   if (fRet) {
      fReindex = true;
      fRequestShutdown = false;
 } else {
      LogPrintf("Aborted block database rebuild. Exiting.\n");
      return false;
 }
} else {
    return InitError(strLoadError);
  }
}

这时如果fLoadedfRequestShutdown还是都为false,则首先建议重新索引:如果fResetfalse,则调用ThreadSafeQuestion()函数。如果该函数后返回的值为true,则把fReindex变量改为truefRequestShutdown变量改为false;否则打印日志内容:“中止块数据库重建。退出”。如果fResettrue,返回初始化错误。
////////////////////////////////////////////////
到此结束while循环>>>>>>>>
////////////////////////////////////////////////
(6)在上面的while循环中的判断语句是fLoadedfRequestShutdown都为false,那么下面的一段代码就是当他们不为false时执行的语句:

    // 由于LoadBlockIndex可能需要几分钟时间,因此用户可能会在上次操作期间请求终止GUI。
    // 如果是这样,退出。 
    //由于程序尚未完全启动,Shutdown()可能是误杀的。
    if (fRequestShutdown)
    {
        LogPrintf("Shutdown requested. Exiting.\n");
        return false;
    }
    if (fLoaded) {
        LogPrintf(" block index %15dms\n", GetTimeMillis() - nStart);
    }

主要还是简单的在日志文件中输出对应的日志信息。
(7)第七步最后是关于创建fee_estimates.dat文件的:

    fs::path est_path = GetDataDir() / FEE_ESTIMATES_FILENAME;
    CAutoFile est_filein(fsbridge::fopen(est_path, "rb"), SER_DISK, CLIENT_VERSION);
    // 允许失败,因为该文件在第一次启动时丢失。
    if (!est_filein.IsNull())
        ::feeEstimator.Read(est_filein);
    fFeeEstimatesInitialized = true; 

由该文件名可以知道,该文件主要是关于费用估算的,通过查找资料可以知道,该文件保存的是在程序关闭之前的估算费用和优先级的统计信息,这些信息会在启动程序时读入。

********************************************
第七步总结:
这一步主要是关于加载区块链数据的,该数据保存在$HOME/.bitcoin目录下。首先计算区块的缓存大小;然后设置加载区块索引;接着进行区块合法性检测;再是区块数据的一系列的操作,包括数据库格式的更新、重放块检测、倒退链状态、验证区块等操作;最后是创建fee_estimates.dat文件保存费用于估算和优先级的统计信息。其中也涉及到一些日志打印和故障信息。
********************************************


你可能感兴趣的:(八、初始化和启动模块(7))