深入理解比特币数据存储

本文由冉小龙和张勇合作完成,致谢:张勇,转载请注明出处

当我们打开 bitcoincash 目录时,我们会看到如下的文件目录,这些文件究竟是什么,具体存储了哪些内容呢?下面我们将一一揭开其神秘面纱。

bitcoincash 文件夹:

bitcoincash.png

blocks 文件夹:

blocks.png

index 文件夹:

index.png

chainstate文件夹:


chainstate.png

通过观察 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.png

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 你都需要去进行全表扫描来验证。

chainstate_file.png

如上图所示,utxo的数据主要存储于chainstate这个文件目录,由于要存储到leveldb中,所以肯定是按照 key、value 的格式将数据准备好。

coin

coin.png
coin_db_key.png

如上所示:key总共包含三部分内容,1 字节的大写 C , 32 字节的 hash,4 字节的序列号。

value 是 coin 被序列化之后的值,具体如下:

image.png

coin 又包含了 txout 结构,具体如下:

image.png

对 nValue 和 scriptPubKey 采用了不同的压缩方式来进行序列化,如下:

image.png

best block

image.png

比特币还往 chainstate 中记录了另一部分信息,首先去判断当前 block 的 hash 是否为 null,不为 null 的话,以 1 字节的大写 B 为 key,32 字节的 block hash 为value,写入 coin 数据库中。

总结:utxo 写入 disk 的数据库为:chainstate,写入数据分为两部分,第一部分:key是outpoin, 由<txid>+<tx out index>组成,其中txid是32字节,tx out index 是用var int的编码方式序列化value 为 coin 序列化之后的大小。第二部分:写入的 key 为 1 字节的DB_BEST_BLOCK 标识,value 为 32 字节的 block hash。

image.png

bitcoin core 0.17 的时候, chainstate 目录做了改动,多写了一部分数据进去,图示如下:

image.png

Note:

在0.17的结构中,第一部分并不会存在很长时间,它只会在触发BatchWrite第一步写入,在整个coinsmap写完之后将这部分删除。

index 文件夹:

image.png

index 文件夹下记录的主要是 blocks 的 index 信息,block index 是block的元数据信息,其中包含和block header信息,高度,以及chain的信息;按照 utxo 存储的思路,我们再去寻找 blocks 中 index 的 key 和 value。

reindex

image.png

index 中写的第一部分数据:key 是 1 字节的 DB_REINDEX_FLAG,value 是 1 字节的布尔值。用来标识是否需要进行 reindex 操作。

txindex

image.png

index 中写的第二部分数据:key 是 1 字节的 DB_TXINDEX 加 32 字节的 hash,value 是序列化之后的 CDiskTxPos,它只有一个成员是,int 类型的 nTxOffset。这些是可选的,只有当'txindex' 被启用时才存在。 每个记录存储:

  • 交易存储在哪个块文件号码中。
  • 哪个文件中的交易所属的块被抵消存储在。
  • 从该块的开始到该交易本身被存储的位置的偏移量。

blockfileinfo

image.png

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
image.png

index 中写的第四部分数据:key 是 1 字节的 DB_FLAG 加上 flag 的名字,value 是 1 字节的布尔值(1 为 true,0 为 false),可以打开或关闭各种类型的标志,目前定义的比如:TxIndex(是否启动交易索引)。

image.png

block 文件夹

block 文件夹下主要存在两种文件,一种是 blk???.dat,用于存储 block,另一种是 rev???.dat,用于存储 undo block。 主要存储格式如下:

blk?????.dat

存储 block 序列化的数据。

image.png

存储格式如下(按照先后顺序):

image.png
MessageStart

MessageMagic 在启动程序时定义,并且在不同网络中定义不同,MessageMagic 分为 netMagic 和 diskMagic :

Mainnet:
image.png
TestNet:
image.png
RegTestNet:
image.png

MessageMagic 是一个 4 byte 的数组,在写入数据的时候调用 FLATDATA 这个宏定义,具体如下:

image.png

FLATDATA 会将vector或者map这种数据结构中的元素按照数组的原始序列dump到disk上。

image.png

write() 函数的第一个参数代表要写入的数据的起始位置,第二个参数代表要写入数据的大小,pbegin 指向 vector 的起始位置,pend指向末尾元素 +1 的位置,所以在这里先写入了 4 byte 的 messageStart。

image.png

BlockSize

BlockSize主要描述 Block 被序列化后的长度,为 4 byte。

image.png

Block 序列化

block 序列化主要序列化两部分,一部分是 BlockHeader 结构,一部分为 transaction 的一个共享指针 vtx:

image.png

第一部分是 BlockHeader:

image.png

第二部分是 vtx:

image.png

CTransaction 主要序列化以下内容:

image.png

总结:

blk????.dat 文件首先写入 4 byte 的 messageMagic,其次写入 4 byte 的 block size,最后写入 block 被序列化之后的数据。

image.png

rev?????.dat

存储 undoblock 序列化的数据。

image.png

MessageStart 和 UndoBlockSize 与 Block 中的相同。

BlockUndo 序列化

BlockUndo 序列化只有 vtxundo 一个对象,vtxundo 是 CTxUndo 的一个 vector ,对其进行序列化操作如下:

image.png

CTxUndo 的序列化操作如下,其中 prevout 是一个 Coin 的 vector:

image.png

Coin的序列化操作如下:

image.png

Coin包含两部分内容,代码如下:

image.png

其中对 TxOut 的序列化如下,对 nValue 和 scriptPubKey 采用了不同的压缩方式来进行序列化:

image.png
BlockUndoCheckSum

具体代码如下:

image.png

将 hashBlock 和 blockundo 的数据写入 CHashWriter 的接口中,获取 CHashWriter 的 hash ,并将 32 字节的 hash 值写入 undofile 文件中。

总结

image.png

blk????.dat 和 rev????.dat 的区别:

blk???.dat 和 rev????.dat 所存储的数据是不一样的,block 存储的是 block header 和 txs 序列化后的数据,undo block 存储的是 txout 被序列化后的数据。

关于文件大小的一些问题:

blk.dat 的默认初始化大小是16M,最大为 128M, rev.dat 的默认初始化大小为 1M。

image.png

在导入 block 时,会去检查磁盘空间,必须大于 50M,否则就会 Disk space is low

image.png

关于在 prune 时, 磁盘要求必须大于 550M:

image.png

bitcoin 要求必须保留 288 个 block, 按每个 block 1M 大小进行计算, 需要 288M, 还需要额外的 15% 的空间去存储 UNDO 的数据, 再加上以 20% 的孤块率, 大约需要 397M 的空间, 这是最低限度, 但我们还需要加上同步块的数据 blk.dat, 需要128M, 再加上约为 15% 的 undo data, 约为147M。 所以整个需要 147M + 397M=544M, 所以设置限度为 550M。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容