比特币源码解析(14) - 可执行程序 - Bitcoind

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012183589/article/details/77982837

0x00 AppInitParameterInteraction

这个函数包括源码中的Step 2和Step 3两个,主要实现的功能是设置区块链运行时的一些参数。

0x01 Step 2 - parameter interactions

    // if using block pruning, then disallow txindex
    if (gArgs.GetArg("-prune", 0)) {
        if (gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX))
            return InitError(_("Prune mode is incompatible with -txindex."));
    }

-prune参数表示启用区块修剪(block pruning),根据bitcoin release note中的描述,

Block pruning allows Bitcoin Core to delete the raw block and undo data once it’s been validated and used to build the databases. At that point, the raw data is used only to relay blocks to other nodes, to handle reorganizations, to look up old transactions (if -txindex is enabled or via the RPC/REST interfaces), or for rescanning the wallet. The block index continues to hold the metadata about all blocks in the blockchain.

区块修剪允许bitcoin core删除raw block和undo data,一旦这些数据已经被验证和更新过数据库。这时候的raw data只能用来转发区块到其他节点、处理区块重组、查看过去的交易(如果启用了-txindex交易索引或者通过RPC/REST接口调用)以及重新扫描钱包。区块索引依然维护所有区块的元数据。

我们知道在比特币运行的本地环境中,有四种类型的数据(在Linux环境下查看~/.bitcoin/目录),

  • raw block,从网络中接收的原始区块信息,对应文件为blk***.dat
  • undo data,在进行chain reorganization时使用的数据,对应文件为rev***.dat。Chain reorganization是指某一个节点发现存在一条比节点当前本地维护的链更长的链,那么该节点就需要进行Chain reorganization,所以这个操作只是针对某一个节点而言的。
  • block index,区块索引,每一个区块都有一个唯一的索引,对应文件为~/.bitcoin/blocks/index下的.ldblevel db数据库文件。
  • UTXO,Unspent transaction output,表示所有未花费的交易,对应文件为~/.bitcoin/chainstate/中的.ldb文件。

而block pruing删除的就是raw block和undo data两种数据,通过-prune=N参数N来指定raw block + undo data数据的大小,单位为MBN的最小值为550,代表288个区块的大小,按照每个block 10 Min的速率,代表2天的时间。

因为block pruning需要删除一些区块的信息,而-txindex是对所有交易建立索引,所以这两者不兼容,如果同时设置了,那么则提示错误。

// -bind and -whitebind can't be set when not listening
size_t nUserBind = gArgs.GetArgs("-bind").size() + gArgs.GetArgs("-whitebind").size();
if (nUserBind != 0 && !gArgs.GetBoolArg("-listen", DEFAULT_LISTEN)) {
    return InitError("Cannot set -bind or -whitebind together with -listen=0");
}

接下来这段代码检测-listenbind之间的冲突问题,也就是如果设置了bind的地址而没有设置listen那么就会报错并退出程序。

    // Make sure enough file descriptors are available
    int nBind = std::max(nUserBind, size_t(1));
    nUserMaxConnections = gArgs.GetArg("-maxconnections", DEFAULT_MAX_PEER_CONNECTIONS);
    nMaxConnections = std::max(nUserMaxConnections, 0);

    // Trim requested connection counts, to fit into system limitations
    nMaxConnections = std::max(std::min(nMaxConnections, (int)(FD_SETSIZE - nBind - MIN_CORE_FILEDESCRIPTORS - MAX_ADDNODE_CONNECTIONS)), 0);
    nFD = RaiseFileDescriptorLimit(nMaxConnections + MIN_CORE_FILEDESCRIPTORS + MAX_ADDNODE_CONNECTIONS);
    if (nFD < MIN_CORE_FILEDESCRIPTORS)
        return InitError(_("Not enough file descriptors available."));
    nMaxConnections = std::min(nFD - MIN_CORE_FILEDESCRIPTORS - MAX_ADDNODE_CONNECTIONS, nMaxConnections);

    if (nMaxConnections < nUserMaxConnections)
        InitWarning(strprintf(_("Reducing -maxconnections from %d to %d, because of system limitations."), nUserMaxConnections, nMaxConnections));

这段代码中注释的意思是确保有足够的文件描述符,文件描述符又是什么呢?

转载: http://blog.csdn.net/cywosp/article/details/38965239

在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。POSIX标准要求每次打开文件时(含socket)必须使用当前进程中*最小可用的文件描述符号码,因此,在网络通信过程中稍不注意就有可能造成串话*。

内核为了不让某一个进程消耗掉所有的文件资源,其也会对单个进程最大打开文件数做默认值处理(称之为用户级限制),默认值一般是1024,使用ulimit -n命令可以查看。在Web服务器中,通过更改系统默认值文件描述符的最大值来优化服务器是最常见的方式之一。

从上述文章中我们知道文件描述符就是一个打开文件的索引,首先代码计算了用户设置的bind的地址数量nBind,然后或者命令行中的-maxconnections,这个参数的默认值为125。

// src/net.h line 75
/** The maximum number of peer connections to maintain. */
static const unsigned int DEFAULT_MAX_PEER_CONNECTIONS = 125;

然后开始计算最大连接数,公式中涉及到几个变量,分别位于如下位置,

// src/compat.h line 27
#define FD_SETSIZE 1024 // max number of fds in fd_set

// src/init.cpp line 89
#define MIN_CORE_FILEDESCRIPTORS 150

// src/net.h line 61
/** Maximum number of addnode outgoing nodes */
static const int MAX_ADDNODE_CONNECTIONS = 8;
  • FD_SETSIZ:如上述参考的文章中所说,代表系统对单个进程的用户级限制,值为1024。
  • MIN_CORE_FILEDESCRIPTORS:直译为最小核心文件描述符数量,但并不懂含义= =
  • MAX_ADDNODE_CONNECTIONS:最大addnode连接数,不懂含义= =

虽然还没有明白这个公式的原理,但是功能大概明白了,首先判断文件描述符的数量是否够用,如果不够用那么直接报错并退出程序;然后判断命令行设置的-maxconnections是否超过了系统支持的最大连接数,如果超过了,那么就提示强制设置为系统的最大连接数。

0x02 Step 3: parameter-to-internal-flags

    if (gArgs.IsArgSet("-debug")) {
        // Special-case: if -debug=0/-nodebug is set, turn off debugging messages
        const std::vector<std::string> categories = gArgs.GetArgs("-debug");

        if (find(categories.begin(), categories.end(), std::string("0")) == categories.end()) {
            for (const auto& cat : categories) {
                uint32_t flag = 0;
                if (!GetLogCategory(&flag, &cat)) {
                    InitWarning(strprintf(_("Unsupported logging category %s=%s."), "-debug", cat));
                    continue;
                }
                logCategories |= flag;
            }
        }
    }

    // Now remove the logging categories which were explicitly excluded
    for (const std::string& cat : gArgs.GetArgs("-debugexclude")) {
        uint32_t flag = 0;
        if (!GetLogCategory(&flag, &cat)) {
            InitWarning(strprintf(_("Unsupported logging category %s=%s."), "-debugexclude", cat));
            continue;
        }
        logCategories &= ~flag;
    }

首先的一段代码是判断应该对哪些目录写入调试日志,所有的目录包括以下类型(src/util.cpp line 220),

const CLogCategoryDesc LogCategories[] =
{
    {BCLog::NONE, "0"},
    {BCLog::NET, "net"},
    {BCLog::TOR, "tor"},
    {BCLog::MEMPOOL, "mempool"},
    {BCLog::HTTP, "http"},
    {BCLog::BENCH, "bench"},
    {BCLog::ZMQ, "zmq"},
    {BCLog::DB, "db"},
    {BCLog::RPC, "rpc"},
    {BCLog::ESTIMATEFEE, "estimatefee"},
    {BCLog::ADDRMAN, "addrman"},
    {BCLog::SELECTCOINS, "selectcoins"},
    {BCLog::REINDEX, "reindex"},
    {BCLog::CMPCTBLOCK, "cmpctblock"},
    {BCLog::RAND, "rand"},
    {BCLog::PRUNE, "prune"},
    {BCLog::PROXY, "proxy"},
    {BCLog::MEMPOOLREJ, "mempoolrej"},
    {BCLog::LIBEVENT, "libevent"},
    {BCLog::COINDB, "coindb"},
    {BCLog::QT, "qt"},
    {BCLog::LEVELDB, "leveldb"},
    {BCLog::ALL, "1"},
    {BCLog::ALL, "all"},
};

每个目录都对应了一个编号,代码中的logCategories变量就是记录所有的日志目录的集合,类型是uint32_t,而目录对应的编号每一个都对应32位中的一位,所以每做一次|操作,就表示将当前的目录编号加进集合。而后面的-debugexclude参数就是从集合中删除掉不想记录日志的目录,这时使用的是&= ~flag操作,~表示每一位取反,与上反码就表示将当前的目录编号从集合中去掉。

再接下来一段代码中的几个if语句就是检查一下不支持的参数,或者有变更的参数命令,比较容易理解。

    // Checkmempool and checkblockindex default to true in regtest mode
    int ratio = std::min<int>(std::max<int>(gArgs.GetArg("-checkmempool", chainparams.DefaultConsistencyChecks() ? 1 : 0), 0), 1000000);
    if (ratio != 0) {
        mempool.setSanityCheck(1.0 / ratio);
    }
    fCheckBlockIndex = gArgs.GetBoolArg("-checkblockindex", chainparams.DefaultConsistencyChecks());
    fCheckpointsEnabled = gArgs.GetBoolArg("-checkpoints", DEFAULT_CHECKPOINTS_ENABLED);

根据帮助信息中的解释,

-checkmempool:表示每隔多少个交易进行一次sanity check。

-checkblockindex:每隔一段时间检查mapBlockIndexsetBlockIndexCandidateschainActivemapBlockUnlinked变量的一致性。

-checkpoints:该变量默认为1,表示不验证当前已经存在的链;如果为0,表示要检查一些校验点的区块信息是否正确,所有校验点的信息也都保存在chainparams中的checkpointdata中。

代码首先判断chainparams中的DefaultConsistencyChecks是否为true,如果这个变量为false,那么ratio=0,也就是不进行sanity check。Sanity check之前在http://blog.csdn.net/pure_lady/article/details/77776716#t2中CTxMemPool类中介绍过,表示检查mempool中所有交易的一致性(没有双花,所有的输入都是合法的)。对于chainparams这个变量的也在http://blog.csdn.net/pure_lady/article/details/77895680中的SelectParams函数中介绍过,首先根据设置的网络MAINTESTNET或者REGTEST选择相应的参数,三个网络根据src/chainparams.cpp中给参数定义不同的值,对于DefaultConsistencyChecks这个参数,MAINTESTNET都为false,而REGTEST中此变量为true

    hashAssumeValid = uint256S(gArgs.GetArg("-assumevalid", chainparams.GetConsensus().defaultAssumeValid.GetHex()));
    if (!hashAssumeValid.IsNull())
        LogPrintf("Assuming ancestors of block %s have valid signatures.\n", hashAssumeValid.GetHex());
    else
        LogPrintf("Validating signatures for all blocks.\n");

-assumevalid=blockid:表示在blockid之前的所有区块都假设正确的,也就是不用再去验证。如果没有设置,那么就要验证之前所有区块的签名信息。

    // mempool limits
    int64_t nMempoolSizeMax = gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000;
    int64_t nMempoolSizeMin = gArgs.GetArg("-limitdescendantsize", DEFAULT_DESCENDANT_SIZE_LIMIT) * 1000 * 40;
    if (nMempoolSizeMax < 0 || nMempoolSizeMax < nMempoolSizeMin)
        return InitError(strprintf(_("-maxmempool must be at least %d MB"), std::ceil(nMempoolSizeMin / 1000000.0)));
    // incremental relay fee sets the minimum feerate increase 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.
    if (gArgs.IsArgSet("-incrementalrelayfee"))
    {
        CAmount n = 0;
        if (!ParseMoney(gArgs.GetArg("-incrementalrelayfee", ""), n))
            return InitError(AmountErrMsg("incrementalrelayfee", gArgs.GetArg("-incrementalrelayfee", "")));
        incrementalRelayFee = CFeeRate(n);
    }

接下来这段代码首先计算mempool的最大值,后面乘以1000000是将单位从MB转换成B,然后计算最小的限制,其中,-limitdescendantsize的含义如下,后面乘以1000将单位从KB转化成B,再乘以40表示最小可以容纳40个这个的交易族。

-limitdescendantsize:如果某个交易在mempool中所有的祖先size之和超过该限制值,那么则拒绝接受该交易。单位为KB。

在每个节点内部都可以设置以下几种费用来避免接收过多的交易,

  • minrelaytxfee:最小的转发费用,如果交易费小于这个值,节点就直接忽略该交易。默认值为0.00001 BTC/KB。
  • dustrelayfee:用来判定一笔交易时候是否是dust tx,如果是的话则忽略该交易。默认值为0.00001BTC/KB。
  • incrementalrelayfee:用来改变mempool最低交易费用的变量,当mempool中的交易数量超过阈值时,交易费用阈值便会增加,增加的程度就由incrementalrelayfee决定。默认值为0.00001BTC/KB。

节点交易处理流程

所以一个full-node对交易的处理流程如下:(1)首先判断交易的费用之和是否大于minrelayfee;(2)然后判断是否是dustrelayfee,如果是的话就转发给其他节点,自己忽略该交易;(3)最后判断费用是否满足当前的费用条件,当前的费用会根据交易数量动态的变化,当交易数量过多时,增加,交易减少时,也减小。

源码中这地方就是判断命令行中是否设置了-incrementalrelayfee,如果设置了就设置变量incrementalRelayFee

设置脚本验证线程数量

    // -par=0 means autodetect, but nScriptCheckThreads==0 means no concurrency
    nScriptCheckThreads = gArgs.GetArg("-par", DEFAULT_SCRIPTCHECK_THREADS);
    if (nScriptCheckThreads <= 0)
        nScriptCheckThreads += GetNumCores();
    if (nScriptCheckThreads <= 1)
        nScriptCheckThreads = 0;
    else if (nScriptCheckThreads > MAX_SCRIPTCHECK_THREADS)
        nScriptCheckThreads = MAX_SCRIPTCHECK_THREADS;

-par=N :设置脚本验证线程数量,取值范围为[-2,16],小于0表示令N个保持空闲,0表示自动检测,1表示不允许并行,大于等于2表示同一时刻最大线程数量,默认值为0。

其中MAX_SCRIPTCHECK_THREADS定义的值为16。

设置prune保留的文件大小

    // block pruning; get the amount of disk space (in MiB) to allot for block & undo files
    int64_t nPruneArg = gArgs.GetArg("-prune", 0);
    if (nPruneArg < 0) {
        return InitError(_("Prune cannot be configured with a negative value."));
    }
    nPruneTarget = (uint64_t) nPruneArg * 1024 * 1024;
    if (nPruneArg == 1) {  // manual pruning: -prune=1
        LogPrintf("Block pruning enabled.  Use RPC call pruneblockchain(height) to manually prune block and undo files.\n");
        nPruneTarget = std::numeric_limits<uint64_t>::max();
        fPruneMode = true;
    } else if (nPruneTarget) {
        if (nPruneTarget < MIN_DISK_SPACE_FOR_BLOCK_FILES) {
            return InitError(strprintf(_("Prune configured below the minimum of %d MiB.  Please use a higher number."), MIN_DISK_SPACE_FOR_BLOCK_FILES / 1024 / 1024));
        }
        LogPrintf("Prune configured to target %uMiB on disk for block and undo files.\n", nPruneTarget / 1024 / 1024);
        fPruneMode = true;
    }

-prune参数在Step 2中已经介绍过,用来删除已经验证过的区块,取值有以下几种:

  • 0:默认值,表示禁止该功能。
  • 1:表示允许手动使用RPC命令删除旧的区块。
  • 大于等于550:表示允许保存的raw block + undo data文件总大小,其中550MB = MIN_DISK_SPACE_FOR_BLOCK_FILES。

注册RPC命令

    RegisterAllCoreRPCCommands(tableRPC);
#ifdef ENABLE_WALLET
    RegisterWalletRPCCommands(tableRPC);
#endif

所谓注册RPC命令,其实解释将信号和处理函数connect起来,方式就是使用boost的signal/slot模式,但是这里做的只是将一些指令添加到一个类型为CRPCTable的tableRPC变量中,这个tableRPC维护了所有的命令和对应的处理函数,收到相应的RPC命令是再调用CRPCTable类中execute函数执行请求。

再看看All core RPC commands包括了哪些命令,

static inline void RegisterAllCoreRPCCommands(CRPCTable &t)
{
    RegisterBlockchainRPCCommands(t);
    RegisterNetRPCCommands(t);
    RegisterMiscRPCCommands(t);
    RegisterMiningRPCCommands(t);
    RegisterRawTransactionRPCCommands(t);
}

可见这句代码注册了几乎所有核心的命令。

猜你喜欢

转载自blog.csdn.net/u012183589/article/details/77982837