探尋比特幣二:數據存儲

前言

本文將會介紹比特幣區塊原始數據的存儲格式,以及各個字段所代表的含義。

區塊數據

比特幣程序將數據存在了4個地方。

  • blocks/blk*.dat的文件中存儲了實際的塊數據,這些數據以網絡格式存儲。它們僅用于重新掃描錢包中丟失的交易,將這些交易重新組織到鏈的不同部分,并將數據塊提供給其他正在同步數據的節點。
  • blocks/index/*是一個levelDB數據庫,存儲著目前已知塊的元數據,這些元數據記錄所有已知的塊以及它們存儲在磁盤上的位置。沒有這些文件,查找一個塊將是非常慢的。
  • chainstate/*是一個levelDB數據庫,以緊湊的形式存儲所有當前未花費的交易以及它們的元數據。這里的數據對于驗證新傳入的塊和交易是必要的。在理論上,這些數據可以從塊數據中重建,但是這需要很長時間。沒有這些數據也可以對數據進行驗證,但是需要現有塊數據進行掃面,這無疑是非常慢的。
  • blocks/rev*.dat中包含了“撤銷”數據,可以將區塊視為鏈的“補丁”(它們消耗一些未花費的輸出并生成新的輸出),那么這些撤銷數據將是反向補丁。如果需要回滾鏈,這些數據將是必須的。

比特幣程序從網絡中接受數據后,會將數據以.dat的形式轉儲到磁盤上。

一個塊文件大約為128MB。每個塊文件會有一個對應的撤銷文件,比如文件blocks/blk1234.datblocks/recv1234.dat對應。

描述

每個塊都包含一些近期的交易記錄,和對之前塊的引用。同時它還包含了一個難以解決的數學難題的答案,每個塊的答案唯一。如果沒有正確的答案,新的塊將不能提交給網絡--“挖礦”的過程本質是競爭的過程,最先找到答案的人將獲得挖礦的獎勵。每個塊中的數學問題是非常難以解決的,但一旦找到有效的解決方案,網絡中的其他部分就很容易確認方案的正確性。對任何給定的塊,會有多個有效的解決方案--找到一個方案即可以宣稱解決了該難題。

比特幣網絡在設計上每小時出6個塊,因此需要自動調整數學問題的難度。每隔2016個塊(大概歷時兩周),所有的比特幣客戶端會將挖出的塊數量和目標數量進行比較,并對目標進行修改。網絡會達成共識,并自動調整生成塊的難度。

每個塊都包含對先前塊的引用,所以現有塊的集合可以形成區塊鏈。然而,該鏈可能會存在分叉,比如在兩個礦工同時到達同一區塊的兩個不同的有效解決方案時。點對點的網絡會在短時間內解決這些分叉,使鏈中只有一個分支存活。

比特幣客戶端會接受“最長”的鏈作為有效鏈。鏈的長度是指具有最大組合難度的鏈,而不是具有最多塊的鏈。

塊結構

比特幣數據塊的結構如下:

大小(字節) 名稱 數據類型 描述
4 magic_number uint32 總是0xD9B4BEF9,作為區塊之間的分隔符
4 block_size uint32 后面數據到塊結束的字節數
80 block_header char[] block header
varies transaction_cnt uint 交易數量
varies transaction char[] 交易詳情

從原始數據中讀取的流程大概如下

  1. 讀取4個字節,比對magic_number
  2. 一旦匹配,讀取后4個字節,得到塊的大小m
  3. 讀取后面m個字節,得到區塊的數據
  4. 返回第一步,讀取下一個區塊

Block Header

block header固定80字節大小,結構如下

大小(字節) 名稱 數據類型 描述
4 version int32_t 版本號
32 previous_block_hash char[32] 前一個block的hash值
32 merkle_root_hash char[32] 區塊內所有交易的merkle hash值
4 time uint32 unix時間戳,礦工挖礦的時間
4 nBits uint32 該塊的標題hash必須小于的值。難度
4 nonce uint32 隨機值,用于產生滿足難度的hash值

hash字段使內部字節順序存儲;其他的值以小端序存儲。

其中,內部字節順序需要以字節為單位逆序讀取,如下面的python代碼:

def format_hash(data):
    # data為讀取的32字節的二進制數據
    return str(hexlify(data[::-1]).decode('utf-8'))

下面是一個header例子

02000000 ........................... Block version: 2

b6ff0b1b1680a2862a30ca44d346d9e8
910d334beb48ca0c0000000000000000 ... Hash of previous block's header
9d10aa52ee949386ca9385695f04ede2
70dda20810decd12bc9b048aaab31471 ... Merkle root

24d95a54 ........................... Unix time: 1415239972
30c31b18 ........................... Target: 0x1bc330 * 256**(0x18-3)
fe9f0864 ........................... Nonce

對header進行兩次hash,可以得到區塊的hash值,示例代碼如下:

def double_sha256(data):
    return hashlib.sha256(hashlib.sha256(data).digest()).digest()

Merkle Root

Merkle樹,或者叫hash樹,是每個葉子節點用數據塊標記的樹,并且每個非葉子節點用其子節點的值加密hash后進行標記。這種數據結構允許高效和安全地驗證大型數據結構的內容。

在比特幣區塊中,Merkle root由交易列表生成,如下圖。

Merkle樹提供了一種驗證區塊中交易的方式。

Target nBits

Target值是256位無符號整數。header的SHA-256散列值必須低于或等于網絡接收的塊的當前目標。這個值越小,生成塊的難度越高。

前面提到的數學難題,即是找到一個隨機數(這個數介于0~2的256次方),使得對header的hash值滿足條件。

礦工們每次取一個隨機值,計算header的hash,如果低于目標,則“挖礦”成功;如果沒有,則遞增隨機數,再次驗證。能否挖到礦,除了取決礦工手里的算力,還要加上一點運氣。

交易

交易的結構如下

大小(字節) 名稱 數據類型 描述
4 version uint32 交易版本號
varint tx_in_count uint 交易輸入數量
varies tx_in tx_in 交易輸入
varint tx_out_count uint 交易輸出數量
varies tx_out tx_out 交易輸出
4 lock_time uint32 鎖定時間

從數據中解析流程大致如下:

  1. 讀取4個字節版本號
  2. 解析varint,得到輸入數量n
  3. 執行1~n次循環,解析交易輸入
  4. 解析varint,得到輸出數量m
  5. 執行1~m次循環,解析交易輸出

一個示例交易數據如下:

01000000 ................................... Version

01 ......................................... Number of inputs
|
| 7b1eabe0209b1fe794124575ef807057
| c77ada2138ae4fa8d6c4de0398a14f3f ......... Outpoint TXID
| 00000000 ................................. Outpoint index number
|
| 49 ....................................... Bytes in sig. script: 73
| | 48 ..................................... Push 72 bytes as data
| | | 30450221008949f0cb400094ad2b5eb3
| | | 99d59d01c14d73d8fe6e96df1a7150de
| | | b388ab8935022079656090d7f6bac4c9
| | | a94e0aad311a4268e082a725f8aeae05
| | | 73fb12ff866a5f01 ..................... Secp256k1 signature
|
| ffffffff ................................. Sequence number: UINT32_MAX

01 ......................................... Number of outputs
| f0ca052a01000000 ......................... Satoshis (49.99990000 BTC)
|
| 19 ....................................... Bytes in pubkey script: 25
| | 76 ..................................... OP_DUP
| | a9 ..................................... OP_HASH160
| | 14 ..................................... Push 20 bytes as data
| | | cbc20a7664f2f69e5355aa427045bc15
| | | e7c6c772 ............................. PubKey hash
| | 88 ..................................... OP_EQUALVERIFY
| | ac ..................................... OP_CHECKSIG

00000000 ................................... locktime: 0 (a block height)

Varint

交易中使用可變長度整數來表示下一條數據中的字節數。對于不同的數值,存儲的空間不一樣。

對于0~252的值,只占用一個字節;對于其他小于0xffffffffffffffff的值,第一個字節將成為長度標識位。值和存儲空間的關系如下表:

存儲空間(字節) 數據類型
>=0 && <=252 1 uint8_t
>=253 && <=0xffff 3 后2個字節uint16_t
>=0x10000 && <=0xffffffff 5 后4個字節uint32_t
>=0x100000000 && <=0xffffffffffffffff 9 后8個字節uint64_t

解析的示例代碼如下:

def decode_varint(data):
    assert(len(data) > 0)
    size = to_int(data[0])
    assert(size <= 255)

    if size < 253:
        return size, 1

    format_ = None
    if size == 253:
        format_ = '<H'
    elif size == 254:
        format_ = '<I'
    elif size == 255:
        format_ = '<Q'
    else:
        assert 0, 'unknown format_ for size: {size}'.format(size=size)
    size = struct.calcsize(format_)
    return struct.unpack(format_, data[1:size+1])[0], size + 1

交易輸入

每個非coinbase的交易輸入都是之前某個交易的交易輸出。

交易輸入的結構如下

大小(字節) 名稱 數據類型 描述
32 previous_output_hash outpoint 前置交易hash
4 previous_output_index uint32 前置交易index
varint script_bytes uint 解鎖腳本長度
varies signature_script char[] 解鎖腳本
4 sequence uint32 序列號

交易輸出

交易輸出的結構如下

大小(字節) 名稱 數據類型 描述
8 value int64 花費的數量,單位是聰
1+ pk_script_size uint pubkey腳本中的字節數量
varies pk_script char[] 花費這筆輸出需要滿足的條件

解析塊文件的程序可以參考下這里

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容