# 读书笔记：比特币的网络协议

By [Aulee](https://paragraph.com/@aulee) · 2023-06-04

---

本文是[Programming Bitcoin](https://book.douban.com/subject/30441608/)第10章的读书笔记，同时涉及第9章的内容。第10章讨论比特币的网络协议。比特币网络为P2P网络。网络的各个节点公布不同的交易、区块和其所知道的其他节点。注意，比特币网络协议对于达成共识并不是关键的。同样的数据可以使用其他协议从一个节点发送到另一个节点，而不影响比特币本身。

比特币网络使用socket协议进行节点间的通讯。本章代码示例如何使用socket通讯请求、接收和验证区块的头部信息（block headers）。

### 如何查找可访问的结点

原书中给出的网络结点’mainet.programmingbitcoin.com’和’testnet.programmingbitcoin.com’已经停止运行。可以到[bitnodes.io](http://bitnodes.io)查找其他可访问的网络结点（reachable nodes）。

### 网络握手

要获得比特币网络节点的数据，要首先通过socket实现与结点的握手。握手的目的是与结点交换协议的版本信息。握手的具体过程如下（来自[这里](https://en.bitcoin.it/wiki/Version_Handshake)）：

    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 // 4:
            time_differential = TWO_WEEKS // 4
        new_target = bits_to_target(previous_bits) * time_differential // TWO_WEEKS
        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.

---

*Originally published on [Aulee](https://paragraph.com/@aulee/WUUIsvD6JLPsshoKgRVd)*
