这篇文章总结下比特币网络相关内容。
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网络路由节点
- 完整区块链节点 包含完整区块链数据库,比特币P2P网络路由节点
- 独立矿工 包含挖矿函数,完整区块链数据库,比特币P2P网络路由节点
- 轻量级(SPV)钱包 包含钱包和比特币P2P网络路由节点,不包含区块链数据库
- 矿池协议服务器 网关路由,用于连接比特币P2P协议网络和其他协议的节点
- 矿池矿工节点 包含挖矿函数,不包含完整区块链数据库,但运行着其他矿池协议
- 轻量级(SPV) Stratum钱包 包含钱包和Stratum协议节点,不包含区块链数据
比特币协议,Stratum协议和矿池协议组成了比特币网络的基础,扩展比特币网络结构如下所示。少量的全节点客户端,少量的独立矿工,矿池及其背后大量矿池矿工,
1.2 接力网络
比特币矿工争分夺秒地进行着工作量证明竞赛,为了成功参与这项竞赛,矿工们需要尽可能地缩短网络延迟,包括挖到区块时对外广播和接收其他节点发起的下一个区块的竞赛。矿工间的挖矿竞赛实质上是计算能力和网络水平的双重比拼。为了优化矿工的网络,2015年,Matt Corallo创建了一个接力网络(Relay Network)向全球比特币矿工提供低延迟网络服务。网络维护了一批亚马逊云主机(Amazon Web Services, AWS),连接着全球大部分的矿工和矿池节点。接力网络如下图所示。
此后,接力网络还推出了付费升级版,叫做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
消息用于建立连接;ADDR
和GETADDR
消息用于地址传播;GETBLOCKS
, INV
和GETDATA
消息用于同步区块链数据。感兴趣的读者,建议完整地查看所有比特币网络消息类型,分析其应用场景。
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, 成功建立连接。整个过程如下图所示:
建立连接部分的更多细节见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请求,获取邻居节点可以连接的节点列表。整个过程如下图所示。
2.3 同步区块数据
连接建立后,两个节点会互相发送同步请求getblocks, 节点比较对方的BestHeight后,区块数较多的一方向区块较少的一方发送inv响应,让落后的节点追上。收到inv响应后,落后的节点开始发送getdata请求数据。整个过程如下图所示:
2.4 断开连接
如果两个节点建立网络连接后没有流量,节点之间会定期发送消息保持连接,如果两个节点超过一定时间没有发送消息,那么认为节点已经断开,并开始寻找新的节点。
考虑到文章篇幅因素,此处省略2.2,2.3和2.4的代码分析部分。
三、简化支付验证 SPV
最后我们介绍比特币网络中的重要组成部分,轻量级钱包。
上面提到过,并不是所有节点都在本地数据库保存着完整区块链,毕竟完整的账本非常消耗存储空间。很多比特币客户端是在智能手机等设备上运行的。为了支持这些设备(最主要就是智能手机),中本聪在白皮书中提到了简化支付验证SPV, 用户客户端只需要保存区块header就可以验证支付。用户如果能够从区块链的某处找到相符的交易,他就可以知道网络已经认可了这笔交易,而且得到了网络的多少个确认。轻量级钱包节点只同步header也大大减少了客户端的网络开销。
SPV背后的技术原理是Bloom Filter。Bloom Filter是一种概率搜索器,它在搜索时不需要完整描述被搜索的模式,这种查询虽然存在一定比例的伪命中,但效率远远高出普通查询。例如,查询名字以字母g结尾的,交易金额的小数部分是0.618的交易。搜索条件的模糊程度和交易隐私泄露程度构成了一种权衡关系。有了SPV, 我们可以在不暴露地址的情况下完成支付验证。
这里需要注意,SPV指的是“支付验证“,而不是“交易验证”。这两种验证有很大区别。”交易验证”非常复杂,涉及到验证是否有足够余额可供支出、是否存在双花、脚本能否通过等等,通常由运行完全节点的矿工来完成。“支付验证”则比较简单,只判断用于“支付”的那笔交易是否已经被验证过,并得到了多少的算力保护(多少确认数)。
四、参考资料
- Mastering Bitcoin 第八章
- 巴比特论坛 什么是SPV钱包(轻钱包)
- 维基百科 Bloom Filter。