本文由冉小龍和張勇合作完成允趟,致謝:張勇,轉(zhuǎn)載請(qǐng)注明出處
當(dāng)我們打開 bitcoincash 目錄時(shí)码倦,我們會(huì)看到如下的文件目錄,這些文件究竟是什么锭碳,具體存儲(chǔ)了哪些內(nèi)容呢袁稽?下面我們將一一揭開其神秘面紗。
bitcoincash 文件夾:
blocks 文件夾:
index 文件夾:
chainstate文件夾:
通過(guò)觀察 bitcoincash 目錄擒抛,我們可以發(fā)現(xiàn)推汽, 比特幣總共存儲(chǔ)了以下內(nèi)容:
文件名稱 | 文件描述 | 存儲(chǔ)形式 |
---|---|---|
chainstate | 存儲(chǔ) utxo 相關(guān)的數(shù)據(jù) | leveldb數(shù)據(jù)庫(kù) |
blocks/index | 存儲(chǔ) blocks 的元數(shù)據(jù)信息 | leveldb數(shù)據(jù)庫(kù) |
blocks/blk?????.dat | 存儲(chǔ) blocks 相關(guān)的數(shù)據(jù)信息,主要包括 block header 和 txs | 磁盤文件 |
blocks/rev?????.dat | 存儲(chǔ) blocks undo 的數(shù)據(jù)歧沪,主要包括每筆交易所花費(fèi)的 out 的信息歹撒。 | 磁盤文件 |
其中 block 的數(shù)據(jù)和 block 的 undo 數(shù)據(jù)是直接存儲(chǔ)到disk上面的,block 的 index 數(shù)據(jù)和 utxo 的數(shù)據(jù)是寫到 leveldb 數(shù)據(jù)庫(kù)中诊胞。
leveldb
為了方便理解 leveldb 的目錄存儲(chǔ)結(jié)構(gòu)暖夭,下面簡(jiǎn)述一下 leveldb 的原理。
leveldb 使用的是 LSMTree 的存儲(chǔ)結(jié)構(gòu)撵孤,其存儲(chǔ)的邏輯大致如上圖所示迈着,具體步驟如下:
- 當(dāng)往 leveldb 中寫入一條數(shù)據(jù)的時(shí)候,首先會(huì)將數(shù)據(jù)寫入 log 文件邪码,log 文件完成之后裕菠,再將數(shù)據(jù)寫入內(nèi)存(memtable)中。
- 當(dāng) memtable 中的數(shù)據(jù)寫滿之后闭专,.log 文件會(huì)被鎖定奴潘,同時(shí)生成 Immutable table 文件,該文件只支持讀操作影钉,不支持寫和刪除画髓,這個(gè)時(shí)候,會(huì)重新生成 .log文 件和memtable 文件斧拍,新寫入一條數(shù)據(jù)的時(shí)候雀扶,會(huì)重新寫入空的 .log 文件和 memtable 中。
- LevelDb 后臺(tái)調(diào)度會(huì)將 Immutable Memtable 的數(shù)據(jù)導(dǎo)出到磁盤肆汹,形成一個(gè)新的SSTable 文件愚墓。SSTable 就是由內(nèi)存中的數(shù)據(jù)不斷導(dǎo)出并進(jìn)行 Compaction 操作后形成的,而且 SSTable 的所有文件是一種層級(jí)結(jié)構(gòu)昂勉,第一層為 Level 0浪册,第二層為 Level 1,依次類推岗照,層級(jí)逐漸增高村象,這也是為何稱之為L(zhǎng)evelDb的原因笆环。
各個(gè)文件的含義:
Current文件:
Current 文件是干什么的呢?這個(gè)文件的內(nèi)容只有一個(gè)信息厚者,就是記載當(dāng)前的 manifest 文件名躁劣。因?yàn)樵?LevleDb 的運(yùn)行過(guò)程中,隨著 Compaction 的進(jìn)行库菲,SSTable 文件會(huì)發(fā)生變化账忘,會(huì)有新的文件產(chǎn)生,老的文件被廢棄熙宇,Manifest 也會(huì)跟著反映這種變化鳖擒,此時(shí)往往會(huì)新生成Manifest 文件來(lái)記載這種變化,而 Current 則用來(lái)指出哪個(gè) Manifest 文件才是我們關(guān)心的那個(gè) Manifest 文件烫止。
Manifest文件
Manifest 文件存儲(chǔ)的是 xxx.ldb 文件的元數(shù)據(jù)信息蒋荚,因?yàn)椋覀冎挥?xxx.ldb 文件馆蠕,我們并不知道它具體屬于哪一個(gè) level期升。這也是 Manifest 文件的作用,每次打開 DB 的時(shí)候互躬,leveldb 都會(huì)去創(chuàng)建這樣一個(gè)文件并在其尾部追加后綴標(biāo)識(shí)吓妆。該文件是以 append 的方式寫入 disk 的。
LOG文件
leveldb 運(yùn)行時(shí)的日志文件吨铸,方便用戶查看行拢。
LOCK文件
它是使用文件實(shí)現(xiàn)的一個(gè) DB 鎖,告知用戶诞吱,一個(gè) leveldb 的實(shí)例在一個(gè)進(jìn)程范圍內(nèi)只允許被打開一次舟奠。
xxx.ldb文件
這個(gè)文件是記錄 leveldb 的數(shù)據(jù)文件(區(qū)別與元數(shù)據(jù)文件),按照 KV 有序的形式寫入數(shù)據(jù)庫(kù)中房维。
level-0 的文件大小就是 memtable 文件做 compaction 之后的大小沼瘫,level-1 10MB、level-2 100MB咙俩、level-3 1000MB 以此類推耿戚。
xxx.log 文件
我們上面說(shuō)過(guò),為了保證數(shù)據(jù)不丟失阿趁,在寫數(shù)據(jù)之前會(huì)先寫入 .log 文件膜蛔,.log 文件存儲(chǔ)的是一系列最近的更新,每個(gè)更新以 append 的方式追加到當(dāng)前的 log 文件中脖阵,當(dāng) log 文件達(dá)到 4MB時(shí)會(huì)轉(zhuǎn)化為一個(gè)有序的文件皂股,并創(chuàng)建新的 log 文件來(lái)記錄最近的更新。這個(gè) log 文件中與上文中提到的 memtable 文件是互相映射的命黔,當(dāng) memtable 文件被寫入 level-0 后呜呐,對(duì)應(yīng)的 log 文件會(huì)被刪除就斤,新的 log 文件會(huì)重新創(chuàng)建,對(duì)應(yīng)新的 memtable蘑辑,以此類推洋机。
綜上所述,我們可以看出洋魂,leveldb 是存儲(chǔ)模型中一個(gè)典型的數(shù)據(jù)與元數(shù)據(jù)分離存儲(chǔ)的數(shù)據(jù)庫(kù)槐秧。
chainstate 文件夾
chainstate 是一個(gè)leveldb的數(shù)據(jù)庫(kù),主要存儲(chǔ)一些 utxo 和 tx 的元數(shù)據(jù)信息忧设。存儲(chǔ) chainstate 的數(shù)據(jù)主要是用來(lái)去驗(yàn)證新進(jìn)來(lái)的 blocks 和 tx 是否是合法的。如果沒(méi)有這個(gè)操作颠通,就意味著對(duì)于每一個(gè)被花費(fèi)的 out 你都需要去進(jìn)行全表掃描來(lái)驗(yàn)證址晕。
如上圖所示,utxo的數(shù)據(jù)主要存儲(chǔ)于chainstate這個(gè)文件目錄顿锰,由于要存儲(chǔ)到leveldb中谨垃,所以肯定是按照 key、value 的格式將數(shù)據(jù)準(zhǔn)備好硼控。
coin
如上所示:key總共包含三部分內(nèi)容刘陶,1 字節(jié)的大寫 C
, 32 字節(jié)的 hash,4 字節(jié)的序列號(hào)牢撼。
value 是 coin 被序列化之后的值匙隔,具體如下:
coin 又包含了 txout 結(jié)構(gòu),具體如下:
對(duì) nValue 和 scriptPubKey 采用了不同的壓縮方式來(lái)進(jìn)行序列化熏版,如下:
best block
比特幣還往 chainstate 中記錄了另一部分信息纷责,首先去判斷當(dāng)前 block 的 hash 是否為 null,不為 null 的話撼短,以 1 字節(jié)的大寫 B
為 key再膳,32 字節(jié)的 block hash 為value,寫入 coin 數(shù)據(jù)庫(kù)中曲横。
總結(jié):utxo 寫入 disk 的數(shù)據(jù)庫(kù)為:chainstate喂柒,寫入數(shù)據(jù)分為兩部分,第一部分:key是outpoin, 由<txid>+<tx out index>組成禾嫉,其中txid是32字節(jié)灾杰,tx out index 是用var int的編碼方式序列化value 為 coin 序列化之后的大小。第二部分:寫入的 key 為 1 字節(jié)的DB_BEST_BLOCK 標(biāo)識(shí)熙参,value 為 32 字節(jié)的 block hash吭露。
在 bitcoin core 0.17 的時(shí)候, chainstate 目錄做了改動(dòng)尊惰,多寫了一部分?jǐn)?shù)據(jù)進(jìn)去讲竿,圖示如下:
Note:
在0.17的結(jié)構(gòu)中泥兰,第一部分并不會(huì)存在很長(zhǎng)時(shí)間,它只會(huì)在觸發(fā)BatchWrite第一步寫入题禀,在整個(gè)coinsmap寫完之后將這部分刪除鞋诗。
index 文件夾:
index 文件夾下記錄的主要是 blocks 的 index 信息,block index 是block的元數(shù)據(jù)信息迈嘹,其中包含和block header信息削彬,高度,以及chain的信息秀仲;按照 utxo 存儲(chǔ)的思路融痛,我們?cè)偃ふ?blocks 中 index 的 key 和 value。
reindex
index 中寫的第一部分?jǐn)?shù)據(jù):key 是 1 字節(jié)的 DB_REINDEX_FLAG神僵,value 是 1 字節(jié)的布爾值雁刷。用來(lái)標(biāo)識(shí)是否需要進(jìn)行 reindex 操作。
txindex
index 中寫的第二部分?jǐn)?shù)據(jù):key 是 1 字節(jié)的 DB_TXINDEX 加 32 字節(jié)的 hash保礼,value 是序列化之后的 CDiskTxPos沛励,它只有一個(gè)成員是,int 類型的 nTxOffset炮障。這些是可選的目派,只有當(dāng)'txindex' 被啟用時(shí)才存在。 每個(gè)記錄存儲(chǔ):
- 交易存儲(chǔ)在哪個(gè)塊文件號(hào)碼中胁赢。
- 哪個(gè)文件中的交易所屬的塊被抵消存儲(chǔ)在企蹭。
- 從該塊的開始到該交易本身被存儲(chǔ)的位置的偏移量。
blockfileinfo
index 中寫的第三部分?jǐn)?shù)據(jù):這部分?jǐn)?shù)據(jù)是比較重要的智末,
fileinfo
首先寫入 fileinfo 數(shù)據(jù)练对,key 是 1 字節(jié)的 DB_BLOCK_FILES 加上 4 字節(jié)的文件編號(hào),value 是 CBlockFileInfo 序列化后的數(shù)據(jù)吹害。
lastFile
其次寫入 lastFile 信息螟凭,key 是 1 字節(jié)的 DB_LAST_BLOCK,value 是 4 字節(jié)的 nLastFile它呀。
blockindex
最后寫入 blockindex 的信息螺男,key 是 1 字節(jié)的 DB_BLOCK_INDEX 加上 32 字節(jié)的 blockhash value是CDiskBlockIndex序列化之后的數(shù)據(jù)。
flag
index 中寫的第四部分?jǐn)?shù)據(jù):key 是 1 字節(jié)的 DB_FLAG 加上 flag 的名字纵穿,value 是 1 字節(jié)的布爾值(1 為 true下隧,0 為 false),可以打開或關(guān)閉各種類型的標(biāo)志谓媒,目前定義的比如:TxIndex(是否啟動(dòng)交易索引)淆院。
block 文件夾
block 文件夾下主要存在兩種文件,一種是 blk???.dat句惯,用于存儲(chǔ) block土辩,另一種是 rev???.dat支救,用于存儲(chǔ) undo block。 主要存儲(chǔ)格式如下:
blk?????.dat
存儲(chǔ) block 序列化的數(shù)據(jù)拷淘。
存儲(chǔ)格式如下(按照先后順序):
MessageStart
MessageMagic 在啟動(dòng)程序時(shí)定義各墨,并且在不同網(wǎng)絡(luò)中定義不同,MessageMagic 分為 netMagic 和 diskMagic :
Mainnet:
TestNet:
RegTestNet:
MessageMagic 是一個(gè) 4 byte 的數(shù)組启涯,在寫入數(shù)據(jù)的時(shí)候調(diào)用 FLATDATA 這個(gè)宏定義贬堵,具體如下:
FLATDATA 會(huì)將vector或者map這種數(shù)據(jù)結(jié)構(gòu)中的元素按照數(shù)組的原始序列dump到disk上。
write() 函數(shù)的第一個(gè)參數(shù)代表要寫入的數(shù)據(jù)的起始位置结洼,第二個(gè)參數(shù)代表要寫入數(shù)據(jù)的大小黎做,pbegin 指向 vector 的起始位置,pend指向末尾元素 +1 的位置松忍,所以在這里先寫入了 4 byte 的 messageStart蒸殿。
BlockSize
BlockSize主要描述 Block 被序列化后的長(zhǎng)度,為 4 byte挽铁。
Block 序列化
block 序列化主要序列化兩部分,一部分是 BlockHeader 結(jié)構(gòu)敞掘,一部分為 transaction 的一個(gè)共享指針 vtx:
第一部分是 BlockHeader:
第二部分是 vtx:
CTransaction 主要序列化以下內(nèi)容:
總結(jié):
blk????.dat 文件首先寫入 4 byte 的 messageMagic叽掘,其次寫入 4 byte 的 block size,最后寫入 block 被序列化之后的數(shù)據(jù)玖雁。
rev?????.dat
存儲(chǔ) undoblock 序列化的數(shù)據(jù)更扁。
MessageStart 和 UndoBlockSize 與 Block 中的相同。
BlockUndo 序列化
BlockUndo 序列化只有 vtxundo 一個(gè)對(duì)象赫冬,vtxundo 是 CTxUndo 的一個(gè) vector 浓镜,對(duì)其進(jìn)行序列化操作如下:
CTxUndo 的序列化操作如下,其中 prevout 是一個(gè) Coin 的 vector:
Coin的序列化操作如下:
Coin包含兩部分內(nèi)容劲厌,代碼如下:
其中對(duì) TxOut 的序列化如下膛薛,對(duì) nValue 和 scriptPubKey 采用了不同的壓縮方式來(lái)進(jìn)行序列化:
BlockUndoCheckSum
具體代碼如下:
將 hashBlock 和 blockundo 的數(shù)據(jù)寫入 CHashWriter 的接口中,獲取 CHashWriter 的 hash 补鼻,并將 32 字節(jié)的 hash 值寫入 undofile 文件中哄啄。
總結(jié)
blk????.dat 和 rev????.dat 的區(qū)別:
blk???.dat 和 rev????.dat 所存儲(chǔ)的數(shù)據(jù)是不一樣的,block 存儲(chǔ)的是 block header 和 txs 序列化后的數(shù)據(jù)风范,undo block 存儲(chǔ)的是 txout 被序列化后的數(shù)據(jù)咨跌。
關(guān)于文件大小的一些問(wèn)題:
blk.dat 的默認(rèn)初始化大小是16M佛舱,最大為 128M惯殊, rev.dat 的默認(rèn)初始化大小為 1M。
在導(dǎo)入 block 時(shí)姑宽,會(huì)去檢查磁盤空間寇漫,必須大于 50M刊殉,否則就會(huì) Disk space is low
關(guān)于在 prune 時(shí)殉摔, 磁盤要求必須大于 550M:
bitcoin 要求必須保留 288 個(gè) block, 按每個(gè) block 1M 大小進(jìn)行計(jì)算冗澈, 需要 288M钦勘, 還需要額外的 15% 的空間去存儲(chǔ) UNDO 的數(shù)據(jù), 再加上以 20% 的孤塊率亚亲, 大約需要 397M 的空間彻采, 這是最低限度, 但我們還需要加上同步塊的數(shù)據(jù) blk.dat捌归, 需要128M肛响, 再加上約為 15% 的 undo data, 約為147M惜索。 所以整個(gè)需要 147M + 397M=544M特笋, 所以設(shè)置限度為 550M。