比特币源码解析(16)

来源:互联网 发布:毒品网络 豆瓣 编辑:程序博客网 时间:2024/06/05 14:31

0x01 AppInitSanityChecks - Step 4 sanity checks

bool AppInitSanityChecks(){    // ********************************************************* Step 4: sanity checks    // Initialize elliptic curve code    std::string sha256_algo = SHA256AutoDetect();    LogPrintf("Using the '%s' SHA256 implementation\n", sha256_algo);    RandomInit();    ECC_Start();    globalVerifyHandle.reset(new ECCVerifyHandle());    // Sanity check    if (!InitSanityCheck())        return InitError(strprintf(_("Initialization sanity check failed. %s is shutting down."), _(PACKAGE_NAME)));    // Probe the data directory lock to give an early error message, if possible    // We cannot hold the data directory lock here, as the forking for daemon() hasn't yet happened,    // and a fork will cause weird behavior to it.    return LockDataDirectory(true);}

椭圆曲线参数初始化

这一步的整个函数都相对较短,代码首先根据cpuid决定使用何种版本的sha256算法,SHA256AutoDetect函数具体实现如下,

std::string SHA256AutoDetect(){#if defined(EXPERIMENTAL_ASM) && (defined(__x86_64__) || defined(__amd64__))    uint32_t eax, ebx, ecx, edx;    if (__get_cpuid(1, &eax, &ebx, &ecx, &edx) && (ecx >> 19) & 1) {        Transform = sha256_sse4::Transform;        assert(SelfTest(Transform));        return "sse4";    }#endif    assert(SelfTest(Transform));    return "standard";}

对于64位的系统通过__get_cpuid获取cpuid,然后通过ecx来获取具体芯片的feature flags,具体可以参考https://en.wikipedia.org/wiki/CPUID,ecx的第19位正好对应sse4.1的指令,其他情况就返回standard。接下来的RandomInit()同样也是根据cpuid来决定是否增加RDRAND作为额外的随机源,对于RDRAND这里https://software.intel.com/en-us/blogs/2012/11/17/the-difference-between-rdrand-and-rdseed有很好的解释,上面链接主要解释RDRANDRDSEED的却别,简单来说就是根据输出的值的作用来决定使用哪个,如果输出是作为其他PRNG(Pseudorandom number generator, 伪随机数生成器)的种子,那么就使用RDSEED;其他情况都使用RDRANDECC_Start()开始进行椭圆曲线参数初始化。比特币中所有的公钥、私钥、签名等密码学技术基本上都是使用的椭圆曲线体系。

Sanity Check

接下来的一句代码进行bitcoin运行时需要的一些基本库的完整性检测,就是调用测试程序来测试它们是否都正常工作,以确保bitcoin运行环境正常。检测的模块包括以下几个部分,

/** Sanity checks *  Ensure that Bitcoin is running in a usable environment with all *  necessary library support. */bool InitSanityCheck(void){    if(!ECC_InitSanityCheck()) {        InitError("Elliptic curve cryptography sanity check failure. Aborting.");        return false;    }    if (!glibc_sanity_test() || !glibcxx_sanity_test())        return false;    if (!Random_SanityCheck()) {        InitError("OS cryptographic RNG sanity check failure. Aborting.");        return false;    }    return true;}
  • ECC_InitSanityCheck():椭圆曲线测试。
  • glibc_sanity_test() & glibcxx_sanity_check():glibc和glibcxx库测试。
  • Random_SanityCheck():随机数生成环境测试。

测试锁定数据目录

LockDataDirectory()的实现如下,

static bool LockDataDirectory(bool probeOnly){    std::string strDataDir = GetDataDir().string();    // Make sure only a single Bitcoin process is using the data directory.    fs::path pathLockFile = GetDataDir() / ".lock";    FILE* file = fsbridge::fopen(pathLockFile, "a"); // empty lock file; created if it doesn't exist.    if (file) fclose(file);    try {        static boost::interprocess::file_lock lock(pathLockFile.string().c_str());        if (!lock.try_lock()) {            return InitError(strprintf(_("Cannot obtain a lock on data directory %s. %s is probably already running."), strDataDir, _(PACKAGE_NAME)));        }        if (probeOnly) {            lock.unlock();        }    } catch(const boost::interprocess::interprocess_exception& e) {        return InitError(strprintf(_("Cannot obtain a lock on data directory %s. %s is probably already running.") + " %s.", strDataDir, _(PACKAGE_NAME), e.what()));    }    return true;}

函数传入一个bool型参数probeOnly,为true表示只是测试是否能进行锁定但并不实际锁定,false表示将数据目录(也就是~/.bitcoin/)进行锁定,这里的锁定是指进程间的锁定,也就是只允许当前进程访问锁定的文件,其他进程不允许访问,如果probeOnlytrue,那么在锁定之后再进行解锁。

0x02 AppInitMain Step 4a: application initialization

终于到主函数了!

创建PID文件

bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler){    const CChainParams& chainparams = Params();    // ********************************************************* Step 4a: application initialization#ifndef WIN32    CreatePidFile(GetPidFile(), getpid());#endif

AppInitMain传入两个参数,threadGroupscheduler,这两个参数到传入前都是空值。然后获取chainparams,这个变量之前出现过多次,主要是包含一些共识的参数,自身是根据选择不同的网络maintestnet或者regtest来生成不同的参数。接下来对于非Windows系统创建进程的PID文件,

转载:http://www.linuxidc.com/Linux/2012-12/76950.htm

(1) pid文件的内容:pid文件为文本文件,内容只有一行, 记录了该进程的ID。 用cat命令可以看到。

(2) pid文件的作用:防止进程启动多个副本。只有获得pid文件(固定路径固定文件名)写入权限(F_WRLCK)的进程才能正常启动并把自身的PID写入该文件中。其它同一个程序的多余进程则自动退出。

通过上面文章我们知道pid文件的作用就是防止进程启动多个副本,从而打乱原有的消息传输。

限定日志大小

    if (gArgs.GetBoolArg("-shrinkdebugfile", logCategories == BCLog::NONE)) {        // Do this first since it both loads a bunch of debug.log into memory,        // and because this needs to happen before any other debug.log printing        ShrinkDebugFile();    }

-shrinkdebugfile:限制日志文件的大小,如果没有设置-debug参数,那么该变量的默认值为1.

如果设置了这个参数那么就会执行ShrinkDebugFile函数,该函数的实现位于src/util.cpp

void ShrinkDebugFile(){    // Amount of debug.log to save at end when shrinking (must fit in memory)    constexpr size_t RECENT_DEBUG_HISTORY_SIZE = 10 * 1000000;    // Scroll debug.log if it's getting too big    fs::path pathLog = GetDataDir() / "debug.log";    FILE* file = fsbridge::fopen(pathLog, "r");    // If debug.log file is more than 10% bigger the RECENT_DEBUG_HISTORY_SIZE    // trim it down by saving only the last RECENT_DEBUG_HISTORY_SIZE bytes    if (file && fs::file_size(pathLog) > 11 * (RECENT_DEBUG_HISTORY_SIZE / 10))    {        // Restart the file with some of the end        std::vector<char> vch(RECENT_DEBUG_HISTORY_SIZE, 0);        fseek(file, -((long)vch.size()), SEEK_END);        int nBytes = fread(vch.data(), 1, vch.size(), file);        fclose(file);        file = fsbridge::fopen(pathLog, "w");        if (file)        {            fwrite(vch.data(), 1, nBytes, file);            fclose(file);        }    }    else if (file != nullptr)        fclose(file);}

我们可以看到该函数首先定义了一个变量RECENT_DEBUG_HISTORY_SIZE,大小为10MB,然后判断debug.log文件的大小,如果小于11MB,那么就不做处理;否则读取文件的最后RECENT_DEBUG_HISTORY_SIZE这么多字节,重新保存到debug.log文件中。

DebugLog显示处理

    if (fPrintToDebugLog)        OpenDebugLog();    if (!fLogTimestamps)        LogPrintf("Startup time: %s\n", DateTimeStrFormat("%Y-%m-%d %H:%M:%S", GetTime()));    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);

fPrintToDebugLog表示是否打开debug.log文件,默认值为true,只有在bench_bitcoin.cpp中才设为falseOpenDebugLog()函数的实现如下:

static void DebugPrintInit(){    assert(mutexDebugLog == nullptr);    mutexDebugLog = new boost::mutex();    vMsgsBeforeOpenLog = new std::list<std::string>;}void OpenDebugLog(){    boost::call_once(&DebugPrintInit, debugPrintInitFlag);    boost::mutex::scoped_lock scoped_lock(*mutexDebugLog);    assert(fileout == nullptr);    assert(vMsgsBeforeOpenLog);    fs::path pathDebug = GetDataDir() / "debug.log";    fileout = fsbridge::fopen(pathDebug, "a");    if (fileout) {        setbuf(fileout, nullptr); // unbuffered        // dump buffered messages from before we opened the log        while (!vMsgsBeforeOpenLog->empty()) {            FileWriteStr(vMsgsBeforeOpenLog->front(), fileout);            vMsgsBeforeOpenLog->pop_front();        }    }    delete vMsgsBeforeOpenLog;    vMsgsBeforeOpenLog = nullptr;}

boost::call_once表示在多线程访问该语句时始终只执行一次调用的函数,其中第一个参数是被调用的函数地址,第二个参数类型为boost::once_flag,代码中使用的是debugPrintInitFlag,该变量的定义为,

static boost::once_flag debugPrintInitFlag = BOOST_ONCE_INIT;

在函数DebugPrintInit()中定义了变量mutexDebugLog,类型为boost::mutex::scoped_lock,用来保证函数中后续代码是线程安全的,scoped_lock类似于我们之前在http://blog.csdn.net/pure_lady/article/details/77675915#t3中讲过的lock_guard, 能够保证在作用域范围内是互斥访问的,离开作用域时由析构函数自动解锁。但是接下来的一段代码有些奇怪,如果按照简单的代码执行顺序来看,在DebugPrintInit()中创建了对象vMsgsBeforeOpenLog,类型为链表,这时链表肯定是空的;而到了OpenDebugLog()函数中直接有个while将vMsgsBeforeOpenLog中的字符串写入到fileout中,空链表有什么好写的?但是用gbd调试程序时,运行到这里发现vMsgsBeforeOpenLog竟然不是空的!

解释

全局搜索vMsgsBeforeOpenLog之后发现,还有个地方使用了这个变量,在src/util.cpp中的LogPrintStr()里,

// src/util.cpp line 343             boost::call_once(&DebugPrintInit, debugPrintInitFlag);        boost::mutex::scoped_lock scoped_lock(*mutexDebugLog);        // buffer if we haven't opened the log yet        if (fileout == nullptr) {            assert(vMsgsBeforeOpenLog);            ret = strTimestamped.length();            vMsgsBeforeOpenLog->push_back(strTimestamped);        }

从上面代码可以发现这里也调用了DebugPrintInit()这个函数,所以远在OpenDebugLog()之前,vMsgsBeforeOpenLog变量就已经创建了,然后LogPrintStr在之后又被调用了很多次,每次都会往vMsgsBeforeOpenLog中写入一些东西,所以到OpenDebugLog()时该变量已经有内容了。

回到源代码,接下来几行开始打印一些提示信息,包括数据目录、最大连接数以及配置文件。

设置Cache大小

    InitSignatureCache();    InitScriptExecutionCache();

-maxsigcachesize=<n>:限制signature cache的大小为n MiB,默认值为32.

这俩函数首先都是从命令行中获取-maxsigcachesize参数的值,然后分别通过signatureCachescriptExecutionCache中的set_bytes()函数设置最大的缓存大小。这个两个变量的类型都是Cache

,至于具体在什么时候用到还得看后面的代码。

创建Script Check Thead

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

nScriptCheckThreads是由参数中-par设定的,位于http://blog.csdn.net/pure_lady/article/details/77982837#t4,这段代码是根据参数来创建线程具体的线程。其中ThreadScriptCheck函数的实现如下,

void ThreadScriptCheck() {    RenameThread("bitcoin-scriptch");    scriptcheckqueue.Thread();}

首先给当前线程重命名为bitcoin-scriptch,然后启动线程,线程启动的流程如下:

1 static CCheckQueue<CScriptCheck> scriptcheckqueue(128); // src/validation.cpp line 15352 //! Worker thread src/checkqueue.h line 136    void Thread()    {        Loop();    }3 bool Loop(bool fMaster = false); // src/checkqueue.h line 694 // src/validation.cpp line 1202  bool CScriptCheck::operator()() {    const CScript &scriptSig = ptxTo->vin[nIn].scriptSig;    const CScriptWitness *witness = &ptxTo->vin[nIn].scriptWitness;    return VerifyScript(scriptSig, scriptPubKey, witness, nFlags, CachingTransactionSignatureChecker(ptxTo, nIn, amount, cacheStore, *txdata), &error);}5 // src/script/interpreter.cpp line 1407  bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const CScriptWitness* witness, unsigned int flags, const BaseSignatureChecker& checker, ScriptError* serror);

脚本验证线程的启动步骤如上流程所示,最开始是在AppInitMain中使用threadGroup中的create_thread来创建新的线程,而每一个线程的回调函数都是ThreadScriptCheck(),在这个回调函数当中主要是通过一个CCheckQueue类型的静态变量scriptcheckqueue执行类中的Thread()成员函数,而Thread()函数又调用一个类中的Loop()私有函数,这个Loop函数就是整个script check的线程控制中心,Loop的实现如下:

    /** Internal function that does bulk of the verification work. */    bool Loop(bool fMaster = false)    {        boost::condition_variable& cond = fMaster ? condMaster : condWorker;        std::vector<T> vChecks;        vChecks.reserve(nBatchSize);        unsigned int nNow = 0;        bool fOk = true;        do {            {                boost::unique_lock<boost::mutex> lock(mutex);                // first do the clean-up of the previous loop run (allowing us to do it in the same critsect)                if (nNow) {                    fAllOk &= fOk;                    nTodo -= nNow;                    if (nTodo == 0 && !fMaster)                        // We processed the last element; inform the master it can exit and return the result                        condMaster.notify_one();                } else {                    // first iteration                    nTotal++;                }                // logically, the do loop starts here                while (queue.empty()) {                    if ((fMaster || fQuit) && nTodo == 0) {                        nTotal--;                        bool fRet = fAllOk;                        // reset the status for new work later                        if (fMaster)                            fAllOk = true;                        // return the current status                        return fRet;                    }                    nIdle++;                    cond.wait(lock); // wait                    nIdle--;                }                // Decide how many work units to process now.                // * Do not try to do everything at once, but aim for increasingly smaller batches so                //   all workers finish approximately simultaneously.                // * Try to account for idle jobs which will instantly start helping.                // * Don't do batches smaller than 1 (duh), or larger than nBatchSize.                nNow = std::max(1U, std::min(nBatchSize, (unsigned int)queue.size() / (nTotal + nIdle + 1)));                vChecks.resize(nNow);                for (unsigned int i = 0; i < nNow; i++) {                    // We want the lock on the mutex to be as short as possible, so swap jobs from the global                    // queue to the local batch vector instead of copying.                    vChecks[i].swap(queue.back());                    queue.pop_back();                }                // Check whether we need to do work at all                fOk = fAllOk;            }            // execute work            for (T& check : vChecks)                if (fOk)                    fOk = check();            vChecks.clear();        } while (true);    }

分析这段代码首先要注意两个变量,第一个是std::vector<T> vChecks;中的T代表什么,从scriptcheckqueue的定义static CCheckQueue<CScriptCheck> scriptcheckqueue(128);来看,T代表的是CScriptCheck,这个CScriptCheck的定义在src/validation.h line 355处,其中的一个最主要的函数就是重载的()操作符,这可以解释上面代码的最后fOk = check();这句,我们稍后再来看这个函数的实现;第二个需要注意的变量是boost::condition_variable& cond 这个变量也是整个函数的核心 ,关于boost condition_variable可以参考http://blog.csdn.net/pure_lady/article/details/78057467#t5这篇文章,可能会更方便理解。

明白了上面两个变量再看起代码来会感觉清晰不少,这里面总共有两种角色,MasterWorker,其中Master负责统计结果,Worker负责执行具体的脚本验证。从源码的执行顺序来看,首先在AppInitMain中执行Thread()函数,也就是创建Worker,此时任务队列queue为空,所有的Worker创建后都在cond.wait(lock)处阻塞,等到有新的任务被加到queue中时,才会被唤醒批量执行任务;最后调用Wait()函数创建一个Master,等待所有的Worker执行完,统计执行结果并返回。

分析完WorkerMaster的工作流程,下面再来分析一下CScriptCheck重载的()操作符,也就是上面代码中的check()函数,它的实现如下(位于src/validation.cpp line 1202):

// src/validation.cpp line 1202bool CScriptCheck::operator()() {    const CScript &scriptSig = ptxTo->vin[nIn].scriptSig;    const CScriptWitness *witness = &ptxTo->vin[nIn].scriptWitness;    return VerifyScript(scriptSig, scriptPubKey, witness, nFlags, CachingTransactionSignatureChecker(ptxTo, nIn, amount, cacheStore, *txdata), &error);}

其中scriptSig就是用户的签名也是解锁脚本,witness是隔离见证中从scriptSig中分离出来的,scriptPubKey也就是锁定脚本,然后调用VerifyScript来验证锁定脚本和解锁脚本是否能正确结合运行。关于脚本的验证和执行以及脚本的结构,包含的具体内容等等,我打算在后面专门再写一章来介绍比特币中的脚本系统,目前就就可当成一个封装的模块,知道它的功能就可以。

原创粉丝点击