比特币源码阅读笔记【网络篇】

这篇文章总结下比特币网络相关内容。

2008年中本聪创造比特币时在白皮书中这样定义比特币:一个点对点的电子现金系统,那时还没有“区块链”这个说法。那段时间,点对点(P2P)网络已经有了广泛的应用,例如Bittorrent和迅雷。P2P网络最大的特点就是网络中没有“特权节点”,所有节点共同承担P2P服务,这就是所谓“去中心化”。比特币的设计初衷就是创造一个去中心化的共识网络。比特币网络指的是运行比特币P2P协议的节点集合。随着比特币生态的发展,除了比特币P2P协议,比特币的节点还可能运行着其他协议, 用于挖矿和轻量级钱包等应用。矿机与矿池软件之间的通讯协议是Stratum,而矿池软件与钱包之间的通讯是bitcoinrpc接口。网关路由服务器在运行比特币P2P协议的同时,还运行着附加协议用于将Stratum协议节点桥接到比特币网络中。这些桥接进比特币网络中的节点属于扩展比特币网络

一、比特币网络

1.1 节点类别

比特币P2P网络的去中心,准确地说是节点之间的关系对等,而不是所有节点功能完全一致。比特币网络中的节点有如下几种功能:1.网络路由(Network Route, 简写为N) 2.完整区块链(Full Blockchain, 简写为B) 3.矿工(Miner, 简写为M) 4.钱包(Wallet, 简写为Wallet)。如果一个比特币节点具有上述全部四种功能,那么这个节点叫做全节点。如果一个节点只保存部分区块,同时使用简化支付验证 (Simplified Payment Verification, SPV)的方法验证交易,那么这个节点叫做SPV节点或者轻量级节点。比特币网络中的大部分节点都不是全节点, 本地数据库只存储部分区块链的情况下,节点依然可以具有钱包功能或者挖矿功能。概括下来,扩展比特币网络中的节点主要分为以下几种:

  • 参考客户端 包含钱包,矿工,完整区块链数据库,比特币P2P网络路由节点 比特币节点类型-参考客户的.png-57.3kB
  • 完整区块链节点 包含完整区块链数据库,比特币P2P网络路由节点 完整区块链节点.png-38.3kB
  • 独立矿工 包含挖矿函数,完整区块链数据库,比特币P2P网络路由节点纯矿工.png-46.5kB
  • 轻量级(SPV)钱包 包含钱包和比特币P2P网络路由节点,不包含区块链数据库 轻量级(SPV)钱包.png-38kB
  • 矿池协议服务器 网关路由,用于连接比特币P2P协议网络和其他协议的节点 矿池协议服务器.png-30.6kB
  • 矿池矿工节点 包含挖矿函数,不包含完整区块链数据库,但运行着其他矿池协议矿池矿工节点.png-27.8kB
  • 轻量级(SPV) Stratum钱包 包含钱包和Stratum协议节点,不包含区块链数据 轻量级Stratum钱包.png-38.9kB

比特币协议,Stratum协议和矿池协议组成了比特币网络的基础,扩展比特币网络结构如下所示。少量的全节点客户端,少量的独立矿工,矿池及其背后大量矿池矿工,
完整网络.png-776.6kB

1.2 接力网络

比特币矿工争分夺秒地进行着工作量证明竞赛,为了成功参与这项竞赛,矿工们需要尽可能地缩短网络延迟,包括挖到区块时对外广播和接收其他节点发起的下一个区块的竞赛。矿工间的挖矿竞赛实质上是计算能力和网络水平的双重比拼。为了优化矿工的网络,2015年,Matt Corallo创建了一个接力网络(Relay Network)向全球比特币矿工提供低延迟网络服务。网络维护了一批亚马逊云主机(Amazon Web Services, AWS),连接着全球大部分的矿工和矿池节点。接力网络如下图所示。
接力网络.png-1007.2kB

此后,接力网络还推出了付费升级版,叫做FIBRE网络,每月需要支付约50美元。FIBRA是一个基于UDP协议的网络,它实现了一种“紧凑块”优化策略,进一步减少了数据传输时间。目前FIBRA网络有6个节点。整个网络维护了一份IP白名单,只有白名单中的矿工才能连接到FIBRE网络,且每个矿工节点只能连接6个节点中的1个。到目前为止,接力网络并没有成为Bitcoin Core的正式部分。接力网络的更多信息,可以参考这里

二、典型场景

下面我们从更微观的角度解释比特币网络。比特币的网络模块主要包括以下几个功能:

  • 建立初始连接
  • 地址传播发现
  • 同步区块数据
  • 断开连接

比特币网络相关的代码主要在src/net.cpp, src/netbase.cpp,src/net_processing。比特币全部网络消息类型定义见src/protocol.h

理解每一种消息的场景和含义,我们就掌握了比特币网络的核心内容。例如,VERSION消息和VERACK消息用于建立连接;ADDRGETADDR消息用于地址传播;GETBLOCKS, INVGETDATA消息用于同步区块链数据。感兴趣的读者,建议完整地查看所有比特币网络消息类型,分析其应用场景。

namespace NetMsgType {
/**
 * The version message provides information about the transmitting node to the
 * receiving node at the beginning of a connection.
 * @see https://bitcoin.org/en/developer-reference#version
 */
extern const char *VERSION;
/**
 * The verack message acknowledges a previously-received version message,
 * informing the connecting node that it can begin to send other messages.
 * @see https://bitcoin.org/en/developer-reference#verack
 */
extern const char *VERACK;
/**
 * The addr (IP address) message relays connection information for peers on the
 * network.
 * @see https://bitcoin.org/en/developer-reference#addr
 */
extern const char *ADDR;
// 其他消息类型省略

2.1 建立连接

在比特币客户端,可以用bitcoin-cli getpeerinfo命令查看可以连接到的网络节点信息:

$ bitcoin-cli getpeerinfo
[{
    "addr": "85.213.199.39:8333",
    "services": "00000001",
    "lastsend": 1405634126,
    "lastrecv": 1405634127,
    "bytessent": 23487651,
    "bytesrecv": 138679099,
    "conntime": 1405021768,
    "pingtime": 0.00000000,
    "version": 70002,
    "subver": "/Satoshi:0.9.2.1/",
    "inbound": false,
    "startingheight": 310131,
    "banscore": 0,
    "syncnode": true
}, {
    "addr": "58.23.244.20:8333",
    "services": "00000001",
    "lastsend": 1405634127,
    "lastrecv": 1405634124,
    "bytessent": 4460918,
    "bytesrecv": 8903575,
    "conntime": 1405559628,
    "pingtime": 0.00000000,
    "version": 70001,
    "subver": "/Satoshi:0.8.6/",
    "inbound": false,
    "startingheight": 311074,
    "banscore": 0,
    "syncnode": false
}]

找到同伴后,客户端就开始与已知的同伴建立TCP连接,默认端口是8333。
比特币客户端之间的连接过程和TCP三次握手一模一样。节点A向节点B发送自己的版本号ver,B收到A的版本号后,如果与自己兼容则确认连接,B返回verack,同时向A发送B自己的版本号,如果A也兼容,A再次返回verack, 成功建立连接。整个过程如下图所示:
初始连接.png-95.9kB

建立连接部分的更多细节见src/net.cpp。输入待连接的地址addrConnect,返回该地址的节点pnode。如果这个节点已经连接,直接返回这个节点。如果是新的节点,尝试建立Socket连接或者通过代理连接。

CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure)
{
    if (pszDest == nullptr) {
        if (IsLocal(addrConnect))
            return nullptr;

        // Look for an existing connection
        CNode* pnode = FindNode(static_cast<CService>(addrConnect));
        if (pnode)
        {
            LogPrintf("Failed to open new connection, already connected\n");
            return nullptr;
        }
    }
    // 省略部分代码
    bool connected = false;
    SOCKET hSocket = INVALID_SOCKET;
    proxyType proxy;
    if (addrConnect.IsValid()) {
        bool proxyConnectionFailed = false;
        // 网络代理部分代码省略
        std::string host;
        int port = default_port;
        SplitHostPort(std::string(pszDest), port, host);
        connected = ConnectThroughProxy(proxy, host, port, hSocket, nConnectTimeout, nullptr);
    }
    if (!connected) {
        CloseSocket(hSocket);
        return nullptr;
    }
    NodeId id = GetNewNodeId();
    uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize();
    CAddress addr_bind = GetBindAddress(hSocket);
    CNode* pnode = new CNode(id, nLocalServices, GetBestHeight(), hSocket, addrConnect, CalculateKeyedNetGroup(addrConnect), nonce, addr_bind, pszDest ? pszDest : "", false);
    pnode->AddRef();

    return pnode;
}

2.2 地址传播发现

一旦建立了连接,节点会向它的所有邻居节点发送addr消息,这条消息包含着节点自己的IP地址,用于向更多节点告知自己。此外,节点还会向它所有的邻居节点发送getaddr请求,获取邻居节点可以连接的节点列表。整个过程如下图所示。
地址传播发现.png-96.2kB

2.3 同步区块数据

连接建立后,两个节点会互相发送同步请求getblocks, 节点比较对方的BestHeight后,区块数较多的一方向区块较少的一方发送inv响应,让落后的节点追上。收到inv响应后,落后的节点开始发送getdata请求数据。整个过程如下图所示:
同步区块链数据.png-158.7kB

2.4 断开连接

如果两个节点建立网络连接后没有流量,节点之间会定期发送消息保持连接,如果两个节点超过一定时间没有发送消息,那么认为节点已经断开,并开始寻找新的节点。

考虑到文章篇幅因素,此处省略2.2,2.3和2.4的代码分析部分。

三、简化支付验证 SPV

最后我们介绍比特币网络中的重要组成部分,轻量级钱包
上面提到过,并不是所有节点都在本地数据库保存着完整区块链,毕竟完整的账本非常消耗存储空间。很多比特币客户端是在智能手机等设备上运行的。为了支持这些设备(最主要就是智能手机),中本聪在白皮书中提到了简化支付验证SPV, 用户客户端只需要保存区块header就可以验证支付。用户如果能够从区块链的某处找到相符的交易,他就可以知道网络已经认可了这笔交易,而且得到了网络的多少个确认。轻量级钱包节点只同步header也大大减少了客户端的网络开销。

SPV节点同步区块header.png-107kB

SPV背后的技术原理是Bloom Filter。Bloom Filter是一种概率搜索器,它在搜索时不需要完整描述被搜索的模式,这种查询虽然存在一定比例的伪命中,但效率远远高出普通查询。例如,查询名字以字母g结尾的,交易金额的小数部分是0.618的交易。搜索条件的模糊程度和交易隐私泄露程度构成了一种权衡关系。有了SPV, 我们可以在不暴露地址的情况下完成支付验证。

这里需要注意,SPV指的是“支付验证“,而不是“交易验证”。这两种验证有很大区别。”交易验证”非常复杂,涉及到验证是否有足够余额可供支出、是否存在双花、脚本能否通过等等,通常由运行完全节点的矿工来完成。“支付验证”则比较简单,只判断用于“支付”的那笔交易是否已经被验证过,并得到了多少的算力保护(多少确认数)。

四、参考资料

没有更多推荐了,返回首页