比特币源码解析(20)

来源:互联网 发布:生成淘宝无线链接地址 编辑:程序博客网 时间:2024/06/05 07:47

0x01 AppInitMain Step 5: verify wallet database integrity

#ifdef ENABLE_WALLET    if (!WalletVerify())        return false;#endif

Step 5主要是验证钱包数据库的完整性,从而避免钱包内容被本地错误的修改。钱包的启用是通过一个宏定义来进行实现的,如果启用了这个宏那么就会进行钱包数据的完整性校验,再来看看WalletVerify的实现,

// src/wallet/init.cpp line 174bool WalletVerify(){    if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET))        return true;    uiInterface.InitMessage(_("Verifying wallet(s)..."));    // Keep track of each wallet absolute path to detect duplicates.    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)) {            // Recover readable keypairs:            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和前面的宏定义是在不同的情况下执行的,宏定义是在编译的时候执行的,而命令行的参数中是在程序执行是才判断的。接下来对于命令行中传入的每一个钱包路径,首先检查文件的路径、是否包含非法字符、是否是regular file或者链接、是否有相同的文件等等。

验证钱包环境

验证完文件基本的属性之后,开始验证上下文环境,通过CWalletDB中的VerifyEnvironment函数 ,而CWalletDB中的VerifyEnvironment又是调用CDB中的VerifyEnvironment,这函数的实现如下:

// src/wallet/db.cpp line 229bool CDB::VerifyEnvironment(const std::string& walletFile, const fs::path& dataDir, std::string& errorStr){    LogPrintf("Using BerkeleyDB version %s\n", DbEnv::version(0, 0, 0));    LogPrintf("Using wallet %s\n", walletFile);    // Wallet file must be a plain filename without a directory    if (walletFile != fs::basename(walletFile) + fs::extension(walletFile))    {        errorStr = strprintf(_("Wallet %s resides outside data directory %s"), walletFile, dataDir.string());        return false;    }    if (!bitdb.Open(dataDir))    {        // try moving the database env out of the way        fs::path pathDatabase = dataDir / "database";        fs::path pathDatabaseBak = dataDir / strprintf("database.%d.bak", GetTime());        try {            fs::rename(pathDatabase, pathDatabaseBak);            LogPrintf("Moved old %s to %s. Retrying.\n", pathDatabase.string(), pathDatabaseBak.string());        } catch (const fs::filesystem_error&) {            // failure is ok (well, not really, but it's not worse than what we started with)        }        // try again        if (!bitdb.Open(dataDir)) {            // if it still fails, it probably means we can't even create the database env            errorStr = strprintf(_("Error initializing wallet database environment %s!"), GetDataDir());            return false;        }    }    return true;}

该函数传入钱包文件名和钱包的绝对路径,首先检查钱包文件名是否是不包含路径的纯文件名,接下来调用一个CDBEnv类型变量bitdb中的open函数,该函数实现如下:

// src/wallet/db.cpp line 67bool CDBEnv::Open(const fs::path& pathIn){    if (fDbEnvInit)        return true;    boost::this_thread::interruption_point();    strPath = pathIn.string();    fs::path pathLogDir = pathIn / "database";    TryCreateDirectories(pathLogDir);    fs::path pathErrorFile = pathIn / "db.log";    LogPrintf("CDBEnv::Open: LogDir=%s ErrorFile=%s\n", pathLogDir.string(), pathErrorFile.string());    unsigned int nEnvFlags = 0;    if (gArgs.GetBoolArg("-privdb", DEFAULT_WALLET_PRIVDB))        nEnvFlags |= DB_PRIVATE;    dbenv->set_lg_dir(pathLogDir.string().c_str());    dbenv->set_cachesize(0, 0x100000, 1); // 1 MiB should be enough for just the wallet    dbenv->set_lg_bsize(0x10000);    dbenv->set_lg_max(1048576);    dbenv->set_lk_max_locks(40000);    dbenv->set_lk_max_objects(40000);    dbenv->set_errfile(fsbridge::fopen(pathErrorFile, "a")); /// debug    dbenv->set_flags(DB_AUTO_COMMIT, 1);    dbenv->set_flags(DB_TXN_WRITE_NOSYNC, 1);    dbenv->log_set_config(DB_LOG_AUTO_REMOVE, 1);    int ret = dbenv->open(strPath.c_str(),                         DB_CREATE |                             DB_INIT_LOCK |                             DB_INIT_LOG |                             DB_INIT_MPOOL |                             DB_INIT_TXN |                             DB_THREAD |                             DB_RECOVER |                             nEnvFlags,                         S_IRUSR | S_IWUSR);    if (ret != 0) {        dbenv->close(0);        return error("CDBEnv::Open: Error %d opening database environment: %s\n", ret, DbEnv::strerror(ret));    }    fDbEnvInit = true;    fMockDb = false;    return true;}

这个函数首先检查数据库文件是否存在,不存在就立即创建;然后设置日志文件,并且通过DbEnv类型变量指针dbenv设置了一系列数据库运行的相关参数,这些函数的介绍参考http://web.mit.edu/jhawk/mnt/spo/subversion/docs/api_cxx/c_index.html。

回到VerifyEnvironment函数, open函数执行成功的话,就直接返回true;否则就进入if语句,之后首先将原来的钱包数据库文件进行备份,然后再次尝试调用open函数,创建数据库文件。

恢复私钥

-salvgewallet:从损坏的钱包文件中尝试回复私钥。

接下来的一个if语句通过调用CWalletDB::Recover()转而调用CDB::Recover(),该函数的实现如下:

// src/wallet/db.cpp line 163bool CDB::Recover(const std::string& filename, void *callbackDataIn, bool (*recoverKVcallback)(void* callbackData, CDataStream ssKey, CDataStream ssValue), std::string& newFilename){    // Recovery procedure:    // move wallet file to walletfilename.timestamp.bak    // Call Salvage with fAggressive=true to    // get as much data as possible.    // Rewrite salvaged data to fresh wallet file    // Set -rescan so any missing transactions will be    // found.    int64_t now = GetTime();    newFilename = strprintf("%s.%d.bak", filename, now);    int result = bitdb.dbenv->dbrename(nullptr, filename.c_str(), nullptr,                                       newFilename.c_str(), DB_AUTO_COMMIT);    if (result == 0)        LogPrintf("Renamed %s to %s\n", filename, newFilename);    else    {        LogPrintf("Failed to rename %s to %s\n", filename, newFilename);        return false;    }    std::vector<CDBEnv::KeyValPair> salvagedData;    bool fSuccess = bitdb.Salvage(newFilename, true, salvagedData);    if (salvagedData.empty())    {        LogPrintf("Salvage(aggressive) found no records in %s.\n", newFilename);        return false;    }    LogPrintf("Salvage(aggressive) found %u records\n", salvagedData.size());    std::unique_ptr<Db> pdbCopy(new Db(bitdb.dbenv, 0));    int ret = pdbCopy->open(nullptr,               // Txn pointer                            filename.c_str(),   // Filename                            "main",             // Logical db name                            DB_BTREE,           // Database type                            DB_CREATE,          // Flags                            0);    if (ret > 0) {        LogPrintf("Cannot create database file %s\n", filename);        pdbCopy->close(0);        return false;    }    DbTxn* ptxn = bitdb.TxnBegin();    for (CDBEnv::KeyValPair& row : salvagedData)    {        if (recoverKVcallback)        {            CDataStream ssKey(row.first, SER_DISK, CLIENT_VERSION);            CDataStream ssValue(row.second, SER_DISK, CLIENT_VERSION);            if (!(*recoverKVcallback)(callbackDataIn, ssKey, ssValue))                continue;        }        Dbt datKey(&row.first[0], row.first.size());        Dbt datValue(&row.second[0], row.second.size());        int ret2 = pdbCopy->put(ptxn, &datKey, &datValue, DB_NOOVERWRITE);        if (ret2 > 0)            fSuccess = false;    }    ptxn->commit(0);    pdbCopy->close(0);    return fSuccess;}

此函数有四个参数,分别表示如下含义:

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

私钥恢复的步骤是首先备份原来的钱包文件,然后调用CDBEnv类中的Salvage函数,这个函数实现的功能是从文件中将公私钥读取出来并保存在到salvagedData中。恢复完之后就将恢复的数据写入到本地数据库中,这个写入的过程都是通过pdbCopy对象来进行的,同时如果定义了recoverKVcallback函数,那么还同时写入到callbackDataIn对象中,用于传给上层调用函数。

验证数据库文件

回到WalletVerify()函数,剩下最后一个函数VerifyDatabaseFile,这个函数又调用CDBEnv类中的Verify函数,这个函数的实现如下:

// src/wallet/db.cpp line 146CDBEnv::VerifyResult CDBEnv::Verify(const std::string& strFile, recoverFunc_type recoverFunc, std::string& out_backup_filename){    LOCK(cs_db);    assert(mapFileUseCount.count(strFile) == 0);    Db db(dbenv, 0);    int result = db.verify(strFile.c_str(), nullptr, nullptr, 0);    if (result == 0)        return VERIFY_OK;    else if (recoverFunc == nullptr)        return RECOVER_FAIL;    // Try to recover:    bool fRecovered = (*recoverFunc)(strFile, out_backup_filename);    return (fRecovered ? RECOVER_OK : RECOVER_FAIL);}

函数的主体又是调用Db中的Verify函数来验证数据库文件的完整性,这个函数的介绍可以参考http://web.mit.edu/jhawk/mnt/spo/subversion/docs/api_cxx/db_verify.html。如果验证通过的话那么就直接返回VERIFY_OK;否则就先看是否设置了恢复函数,如果没有就返回RECOVER_FAIL;如果设置了,那么就调用恢复函数,并返回恢复函数执行的结果。再回到原来的调用的地方可以发现恢复函数被设置成了CWalletDB::Recover函数,所以这里都会调用这个函数。