探寻比特币二:数据存储

前言

本文将会介绍比特币区块原始数据的存储格式,以及各个字段所代表的含义。

区块数据

比特币程序将数据存在了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阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容