七、初始化和启动模块(6)

(十四)继续看bitcoind.cpp中的第168行

  fRet = AppInitMain(threadGroup, scheduler);

该行代码是一个赋值语句,主要是调用了AppInitMain()函数。AppInitMain()函数的的声明在init.h中的57行:

/**
 * 比特币核心的主要初始化。
 *注意: 这只能在守护进程之后完成。 如果这个函数失败,调用Shutdown()。
 *前提: 应该解析参数并读取配置文件,应该调用AppInitLockDataDirectory。
 */
bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler);

该函数的声明知道这个函数会在调用AppInitLockDataDirectory()函数之后,事实上确实是在它后面执行的,而且是比特币核心的主要初始化,而在实现文件中确实是有大量的初始化代码的,该函数实现在init.cpp的1223-1735行,500多行的代码,包括初始化bitcoin的第4a步一直到第12步共九步的初始化。
下面会对这些大量的代码详细说明,但是对之前提到的函数只是简单的解释,而且整体上也可能没有前面那样逐行详细解释,更多的是对功能块的功能讲解。


1. 选择网路

这个部分在AppInitMain()函数的最开始执行:

const CChainParams& chainparams = Params();

这个部分主要是关于运行网络的选择(运行网络前面的四、初始化和启动模块(3)已经说过了,有私有网络、测试网络和主网络,默认为主网络),如果我们在启动比特币核心程序时,没有设置相应网络参数,则默认运行主链,否则将根据输入的参数启动相应网络。

2. 第4a步:应用程序初始化
2-1 pid文件

这个是一段条件编译代码:

#ifndef WIN32
    CreatePidFile(GetPidFile(), getpid());
#endif

这个主要是在非Windows系统中编译的部分,通过CreatePidFile()函数创建进程编号记录文件,该文件名为bitcoind.pid。
通过在ubuntu系统中对编译好的bitcoin客户端在终端执行bitcoind命令的尝试后,发现在bitcoin的数据目录文件夹$HOME/.bitcoin中会立刻出现三个新的文件和文件夹如图所示:

新出现的部分
其中就会出现bitcoind.pid文件,而这个文件在未执行这个命令或者终止执行该命令时是没有的。
bitcoind.pid文件中记录了比特币核心的进程号,如图所示:
bitcoind.pid记录进程号
示例图中说明该比特币核心的进程号为2570。
该文件的目的其实和前面的目录锁文件.lock相似,它是为了防止出现多个比特币核心程序,始终在文件中记录第一个启动的程序的pid,防止后面新启动的比特币程序再使用它。
pid文件的详细作用可以参考下面的资料:
http://blog.csdn.net/yinqingwang/article/details/52841744

2-2调试日志输出

(1)在AppInitMain()函数第4a步中也牵涉到一些调试日志输出到日志文件debug.log中情况,其中最开始会有个-shrinkdebugfile参数的处理:

if (gArgs.GetBoolArg("-shrinkdebugfile", logCategories == BCLog::NONE)) {
        //首先这样做,不仅因为它能将一堆debug.log加载到内存中去,而且因为这也需要在任何其他debug.log打印之前发生。
        ShrinkDebugFile();
    }

其中-shrinkdebugfile参数为压缩日志文件。该参数在帮助文件中的解释为:当客户端启动时,对debug.log文件进行压缩处理。默认为1,即在不进行调试时会进行压缩操作。此处的含义为,当启动不使用(一般都不会使用)该参数时会默认执行ShrinkDebugFile()函数,该函数就是具体的调试日志压缩处理过程函数。
(2)在对debug.log文件进行压缩处理后,会有两行代码:

    if (fPrintToDebugLog)
        OpenDebugLog();

因为fPrintToDebugLog变量(定义在util.cpp第95行)默认为true,则程序将用OpenDebugLog()函数正式打开debug.log文件,实现程序运行过程中的记录,方便调试用。
其中OpenDebugLog()函数(定义在util.cpp的192行)完成的是debug.log文件的打开,打开方式是增加内容“a”模式,即启动后程序将在上一次日志信息的基础上添加本次运行的日志内容。
OpenDebugLog()函数中也出现了vMsgsBeforeOpenLog包含内容的打印输出,vMsgsBeforeOpenLog为日志文件未打开之前,预先存储的一些打印输出信息(预先存储信息参考四、初始化和启动模块(3)的第(八)部分)。
至此完成了日志文件的打开操作,并完成了预先存储日志信息的输出。
(3)紧接着是时间戳信息在日志文件中的输出处理代码:

    if (!fLogTimestamps)
        LogPrintf("Startup time: %s\n", DateTimeStrFormat("%Y-%m-%d %H:%M:%S", GetTime()));

fLogTimestamps的定义在util.cpp的第97行,默认值为DEFAULT_LOGTIMESTAMPS,而DEFAULT_LOGTIMESTAMPS在util.h中的第37行,为true,意味着一般if后的语句不执行,即此处不单独打印“Startup time:(+时间)”这样的时间信息。fLogTimestamps主要是默认在让日志的每一行都带有时间戳信息,而像如下图所示的:

时间戳+输入内容格式
这种时间戳加输入的日志内容这样格式的实现代码,是在位于util.cpp中的300-326行的LogTimestampStr()函数中。
(4)接下来就是在日志文件中输出4行的日志内容:

    LogPrintf("Default data directory %s\n", GetDefaultDataDir().string());
    LogPrintf("Using data directory %s\n", GetDataDir().string());
    LogPrintf("Using config file %s\n", GetConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME)).string());
    LogPrintf("Using at most %i automatic connections (%i file descriptors available)\n", nMaxConnections, nFD);

输出的结果如下图所示:

其中调用了三个函数得到路径和两个变量得到最大连接数和文件描述符个数。

2-3 签名缓存信息

接着会遇到一个签名缓存函数:

 InitSignatureCache();

这个函数声明在script/sigcache.h中54行,实现在script/sigcache.cpp中73-81行:

// 要在AppInitMain()函数的基本测试设置中调用一次以初始化签名缓存。
void InitSignatureCache()
{
    // nMaxCacheSize是无符号类型变量。如果-maxsigcachesize参数设置成0,
    // setup_bytes()函数将会创建最小可能的缓存 (2个元素)。
    size_t nMaxCacheSize = std::min(std::max((int64_t)0, gArgs.GetArg("-maxsigcachesize", DEFAULT_MAX_SIG_CACHE_SIZE) / 2), MAX_MAX_SIG_CACHE_SIZE) * ((size_t) 1 << 20);
    size_t nElems = signatureCache.setup_bytes(nMaxCacheSize);
    LogPrintf("Using %zu MiB out of %zu/2 requested for signature cache, able to store %zu elements\n",
            (nElems*sizeof(uint256)) >>20, (nMaxCacheSize*2)>>20, nElems);
}

此处有个-maxsigcachesize参数,此参数在帮助文件中的解释为:将签名缓存和脚本执行缓存大小的总和限制为n MB,默认为32MB。此处的设置为:如果设置了-maxsigcachesize参数,而且大于32MB,那么nMaxCacheSize变量值将是-maxsigcachesize参数设定值的一半或者16384这两个数中的最小者,然后把该值乘以1048576(2^20)。
nMaxCacheSize变量值也是最大签名缓存大小值。nElems变量就是在最大签名缓存时能存储的签名数。然后在日志文件中输出最大签名缓存大小、签名数等信息。

2-4 脚本执行缓存信息

接着会出现一个初始化脚本执行缓存函数:

InitScriptExecutionCache();

该函数和上面的签名缓存信息函数类似,该函数的声明在validation.h中第393行:

/** 初始化脚本执行缓存 */
void InitScriptExecutionCache();

该函数的实现代码在validation.cpp中的1236-1243行:

void InitScriptExecutionCache() {
    // nMaxCacheSize is unsigned. If -maxsigcachesize is set to zero,
    // setup_bytes creates the minimum possible cache (2 elements).
    size_t nMaxCacheSize = std::min(std::max((int64_t)0, gArgs.GetArg("-maxsigcachesize", DEFAULT_MAX_SIG_CACHE_SIZE) / 2), MAX_MAX_SIG_CACHE_SIZE) * ((size_t) 1 << 20);
    size_t nElems = scriptExecutionCache.setup_bytes(nMaxCacheSize);
    LogPrintf("Using %zu MiB out of %zu/2 requested for script execution cache, able to store %zu elements\n",
            (nElems*sizeof(uint256)) >>20, (nMaxCacheSize*2)>>20, nElems);
}

可以比较发现这个函数和InitSignatureCache()函数的数的计算完全一样,就是把一些名词修改了,在日志文件中可以得到如下图所示的结果:

可见这两个函数是完全类似的功能,此处的脚本执行缓存和之前的签名缓存相等。

2-5 脚本验证线程

接着会出现一段关于脚本验证线程的代码:

 LogPrintf("Using %u threads for script verification\n", nScriptCheckThreads);
 if (nScriptCheckThreads) {
    for (int i=0; i<nScriptCheckThreads-1; i++)
        threadGroup.create_thread(&ThreadScriptCheck);
    }

开始是在日志文件中打印消息:

日志打印示例
“使用0个线程进行脚本验证。”
变量nScriptCheckThreads定义在validation.cpp为0。后面的判断语句为如果该变量不为0,假设为n,则会创建n个线程用来验证脚本。

2-6 轻量级任务调度
    // 启动轻量级任务调度线程
    CScheduler::Function serviceLoop = boost::bind(&CScheduler::serviceQueue, &scheduler);
    threadGroup.create_thread(boost::bind(&TraceThread<CScheduler::Function>, "scheduler", serviceLoop));

从注释文件中可以知道:这部分的功能就是启动一个轻量级的程序,这个程序功能是用来做任务调度的。
(1)代码第一行的效果就是:用boost::bind()函数绑定类的成员函数serviceQueue()和类的对象scheduler,最后的serviceLoop函数对象就可以等效于scheduler.serviceQueue()
(2)代码的第二行通过线程组对象threadGroup实例化create_thread()函数来创建新的线程,线程的执行函数为boost::bind()函数返回的函数对象。
bind()函数中有个TraceThread()函数,该函数在util.h的295行实现,该函数的功能在注释文件中指的是:这是一个封装器,而且仅调用func一次。由该函数的表示:TraceThread<CScheduler::Function>可以知道:该函数在实际调用时传入的函数类型为CScheduer::Function。结合bind()函数就是:TraceThread()函数实例化对象为serviceLoop函数。其中还有第一个参数为"scheduler",这个参数就是重命名线程为scheduler。
(3)整体来看这两段代码就是:创建一个线程,该线程将调用一次scheduler.serviceQueue()决定的serviceLoop函数,并且会重新命名该线程为scheduler。而serviceQueue()函数就是关于任务调度的函数。

2-7 注册后台处理信号
GetMainSignals().RegisterBackgroundSignalScheduler(scheduler);

该段代码中GetMainSignals()在validationinterface.cpp中53行实现:

CMainSignals& GetMainSignals()
{
    return g_signals;
}

它的返回值为:g_signals,这是一个CMainSignals类型的静态变量。那么该行代码就是调用CMainSignals类中的成员函数RegisterBackgroundSignalScheduler()该成员函数是一个公开方法,在validationinterface.h的79行声明:

 /**注册一个CScheduler,为了给应该在后台运行的回调(只能被调用一次)。 */
 void RegisterBackgroundSignalScheduler(CScheduler& scheduler);

该函数的实现在validationinterface.cpp的40行:

void CMainSignals::RegisterBackgroundSignalScheduler(CScheduler& scheduler) {
    assert(!m_internals);
    m_internals.reset(new MainSignalsInstance(&scheduler));
}

可以知道:通过这个函数,将CMainSignals类中的unique_ptr<MainSignalsInstance>指针类型的成员变量m_internals赋值为一个新建的对象,这个新建的对象类型为结构体MainSignalsInstance。总之这个函数是设置变量m_internals成为结构体MainSignalsInstance类型,用来注册后台处理信号。

2-8 启动RPCServer、HTTPServer

接下来是一段判断参数的代码:

    /* 已经启动RPC服务器。 它将以“热身”模式启动进程,而不是已经真正的进程调用(但它将表示服务器在那里并且稍后准备就绪的连接)。 初始化完成后,热身模式将被禁用。
     */
    if (gArgs.GetBoolArg("-server", false))
    {
        uiInterface.InitMessage.connect(SetRPCWarmupStatus);
        if (!AppInitServers(threadGroup))
            return InitError(_("Unable to start HTTP server. See debug log for details."));
    }

(1)此处涉及到一个参数:-server
该函数在帮助文件中的解释为:接受命令行和JSON-RPC命令。此处的判断语句为:如果在命令行中有-server命令,就执行下面的语句,如果没有该命令则不执行。
(2)在执行语句中首先给InitMessage信号添加一个新的执行函数SetRPCWarmupStatus,该执行函数的声明在rpc/server.h中的61行:

/**
 * 设置RPC预热状态。 完成此操作后,所有的RPC调用都将立即通过RPC_IN_WARMUP出错。
 */
void SetRPCWarmupStatus(const std::string& newStatus);

函数的实现在rpc/server.cpp中340行:

void SetRPCWarmupStatus(const std::string& newStatus)
{
    LOCK(cs_rpcWarmup);
    rpcWarmupStatus = newStatus;
}

其中rpcWarmupStatus是一个静态的string类型的全局变量,则函数的作用就是将新的参数值赋给rpcWarmupStatus变量,完成RPC预热状态的设置。
(3)然后又会出现一个判断语句,这个判断语句主要涉及到AppInitServers()函数的调用。该函数的实现在init.cpp中的725行:

bool AppInitServers(boost::thread_group& threadGroup)
{
    RPCServer::OnStarted(&OnRPCStarted);
    RPCServer::OnStopped(&OnRPCStopped);
    RPCServer::OnPreCommand(&OnRPCPreCommand);
    if (!InitHTTPServer())
        return false;
    if (!StartRPC())
        return false;
    if (!StartHTTPRPC())
        return false;
    if (gArgs.GetBoolArg("-rest", DEFAULT_REST_ENABLE) && !StartREST())
        return false;
    if (!StartHTTPServer())
        return false;
    return true;
}

①从该函数的代码中可以知道:它首先的三行是调用了RPCServer类中的三个函数,这三个函数的功能分别是连接到各自对应的信号槽,这三个信号槽分别做了一些信号的连接工作:OnRPCStarted负责将RPCNotifyBlockChange连接到NotifyBlockTip信号上;OnRPCStopped负责将连接解除,并做一些其他的清除工作;OnRPCPreCommand检查在安全模式下是否有警告消息,如果有那么就抛出相应的异常。
InitHTTPServer():初始化http server。
从这个函数开始的为5个判断语句,调用了5个不同开启服务函数,当都开启成功后才返回true,否则返回false。现在我们先看第一个函数:InitHTTPServer(),这个是初始化http server的。该函数的声明在httpserver.h中的24行:

/** 初始化HTTP服务器。 在RegisterHTTPHandler或EventBase()之前调用它。
 */
bool InitHTTPServer();

由注释也只道这是一个初始化HTTP服务器的函数。该函数的实现在httpserver.cpp中的378-434行:

bool InitHTTPServer()
{
    if (!InitHTTPAllowList())
        return false;

    if (gArgs.GetBoolArg("-rpcssl", false)) {
        uiInterface.ThreadSafeMessageBox(
            "SSL mode for RPC (-rpcssl) is no longer supported.",
            "", CClientUIInterface::MSG_ERROR);
        return false;
    }

    //将libevent的日志记录重定向到我们自己的日志
    event_set_log_callback(&libevent_log_cb);
    //更新libevent的日志处理。 如果我们的libevent版本不支持调试日志记录,则返回false,
    //在这种情况下,我们应该清除BCLog :: LIBEVENT标志。
    if (!UpdateHTTPServerLogging(logCategories & BCLog::LIBEVENT)) {
        logCategories &= ~BCLog::LIBEVENT;
    }

#ifdef WIN32
    evthread_use_windows_threads();
#else
    evthread_use_pthreads();
#endif

    raii_event_base base_ctr = obtain_event_base();

    /* 创建一个新的evhttp对象来处理请求。 */
    raii_evhttp http_ctr = obtain_evhttp(base_ctr.get());
    struct evhttp* http = http_ctr.get();
    if (!http) {
        LogPrintf("couldn't create evhttp. Exiting.\n");
        return false;
    }

    evhttp_set_timeout(http, gArgs.GetArg("-rpcservertimeout", DEFAULT_HTTP_SERVER_TIMEOUT));
    evhttp_set_max_headers_size(http, MAX_HEADERS_SIZE);
    evhttp_set_max_body_size(http, MAX_SIZE);
    evhttp_set_gencb(http, http_request_cb, nullptr);

    if (!HTTPBindAddresses(http)) {
        LogPrintf("Unable to bind any endpoint for RPC server\n");
        return false;
    }

    LogPrint(BCLog::HTTP, "Initialized HTTP server\n");
    int workQueueDepth = std::max((long)gArgs.GetArg("-rpcworkqueue", DEFAULT_HTTP_WORKQUEUE), 1L);
    LogPrintf("HTTP: creating work queue of depth %d\n", workQueueDepth);

    workQueue = new WorkQueue<HTTPClosure>(workQueueDepth);
    // 通过.release()将所有权转移到eventBase / HTTP
    eventBase = base_ctr.release();
    eventHTTP = http_ctr.release();
    return true;
}

(a)380-381,这两行是初始化HTTP Server。首先通过InitHTTPAllowList()函数来初始化访问控制列表,函数中有个变量rpc_allow_subnets,它是一个vector类型的变量,它的作用是保存所有允许访问的主机ip。这个函数的作用是:首先添加本地的地址,然后从命令行中的-rpcallowip参数读取ip列表,读取时检测地址的有效性,最后打印出允许访问的所有ip列表。
(b)383-388,这部分是看命令行中是否设置了-rpcssl参数,由于目前的版本不支持ssl,所以如果设置了这个参数就报错。
(c)391-397行就是将libevent的日志记录重定向到我们自己的日志。
(d)最后的399-434都是初始化基于libevent的http协议。
首先代码根据系统环境判断使用windows线程还是其他环境下的线程,接下来就是基于libevent的http协议的实现:

采用libevent的原因有以下几个方面:

  • 事件驱动,高性能;
  • 轻量级,专注于网络;
  • 跨平台,支持各主流平台;
  • 支持多种I/O多路复用技术,epoll、poll、dev/poll、select和kqueue等;
  • 支持I/O,定时器和信号等事件。

基于libevent实现的http协议主要有以下这么几个步骤:

  1. event_base base = event_base_new(),首先新建event_base对象;
  2. evhttp http = evhttp_new(base),然后新建evhttp对象;
  3. evhttp_bind_socket(http, "0.0.0.0", port),接下来绑定ip地址和端口;
  4. evhttp_set_gencb(http, http_call_back, NULL),然后设置请求处理函数http_call_back;
  5. event_base_dispatch(base), 最后派发事件循环

在这里使用的基于libevent的http协议基本上就是把简单版的http协议封装了一下,原来的变量类型也都换了一个新的类型,这个新类型的前面都加上了raiiraii就是为了避免申请内存但是没有释放从而导致内存泄漏的情况出现所使用的一种技术,这种技术能够在对象离开作用域是自动释放。关于RALL详细信息可以参考:
http://blog.csdn.net/doc_sgl/article/details/43028009

http server 请求回调函数
415-417行的三个evhttp_set_xxx函数都是设置连接的限制条件,http_request_cb为请求的回调函数。

StartRPC():启动RPC服务。
继续看StartRPC()函数,该函数声明在rpc/server.h中的191行,实现在rpc/server.cpp的312行:

bool StartRPC()
{
    LogPrint(BCLog::RPC, "Starting RPC\n");
    fRPCRunning = true;
    g_rpcSignals.Started();
    return true;
}

启动RPC就是将之前的连接到Started的信号全部触发运行,并修改变量fRPCRunning为true。
StartHTTPRPC():启动HTTP RPC服务。
再看StartHTTPRPC()函数,该函数声明在httprpc.h的14行:

/** 启动HTTP RPC子系统。
 * 前提:HTTP和RPC已经被启动了。
 */
bool StartHTTPRPC();

该函数的实现在httprpc.cpp的229-244行:

bool StartHTTPRPC()
{
    LogPrint(BCLog::RPC, "Starting HTTP RPC server\n");
    if (!InitRPCAuthentication())
        return false;

    RegisterHTTPHandler("/", true, HTTPReq_JSONRPC);
#ifdef ENABLE_WALLET
    //一旦我们切换到更好的端点支持和API版本,ifdef可以被删除。
    RegisterHTTPHandler("/wallet/", false, HTTPReq_JSONRPC);
#endif
    assert(EventBase());
    httpRPCTimerInterface = new HTTPRPCTimerInterface(EventBase());
    RPCSetTimerInterface(httpRPCTimerInterface);
    return true;
}

此时会由一个函数:InitRPCAuthentication(),用来验证用户的身份,并且这个函数只进行了验证环境的初始化,还没有进行真正的验证过程。
然后会由RegisterHTTPHandler()函数来注册url处理函数。该函数的第一个参数是请求的路径,第二个true表示的是精确匹配,最后一个参数是处理的函数。紧跟着相同的会有个条件编译语句,也是由RegisterHTTPHandler()函数来控制,第一个参数是请求的路径,第二个false表示的是前缀匹配,最后一个参数是处理的函数。
最后是设置http定时器接口,就是在指定时间间隔后执行某个函数一次。
StartREST():启动REST。
然后就是启动REST服务了,在该部分会有一个参数的判断,这个参数是-rest,该参数在帮助文件中的解释为:接受公共REST请求。默认为false。此处的含义是:通过命令行的-rest命令能表示启动REST服务。REST服务具体就是将一堆URL路径和对应的处理函数通过RegisterHTTPHandler函数存储到pathHandlers中,以便在对应的请求到达时能调用对应的函数进行处理,这里面使用的都是前缀匹配。
StartHTTPServer():启动HTTP server。
这个是最后一个函数了,该函数声明在httpserver.h中的29行:

/** 启动HTTP服务。
 * 这与InitHTTPServer是分开的,为用户提供无竞争条件的时间来在InitHTTPServer和StartHTTPServer之间注册它们的处理程序。
 */
bool StartHTTPServer();

该函数的实现在httpserver.cpp的453-467行:

bool StartHTTPServer()
{
    LogPrint(BCLog::HTTP, "Starting HTTP server\n");
    int rpcThreads = std::max((long)gArgs.GetArg("-rpcthreads", DEFAULT_HTTP_THREADS), 1L);
    LogPrintf("HTTP: starting %d worker threads\n", rpcThreads);
    std::packaged_task<bool(event_base*, evhttp*)> task(ThreadHTTP);
    threadResult = task.get_future();
    threadHTTP = std::thread(std::move(task), eventBase, eventHTTP);

    for (int i = 0; i < rpcThreads; i++) {
        std::thread rpc_worker(HTTPWorkQueueRun, workQueue);
        rpc_worker.detach();
    }
    return true;
}

程序首先从命令行中通过-rpcthreads参数获取rpc执行的最大线程数,接下来使用了<future>库中的packaged_task类创建了一个task对象,然后绑定了函数ThreadHTTP,并将返回最终的结果保存在threadResult中,再然后创建了线程threadHTTP来执行任务。

thread()函数中第一个参数使用了std::move()函数,这个函数作用是返回输出参数的右值类型,与右值相对应的有左值类型,这两者的区别是:右值类型只能出现在赋值语句的右边,一般的情况有常数、临时变量(函数返回值)等;左值则可以出现在等号的两边,同时需要进行初始化,普通的变量都是左值类型。

接下来的程序根据命令行设置的rpc线程数创建对应的rpc_worker来执行workQueue,创建完线程之后便让线程从当前线程脱离出去,通过detach()操作,交给了系统去管理。

AppInitServers()函数总结:
AppInitServers()函数主要就是HTTP Server的初始化,将外部的请求和内部相应的处理函数对应起来,并做好相应的任务分配。

2-9 定义变量nStart

这个是第4a步的最后了,就是一个简单的变量的定义,此时定义变量名为nStart,该变量的类型为int64_t

********************************************
第4a步总结:
这个步骤为应用程序的初始化:
包括启动时创建一个.pid文件来保证只有一个比特币核心程序运行;启动时压缩debug.log文件,打开日志文件,并增加一些调试日志,包括在每行输出内容前加时间戳、数据文件路径、签名和脚本执行的缓存信息等;然后打开脚本验证线程并启动一个轻量级的用来做任务调度的线程;最后注册后台处理信号并启动RPCServer、HTTPServer服务。
总之是一个进一步保证程序的唯一性、完善调试日志内容、开启任务调度线程、启动RPC和HTTP服务等的初始化步骤。
********************************************


3. 第五步:验证钱包数据库的完整性

这一步只有简单的四行代码:

   // ********第五步: 验证钱包数据库的完整性
#ifdef ENABLE_WALLET
    if (!CWallet::Verify())
        return false;
#endif

这是一个条件编译程序段:如果有ENABLE_WALLET宏定义,则执行CWallet::Verify()函数。
(1)对于ENABLE_WALLET宏定义在config\bitcoind-config.h中有定义,为1:

/* 定义为1以启用钱包功能 */
#define ENABLE_WALLET 1

可以知道定义为1为启动钱包功能。那么如果总会是1的话,为什么在这里加上一个条件编译语句呢?通过思考,我找到前面我写过的一篇编译源码过程的文章,我发现在编译源码时是可以禁用钱包功能的:

禁用钱包功能
只是我在编译时使用的是:

./configure --without-gui --with-incompatible-bdb

当不使用禁用钱包命令时,默认是开启钱包功能的,所以我这里会有ENABLE_WALLET宏定义,且值是1,默认开启的。
(2)CWallet::Verify()函数就是验证钱包数据库的完整性了。该函数的声明在wallet/wallet.h的1064行:

    //! 负责阅读和验证-wallet参数并验证钱包数据库。
    //  只要有一个钱包被加载,这个功能就会对钱包进行补救。
    // (CWallet::ParameterInteraction forbids -salvagewallet, -zapwallettxes or -upgradewallet with multiwallet).
    static bool Verify();

该函数的实现在wallet/wallet.cpp中的497-552行:

bool CWallet::Verify()
{
    if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET))
        return true;

    uiInterface.InitMessage(_("Verifying wallet(s)..."));

    // 跟踪每个钱包绝对路径以检测重复项。
    std::set<fs::path> wallet_paths;

    for (const std::string& walletFile : gArgs.GetArgs("-wallet")) {
        if (boost::filesystem::path(walletFile).filename() != walletFile) {
            return InitError(strprintf(_("Error loading wallet %s. -wallet parameter must only specify a filename (not a path)."), walletFile));
        }

        if (SanitizeString(walletFile, SAFE_CHARS_FILENAME) != walletFile) {
            return InitError(strprintf(_("Error loading wallet %s. Invalid characters in -wallet filename."), walletFile));
        }

        fs::path wallet_path = fs::absolute(walletFile, GetDataDir());

        if (fs::exists(wallet_path) && (!fs::is_regular_file(wallet_path) || fs::is_symlink(wallet_path))) {
            return InitError(strprintf(_("Error loading wallet %s. -wallet filename must be a regular file."), walletFile));
        }

        if (!wallet_paths.insert(wallet_path).second) {
            return InitError(strprintf(_("Error loading wallet %s. Duplicate -wallet filename specified."), walletFile));
        }

        std::string strError;
        if (!CWalletDB::VerifyEnvironment(walletFile, GetDataDir().string(), strError)) {
            return InitError(strError);
        }

        if (gArgs.GetBoolArg("-salvagewallet", false)) {
            // 恢复可读密钥对:
            CWallet dummyWallet;
            std::string backup_filename;
            if (!CWalletDB::Recover(walletFile, (void *)&dummyWallet, CWalletDB::RecoverKeysOnlyFilter, backup_filename)) {
                return false;
            }
        }

        std::string strWarning;
        bool dbV = CWalletDB::VerifyDatabaseFile(walletFile, GetDataDir().string(), strWarning, strError);
        if (!strWarning.empty()) {
            InitWarning(strWarning);
        }
        if (!dbV) {
            InitError(strError);
            return false;
        }
    }

    return true;
}

①此函数首先判断命令行中是否含有-disablewallet命令,如果含有这个命令则表示禁用了钱包功能,那么就没有接下来检查完整性的必要了,就直接返回true,结束该函数。
////////////////////////////////////////////////////////////
注意:
看到这出现了两个禁用钱包功能命令:一个是在编译时的

./configure --without-gui --disable-wallet

命令,这个是在编译时禁用钱包功能,决定宏定义ENABLE_WALLET是否包含在程序中,此时的禁用钱包功能是完全的禁用,就是客户端中根本就没有这个模块;而-disablewallet命令是在编译时没有关闭钱包功能(默认就是打开钱包功能),在执行CWallet::Verify()函数来检验钱包数据库的完整性时,因为检测到在命令行中输入了-disablewallet命令就知道运行客户端时禁止了钱包功能,就会立刻结束函数的运行。但是这个是暂时的禁止钱包功能,如果下次重新打开客户端,不输入这个命令,就会继续执行下面的代码。
总之,前面的禁用钱包功能是在客户端没有完成的编译过程中的禁止,它是去除钱包功能模块,是“根本就没有钱包功能(想用都用不了)”;而这个-disablewallet命令是带有钱包功能的客户端在运行是刻意加上这个命令来禁止钱包功能,是“有钱包功能,但就是不用(有却不用)”。
////////////////////////////////////////////////////////////

②接下来对于命令行中传入的每一个钱包路径,首先检查文件的路径、是否包含非法字符、是否是regular file或者链接、是否有相同的文件等。
③然后用CWalletDB::VerifyEnvironment()函数验证钱包的环境。
该函数传入钱包文件名和钱包的绝对路径,首先检查钱包文件名是否是不包含路径的纯文件名,接下来调用一个CDBEnv类型变量bitdb中的open函数(open函数首先检查数据库文件是否存在,不存在就立即创建;然后设置日志文件,并且通过DbEnv类型变量指针dbenv设置了一系列数据库运行的相关参数)。open函数执行成功的话,就直接返回true;否则就进入if语句,之后首先将原来的钱包数据库文件进行备份,然后再次尝试调用open函数,创建数据库文件。
④然后会有一个判断是否含有-salvagewallet命令的判断语句,其中-salvagewallet命令通过参照帮助文件可以知道它的作用是:尝试在启动时从损坏的钱包中恢复私钥。所以后面的代码都是执行在钱包中恢复私钥的作用的,这个工作是由CWalletDB::Recover()函数完成的。这个函数中有4个重要的参数:

  • filename:待恢复的钱包文件名;
  • callbackDataIn:恢复数据写入对象;
  • recoverKVcallback:回调函数,用来将恢复数据写入callbackDataIn
  • newFilename:备份文件名。

私钥恢复的步骤是首先备份原来的钱包文件,然后调用CDBEnv类中的Salvage函数,这个函数实现的功能是从文件中将公私钥读取出来并保存在到salvagedData中。恢复完之后就将恢复的数据写入到本地数据库中,这个写入的过程都是通过pdbCopy对象来进行的,同时如果定义了recoverKVcallback函数,那么还同时写入到callbackDataIn对象中,用于传给上层调用函数。
⑤最后会出现一个CWalletDB::VerifyDatabaseFile()函数,这个函数主要是用来验证数据库文件的。这个函数的最终实现是:如果验证通过的话那么就直接返回VERIFY_OK;否则就先看是否设置了恢复函数,如果没有就返回RECOVER_FAIL;如果设置了恢复函数,那么就调用恢复函数,并返回恢复函数执行的结果。由前面可以发现恢复函数被设置成了CWalletDB::Recover函数,所以这里都会调用这个函数。

********************************************
第五步总结:
这一步主要是验证钱包数据库的完整性的,当编译时没有禁用钱包功能(默认开启钱包功能)时会执行这一步,主要由Verify()函数来完成验证工作:包括是否在命令行禁用钱包功能、检查钱包路径、验证钱包环境、恢复私钥、验证钱包的数据库文件等。这个主要是针对钱包的初始化,保证钱包的完整性,方便钱包后面的使用。
********************************************


下面的第六步:网络初始化和第七步:加载块链 占据着AppInitMain()最多的代码量,也是该函数最重要的工作,会在后面详细解析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,585评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,923评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,314评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,346评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,718评论 3 291
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,828评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,020评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,758评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,486评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,722评论 2 251
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,196评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,546评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,211评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,132评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,916评论 0 200
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,904评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,758评论 2 274

推荐阅读更多精彩内容

  • (九)继续看bitcoind.cpp中的132-136行 对它的注释为: InitError将被调用,并有详细的错...
    风来雾去阅读 1,761评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,105评论 18 139
  • linux资料总章2.1 1.0写的不好抱歉 但是2.0已经改了很多 但是错误还是无法避免 以后资料会慢慢更新 大...
    数据革命阅读 12,028评论 2 34
  • 有一天我不再年轻 我已经看到看到 那一天远山青翠 一百年前你曾拨弄的发丝 迎风飞舞 岁月无声我也无泪 那一天我就有...
    行走的卓玛阅读 189评论 0 0
  • 说起闹市,首先想到的是云南丽江,那是一个“时间停止”的地方。 第一次去丽江,事先并没有做功课。绿皮车开的很慢,沿途...
    香果季客服小小阅读 110评论 0 0