美文网首页
bitcoin数据存储

bitcoin数据存储

作者: wolf4j | 来源:发表于2018-07-27 14:09 被阅读396次

bitcoin数据存储

在比特币业务中需要存储的数据主要分为下面四种:

  1. blocks/blk*.dat: 比特币中的blocks,会通过网络的形式dumped到disk上。一般他们在以下情况中被使用:重新扫描wallet中丢失的交易;链的重组;将blocks的数据提供给其它正在同步的节点。这里是直接写文件的。
  2. blocks/index/*:这里主要存储一些blocks的元数据信息以及blocks在disk上的pos信息,这些信息会存储到LevelDB中。
  3. chainstate/*:这里主要存储当前还没有花费的所有的交易输出以及交易的元数据信息都会在这里存储,在存储的时候会做一些简单的压缩。存储chainstate的数据主要是用来去验证新进来的blocks和tx是否是合法的。如果没有这个操作,就意味着对于每一个被花费的out你都需要去进行全表扫描来验证。这个也是存储到LevelDB上。
  4. blocks/rev*.dat:这个是直接写文件的。存储的是一些undo的blocks数据(其实还是存储的blocks数据),这里的作用和mysql中的undo log一致,用于“出错”后的错误恢复。(这里的出错需要做特殊理解)
image.png

个人理解:比特币中使用LevelDB的冗余存储方案来存储数据,更像拿空间来换时间。

磁盘空间究竟需要多大呢?

static const uint64_t MIN_DISK_SPACE_FOR_BLOCK_FILES = 550 * 1024 * 1024;

blk????.dat和rev????.dat加起来至少为550MB

计算过程:
  1. 每一个block的大小为1MB,288 blocks= 288MB。
  2. 为undo data 添加15%= 331MB
  3. 为孤块增加20%= 397MB

在我们进行删除操作的时候,需要考虑两个点,一个是最少需要的空间是孤块的那397MB,因为这个点你没办法避免,必须预留出来这么多大小来存储孤块;另一个点我们需要去考量block file的那128MB和undo data的147MB的数据,加起来总共是545MB,所以这里设置了至少需要550MB的磁盘空间。

blocks为什么是288呢?

static const unsigned int MIN_BLOCKS_TO_KEEP = 288;

尴尬的是,这里并没有具体的原因,但是为了避免最长链的切换,数据的回滚等操作,这里暂存了两天的数据量(一天是144个blocks)。要求你至少保存距离当前最长链的tip点往后推288个blocks的数据。

Raw Block data (blk*.dat)

blocks最后存储的blk*.dat的文件是通过网络接收的。

block files大约为128 MB,为了防止过多的碎片,以16MB为一个chunk进行分配。截至2015年10月,block chain存储了大约365个块文件中,总共大约45 GB。

每个block文件(blk1234.dat)都有一个相应的undo block文件(rev1234.dat),其中包含在重组(fork)时从blockchain中删除block所需的数据。

有关block file的信息存储是使用block index的两个前缀字符来标识,这些数据最后会存到Leveldb中:

  • 文件本身的一些数据是使用block index中的“f”前缀来存储到leveledb中(key:“fxxxx”,其中“xxxx”是4位数文件编号),包括:

    • 存储在文件中的blocks的数量
    • 文件大小(以及相应的undos文件大小)
    • 文件中的最低和最高block
    • 时间戳 - 文件中的earlier and latest blocks
  • 使用block index中的前缀“b”(“b”=block)来标识磁盘上特定块的位置的信息:

    • 每个块包含一个block在disk上所处位置的指针(文件号和偏移量)

代码如何访问Block data

block file通过以下方式访问:

  1. DiskBlockPos:一个struct,是一个指针,用来指向block在磁盘上的位置的(文件号和偏移量。)

chain.cpp

struct CDiskBlockPos {
    int nFile;
    unsigned int nPos;
}
  1. vInfoBlockFiles:一个BlockFileInfo对象的vector。此变量的具体功能如下:

chain.cpp

class CBlockFileInfo {
public:
    //!< number of blocks stored in file; 文件中区块的数量
    unsigned int nBlocks;
    //!< number of used bytes of block file 区块文件中的字节数
    unsigned int nSize;
    //!< number of used bytes in the undo file  undo文件中的字节数
    unsigned int nUndoSize;
    //!< lowest height of block in file  该文件中最低的区块号
    unsigned int nHeightFirst;
    //!< highest height of block in file    该文件中最高的区块号
    unsigned int nHeightLast;
    //!< earliest time of block in file     该文件中最早的区块时间
    uint64_t nTimeFirst;
    //!< latest time of block in file       该文件中最后的区块时间
    uint64_t nTimeLast;
}

validation.cpp

std::vector<CBlockFileInfo> vinfoBlockFile;
  • 确定新块是否适合当前文件或是否需要创建新文件(也就是来一个新block看当前这个文件能不能存的下,存不下就重新创建一个文件来存储)
  • 通过block和undo file计算磁盘使用总量(这个计算并不适合go这种带GC的语言,因为它没办法保证某一个对象具体是分配在堆上还是栈上,GC语言的封装导致了我们无法对内存的使用做精准的控制)
  • 迭代block files并找到可以pruned的块文件(这个类似一个优化操作,比如:你申请了10GB的空间,现在马上存储到10GB这个数量级,它会将之前的一些blocks从这个文件中移除,为什么可以删除?因为之前的blocks已经是相对稳定的数据,并且你的utxo已经全部加载过来,如果由于某种极其特殊的原因你需要这些blocks数据,仍然可以从全网再拉取回来,所以是可以删除的)

在AcceptBlock中,块一接收就会写入磁盘。

validation.cpp AcceptBlock

/**
 * Store block on disk. If dbp is non-null, the file is known to already reside
 * on disk.
 * 将区块数据存储在磁盘上。 如果dbp非空,这个文件是已经在磁盘上的文件
 * pblock(in) 将写入磁盘的块,state(out):写入时的状态,pindex(out): 创建该区块的索引,
 * fRequested(in):是否要求强制处理该区块;当块不是来自网络,或来自白名单节点的块数据
 * dbp(in): 块写入磁盘的位置;存在时,将要写入文件已知; (对于一个新块,该参数为nil)
 * fNewBlock(out):该区块是否为新块
 */
static bool AcceptBlock(const Config &config,
                        const std::shared_ptr<const CBlock> &pblock,
                        CValidationState &state, CBlockIndex **ppindex,
                        bool fRequested, const CDiskBlockPos *dbp,
                        bool *fNewBlock) {
    AssertLockHeld(cs_main);

    const CBlock &block = *pblock;
    if (fNewBlock) {
        *fNewBlock = false;
    }

    CBlockIndex *pindexDummy = nullptr;
    CBlockIndex *&pindex = ppindex ? *ppindex : pindexDummy;
    // pindex(out)  block(in)
    //1. 接收块头,出错,退出。
    if (!AcceptBlockHeader(config, block, state, &pindex)) {
        return false;
    }

    // Try to process all requested blocks that we don't have, but only
    // process an unrequested block if it's new and has enough work to
    // advance our tip, and isn't too many blocks ahead.
    // 此处对块的处理分为两类:我们请求的;不是我们请求的。
    // 尝试处理所有的我们请求的块;
    // 对于未请求的块,只有当它是新块,且比当前链的Tip含有更多的工作量,
    // 且该块不是太超前的块,满足上述三个请求,才会处理该块。
    //2. 查看是否节点已经含有了该块的数据; 是否该块的工作量大于当前主链的工作量。
    // 是否该块太超前。
    bool fAlreadyHave = pindex->nStatus & BLOCK_HAVE_DATA;
    bool fHasMoreWork =
        (chainActive.Tip() ? pindex->nChainWork > chainActive.Tip()->nChainWork
                           : true);
    // Blocks that are too out-of-order needlessly limit the effectiveness of
    // pruning, because pruning will not delete block files that contain any
    // blocks which are too close in height to the tip.  Apply this test
    // regardless of whether pruning is enabled; it should generally be safe to
    // not process unrequested blocks.
    // 太过乱序的块限制了裁剪的效率,因为裁剪不会删除包含距离激活链Tip 太近的区块文件。无论是否裁剪,
    // 都进行该测试,当不处理未请求的块,一般是安全的。
    bool fTooFarAhead =
        (pindex->nHeight > int(chainActive.Height() + MIN_BLOCKS_TO_KEEP));

    // TODO: Decouple this function from the block download logic by removing
    // fRequested
    // This requires some new chain datastructure to efficiently look up if a
    // block is in a chain leading to a candidate for best tip, despite not
    // being such a candidate itself.
    // 通过从本函数的参数列表中移除 fRequested,来从块的下载逻辑解耦这个函数。

    // TODO: deal better with return value and error conditions for duplicate
    //3. and unrequested blocks.  这个块已存在,返回TRUE
    if (fAlreadyHave) {
        return true;
    }

    // If we didn't ask for it:
    //4. 如果没有请求这个接收的块,进入下列条件
    if (!fRequested) {
        // This is a previously-processed block that was pruned.
        // 是一个已被处理但又被裁减的区块。因为nTx状态只有在块被完整验证和接收后,才会进行赋值。
        if (pindex->nTx != 0) {
            return true;
        }

        // Don't process less-work chains.
        // 没有足够的工作量,不处理
        if (!fHasMoreWork) {
            return true;
        }

        // Block height is too high.
        // 该块太靠前,不处理
        if (fTooFarAhead) {
            return true;
        }
    }

    //5. 赋值为新块
    if (fNewBlock) {
        *fNewBlock = true;
    }

    const CChainParams &chainparams = config.GetChainParams();
    //6. 检查块的版本号,交易成熟度,以及coinbase的签名脚本(在BIP34之后,前几个字节必须为块高。)
    if (!CheckBlock(config, block, state, chainparams.GetConsensus()) ||
        !ContextualCheckBlock(config, block, state, chainparams.GetConsensus(),
                              pindex->pprev)) {
        if (state.IsInvalid() && !state.CorruptionPossible()) {
            pindex->nStatus |= BLOCK_FAILED_VALID;
            setDirtyBlockIndex.insert(pindex);
        }
        return error("%s: %s (block %s)", __func__, FormatStateMessage(state),
                     block.GetHash().ToString());
    }

    // Header is valid/has work, merkle tree and segwit merkle tree are
    // good...RELAY NOW (but if it does not build on our best tip, let the
    // SendMessages loop relay it)
    //7. 如果当前节点状态不在下载状态,且这个新块为当前节点的Tip块,那么向外中继该块。
    if (!IsInitialBlockDownload() && chainActive.Tip() == pindex->pprev) {
        GetMainSignals().NewPoWValidBlock(pindex, pblock);
    }

    int nHeight = pindex->nHeight;

    // Write block to history file
    //8. 写块数据至文件
    try {
        //8.1 获取块的序列化后的大小
        unsigned int nBlockSize =
            ::GetSerializeSize(block, SER_DISK, CLIENT_VERSION);
        CDiskBlockPos blockPos;
        if (dbp != nullptr) {
            blockPos = *dbp;
        }
        //2. 查找写该区块在文件中的位置
        if (!FindBlockPos(state, blockPos, nBlockSize + 8, nHeight,
                          block.GetBlockTime(), dbp != nullptr)) {
            return error("AcceptBlock(): FindBlockPos failed");
        }
        //3. 将块和交易数据写文件;将block和报头信息写入文件。(报头信息用来表示:当前的块是属于哪条链:主链,测试链,regtest)
        if (dbp == nullptr) {
            if (!WriteBlockToDisk(block, blockPos,
                                  chainparams.MessageStart())) {
                AbortNode(state, "Failed to write block");
            }
        }
        //4. 修改块的索引状态;标识已接受到一个块的所有交易信息
        if (!ReceivedBlockTransactions(block, state, pindex, blockPos)) {
            return error("AcceptBlock(): ReceivedBlockTransactions failed");
        }
    } catch (const std::runtime_error &e) {
        return AbortNode(state, std::string("System error: ") + e.what());
    }
    //5. 刷新数据到磁盘
    if (fCheckForPruning) {
        // we just allocated more disk space for block files.
        FlushStateToDisk(state, FLUSH_STATE_NONE);
    }

    return true;
}

实际触发磁盘的写操作在WriteBlockToDisk 中实现。

validation.cpp WriteBlockToDisk

//将块和报头标识符 写入文件
//block(in):将要写入的区块; pos(in/out):赋值块写入信息的起始位置;messagestart(in):写入的报头的信息
bool WriteBlockToDisk(const CBlock &block, CDiskBlockPos &pos,
                      const CMessageHeader::MessageStartChars &messageStart) {
    // Open history file to append
    CAutoFile fileout(OpenBlockFile(pos), SER_DISK, CLIENT_VERSION);
    if (fileout.IsNull())
        return error("WriteBlockToDisk: OpenBlockFile failed");

    // Write index header       计算块的大小
    unsigned int nSize = GetSerializeSize(fileout, block);
    fileout << FLATDATA(messageStart) << nSize;     //将报头信息 和 字节大小写入文件

    // Write block      写区块
    long fileOutPos = ftell(fileout.Get());     //获取文件此时的偏移位置
    if (fileOutPos < 0) return error("WriteBlockToDisk: ftell failed");
    pos.nPos = (unsigned int)fileOutPos;
    fileout << block;

    return true;
}

Block index (leveldb)

block index涵盖了所有已知块的元数据,包括 block 存储在磁盘上的位置。

注意:“已知blocks”集是最长链的超集,因为它包含已接收和处理但不属于active chain的blocks。例如,在链的重组过程中与active chain分离的orphaned blocks

a) Block Tree

使用block tree来描述存储在磁盘上的已知的blocks信息可能更合适一点,因为区块链并不是一个单纯的单链表,而是基于主链的许多分支结构,看起来更像一个tree。代码实现主要是通过CBlockTreeDB这个包装类来进行访问。注意:短暂的分叉是完全没问题的,不同的节点会有稍微不同的block tree,关键看他们是不是都认同这个active chain

txdb.h

/** Access to the block database (blocks/index/) */
class CBlockTreeDB : public CDBWrapper {
public:
    CBlockTreeDB(size_t nCacheSize, bool fMemory = false, bool fWipe = false);

private:
    CBlockTreeDB(const CBlockTreeDB &);
    void operator=(const CBlockTreeDB &);

public:
    bool WriteBatchSync(
        const std::vector<std::pair<int, const CBlockFileInfo *>> &fileInfo,
        int nLastFile, const std::vector<const CBlockIndex *> &blockinfo);
    bool ReadBlockFileInfo(int nFile, CBlockFileInfo &fileinfo);
    bool ReadLastBlockFile(int &nFile);
    bool WriteReindexing(bool fReindex);
    bool ReadReindexing(bool &fReindex);
    bool ReadTxIndex(const uint256 &txid, CDiskTxPos &pos);
    bool WriteTxIndex(const std::vector<std::pair<uint256, CDiskTxPos>> &list);
    bool WriteFlag(const std::string &name, bool fValue);
    bool ReadFlag(const std::string &name, bool &fValue);
    bool LoadBlockIndexGuts(
        std::function<CBlockIndex *(const uint256 &)> insertBlockIndex);
};

NODE:这里简单说明一下CDBWrapper,在bitcoin中,数据并没有直接存leveldb,而是用CDBWrapper对leveldb进行来一个简单的封装。具体详见我的;另一篇文章https://www.jianshu.com/p/c3aed25762d0

Key-value pairs

数据存储到LevelDB中,使用的key/value是:

在LevelDB中,使用的键/值对解释如下:

'b' + 32 字节的 block hash -> 记录块索引,每个记录存储:
* 块头(block header)
* 高度(height)
* 交易的数量
* 这个块在多大程度上被验证
* 块数据被存储在哪个文件中
* undo data 被存储在哪个文件中。
'f' + 4 字节的文件编号 -> 记录文件信息。每个记录存储:
* 存储在具有该编号的块文件中的块的数量
* 具有该编号的块文件的大小($ DATADIR / blocks / blkNNNNN.dat)
* 具有该编号的撤销文件的大小($ DATADIR / blocks / revNNNNN.dat)。
* 使用该编号存储在块文件中的块的最低和最高高度。
* 使用该编号存储在块文件中的块的最小和最大时间戳。
'l' - > 4个字节的文件号:使用的最后一个块文件号。
'R' - > 1字节布尔值(如果为“1”):是否处于重新索引过程中。
'F'+ 1个字节的标志名长度+标志名字符串 - > 1个字节布尔型('1'为真,'0'为假):可以打开或关闭的各种标志。 目前定义的标志包括:
 * 'txindex':是否启用事务索引。
't'+ 32字节的交易 hash - >记录交易索引。 这些是可选的,只有当'txindex'被启用时才存在。 每个记录存储:
 * 交易存储在哪个块文件号码中。
 * 哪个文件中的交易所属的块被抵消存储在。
 * 从该块的开始到该交易本身被存储的位置的偏移量。
Data Access Layer

通过CBlockTreeDB包装类访问数据库,代码是现在:++txdb.h++

CBlockIndex

存储在数据库中的block在内存中表示为CBlockIndex对象。在收到header之后首先创建blcokindex的对象;代码不等待接收完整block数据。当通过网络接收header时,它们将被流式传输到CBlockHeaders的向量中,然后检查它们。检查合法之后会创建一个新的CBlockIndex,并将其存储到数据库中。

CBlock / CBlockHeader

注意:这些blockindex对象与LevelDB中的 / blocks 几乎没有关系。CBlock保存block中的全部交易,其数据存储在两个位置 - 完整原始格式:blk ???.dat文件,以及UTXO数据库中的pruned格式。(utxo集合中只保存交易的out),block index数据库不关心这些细节,因为它只保存block的元数据。

Loading the block database into memory

整个数据库在启动时会加载到内存中,函数实现在LoadBlockIndexGuts(txdb.cpp)。这个过程只需要几秒钟。

bool CBlockTreeDB::LoadBlockIndexGuts(
    std::function<CBlockIndex *(const uint256 &)> insertBlockIndex) {
    std::unique_ptr<CDBIterator> pcursor(NewIterator());

    pcursor->Seek(std::make_pair(DB_BLOCK_INDEX, uint256()));

    // Load mapBlockIndex
    while (pcursor->Valid()) {
        boost::this_thread::interruption_point();
        std::pair<char, uint256> key;
        if (!pcursor->GetKey(key) || key.first != DB_BLOCK_INDEX) {
            break;
        }

        CDiskBlockIndex diskindex;
        if (!pcursor->GetValue(diskindex)) {
            return error("LoadBlockIndex() : failed to read value");
        }

        // Construct block index object
        CBlockIndex *pindexNew = insertBlockIndex(diskindex.GetBlockHash());
        pindexNew->pprev = insertBlockIndex(diskindex.hashPrev);
        pindexNew->nHeight = diskindex.nHeight;
        pindexNew->nFile = diskindex.nFile;
        pindexNew->nDataPos = diskindex.nDataPos;
        pindexNew->nUndoPos = diskindex.nUndoPos;
        pindexNew->nVersion = diskindex.nVersion;
        pindexNew->hashMerkleRoot = diskindex.hashMerkleRoot;
        pindexNew->nTime = diskindex.nTime;
        pindexNew->nBits = diskindex.nBits;
        pindexNew->nNonce = diskindex.nNonce;
        pindexNew->nStatus = diskindex.nStatus;
        pindexNew->nTx = diskindex.nTx;

        if (!CheckProofOfWork(pindexNew->GetBlockHash(), pindexNew->nBits,
                              Params().GetConsensus()))
            return error("LoadBlockIndex(): CheckProofOfWork failed: %s",
                         pindexNew->ToString());

        pcursor->Next();
    }

    return true;
}

blocks('b' key)被加载到全局“mapBlockIndex”变量中。“mapBlockIndex”是一个unordered_map,它为整个block tree中的每个block保存CBlockIndex结构;不只是active chain

typedef std::unordered_map<uint256, CBlockIndex *, BlockHasher> BlockMap;
extern BlockMap mapBlockIndex;   //map : 一个块的hash和它的块索引(key,value 指的为同一个块)

block files的元数据('f'键)被加载到vInfoBlockFiles中。

这里就可以看到为什么我们在存储的时候会按照一定的前缀规则来进行存储。

The UTXO set (chainstate leveldb)

“Ultraprune”背后的想法是减少(prune)过去交易集合的大小,仅保留过去交易中验证以后交易所必需的那些部分。(也就是只保留交易的out)

假设您有一个交易T1,它接收两个输入并发送到3个输出:O1,O2,O3。其中两个输出(O1,O2)在后来的交易T2中被用作输入。一旦T2被开采,T1只有一个感兴趣的out(O3)。这里我们只要保留有用的数据就行,没有理由保持T1完整的结构,也就是仅包括O3(锁定脚本和数量)和关于T1的某些基本信息(高度,是否是coinbase等)

class Coin {
    //! Unspent transaction output. 未花费的交易输出
    CTxOut out;

    //! Whether containing transaction was a coinbase and height at which the
    //! transaction was included into a block.
    // 是否包含的交易未coinbase交易,并且该交易包含在某个高度的块。
    uint32_t nHeightAndIsCoinBase;
}

UTXO(Unspent Transaction Out):交易的输出。这通俗地称为“coin”。因此,UTXO db有时被称为“coins数据库”。

Provably Unspendable:“如果不能满足其scriptPubKey,那么就可以验证出来coin是不可靠的。例如,OP_RETURN。可以从utxo数据库中删除可证明不可靠的coins,无论其数量多少。

Key-value pairs

chainstate levelDB中的记录是:

'c'+ 32字节的交易hash - >记录该交易未使用的交易输出。 这些记录仅对至少有一个未使用输出的事务处理。 每个记录存储:
* 交易的版本。
* 交易是否是一个coinbase或没有。
* 哪个高度块包含交易。
* 该交易的哪些输出未使用。
* scriptPubKey和那些未使用输出的数量。
 'B' - > 32字节block hash:数据库表示未使用的交易输出的 block hash。
Data Access Layer and Caching

访问UTXO数据库比block index复杂得多。这是因为它的性能对比特币系统的整体性能至关重要。block index对性能不是那么重要,因为只有几十万块,运行在不错的硬件上的节点可以在几秒钟内检索并回滚它们(并且不需要经常这样做。)另一方面,UTXO数据库中有数百万个coins,必须检查和修改每个进入mempool或包含在块中的每个交易的输入。

正如sipa在ultraprune提交中所说:

基本数据结构是CCoins(代表单个交易的coin)和CCoinsView(代表coin数据库的状态)。CCoinsView有几种实现方式。一个虚拟的,一个由coins数据库(coins.dat)支持,一个由内存池支持,另一个在其上添加缓存。

在bitcoin 0.11中,CoinsView的实例化是:

  • dummy
  • database
  • pCoinsTip (a cache backed by the database)
  • "validation cache" (used when backed by pCoinsTip, in use when connecting a block)

与该缓存的那条chain分开的是内存池的CoinsView,它由数据库支持。

具体设计如下:

      CCoinsView (abstract class)
             /            \
         ViewDB          ViewBacked 
      (database)          /      \
                   ViewMempool   ViewCache

每个class都有一个关键特征:

  • View是基类,声明验证coin存在的方法(HaveCoins),检索coin(GetCoins)等。
  • ViewDB是与LevelDB交互的代码。
  • ViewBacked有一个指向另一个View的指针;因此它被UTXO集的另一个视图(版本)“支持”。
  • ViewCache有一个缓存(CCoins的map)。
  • ViewMempool将mempool与view相关联。

上面是定义的类;而对象图是:

            Database       
           /       \
       MemPool     Blockchain cache (pcoinsTip) 
     View/Cache            \
                         Validation cache
Loading the UTXO set

访问coins database:init.cpp

//初始化CoinsViewDB,它配备了从LevelDB中load coins的方法。
pcoinsdbview = new CCoinsViewDB(nCoinDBCache, false,fReindex || fReindexChainState);
//错误捕获器基本可以忽略
pcoinscatcher = new CCoinsViewErrorCatcher(pcoinsdbview);
//初始化pCoinsTip,它是表示active chain状态的缓存,是数据库view的后端(backed)。
pcoinsTip = new CCoinsViewCache(pcoinscatcher);
Cache vs. Database

coins.cpp中的FetchCoins函数实现了如何使用缓存与数据库:

1   CCoinsMap::iterator it = cacheCoins.find(txid);
2   if (it != cacheCoins.end())
3     return it;
4   CCoins tmp;
5   if (!base->GetCoins(txid, tmp))
6     return cacheCoins.end();
7   CCoinsMap::iterator ret = cacheCoins.insert(std::make_pair(txid, CCoinsCacheEntry())).first;
  • 首先,代码在缓存中搜索给定交易ID的coins (第1行)
  • 如果找到,它返回“提取”的coins (2-3行)
  • 如果不是,则搜索数据库 (第5行)
  • 如果在数据库中找到,它会更新缓存(第7行)
Flushing the Validation Cache to the Blockchain Cache

在cache超出它的大小范围之前,连接块之后,将刷新validation cache到blockchain cache。cache具体的范围由ConnectTip这个函数来确定。
具体代码分析如下:

/**
 * Connect a new block to chainActive. pblock is either nullptr or a pointer to
 * a CBlock corresponding to pindexNew, to bypass loading it again from disk.
 *
 * The block is always added to connectTrace (either after loading from disk or
 * by copying pblock) - if that is not intended, care must be taken to remove
 * the last entry in blocksConnected in case of failure.
 * 接收一个新的区块到主链。pblock指向一个nil或指向一个pindexNew对应的block,以便绕过从硬盘加载它。
 * 该块被一直添加到connectTrace(当从磁盘加载或复制pblock之后),-- 如果不是明确要求,
 * 一旦失败,必须移除blocksConnected 的最后一项
 * pindexNew(in):当前链接的块的索引;pblock(in):链接的区块数据,有可能为nil.
 * connectTrace(out):
 */
static bool ConnectTip(const Config &config, CValidationState &state,
                       CBlockIndex *pindexNew,
                       const std::shared_ptr<const CBlock> &pblock,
                       ConnectTrace &connectTrace
) {
    const CChainParams &chainparams = config.GetChainParams();
    //注意:链接到链上的块的父块必须在激活链的Tip上(只有这样的块才可以链接到链上)
    assert(pindexNew->pprev == chainActive.Tip());
    // Read block from disk.
    int64_t nTime1 = GetTimeMicros();
    //1. 如果块的指针为空,就从磁盘上读取该数据,并将该块数据和它对应的索引加入 connectTrace 尾部。
    if (!pblock) {
        // 做一个空的共享指针
        std::shared_ptr<CBlock> pblockNew = std::make_shared<CBlock>();
        // 将该块的索引,以及块数据的指针添加进跟踪结构
        connectTrace.blocksConnected.emplace_back(pindexNew, pblockNew);
        // 从磁盘中读取该块的数据
        if (!ReadBlockFromDisk(*pblockNew, pindexNew,
                               chainparams.GetConsensus()))
            return AbortNode(state, "Failed to read block");
    } else {
        // 将该块的索引,以及块数据的指针添加进跟踪结构
        connectTrace.blocksConnected.emplace_back(pindexNew, pblock);
    }

    //2. 拿到将要链接的块的数据
    const CBlock &blockConnecting = *connectTrace.blocksConnected.back().second;
    // Apply the block atomically to the chain state.
    int64_t nTime2 = GetTimeMicros();
    nTimeReadFromDisk += nTime2 - nTime1;
    int64_t nTime3;
    LogPrint("bench", "  - Load block from disk: %.2fms [%.2fs]\n",
             (nTime2 - nTime1) * 0.001, nTimeReadFromDisk * 0.000001);
    {
        CCoinsViewCache view(pcoinsTip);
        // 链接区块
        bool rv = ConnectBlock(config, blockConnecting, state, pindexNew, view,
                               chainparams);
        // 发送信号,标识块链接的情况
        GetMainSignals().BlockChecked(blockConnecting, state);
        // 如果块链接失败,标记状态,并返回
        if (!rv) {
            if (state.IsInvalid()) {
                InvalidBlockFound(pindexNew, state);
            }
            return error("ConnectTip(): ConnectBlock %s failed",
                         pindexNew->GetBlockHash().ToString());
        }
        nTime3 = GetTimeMicros();
        nTimeConnectTotal += nTime3 - nTime2;
        LogPrint("bench", "  - Connect total: %.2fms [%.2fs]\n",
                 (nTime3 - nTime2) * 0.001, nTimeConnectTotal * 0.000001);
        bool flushed = view.Flush();
        assert(flushed);
    }
    int64_t nTime4 = GetTimeMicros();
    nTimeFlush += nTime4 - nTime3;
    LogPrint("bench", "  - Flush: %.2fms [%.2fs]\n", (nTime4 - nTime3) * 0.001,
             nTimeFlush * 0.000001);
    // Write the chain state to disk, if necessary.
    // 新块成功链接到块链上, 然后将块链的状态写入磁盘。
    if (!FlushStateToDisk(state, FLUSH_STATE_IF_NEEDED)) return false;
    int64_t nTime5 = GetTimeMicros();
    nTimeChainState += nTime5 - nTime4;
    LogPrint("bench", "  - Writing chainstate: %.2fms [%.2fs]\n",
             (nTime5 - nTime4) * 0.001, nTimeChainState * 0.000001);
    // Remove conflicting transactions from the mempool.;
    // 从交易池中移除该链接块上的所有交易;(因为这些交易已被打包)
    mempool.removeForBlock(blockConnecting.vtx, pindexNew->nHeight);
    // Update chainActive & related variables.
    // 更新链的顶端,
    UpdateTip(config, pindexNew);

    int64_t nTime6 = GetTimeMicros();
    nTimePostConnect += nTime6 - nTime5;
    nTimeTotal += nTime6 - nTime1;
    LogPrint("bench", "  - Connect postprocess: %.2fms [%.2fs]\n",
             (nTime6 - nTime5) * 0.001, nTimePostConnect * 0.000001);
    LogPrint("bench", "- Connect block: %.2fms [%.2fs]\n",
             (nTime6 - nTime1) * 0.001, nTimeTotal * 0.000001);
    return true;
}

在上述代码中,有一个ConnectBlock的函数调用,在这段时间内,new coins会暂时存储到validation cache中。具体参照 UpdateCoins代码的逻辑实现:

//tx(in):更新到UTXO中的交易; input(in):UTXO集合; txundo(in/out):获取该交易的undo信息(即当前交易花费的UTXO集合),然后写入文件。
//nHeight(in):该交易被打包的块高度;
void UpdateCoins(const CTransaction &tx, CCoinsViewCache &inputs,
                 CTxUndo &txundo, int nHeight) {
    // Mark inputs spent. 标识交易输入花费
    //1. 只有非coinbase交易才有undo信息。undo信息为该交易所花费的UTXO。
    if (!tx.IsCoinBase()) {
        txundo.vprevout.reserve(tx.vin.size());
        //2. 遍历交易的所有交易输入。
        for (const CTxIn &txin : tx.vin) {
            txundo.vprevout.emplace_back();     //先向后追加一个空的元素。
            //3. 花费这个引用的交易输入。
            bool is_spent =
                inputs.SpendCoin(txin.prevout, &txundo.vprevout.back());
            assert(is_spent);
        }
    }

    // Add outputs. 添加交易输出;
    // 将该交易的交易输出添加进UTXO集合中
    AddCoins(inputs, tx, nHeight);
}

在代码的末尾,会调用FlushStateToDisk将validation cache刷新下去。由于它的“parent view”也是一个cache(pcoinsTip, the "blockchain cache"),代码最后会调用“父亲“的ViewCache::BatchWrite,将update之后的coins交换到自己的cache中。

Flushing the Blockchain Cache to the Database

flush这个validation cache相对简单,只是在内存中把两块内存进行shuffled。刷新blockchain cache相对复杂一点,

  • 在最开始,刷新blockchain cache(pcoinsTip)的机制与validation cache的机制相同:Flush()在其后端调用BatchWrite(“base”指针)函数,这个是在database view的基础上。代码实现如下:
bool CCoinsViewCache::Flush() {
    bool fOk = base->BatchWrite(cacheCoins, hashBlock);
    cacheCoins.clear();
    cachedCoinsUsage = 0;
    return fOk;
}
  • 然后Flush()被FlushStateToDisk 调用。
// Flush the chainstate (which may refer to block index entries).
if (!pcoinsTip->Flush()) {
if (!pcoinsTip->Flush()) {
    return AbortNode(state, "Failed to write to coin database");
}

对于给定的模式,FlushStateToDisk又被其他几个不同的地方调用:

Flush Mode Description When called
IF_NEEDED 仅在cache超出其大小限制时才刷新。 在ConnectTip / DisconnectTip 块并刷新validation cache之后
ALWAYS Flush cache. 仅在初始化期间
PERIODIC 这种mode下,代码会考虑其他数据点来决定是否刷新。code 是否超出了其代码的限制?刷新缓存已经很长时间了吗?如果是,那么继续。 在ActivateBestChain()结束时

为了避免程序奔溃恢复时,需要下载大量的blocks信息,所以在这里的处理思路是去频繁的flush blockchain cache这一块缓存,但是coins cache不一样,他不会频繁的去刷新,因为我们需要尽可能的去构建一个较大的coins cache。

具体来说,blockchain cache保证每小时flush一次,而coins cache每天flush一次。

代码角度理解flushStateMode的几种模式:

// The cache is large and we're within 10% and 200 MiB or 50% and 50MiB
        // of the limit, but we have time now (not in the middle of a block
        // processing).
        bool fCacheLarge =
            mode == FLUSH_STATE_PERIODIC &&
            cacheSize >
                std::min(std::max(nTotalSpace / 2,
                                  nTotalSpace -
                                      MIN_BLOCK_COINSDB_USAGE * 1024 * 1024),
                         std::max((9 * nTotalSpace) / 10,
                                  nTotalSpace -
                                      MAX_BLOCK_COINSDB_USAGE * 1024 * 1024));
        // The cache is over the limit, we have to write now.
        bool fCacheCritical =
            mode == FLUSH_STATE_IF_NEEDED && cacheSize > nTotalSpace;
        // It's been a while since we wrote the block index to disk. Do this
        // frequently, so we don't need to redownload after a crash.
        // 因为已经将块的索引写入磁盘一段时间了。经常这样操作,所以当软件崩溃后,我们不需要重新下载。
        bool fPeriodicWrite =
            mode == FLUSH_STATE_PERIODIC &&
            nNow > nLastWrite + (int64_t)DATABASE_WRITE_INTERVAL * 1000000;
        // It's been very long since we flushed the cache. Do this infrequently,
        // to optimize cache usage.
        bool fPeriodicFlush =
            mode == FLUSH_STATE_PERIODIC &&
            nNow > nLastFlush + (int64_t)DATABASE_FLUSH_INTERVAL * 1000000;
        // Combine all conditions that result in a full cache flush.
        bool fDoFullFlush = (mode == FLUSH_STATE_ALWAYS) || fCacheLarge ||
                            fCacheCritical || fPeriodicFlush || fFlushForPrune;

触发flush操作的三种模式以及他们被触发的时机。


在上述过程中,我们提到了两个cache逻辑,一个是validation cache,一个是blockchain cache,validation cache其实就是coinviewcache的一个临时变量,代码具体如下:

ConnectTip

{
        CCoinsViewCache view(pcoinsTip);
        // 链接区块
        bool rv = ConnectBlock(config, blockConnecting, state, pindexNew, view,
                               chainparams);
        // 发送信号,标识块链接的情况
        GetMainSignals().BlockChecked(blockConnecting, state);
        // 如果块链接失败,标记状态,并返回
        if (!rv) {
            if (state.IsInvalid()) {
                InvalidBlockFound(pindexNew, state);
            }
            return error("ConnectTip(): ConnectBlock %s failed",
                         pindexNew->GetBlockHash().ToString());
        }
        nTime3 = GetTimeMicros();
        nTimeConnectTotal += nTime3 - nTime2;
        LogPrint("bench", "  - Connect total: %.2fms [%.2fs]\n",
                 (nTime3 - nTime2) * 0.001, nTimeConnectTotal * 0.000001);
        bool flushed = view.Flush();
        assert(flushed);
    }
image.png

如上图所示,这么做的好处就是,validation cache相当于coinview cache的一个临时副本,因为validation cache里面的逻辑会失败,即使失败也不会对coinview cache本身造成影响,这样coinsview cache对coins DB的视图一直是保持不变的。而上述一直所说的blockchain cache就是指coinview cache。

Raw undo data (rev*.dat)

undos数据包含disconnect或“roll back”一个block所需的信息:具体而言,是相关块所花费的coins。

因此,正在写入的数据本质上是一组CTxOut对象。

事实是,如果coin是其tx所花费的最后一个,则undo data需要存储交易的元数据(txn的块高度,是否是coinbase,它的版本version)。因此,如果您有一个交易T,其输出O1,O2,O3按此顺序消耗,对于O1和O2,所有将写入undo file的是数量和脚本。对于03,undo file将具有金额,脚本,加上T的高度和版本,以及T是否为coinbase。

下面的代码实现了将undo data写入原始文件:

bool UndoWriteToDisk(const CBlockUndo &blockundo, CDiskBlockPos &pos,
                     const uint256 &hashBlock,
                     const CMessageHeader::MessageStartChars &messageStart) {
    // Open history file to append
    CAutoFile fileout(OpenUndoFile(pos), SER_DISK, CLIENT_VERSION);
    if (fileout.IsNull()) return error("%s: OpenUndoFile failed", __func__);

    // Write index header
    unsigned int nSize = GetSerializeSize(fileout, blockundo);
    fileout << FLATDATA(messageStart) << nSize;

    // Write undo data
    long fileOutPos = ftell(fileout.Get());
    if (fileOutPos < 0) return error("%s: ftell failed", __func__);
    pos.nPos = (unsigned int)fileOutPos;
    fileout << blockundo; //将undo data的数据写入raw file文件中

    // calculate & write checksum
    CHashWriter hasher(SER_GETHASH, PROTOCOL_VERSION);
    hasher << hashBlock;
    hasher << blockundo;
    fileout << hasher.GetHash();

    return true;
}

fileout << blockundo;这行代码调用CBlockUndo上的序列化函数 - 它基本上只是一个coin向量(CTxOuts)。最后,将checksum写入undo file。在初始化期间使用校验和来验证正在检查的任何undo data是否完整。

disconnecting块时需要使用undo data。

/**
 * Undo the effects of this block (with given index) on the UTXO set represented
 * by coins. When UNCLEAN or FAILED is returned, view is left in an
 * indeterminate state.
 * 撤销这个块在UTXO集合上的影响。当UNCLEAN或FAILED状态返回时,UTXO集合视角将处于不定的状态
 * 参二 是 参一的块索引。
 */
static DisconnectResult DisconnectBlock(const CBlock &block,
                                        const CBlockIndex *pindex,
                                        CCoinsViewCache &view) {
    //1. 拒绝的区块必须是UTXO集合中的最高块
    assert(pindex->GetBlockHash() == view.GetBestBlock());

    CBlockUndo blockUndo;

    // 获取pindex 中undo数据所在的文件位置
    CDiskBlockPos pos = pindex->GetUndoPos();
    if (pos.IsNull()) {
        error("DisconnectBlock(): no undo data available");
        return DISCONNECT_FAILED;
    }

    // 从磁盘读取undo数据
    if (!UndoReadFromDisk(blockUndo, pos, pindex->pprev->GetBlockHash())) {
        error("DisconnectBlock(): failure reading undo data");
        return DISCONNECT_FAILED;
    }

    // 应用块的撤销文件,撤销块
    return ApplyBlockUndo(blockUndo, block, pindex, view);
}

最后来一张整个代码的调用关系逻辑,这个是总结的copernicus项目的逻辑,cpp的那个版本思路也是一样的。

image.png

家境清寒,整理不易。

image.png

相关文章

网友评论

      本文标题:bitcoin数据存储

      本文链接:https://www.haomeiwen.com/subject/jozwmftx.html