比特币源码剖析(十三)
本篇主要分析 Step 7: load block chain 第七步加载区块链的详细过程。
源码剖析
3.11.7.第七步,加载区块链到内存。这部分代码实现在“init.cpp”文件的 AppInit2(…) 函数中。
bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) // 3.11.程序初始化,共 12 步
{
...
// ********************************************************* Step 7: load block chain // 加载区块链数据(区块数据目录 .bitcoin/blocks/)
fReindex = GetBoolArg("-reindex", false); // 再索引标志(重新生成 rev 文件),默认关闭
// Upgrading to 0.8; hard-link the old blknnnn.dat files into /blocks/ // 1.升级到 0.8;硬链接旧的区块数据文件 blknnnn.dat 到 /blocks/ 目录下
boost::filesystem::path blocksDir = GetDataDir() / "blocks"; // 兼容老版的区块格式,区块文件扩容
if (!boost::filesystem::exists(blocksDir)) // 若该目录不存在
{
boost::filesystem::create_directories(blocksDir); // 则创建区块数据目录
bool linked = false;
for (unsigned int i = 1; i < 10000; i++) { // 遍历原区块数据文件
boost::filesystem::path source = GetDataDir() / strprintf("blk%04u.dat", i); // 旧版区块数据文件名
if (!boost::filesystem::exists(source)) break;
boost::filesystem::path dest = blocksDir / strprintf("blk%05u.dat", i-1); // 新版区块数据文件名,统一放在 blocks 目录下
try {
boost::filesystem::create_hard_link(source, dest); // 若存在旧版区块数据文件,则建立硬链接,以兼容新版
LogPrintf("Hardlinked %s -> %s\n", source.string(), dest.string());
linked = true; // 将链接标志设置为 true
} catch (const boost::filesystem::filesystem_error& e) {
// Note: hardlink creation failing is not a disaster, it just means
// blocks will get re-downloaded from peers.
LogPrintf("Error hardlinking blk%04u.dat: %s\n", i, e.what());
break;
}
}
if (linked) // 若建立了硬链接,则设置再索引标志为 true
{
fReindex = true;
}
}
// cache size calculations // 2.缓存大小计算
int64_t nTotalCache = (GetArg("-dbcache", nDefaultDbCache) << 20); // 总缓存大小
nTotalCache = std::max(nTotalCache, nMinDbCache << 20); // total cache cannot be less than nMinDbCache // 总缓存不能低于 nMinDbCache
nTotalCache = std::min(nTotalCache, nMaxDbCache << 20); // total cache cannot be greated than nMaxDbcache // 总缓存不能高于 nMaxDbcache
int64_t nBlockTreeDBCache = nTotalCache / 8; // 区块树数据库缓存大小
if (nBlockTreeDBCache > (1 << 21) && !GetBoolArg("-txindex", DEFAULT_TXINDEX))
nBlockTreeDBCache = (1 << 21); // block tree db cache shouldn't be larger than 2 MiB
nTotalCache -= nBlockTreeDBCache;
int64_t nCoinDBCache = std::min(nTotalCache / 2, (nTotalCache / 4) + (1 << 23)); // use 25%-50% of the remainder for disk cache // 币数据库缓存大小
nTotalCache -= nCoinDBCache;
nCoinCacheUsage = nTotalCache; // the rest goes to in-memory cache // 比缓存用量
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\n", nCoinCacheUsage * (1.0 / 1024 / 1024));
bool fLoaded = false; // 加载标志,表示加载区块索引是否成功,初始为 false
while (!fLoaded) { // 3.若第一次没有加载成功,再加载一遍
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, fReindex); // 区块索引
pcoinsdbview = new CCoinsViewDB(nCoinDBCache, false, fReindex);
pcoinscatcher = new CCoinsViewErrorCatcher(pcoinsdbview);
pcoinsTip = new CCoinsViewCache(pcoinscatcher);
if (fReindex) { // 默认 false
pblocktree->WriteReindexing(true); // 3.1.写入再索引标志为 true(区块数据库 leveldb)
//If we're reindexing in prune mode, wipe away unusable block files and all undo data files
if (fPruneMode) // 如果我们在修剪模式(修剪已确认的区块)下进行再索引,
CleanupBlockRevFiles(); // 清空无用的块文件(blk)和所有恢复数据文件(rev)
}
if (!LoadBlockIndex()) { // 3.2.从磁盘加载区块索引树和币数据库
strLoadError = _("Error loading block database");
break;
}
// If the loaded chain has a wrong genesis, bail out immediately // 如果加载的链的创世区块错误,马上补救
// (we're likely using a testnet datadir, or the other way around). // (我们可能使用测试网的数据目录,或者相反)。
if (!mapBlockIndex.empty() && mapBlockIndex.count(chainparams.GetConsensus().hashGenesisBlock) == 0) // 检查 mapBlockIndex 是否为空,且是否加载了创世区块索引(通过哈希查找)
return InitError(_("Incorrect or no genesis block found. Wrong datadir for network?"));
// Initialize the block index (no-op if non-empty database was already loaded) // 初始化区块索引(如果非空数据库已经加载则无操作)
if (!InitBlockIndex(chainparams)) { // 3.3.初始化区块索引到磁盘
strLoadError = _("Error initializing block database");
break;
}
// Check for changed -txindex state // 检查 -txindex 改变的状态
if (fTxIndex != GetBoolArg("-txindex", DEFAULT_TXINDEX)) { // 检查 fTxIndex 标志,在 LoadBlockIndex 函数中可能被改变
strLoadError = _("You need to rebuild the database using -reindex to change -txindex");
break;
}
// Check for changed -prune state. What we are concerned about is a user who has pruned blocks // 检查 -prune 改变的状态。我们关注的时过去曾修剪过的区块,
// in the past, but is now trying to run unpruned. // 但现在尝试运行未修剪过的区块。
if (fHavePruned && !fPruneMode) { // 检查 fHavePruned 标志,用户删了一些文件后,又先在未修剪模式中运行
strLoadError = _("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain");
break;
}
uiInterface.InitMessage(_("Verifying blocks...")); // 开始验证区块
if (fHavePruned && GetArg("-checkblocks", DEFAULT_CHECKBLOCKS) > MIN_BLOCKS_TO_KEEP) { // pending
LogPrintf("Prune: pruned datadir may not have more than %d blocks; -checkblocks=%d may fail\n",
MIN_BLOCKS_TO_KEEP, GetArg("-checkblocks", DEFAULT_CHECKBLOCKS));
}
{
LOCK(cs_main);
CBlockIndex* tip = chainActive.Tip(); // 获取激活的链尖区块索引
if (tip && tip->nTime > GetAdjustedTime() + 2 * 60 * 60) { // 链尖区块时间不能比当前时间快 2h
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, GetArg("-checklevel", DEFAULT_CHECKLEVEL),
GetArg("-checkblocks", DEFAULT_CHECKBLOCKS))) { // 验证数据库,验证等级默认 3,验证块数默认 288
strLoadError = _("Corrupted block database detected");
break;
}
} catch (const std::exception& e) {
if (fDebug) LogPrintf("%s\n", e.what());
strLoadError = _("Error opening block database");
break;
}
fLoaded = true; // 加载成功
} while(false);
if (!fLoaded) { // 3.4.若加载失败
// first suggest a reindex // 首次建议再索引
if (!fReset) { // =fReindex
bool fRet = uiInterface.ThreadSafeMessageBox(
strLoadError + ".\n\n" + _("Do you want to rebuild the block database now?"),
"", CClientUIInterface::MSG_ERROR | CClientUIInterface::BTN_ABORT); // 弹出交互框,针对 qt
if (fRet) {
fReindex = true; // 再索引标志置为 true,下次再加载区块索引
fRequestShutdown = false; // 请求关闭标志置为 false
} else {
LogPrintf("Aborted block database rebuild. Exiting.\n");
return false;
}
} else {
return InitError(strLoadError);
}
}
} // end of while load
// As LoadBlockIndex can take several minutes, it's possible the user // LoadBlockIndex 会花几分钟,在最后一次操作期间,用户可能请求关闭 GUI。
// requested to kill the GUI during the last operation. If so, exit. // 如此,便退出。
// As the program has not fully started yet, Shutdown() is possibly overkill. // 问题是还未完全启动,Shutdown() 可能杀伤力过大。
if (fRequestShutdown) // 若用户在加载区块期间请求关闭
{
LogPrintf("Shutdown requested. Exiting.\n");
return false; // 不调用 Shutdown() 直接退出
}
LogPrintf(" block index %15dms\n", GetTimeMillis() - nStart); // 记录区块索引时间
boost::filesystem::path est_path = GetDataDir() / FEE_ESTIMATES_FILENAME; // 拼接费用估计文件路径
CAutoFile est_filein(fopen(est_path.string().c_str(), "rb"), SER_DISK, CLIENT_VERSION); // 打开(首次创建)该文件并创建估费文件对象
// Allowed to fail as this file IS missing on first startup. // 允许失败,因为首次启动时该文件不存在。
if (!est_filein.IsNull()) // 若该文件存在
mempool.ReadFeeEstimates(est_filein); // 内存池读取估计费用
fFeeEstimatesInitialized = true; // 费用估计初始化状态标志置为 true
...
}
1.兼容旧版客户端,创建区块数据文件的硬链接。
2.计算各部分缓存大小。
3.加载区块链。
3.1.写入再索引标志到区块数据库,并清空无用的区块文件和全部恢复文件。
3.2.加载区块索引。
3.3.初始化区块索引到磁盘上。
3.4.区块链加载失败处理。
4.退出处理,这里不调用 Shutdown() 直接退出。
5.费用估计。
3.1.先调用 pblocktree->WriteReindexing(true) 把再索引标志写入区块数据库(leveldb), 该函数声明在“txdb.h”文件的 CblockTreeDB 类中。
/** Access to the block database (blocks/index/) */ // 访问区块数据库(/blocks/index)
class CBlockTreeDB : public CDBWrapper
{
...
bool WriteReindexing(bool fReindex); // 写入再索引标志
...
};
实现在“txdb.cpp”文件中,入参为:true。
static const char DB_REINDEX_FLAG = 'R';
...
bool CBlockTreeDB::WriteReindexing(bool fReindexing) { // true
if (fReindexing)
return Write(DB_REINDEX_FLAG, '1'); // 'R'
else
return Erase(DB_REINDEX_FLAG);
}
再调用 CleanupBlockRevFiles() 函数删除全部的 rev 文件和无用的区块(blk)文件, 该函数定义再“init.cpp”文件中。
// If we're using -prune with -reindex, then delete block files that will be ignored by the
// reindex. Since reindexing works by starting at block file 0 and looping until a blockfile
// is missing, do the same here to delete any later block files after a gap. Also delete all
// rev files since they'll be rewritten by the reindex anyway. This ensures that vinfoBlockFile
// is in sync with what's actually on disk by the time we start downloading, so that pruning
// works correctly. // 如果我们同时使用 -prune 和 -reindex,然后删除将被再索引忽略的区块文件。因为再索引的工作原理是从区块文件 0 开始循环知道一个区块文件丢失,所以在此执行相同的操作来删除丢失文件后续的区块文件。同时删除所有恢复文件,因为它们将通过再索引重写。这确保了区块文件与我们开始下载时实际在磁盘上的内容同步,因此修剪工作正常。
void CleanupBlockRevFiles() // 删除某个缺失区块之后的所有区块数据,和前缀为 rev 的文件
{
using namespace boost::filesystem;
map<string, path> mapBlockFiles; // <区块文件索引(?????), 区块文件路径(path)>
// Glob all blk?????.dat and rev?????.dat files from the blocks directory. // 从区块目录全部区块和恢复数据文件。
// Remove the rev files immediately and insert the blk file paths into an // 立刻移除恢复文件并把区块文件路径
// ordered map keyed by block file index. // 插入一个键为区块文件索引的有序映射列表中。
LogPrintf("Removing unusable blk?????.dat and rev?????.dat files for -reindex with -prune\n");
path blocksdir = GetDataDir() / "blocks"; // 拼接区块数据目录
for (directory_iterator it(blocksdir); it != directory_iterator(); it++) { // 遍历区块目录下的文件(directory_iterator 默认构造函数,指向目录尾部)
if (is_regular_file(*it) && // 如果是普通文件,且
it->path().filename().string().length() == 12 && // 文件名的长度为 12,且
it->path().filename().string().substr(8,4) == ".dat") // 后 4 个字符为 ".dat"
{ // 文件校验(包括文件名,文件格式)
if (it->path().filename().string().substr(0,3) == "blk") // 若为区块文件
mapBlockFiles[it->path().filename().string().substr(3,5)] = it->path(); // 把区块文件索引与该文件路径配对插入去快文件映射列表中
else if (it->path().filename().string().substr(0,3) == "rev") // 若为恢复文件
remove(it->path()); // 移除 rev 文件
}
}
// Remove all block files that aren't part of a contiguous set starting at // 通过维持单独的计数器,
// zero by walking the ordered map (keys are block file indices) by // 遍历有序映射列表(键为区块文件索引)
// keeping a separate counter. Once we hit a gap (or if 0 doesn't exist) // 删除所有不属于从 0 开始的连续块文件
// start removing block files. // 一旦我们抵达间断的区块(或 0 不存在),则开始删除区块文件。
int nContigCounter = 0; // 检查缺失的 blk 文件,删除缺失的 blk 后的所有 blk 文件
BOOST_FOREACH(const PAIRTYPE(string, path)& item, mapBlockFiles) {
if (atoi(item.first) == nContigCounter) { // 从 0 开始
nContigCounter++; // 若文件连续,计数器加 1
continue; // 跳过该文件,比较下一个文件
} // 否则
remove(item.second); // 从该文件开始删除后面所有的文件
}
}
3.2.调用 LoadBlockIndex() 加载区块索引,该函数声明在“main.h”文件中。
/** Load the block tree and coins database from disk */
bool LoadBlockIndex(); // 从磁盘加载区块树和币的数据库
实现“main.cpp”文件中,没有入参。
bool static LoadBlockIndexDB()
{
const CChainParams& chainparams = Params(); // 获取网络链参数
if (!pblocktree->LoadBlockIndexGuts()) // 区块树加载区块索引框架
return false;
boost::this_thread::interruption_point(); // 打个断点
// Calculate nChainWork // 计算链工作量
vector<pair<int, CBlockIndex*> > vSortedByHeight; // 通过高度排序的有序区块高度索引映射列表
vSortedByHeight.reserve(mapBlockIndex.size()); // 预开辟与区块索引映射列表等大的空间
BOOST_FOREACH(const PAIRTYPE(uint256, CBlockIndex*)& item, mapBlockIndex) // 遍历区块索引映射列表
{
CBlockIndex* pindex = item.second; // 获取区块索引
vSortedByHeight.push_back(make_pair(pindex->nHeight, pindex)); // 与区块高度配对加入待排序列表
}
sort(vSortedByHeight.begin(), vSortedByHeight.end()); // 按高度排序
BOOST_FOREACH(const PAIRTYPE(int, CBlockIndex*)& item, vSortedByHeight) // 遍历有序的区块索引映射列表
{
CBlockIndex* pindex = item.second; // 获取区块索引
pindex->nChainWork = (pindex->pprev ? pindex->pprev->nChainWork : 0) + GetBlockProof(*pindex); // 若该区块的前一个区块存在,则获取前一个区块的链工作量,再加上该区块的工作量证明
// We can link the chain of blocks for which we've received transactions at some point. // 我们可以连接到区块链用于接收某些节点的交易。
// Pruned nodes may have deleted the block. // 修剪节点可能会删除区块。
if (pindex->nTx > 0) { // 若该区块的交易数大于 0
if (pindex->pprev) {
if (pindex->pprev->nChainTx) { // 如果前一个区块存在链交易
pindex->nChainTx = pindex->pprev->nChainTx + pindex->nTx; // 用前一个区块的链交易 + 该区块的交易 得到该区块的链交易
} else { // 否则
pindex->nChainTx = 0; // 该区块的链交易为 0
mapBlocksUnlinked.insert(std::make_pair(pindex->pprev, pindex)); // 与前一个区块的索引配对插入未连接的区块映射列表
}
} else { // 否则
pindex->nChainTx = pindex->nTx; // 区块的链交易数等于区块的交易数
}
}
if (pindex->IsValid(BLOCK_VALID_TRANSACTIONS) && (pindex->nChainTx || pindex->pprev == NULL)) // 若该区块交易有效,且有链交易,或前一个区块不存在
setBlockIndexCandidates.insert(pindex); // 插入区块索引候选集
if (pindex->nStatus & BLOCK_FAILED_MASK && (!pindexBestInvalid || pindex->nChainWork > pindexBestInvalid->nChainWork)) // 若区块状态为 BLOCK_FAILED_MASK,且最佳无效区块为空,或链工作大于最佳无效区块
pindexBestInvalid = pindex; // 该区块为最佳无效区块
if (pindex->pprev) // 若前一个区块存在
pindex->BuildSkip();
if (pindex->IsValid(BLOCK_VALID_TREE) && (pindexBestHeader == NULL || CBlockIndexWorkComparator()(pindexBestHeader, pindex)))
pindexBestHeader = pindex; // 该区块索引为最佳区块头索引
}
// Load block file info // 加载区块文件信息
pblocktree->ReadLastBlockFile(nLastBlockFile); // 读取最后一个区块文件
vinfoBlockFile.resize(nLastBlockFile + 1); // 预开辟相同的空间
LogPrintf("%s: last block file = %i\n", __func__, nLastBlockFile);
for (int nFile = 0; nFile <= nLastBlockFile; nFile++) { // 遍历区块文件
pblocktree->ReadBlockFileInfo(nFile, vinfoBlockFile[nFile]); // 读区块文件信息
}
LogPrintf("%s: last block file info: %s\n", __func__, vinfoBlockFile[nLastBlockFile].ToString());
for (int nFile = nLastBlockFile + 1; true; nFile++) { // 从最后一个文件号加 1 开始
CBlockFileInfo info;
if (pblocktree->ReadBlockFileInfo(nFile, info)) { // 读取
vinfoBlockFile.push_back(info); // 并加入区块文件信息列表
} else { // 若读取失败(文件不存在)
break; // 跳出
}
}
// Check presence of blk files // 检查区块文件是否存在
LogPrintf("Checking all blk files are present...\n");
set<int> setBlkDataFiles; // 用于保存所有区块数据文件的序号
BOOST_FOREACH(const PAIRTYPE(uint256, CBlockIndex*)& item, mapBlockIndex) // 遍历区块索引映射列表
{
CBlockIndex* pindex = item.second; // 获取区块索引
if (pindex->nStatus & BLOCK_HAVE_DATA) { // 若区块状态为 BLOCK_HAVE_DATA
setBlkDataFiles.insert(pindex->nFile); // 把该区块所在文件号插入区块数据文件集合中
}
}
for (std::set<int>::iterator it = setBlkDataFiles.begin(); it != setBlkDataFiles.end(); it++) // 遍历区块数据文件集合
{
CDiskBlockPos pos(*it, 0); // 创建磁盘区块位置对象
if (CAutoFile(OpenBlockFile(pos, true), SER_DISK, CLIENT_VERSION).IsNull()) { // 创建文件指针托管临时对象
return false;
}
}
// Check whether we have ever pruned block & undo files // 检查我们是否曾修剪过区块和恢复文件
pblocktree->ReadFlag("prunedblockfiles", fHavePruned); // 读取修剪区块文件标志
if (fHavePruned)
LogPrintf("LoadBlockIndexDB(): Block files have previously been pruned\n");
// Check whether we need to continue reindexing // 检查我们是否需要继续再索引
bool fReindexing = false;
pblocktree->ReadReindexing(fReindexing); // 读取再索引标志
fReindex |= fReindexing;
// Check whether we have a transaction index // 检查我们是否有交易索引
pblocktree->ReadFlag("txindex", fTxIndex); // 读取交易索引标志
LogPrintf("%s: transaction index %s\n", __func__, fTxIndex ? "enabled" : "disabled");
// Load pointer to end of best chain // 加载指向最佳链尾部的指针
BlockMap::iterator it = mapBlockIndex.find(pcoinsTip->GetBestBlock()); // 获取最佳区块的索引
if (it == mapBlockIndex.end()) // 若未找到
return true; // 直接返回 true
chainActive.SetTip(it->second); // 若存在,则设置该区块索引为激活链的链尖(放入区块索引列表中)
PruneBlockIndexCandidates(); // 修剪区块索引候选
LogPrintf("%s: hashBestChain=%s height=%d date=%s progress=%f\n", __func__,
chainActive.Tip()->GetBlockHash().ToString(), chainActive.Height(),
DateTimeStrFormat("%Y-%m-%d %H:%M:%S", chainActive.Tip()->GetBlockTime()),
Checkpoints::GuessVerificationProgress(chainparams.Checkpoints(), chainActive.Tip()));
return true; // 加载成功返回 true
}
...
bool LoadBlockIndex()
{
// Load block index from databases // 从数据库加载区块索引
if (!fReindex && !LoadBlockIndexDB()) // 若未开启再索引,则加载区块索引数据库,否则不加载,在后面会重新索引
return false;
return true; // 加载成功返回 true
}
3.3.调用 InitBlockIndex(chainparams) 初始化区块树/索引数据库到磁盘,该函数声明在“main.h”文件中。
/** Initialize a new block tree database + block data on disk */
bool InitBlockIndex(const CChainParams& chainparams); // 初始化一个新的区块树数据库+区块数据到磁盘
实现在“main.cpp”文件中,入参为:链参数对象的引用。
bool InitBlockIndex(const CChainParams& chainparams)
{
LOCK(cs_main); // 线程安全锁
// Initialize global variables that cannot be constructed at startup.
recentRejects.reset(new CRollingBloomFilter(120000, 0.000001)); // 初始化不能再启动时创建的全局对象
// Check whether we're already initialized
if (chainActive.Genesis() != NULL) // 检查是否初始化了创世区块索引
return true;
// Use the provided setting for -txindex in the new database // 在新数据库中对 -txindex 使用提供的设置
fTxIndex = GetBoolArg("-txindex", DEFAULT_TXINDEX); // 先获取默认设置
pblocktree->WriteFlag("txindex", fTxIndex); // 写入数据库
LogPrintf("Initializing databases...\n");
// Only add the genesis block if not reindexing (in which case we reuse the one already on disk)
if (!fReindex) { // 如果不再索引只添加创世区块(此时我们重用磁盘上已存在的创世区块所在的区块文件)
try {
CBlock &block = const_cast<CBlock&>(chainparams.GenesisBlock()); // 获取创世区块的引用
// Start new block file // 开始新的区块文件
unsigned int nBlockSize = ::GetSerializeSize(block, SER_DISK, CLIENT_VERSION); // 获取序列化大小
CDiskBlockPos blockPos;
CValidationState state;
if (!FindBlockPos(state, blockPos, nBlockSize+8, 0, block.GetBlockTime())) // 获取区块状态和位置
return error("LoadBlockIndex(): FindBlockPos failed");
if (!WriteBlockToDisk(block, blockPos, chainparams.MessageStart())) // 把区块信息(状态和位置)写到磁盘
return error("LoadBlockIndex(): writing genesis block to disk failed");
CBlockIndex *pindex = AddToBlockIndex(block); // 添加到区块索引
if (!ReceivedBlockTransactions(block, state, pindex, blockPos)) // 接收区块交易
return error("LoadBlockIndex(): genesis block not accepted");
if (!ActivateBestChain(state, chainparams, &block)) // 激活最佳链
return error("LoadBlockIndex(): genesis block cannot be activated");
// Force a chainstate write so that when we VerifyDB in a moment, it doesn't check stale data // 强制把链状态写入磁盘,以至于当我们一段时间内验证数据库时,不会检查旧数据
return FlushStateToDisk(state, FLUSH_STATE_ALWAYS);
} catch (const std::runtime_error& e) {
return error("LoadBlockIndex(): failed to initialize block database: %s", e.what());
}
}
return true; // 成功返回 true
}