本文是 Programming Bitcoin 第10章的读书笔记,同时涉及第9章的内容。第10章讨论比特币的网络协议。比特币网络为P2P网络。网络的各个节点公布不同的交易、区块和其所知道的其他节点。注意,比特币网络协议对于达成共识并不是关键的。同样的数据可以使用其他协议从一个节点发送到另一个节点,而不影响比特币本身。
比特币网络使用socket协议进行节点间的通讯。本章代码示例如何使用socket通讯请求、接收和验证区块的头部信息(block headers)。
原书中给出的网络结点’mainet.programmingbitcoin.com’和’testnet.programmingbitcoin.com’已经停止运行。可以到 bitnodes.io 查找其他可访问的网络结点(reachable nodes)。
要获得比特币网络节点的数据,要首先通过socket实现与结点的握手。握手的目的是与结点交换协议的版本信息。握手的具体过程如下(来自 这里 ):
L -> R: Send version message with the local peer's version
R -> L: Send version message back
R -> L: Send verack message
R: Sets version to the minimum of the 2 versions
L -> R: Send verack message after receiving version message from R
L: Sets version to the minimum of the 2 versions
L向R发送版本(version)信息,R向L发回自己的版本信息,并发送版本接收信息(VerAck)。R将协议版本定为两个版本之较小者。L在接收到R的版本信息后向R发送VerAck信息。L也将协议版本设定为两个版本之较小者。
由此可知,原书第183页的代码有个小错误:
host = "xx.xx.xx.xx"
node = SimpleNode(host, testnet=False )
version = VersionMessage()
node.send(version)
verack_received = False
version_received = False
while not verack_received and not version_received:
message = node.wait_for(VersionMessage, VerAckMessage)
if message.command == VerAckMessage.command:
verack_received = True
else:
version_received = True
在上述代码的 while not verack_received and not version_received: 一行中的 and 应该为 or 。否则不能完成全部握手过程。
在完成握手后,就可以获得区块头部(block headers)信息了。区块数据由头部和交易(tx)数据组成。头部数据为每个区块的原数据,由如下部分组成:
Version
Previous block
Merkle root
timestamp
bits
nonce
我们依据上述区块头部信息验证区块是否达成proof of work。我们计算该区块头部数据的哈希值,以验证其是否达到了一定的工作量标准。该值一定要足够的小才符合标准。为达此目的,区块的编纂者(即挖矿者)必须通过不断试错Merkel root和nonce值来找到足够小的hash值。找到这样的小数的概率是很低的,因此需要进行大量次的哈希运算,如同为找到一点金子要淘选大量的泥沙。故名挖矿。
可以设想,随着投入挖矿的算力不同,区块产生的速度会发生波动。这对于保证交易的及时清算是非常不利的。因此,比特币通过动态调整proof of work难度的办法,把区块挖出的速度控制在每10分钟一个左右。这样,在两周时间里,就可产生约6×24×14=2016个区块。为达成此目标,比特币每生成2016个区块就调整一次哈希运算的目标值(哈希值必须小于这个目标值),以适应算力的变化。这个值放在区块头部的bits字段里。
在区块的头部数据里,还包括其所指向的上一个区块的哈希,即previous block字段。此即区块链之称谓的由来。区块就是这样连缀起来的。
因此在验证一个区块头部时,应包含有以下三方面的内容:
1.区块所指向的上一个区块是否真实地是其上一个区块。
2.区块给自己设定的bits是否是根据上一个2016个区块的周期的速度调整的目标值。区块是不能自降挖矿难度的。
3.区块是否达成了proof of work的标准。
因为验证区块头部数据需要我们使用区块生成速度的历史数据,因此完整的验证过程必须从第一个区块(genesis block)开始,并依次验证所有区块。书中的代码如下:
host = "xx.xx.xx.xx"
node = SimpleNode(host, testnet= False)
node.handshake()
previous = (Block.parse(BytesIO(GENESIS_BLOCK)))
first_epoch_timestamp = previous.timestamp
expected_bits = LOWEST_BITS
count = 1
for _ in range(19 ):
getheaders = GetHeadersMessage(start_block= previous.hash())
node.send (getheaders)
headers = node.wait_for(HeadersMessage)
for header in headers.blocks:
if not header.check_pow():
raise RuntimeError('bad PoW at block {}' .format(count))
if header.prev_block ! = previous.hash():
raise RuntimeError('discontinuous block at {}' .format(count))
if count % 2016 = = 0 :
time_diff = previous.timestamp - first_epoch_timestamp
expected_bits = calculate_new_bits(previous.bits, time_diff)
print(expected_bits.hex())
first_epoch_timestamp = header.timestamp
if header.bits ! = expected_bits:
raise RuntimeError('bad bits at block {}' .format(count))
previous = header
count + = 1
注意要想成功运行上述代码,需注意其中函数 calculate_new_bits 定义的细节:
def calculate_new_bits(previous_bits, time_differential):
if time_differential > TWO_WEEKS * 4 :
time_differential = TWO_WEEKS * 4
if time_differential < TWO_WEEKS
time_differential = TWO_WEEKS
new_target = bits_to_target(previous_bits) * time_differential
if new_target > MAX_TARGET:
new_target = MAX_TARGET
return target_to_bits(new_target)
该函数出现在书中第175页的代码和176页的Exercise 13,及278页的答案中。注意书中忽略了
if new_target > MAX_TARGET:
new_target = MAX_TARGET
导致程序报错。这段代码是说当新调整的哈希目标超过最大目标时,使用最大目标。
Song, Jimmy. Programming bitcoin: Learn how to program bitcoin from scratch . O'Reilly Media, 2019.