本文由冉小龙和张勇合作完成,致谢:张勇,转载请注明出处
当我们打开 bitcoincash 目录时,我们会看到如下的文件目录,这些文件究竟是什么,具体存储了哪些内容呢?下面我们将一一揭开其神秘面纱。
bitcoincash 文件夹:
blocks 文件夹:
index 文件夹:
chainstate文件夹:
通过观察 bitcoincash 目录,我们可以发现, 比特币总共存储了以下内容:
文件名称 | 文件描述 | 存储形式 |
---|---|---|
chainstate | 存储 utxo 相关的数据 | leveldb数据库 |
blocks/index | 存储 blocks 的元数据信息 | leveldb数据库 |
blocks/blk?????.dat | 存储 blocks 相关的数据信息,主要包括 block header 和 txs | 磁盘文件 |
blocks/rev?????.dat | 存储 blocks undo 的数据,主要包括每笔交易所花费的 out 的信息。 | 磁盘文件 |
其中 block 的数据和 block 的 undo 数据是直接存储到disk上面的,block 的 index 数据和 utxo 的数据是写到 leveldb 数据库中。
leveldb
为了方便理解 leveldb 的目录存储结构,下面简述一下 leveldb 的原理。
leveldb 使用的是 LSMTree 的存储结构,其存储的逻辑大致如上图所示,具体步骤如下:
- 当往 leveldb 中写入一条数据的时候,首先会将数据写入 log 文件,log 文件完成之后,再将数据写入内存(memtable)中。
- 当 memtable 中的数据写满之后,.log 文件会被锁定,同时生成 Immutable table 文件,该文件只支持读操作,不支持写和删除,这个时候,会重新生成 .log文 件和memtable 文件,新写入一条数据的时候,会重新写入空的 .log 文件和 memtable 中。
- LevelDb 后台调度会将 Immutable Memtable 的数据导出到磁盘,形成一个新的SSTable 文件。SSTable 就是由内存中的数据不断导出并进行 Compaction 操作后形成的,而且 SSTable 的所有文件是一种层级结构,第一层为 Level 0,第二层为 Level 1,依次类推,层级逐渐增高,这也是为何称之为LevelDb的原因。
各个文件的含义:
Current文件:
Current 文件是干什么的呢?这个文件的内容只有一个信息,就是记载当前的 manifest 文件名。因为在 LevleDb 的运行过程中,随着 Compaction 的进行,SSTable 文件会发生变化,会有新的文件产生,老的文件被废弃,Manifest 也会跟着反映这种变化,此时往往会新生成Manifest 文件来记载这种变化,而 Current 则用来指出哪个 Manifest 文件才是我们关心的那个 Manifest 文件。
Manifest文件
Manifest 文件存储的是 xxx.ldb 文件的元数据信息,因为,我们只有 xxx.ldb 文件,我们并不知道它具体属于哪一个 level。这也是 Manifest 文件的作用,每次打开 DB 的时候,leveldb 都会去创建这样一个文件并在其尾部追加后缀标识。该文件是以 append 的方式写入 disk 的。
LOG文件
leveldb 运行时的日志文件,方便用户查看。
LOCK文件
它是使用文件实现的一个 DB 锁,告知用户,一个 leveldb 的实例在一个进程范围内只允许被打开一次。
xxx.ldb文件
这个文件是记录 leveldb 的数据文件(区别与元数据文件),按照 KV 有序的形式写入数据库中。
level-0 的文件大小就是 memtable 文件做 compaction 之后的大小,level-1 10MB、level-2 100MB、level-3 1000MB 以此类推。
xxx.log 文件
我们上面说过,为了保证数据不丢失,在写数据之前会先写入 .log 文件,.log 文件存储的是一系列最近的更新,每个更新以 append 的方式追加到当前的 log 文件中,当 log 文件达到 4MB时会转化为一个有序的文件,并创建新的 log 文件来记录最近的更新。这个 log 文件中与上文中提到的 memtable 文件是互相映射的,当 memtable 文件被写入 level-0 后,对应的 log 文件会被删除,新的 log 文件会重新创建,对应新的 memtable,以此类推。
综上所述,我们可以看出,leveldb 是存储模型中一个典型的数据与元数据分离存储的数据库。
chainstate 文件夹
chainstate 是一个leveldb的数据库,主要存储一些 utxo 和 tx 的元数据信息。存储 chainstate 的数据主要是用来去验证新进来的 blocks 和 tx 是否是合法的。如果没有这个操作,就意味着对于每一个被花费的 out 你都需要去进行全表扫描来验证。
如上图所示,utxo的数据主要存储于chainstate这个文件目录,由于要存储到leveldb中,所以肯定是按照 key、value 的格式将数据准备好。
coin
如上所示:key总共包含三部分内容,1 字节的大写 C
, 32 字节的 hash,4 字节的序列号。
value 是 coin 被序列化之后的值,具体如下:
coin 又包含了 txout 结构,具体如下:
对 nValue 和 scriptPubKey 采用了不同的压缩方式来进行序列化,如下:
best block
比特币还往 chainstate 中记录了另一部分信息,首先去判断当前 block 的 hash 是否为 null,不为 null 的话,以 1 字节的大写 B
为 key,32 字节的 block hash 为value,写入 coin 数据库中。
总结:utxo 写入 disk 的数据库为:chainstate,写入数据分为两部分,第一部分:key是outpoin, 由
在 bitcoin core 0.17 的时候, chainstate 目录做了改动,多写了一部分数据进去,图示如下:
Note:
在0.17的结构中,第一部分并不会存在很长时间,它只会在触发BatchWrite第一步写入,在整个coinsmap写完之后将这部分删除。
index 文件夹:
index 文件夹下记录的主要是 blocks 的 index 信息,block index 是block的元数据信息,其中包含和block header信息,高度,以及chain的信息;按照 utxo 存储的思路,我们再去寻找 blocks 中 index 的 key 和 value。
reindex
index 中写的第一部分数据:key 是 1 字节的 DB_REINDEX_FLAG,value 是 1 字节的布尔值。用来标识是否需要进行 reindex 操作。
txindex
index 中写的第二部分数据:key 是 1 字节的 DB_TXINDEX 加 32 字节的 hash,value 是序列化之后的 CDiskTxPos,它只有一个成员是,int 类型的 nTxOffset。这些是可选的,只有当'txindex' 被启用时才存在。 每个记录存储:
- 交易存储在哪个块文件号码中。
- 哪个文件中的交易所属的块被抵消存储在。
- 从该块的开始到该交易本身被存储的位置的偏移量。
blockfileinfo
index 中写的第三部分数据:这部分数据是比较重要的,
fileinfo
首先写入 fileinfo 数据,key 是 1 字节的 DB_BLOCK_FILES 加上 4 字节的文件编号,value 是 CBlockFileInfo 序列化后的数据。
lastFile
其次写入 lastFile 信息,key 是 1 字节的 DB_LAST_BLOCK,value 是 4 字节的 nLastFile。
blockindex
最后写入 blockindex 的信息,key 是 1 字节的 DB_BLOCK_INDEX 加上 32 字节的 blockhash value是CDiskBlockIndex序列化之后的数据。
flag
index 中写的第四部分数据:key 是 1 字节的 DB_FLAG 加上 flag 的名字,value 是 1 字节的布尔值(1 为 true,0 为 false),可以打开或关闭各种类型的标志,目前定义的比如:TxIndex(是否启动交易索引)。
block 文件夹
block 文件夹下主要存在两种文件,一种是 blk???.dat,用于存储 block,另一种是 rev???.dat,用于存储 undo block。 主要存储格式如下:
blk?????.dat
存储 block 序列化的数据。
存储格式如下(按照先后顺序):
MessageStart
MessageMagic 在启动程序时定义,并且在不同网络中定义不同,MessageMagic 分为 netMagic 和 diskMagic :
Mainnet:
TestNet:
RegTestNet:
MessageMagic 是一个 4 byte 的数组,在写入数据的时候调用 FLATDATA 这个宏定义,具体如下:
FLATDATA 会将vector或者map这种数据结构中的元素按照数组的原始序列dump到disk上。
write() 函数的第一个参数代表要写入的数据的起始位置,第二个参数代表要写入数据的大小,pbegin 指向 vector 的起始位置,pend指向末尾元素 +1 的位置,所以在这里先写入了 4 byte 的 messageStart。
BlockSize
BlockSize主要描述 Block 被序列化后的长度,为 4 byte。
Block 序列化
block 序列化主要序列化两部分,一部分是 BlockHeader 结构,一部分为 transaction 的一个共享指针 vtx:
第一部分是 BlockHeader:
第二部分是 vtx:
CTransaction 主要序列化以下内容:
总结:
blk????.dat 文件首先写入 4 byte 的 messageMagic,其次写入 4 byte 的 block size,最后写入 block 被序列化之后的数据。
rev?????.dat
存储 undoblock 序列化的数据。
MessageStart 和 UndoBlockSize 与 Block 中的相同。
BlockUndo 序列化
BlockUndo 序列化只有 vtxundo 一个对象,vtxundo 是 CTxUndo 的一个 vector ,对其进行序列化操作如下:
CTxUndo 的序列化操作如下,其中 prevout 是一个 Coin 的 vector:
Coin的序列化操作如下:
Coin包含两部分内容,代码如下:
其中对 TxOut 的序列化如下,对 nValue 和 scriptPubKey 采用了不同的压缩方式来进行序列化:
BlockUndoCheckSum
具体代码如下:
将 hashBlock 和 blockundo 的数据写入 CHashWriter 的接口中,获取 CHashWriter 的 hash ,并将 32 字节的 hash 值写入 undofile 文件中。
总结
blk????.dat 和 rev????.dat 的区别:
blk???.dat 和 rev????.dat 所存储的数据是不一样的,block 存储的是 block header 和 txs 序列化后的数据,undo block 存储的是 txout 被序列化后的数据。
关于文件大小的一些问题:
blk.dat 的默认初始化大小是16M,最大为 128M, rev.dat 的默认初始化大小为 1M。
在导入 block 时,会去检查磁盘空间,必须大于 50M,否则就会 Disk space is low
关于在 prune 时, 磁盘要求必须大于 550M:
bitcoin 要求必须保留 288 个 block, 按每个 block 1M 大小进行计算, 需要 288M, 还需要额外的 15% 的空间去存储 UNDO 的数据, 再加上以 20% 的孤块率, 大约需要 397M 的空间, 这是最低限度, 但我们还需要加上同步块的数据 blk.dat, 需要128M, 再加上约为 15% 的 undo data, 约为147M。 所以整个需要 147M + 397M=544M, 所以设置限度为 550M。