第十章. 比特币网络

  • berry
  • 发布于 2025-02-09 10:44
  • 阅读 12

第十章. 比特币网络

综合介绍

比特币是建立在互联网之上的对等网络架构。 对等网络或P2P的术语意味着参与网络的全部节点彼此对等,它们都能执行相同的功能,并且没有“特殊”的节点。网络节点以“平面”拓扑连接成网状网络。在网络内部没有服务器、没有中心化的服务,也没有层次结构。P2P网络中的节点同时提供和消耗服务。P2P网络具有固有的韧性、去中心化和开放性。一个著名的P2P网络架构示例是早期的互联网本身,在IP网络中的节点是平等的。今天的互联网架构更加层次化,但是互联网协议仍保留着其平面拓扑的本质。除了比特币和互联网之外,P2P技术最大和最成功的应用是文件共享,Napster是其先驱,而BitTorrent则是这种架构的最新演变。

比特币的P2P网络架构远不止是一种拓扑选择。比特币是一个经设计的P2P数字现金系统,网络架构既是这一核心特性的反映,也是其基础。对控制的去中心化是一个核心设计原则,只有通过平面和去中心化的P2P共识网络才能实现和维护。

术语“比特币网络”指的是运行比特币P2P协议的节点集合。除了比特币P2P协议之外,还有其他用于挖矿和轻量级钱包的协议。这些附加协议由网关路由服务器提供,这些服务器使用比特币P2P协议访问比特币网络,然后将该网络扩展到运行其他协议的节点。例如,Stratum服务器通过Stratum协议连接Stratum挖矿节点到主比特币网络,并将Stratum协议桥接到比特币P2P协议。除了基本的比特币P2P协议之外,我们将在本章中描述一些最常用的这些协议。

节点类型和任务

尽管比特币P2P网络中的全节点(对等节点)彼此平等,但它们可能根据它们支持的功能而承担不同的角色。比特币全节点验证区块并可能包含其他功能,如路由、挖矿和钱包服务。

一些节点,称为归档全节点,还维护着完整且最新的区块链副本。这些节点可以为仅存储区块链子集并使用简化支付验证(SPV)方法部分验证交易的客户端提供数据。这些客户端称为轻量级客户端。

矿工通过运行专用硬件来解决工作量证明算法来竞争创建新的区块。一些矿工运行全节点,验证区块链上的每个区块,而其他一些矿工则是参与池挖矿的客户端,依赖于池服务器提供工作。

用户钱包可能会连接到用户自己的全节点,这在桌面比特币客户端中有时是这样的情况,但许多用户钱包,尤其是在资源受限的设备上运行的钱包,如智能手机上的钱包,是轻量级节点。

除了比特币P2P协议中的主要节点类型外,还有运行其他协议的服务器和节点,如专用的挖矿池协议和轻量级客户端访问协议。

网络

截至目前,主比特币网络使用比特币P2P协议运行,约有10,000个监听节点运行着各种版本的比特币核心(Bitcoin Core),还有几百个节点运行着各种其他实现的比特币P2P协议,如BitcoinJ、btcd和bcoin。在比特币P2P网络中,只有很小一部分节点也是挖矿节点。各种个人和公司通过运行归档完整节点与比特币网络接口,这些节点拥有区块链的完整副本和网络节点,但没有挖矿或钱包功能。这些节点充当网络边缘路由器,允许在其上构建各种其他服务(如交易所、钱包、区块浏览器、商家支付处理等)。

紧凑块传输

当矿工发现一个新的区块时,他们会向比特币网络(包括其他矿工)宣布。发现该区块的矿工可以立即在其上构建;尚未获知该区块的其他矿工将继续在前一个区块上构建,直到他们获知为止。

如果在他们获知新区块之前,其他矿工之一创建了一个区块,他们的区块将与第一个矿工的新区块竞争。只有一个区块会被包含在所有完整节点使用的区块链中,并且矿工只有在被广泛接受的区块中才能获得收益。

先有第二个区块构建在其上的任何一个区块获胜(除非有另一个接近平局的情况),这称为区块发现竞赛,在图10-1中进行了说明。区块发现竞赛使得最大的矿工获得了优势,因此它们与比特币的基本去中心化原则相矛盾。为了防止区块发现竞赛,并允许任何规模的矿工平等参与比特币挖矿的抽奖,极其有用的是尽量减少一个矿工宣布新区块与其他矿工接收到该区块之间的时间。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/10.1.png" alt=""><figcaption><p>图 10-1. 需要挖矿竞赛的区块链分叉</p></figcaption></figure>

2015年,比特币核心的一个新版本添加了一项名为紧凑块传输(在BIP152中指定)的功能,可以更快地传输新区块并减少带宽使用。

作为背景,中继未确认交易的完整节点也会将许多这些交易存储在其内存池中(参见第244页的“内存池和孤立池”)。当其中一些交易在新的区块中得到确认时,节点不需要再收到这些交易的副本。

紧凑块传输不再接收冗余的未确认交易,而是允许节点将每个交易发送一个短的6字节标识符。当你的节点收到一个或多个标识符的紧凑块时,它会检查其内存池中是否存在这些交易,并在找到时使用它们。对于在本地节点的内存池中找不到的任何交易,你的节点可以向对等方发送请求以获取副本。

相反,如果远程节点认为你的节点的内存池中没有某些在区块中出现的交易,它可以在紧凑块中包含这些交易的副本。例如,比特币核心始终发送一个区块的 Coinbase 交易。

如果远程节点猜对了你的节点在其内存池中有哪些交易,以及哪些交易没有,它将以几乎理论上可能的效率发送一个区块(对于典型的区块,其效率将在97%到99%之间)。

注意: 紧凑块传输并不会减小区块的大小。它只是防止了节点已经拥有的信息的冗余传输。当一个节点之前没有关于一个区块的信息,例如当一个节点首次启动时,它必须接收每个区块的完整副本。 {% endhint %}

比特币核心当前实现了两种发送紧凑块的模式,如图10-2所示:

低带宽模式

当你的节点请求对等方使用低带宽模式时(默认情况下),该对等方将告知你的节点一个新区块的32字节标识符(头哈希),但不会向你的节点发送任何有关该区块的详细信息。如果你的节点首先从其他来源获取到该区块,这将避免浪费更多的带宽来获取该区块的冗余副本。如果你的节点确实需要该区块,它将请求一个紧凑块。

高带宽模式

当你的节点请求对等方使用高带宽模式时,该对等方将在完全验证该区块的有效性之前,向你的节点发送一个新区块的紧凑块。该对等方进行的唯一验证是确保区块的头包含正确数量的工作证明。由于工作证明的生成成本昂贵(根据当前情况约为150,000美元),矿工不太可能伪造它只是为了浪费中继节点的带宽。在传输之前跳过验证允许新区块在网络中的每个跳点之间以最小的延迟传播。

高带宽模式的缺点是,你的节点很可能会从每个选择的高带宽对等方接收到冗余信息。截至目前,比特币核心目前只要求三个对等方使用高带宽模式(并尝试选择已经快速宣布区块历史的对等方)。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/10.2.png" alt=""><figcaption><p>图 10-2. BIP152模式比较(来自BIP152)。阴影条表示节点验证区块所需的时间。</p></figcaption></figure>

这两种方法的名称(取自BIP152)可能有些令人困惑。低带宽模式通过在大多数情况下不发送区块来节省带宽。高带宽模式比低带宽模式使用更多的带宽,但在大多数情况下,比实施紧凑块之前的区块传输使用的带宽要少得多。

私有区块传输网络

尽管紧凑块在最大程度上缩短了区块在网络中传播所需的时间,但仍然有可能进一步减少延迟。然而,与紧凑块不同,其他解决方案涉及到的权衡使它们无法在公共P2P中继网络中使用或不适合。因此,人们已经开始尝试为区块建立私有中继网络。

一种简单的技术是预先选择终点之间的路由。例如,一个中继网络,其中的服务器运行在靠近主要跨洋光纤线的数据中心中,可能能够比等待区块到达距离光纤线几公里之远的家庭用户节点更快地转发新的区块。

另一种更复杂的技术是前向纠错编码(FEC)。这允许将紧凑块消息分成几部分,并附加额外的数据到每个部分。如果有任何部分没有被接收到,那么这部分可以从已接收到的部分重建。根据设置,如果部分丢失,最多可以重建几部分。

FEC避免了由于底层网络连接的问题而导致紧凑块(或其某些部分)未到达的问题。这些问题经常发生,但我们通常不会注意到,因为我们大多使用自动重新请求丢失数据的协议。然而,请求丢失的数据会使接收数据的时间延长三倍。例如:

  1. Alice向Bob发送一些数据。
  2. Bob未收到数据(或数据损坏)。Bob重新向Alice请求数据。
  3. Alice再次发送数据。

第三种技术是假设接收数据的所有节点几乎都具有相同的内存池中的大部分交易,因此它们都可以接受相同的紧凑块。这不仅节省了在每个跳点计算紧凑块的时间,而且意味着每个跳点可以在验证它们之前简单地将FEC数据包中继到下一个跳点。

前述每种方法的权衡是,它们与中心化结构很好地配合,但在无法相互信任的分散网络中效果不佳。位于数据中心的服务器需要花费金钱,并且通常可以被数据中心的运营者访问,这使它们比安全的家用计算机不太可靠。在验证之前传输数据很容易浪费带宽,因此它只能在私有网络中合理地使用,在这种网络中各方之间存在一定程度的信任和责任。

原始的比特币中继网络是由开发者Matt Corallo于2015年创建的,旨在实现矿工之间的快速区块同步,延迟极低。该网络由托管在全球基础设施上的几个虚拟专用服务器(VPSes)组成,用于连接大多数矿工和矿池。

2016年,随着由开发者Matt Corallo创建的快速互联网比特币中继引擎(FIBRE)的推出,原始的比特币中继网络被替换。FIBRE是一种软件,允许在节点网络中运行基于UDP的中继网络,用于中继区块。FIBRE实现了FEC和紧凑块优化,进一步减少了传输数据量和网络延迟。

网络发现

当新节点启动时,它必须发现网络上的其他比特币节点才能参与其中。要启动此过程,新节点必须至少发现网络上的一个现有节点并连接到它。其他节点的地理位置无关紧要;比特币网络拓扑结构不是地理上定义的。因此,可以随机选择任何现有的比特币节点。

要连接到已知的对等方,节点会建立一个TCP连接,通常连接到端口8333(通常被称为比特币使用的端口),或者如果提供了其他端口,则连接到替代端口。建立连接后,节点将通过发送版本消息(见图10-3)开始“握手”,该消息包含基本的识别信息,包括:

版本(Version):

客户端“发言”的比特币P2P协议版本(例如,70002)

本地服务(nLocalServices):

节点支持的本地服务列表

时间(nTime):

当前时间

addrYou:

远程节点的IP地址,从本节点看到的

addrMe:

本地节点的IP地址,由本地节点发现的

subver:

显示在此节点上运行的软件类型的子版本(例如,/Satoshi:0.9.2.1/)

最佳高度(BestHeight):

此节点区块链的块高度

fRelay:

由BIP37添加的字段,用于请求不接收未确认的交易

版本消息始终是任何对等方向另一个对等方发送的第一条消息。接收版本消息的本地对等方将检查远程对等方报告的版本,并决定远程对等方是否兼容。如果远程对等方是兼容的,则本地对等方将确认版本消息并通过发送verack建立连接。

新节点如何找到对等节点?第一种方法是使用一些DNS种子进行DNS查询,这些DNS种子是提供比特币节点IP地址列表的DNS服务器。其中一些DNS种子提供稳定的比特币监听节点的IP地址静态列表。一些DNS种子是BIND(Berkeley Internet Name Daemon)的自定义实现,它们从由网络爬虫或长时间运行的比特币节点收集的IP地址列表中返回随机子集。比特币核心客户端包含几个不同DNS种子的名称。不同DNS种子的所有权和实现的多样性为初始引导过程提供了高可靠性水平。在比特币核心客户端中,使用DNS种子的选项由选项开关-dnsseed控制(默认设置为1,以使用DNS种子)。

另外,一个对网络一无所知的引导节点必须至少给出一个比特币节点的IP地址,之后它可以通过进一步的介绍建立连接。命令行参数-seednode可用于仅使用它作为种子进行介绍的连接到一个节点。在使用初始种子节点进行介绍后,客户端将与其断开连接并使用新发现的对等节点。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/10.3.png" alt=""><figcaption><p>图 10-3. 对等方之间的初始握手</p></figcaption></figure>

一旦建立了一个或多个连接,新节点将向其邻居发送一个包含自己 IP 地址的 addr 消息。邻居们将转发 addr 消息给它们的邻居,确保新连接的节点变得广为人知并且连接更加稳固。此外,新连接的节点可以向其邻居发送 getaddr,请求它们返回其他节点的 IP 地址列表。这样,一个节点就可以找到要连接的节点,并在网络上广告自己的存在,以便其他节点找到它。下图展示了地址发现协议。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/10.4.png" alt=""><figcaption><p>图 10-4. 地址传播和发现</p></figcaption></figure>

节点必须连接到几个不同的对等节点才能建立进入比特币网络的多样化路径。路径并不可靠 - 节点会不断加入和离开网络 - 因此节点必须在失去旧连接时继续发现新节点,并在引导其他节点时提供帮助。引导只需要连接到一个节点,因为第一个节点可以向其对等节点提供介绍,而这些对等节点可以提供进一步的介绍。连接到多个节点是不必要的,也是对网络资源的浪费。引导完成后,节点将记住其最近成功的对等节点连接,因此如果重新启动,它可以快速重新与其以前的对等网络建立连接。如果没有任何以前的对等节点响应其连接请求,则节点可以再次使用种子节点进行引导。

在运行比特币核心客户端的节点上,你可以使用命令 getpeerinfo 列出对等连接:

$ bitcoin - cli getpeerinfo
[{
    "id": 0,
    "addr": "82.64.116.5:8333",
    "addrbind": "192.168.0.133:50564",
    "addrlocal": "72.253.6.11:50564",
    "network": "ipv4",
    "services": "0000000000000409",
    "servicesnames": [
        "NETWORK",
        "WITNESS",
        "NETWORK_LIMITED"
    ],
    "lastsend": 1683829947,
    "lastrecv": 1683829989,
    "last_transaction": 0,
    "last_block": 1683829989,
    "bytessent": 3558504,
    "bytesrecv": 6016081,
    "conntime": 1683647841,
    "timeoffset": 0,
    "pingtime": 0.204744,
    "minping": 0.20337,
    "version": 70016,
    "subver": "/Satoshi:24.0.1/",
    "inbound": false,
    "bip152_hb_to": true,
    "bip152_hb_from": false,
    "startingheight": 788954,
    "presynced_headers": -1,
    "synced_headers": 789281,
    "synced_blocks": 789281,
    "inflight": [],
    "relaytxes": false,
    "minfeefilter": 0.00000000,
    "addr_relay_enabled": false,
    "addr_processed": 0,
    "addr_rate_limited": 0,
    "permissions": [],
    "bytessent_per_msg": {
        ...
    },
    "bytesrecv_per_msg": {
        ...
    },
    "connection_type": "block-relay-only"
}, ]

要覆盖对等节点的自动管理并指定 IP 地址列表,用户可以提供选项 -connect= 并指定一个或多个 IP 地址。如果使用了此选项,则节点将只连接到选定的 IP 地址,而不会自动发现和维护对等连接。如果连接上没有流量,节点将定期发送消息以维持连接。

如果节点在某个连接上长时间没有通信,就会被认为已断开连接,并寻找一个新的对等节点。因此,网络会动态调整以适应短暂的节点和网络问题,并在没有任何中央控制的情况下根据需要有机地增长和收缩。

全节点

全节点是在具有最多工作量证明的有效区块链上验证每个区块中的每笔交易的节点。

全节点独立处理每个区块,从第一个区块(创世区块)开始,一直构建到网络中已知的最新区块。全节点可以独立且具有权威性地验证任何交易。全节点依赖于网络来接收关于新交易区块的更新信息,然后验证并将其合并到本地视图中,以确定哪些脚本控制着哪些比特币,即未花费交易输出(UTXO)的集合。

运行全节点可以让你获得纯粹的比特币体验:独立验证所有交易,无需依赖或信任其他系统。

有一些替代的全节点实现,使用了不同的编程语言和软件架构,或者做出了不同的设计决策。然而,最常见的实现是比特币核心(Bitcoin Core)。超过 95% 的比特币网络上的全节点运行着不同版本的比特币核心。它在版本消息中发送的子版本字符串中被标识为“Satoshi”,如我们之前所见的 getpeerinfo 命令所示;例如,/Satoshi:24.0.1/。

交换“存货”

在连接到对等节点后,全节点将尝试构建完整的区块头链。如果它是一个全新的节点,并且没有任何区块链,那么它只知道一个区块,即创世区块,该区块静态地嵌入在客户端软件中。从区块 #0(创世区块)开始,新节点将需要下载数十万个区块才能与网络同步并重新建立完整的区块链。

同步区块链的过程始于版本消息,因为其中包含 BestHeight,即节点当前的区块链高度(区块数量)。一个节点将会收到来自其对等节点的版本消息,了解它们各自拥有的区块数量,并将其与自己的区块链进行比较。对等节点将交换一个包含其本地区块链顶部区块哈希的 getheaders 消息。其中一个对等节点将能够识别接收到的哈希属于一个不在顶部的区块,而是属于一个旧的区块,从而推断出自己的本地区块链比远程节点的区块链更长。

拥有更长区块链的对等节点比其他节点拥有更多区块,并且可以确定其他节点需要哪些区块头来“赶上”。它将识别出要共享的前 2,000 个区块头,并使用一个 headers 消息进行分享。节点会持续请求额外的区块头,直到收到远程对等节点声称拥有的每个区块头为止。

同时,节点将开始使用一个 getdata 消息为先前收到的每个区块头请求区块。节点将从每个已选对等节点请求不同的区块,这使得它可以断开与明显比平均速度慢的对等节点的连接,以寻找更新(可能更快)的对等节点。

例如,假设一个节点只有创世区块。然后,它将从对等节点接收到一个包含链中接下来的 2,000 个区块头的 headers 消息。它将开始向所有连接的对等节点请求区块,并保持一个最多包含 1,024 个区块的队列。区块需要按顺序进行验证,因此,如果队列中最旧的区块(节点下一个需要验证的区块)尚未收到,则节点会断开与应该提供该区块的对等节点的连接。然后,它会找到一个新的对等节点,该对等节点可能能够在节点的所有其他对等节点能够提供 1,023 个区块之前提供一个区块。

每个区块到达后,它都会被添加到区块链中,正如我们将在第 11 章中看到的那样。随着本地区块链逐渐建立起来,将请求和接收更多的区块,这个过程会一直持续,直到节点赶上网络的其余部分。

每当节点长时间离线时,比较本地区块链与对等节点并检索任何缺失区块的过程都会发生。

轻量级客户端

许多比特币客户端都设计用于空间和功耗受限的设备,比如智能手机、平板电脑或嵌入式系统。对于这些设备,使用一种简化的支付验证(SPV)方法,使它们能够在不验证完整区块链的情况下运行。这种类型的客户端称为轻量级客户端。

轻量级客户端仅下载区块头,不下载每个区块中包含的交易。没有交易的区块头链约比完整区块链小约 10,000 倍。轻量级客户端无法构建所有可用于支出的 UTXO 的完整图片,因为它们不知道网络上的所有交易。相反,它们使用略有不同的方法验证交易,依赖对等节点根据需求提供区块链相关部分的部分视图。

作为类比,全节点就像是身处陌生城市的游客,装备有每条街道和每个地址的详细地图。相比之下,轻量级客户端就像是身处陌生城市的游客,向随机的陌生人寻求逐步指示,而只知道一个主要大道。虽然两位游客都可以通过访问街道来验证其存在,但没有地图的游客不知道侧街上有什么,也不知道还有哪些其他街道。站在 23 Church Street 前,没有地图的游客无法知道城市中是否有十几个其他的“23 Church Street”地址,以及这是否是正确的地址。没有地图的游客最好的机会就是询问足够多的人,并希望其中一些人不是试图抢劫他。

轻量级客户端通过参考交易在区块链中的深度来验证交易。全节点将构建一个经过完全验证的数千个区块和数百万笔交易的链,一直向下(时间上)延伸到创世区块。轻量级客户端将验证所有区块的工作量证明(但不验证区块及其所有交易的有效性),并将该链与感兴趣的交易链接起来。

例如,当查看第 800,000 个区块中的交易时,全节点将验证自创世区块以来所有 800,000 个区块,并构建一个完整的 UTXO 数据库,通过确认交易存在且其输出仍未花费来验证交易的有效性。轻量级客户端只能验证交易是否存在。客户端使用 Merkle 路径将交易与包含它的区块建立关联。然后,轻量级客户端等待直到看到块 800,001 到 800,006 堆叠在包含该交易的块的上方,然后通过建立其位于块 800,006 到 800,001 下面的深度来验证它。网络中的其他节点接受区块 800,000 并且矿工们在其之上产生了六个以上的区块,通过代理证明交易确实存在。

轻量级客户端通常无法被说服相信某个交易确实存在于一个区块中,而事实上该交易并不存在。轻量级客户端通过请求 Merkle 路径证明并验证区块链中的工作量证明来确认一个交易在一个区块中的存在。然而,一个交易的存在可以对轻量级客户端“隐藏”。轻量级客户端确实可以验证一个交易存在,但不能验证某个交易,比如同一 UTXO 的双重花费,不存在,因为它没有所有交易的记录。这种漏洞可以被用于对轻量级客户端进行拒绝服务攻击或双重支付攻击。为了防御这一点,轻量级客户端需要随机连接到几个客户端,以增加与至少一个诚实节点接触的概率。这种随机连接的需要意味着轻量级客户端也容易受到网络分割攻击或 Sybil 攻击的影响,其中它们连接到虚假节点或虚假网络,并且无法访问诚实节点或真实的比特币网络。

对于许多实际目的来说,连接良好的轻量级客户端已经足够安全,实现了资源需求、实用性和安全性之间的平衡。然而,要实现绝对的安全性,运行一个全节点是无可替代的。

注意: 全节点通过检查下面的成千上万个区块的整个链条来验证交易,以确保UTXO存在且未被花费,而轻量级客户端仅证明交易存在,并检查包含该交易的区块是否被其上方的少数区块所覆盖。 {% endhint %}

为了获取验证交易是否属于链的块头,轻量级客户端使用getheaders消息。响应的对等节点将使用单个headers消息发送最多2,000个块头。请参阅图10-5中的插图。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/10.5.png" alt=""><figcaption><p>图 10-5. 轻量级客户端同步区块头</p></figcaption></figure>

区块头允许轻量级客户端验证任何单个区块是否属于具有最多工作量证明的区块链,但它们并不告诉客户端哪些区块包含对其钱包有兴趣的交易。客户端可以下载每个区块并进行检查,但这将使用大量资源,相当于运行完整节点所需的资源的一部分,因此开发人员寻找其他解决方案。

在轻量级客户端引入不久后,比特币开发人员添加了一项名为布隆过滤器的功能,旨在减少轻量级客户端需要使用的带宽,以了解其传入和传出交易。布隆过滤器允许轻量级客户端接收交易的子集,而不直接透露它们感兴趣的确切地址,通过使用概率而不是固定模式的过滤机制。

布隆过滤器

布隆过滤器是一种概率性搜索过滤器,用于描述所需的模式而不精确指定。布隆过滤器提供了一种有效的方式来表达搜索模式,同时保护隐私。轻量级客户端使用布隆过滤器向其对等节点请求与特定模式匹配的交易,而不会准确透露它们正在搜索的确切地址、密钥或交易。

在我们之前的类比中,一个没有地图的游客正在寻找一个特定的地址,“23 Church St.”。如果他们向陌生人询问这条街的方向,他们无意中就透露了自己的目的地。布隆过滤器就像是在询问:“这个街区是否有任何以 R-C-H 结尾的街道?”这样的问题相对于询问“23 Church St.”,透露的信息稍微少一些。通过调整搜索的精度,游客可以更详细地指定所需的地址,比如“以 U-R-C-H 结尾”,或者更不详细,比如“以 H 结尾”。通过改变搜索的精度,游客可以在获取更具体结果或更好的隐私之间进行权衡。如果他们要求一个不太具体的模式,他们会得到更多可能的地址和更好的隐私,但很多结果是不相关的。如果他们要求一个非常具体的模式,他们会得到更少的结果,但会失去隐私。

布隆过滤器通过允许轻量级客户端指定一个搜索模式来实现这一功能,该模式可以调整到精确度或隐私方面。一个更具体的布隆过滤器会产生准确的结果,但会透露轻量级客户端感兴趣的模式,从而透露用户钱包的地址。一个不太具体的布隆过滤器会产生更多关于更多交易的数据,其中许多与客户端无关,但会使客户端能够保持更好的隐私。

布隆过滤器工作原理

布隆过滤器被实现为一个可变大小的包含 N 个二进制数字(一个位域)和可变数量的 M 个哈希函数的数组。这些哈希函数被设计成总是产生一个介于 1 和 N 之间的输出,对应于二进制数字数组。哈希函数是确定性生成的,因此任何实现布隆过滤器的客户端都将始终使用相同的哈希函数,并为特定输入获得相同的结果。通过选择不同长度(N)的布隆过滤器和不同数量(M)的哈希函数,可以调整布隆过滤器,从而变化精度水平,进而影响隐私保护程度。

在图 10-6 中,我们使用一个非常小的 16 位数组和一组三个哈希函数来演示布隆过滤器的工作原理。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/10.6.png" alt=""><figcaption><p>图 10-6. 一个简单的布隆过滤器示例,使用16位字段和三个哈希函数</p></figcaption></figure>

布隆过滤器的初始化是将位数组全部设置为零。要向布隆过滤器添加一个模式,首先依次对该模式进行每个哈希函数的哈希计算。将第一个哈希函数应用于输入,得到一个介于1和N之间的数字。找到数组中相应的位(从1到N进行索引),并将其设置为1,从而记录哈希函数的输出。然后,使用下一个哈希函数来设置另一个位,依此类推。一旦所有M个哈希函数都被应用,搜索模式就会被“记录”在布隆过滤器中,作为M个从0变为1的位。

图10-7是将一个模式“A”添加到图10-6中显示的简单布隆过滤器的示例。

添加第二个模式与重复此过程一样简单。模式依次通过每个哈希函数进行哈希计算,然后通过将位设置为1来记录结果。需要注意的是,随着布隆过滤器填充更多模式,哈希函数的结果可能与已经设置为1的位重叠,此时该位不会更改。实质上,随着更多模式记录在重叠的位上,布隆过滤器开始饱和,其中更多的位被设置为1,过滤器的准确性降低。这就是为什么该过滤器是一种概率性数据结构的原因——随着添加更多模式,其准确性会降低。准确性取决于添加的模式数量与位数组(N)和哈希函数数量(M)的大小。更大的位数组和更多的哈希函数可以以更高的准确性记录更多的模式。较小的位数组或较少的哈希函数将记录较少的模式,并产生较低的准确性。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/10.7.png" alt=""><figcaption><p>图 10-7. 将模式“A”添加到我们的简单布隆过滤器中</p></figcaption></figure>

图10-8是将第二个模式“B”添加到简单布隆过滤器中的示例。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/10.8.png" alt=""><figcaption><p>图 10-8. 将模式“B”添加到我们的简单布隆过滤器中。</p></figcaption></figure>

要测试一个模式是否存在于布隆过滤器中,需要对该模式应用每个哈希函数,并将结果与位数组进行比较。如果由哈希函数索引的所有位都被设置为1,那么该模式很可能被记录在布隆过滤器中。然而,由于这些位可能是由于多个模式的重叠而设置的,因此结果并不确定,而是概率性的。简单来说,布隆过滤器的正匹配是一个“也许,是”的情况。

图10-9是在简单布隆过滤器中测试模式“X”存在性的示例。由于相应的位被设置为1,所以该模式很可能匹配。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/10.9.png" alt=""><figcaption><p>图 10-9. 在布隆过滤器中测试模式“X”的存在性。结果是一个概率性的正匹配,意味着“可能”</p></figcaption></figure>

相反,如果对模式进行布隆过滤器测试,并且任何一个位设置为0,则证明该模式未记录在布隆过滤器中。负结果不是概率,而是确定性。简单来说,布隆过滤器的负匹配是“绝对不是!”

图10-10是对简单布隆过滤器中测试模式“Y”的存在性的示例。其中一个相应的位设置为0,因此该模式绝对不是匹配项。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/10.10.png" alt=""><figcaption><p>图 10-10. 测试在布隆过滤器中模式“Y”的存在性。结果是明确的负匹配,意味着“绝对不是!”</p></figcaption></figure>

轻量级客户端如何使用布隆过滤器

轻量级客户端使用布隆过滤器来过滤从其对等节点接收的交易(以及包含这些交易的区块),仅选择对轻量级客户端感兴趣的交易,而不会准确地揭示感兴趣的地址或密钥。

轻量级客户端将初始化一个布隆过滤器为“空”的状态;在这种状态下,布隆过滤器不会匹配任何模式。然后,轻量级客户端会列出其感兴趣的所有地址、密钥和哈希值。它会通过从其钱包控制的任何未花费交易输出(UTXO)中提取公钥哈希、脚本哈希和交易ID来执行此操作。然后,轻量级客户端将每个值添加到布隆过滤器中,以便如果这些模式存在于交易中,则布隆过滤器会“匹配”,而不会揭示模式本身。

然后,轻量级客户端将向对等节点发送一个filterload消息,其中包含用于连接的布隆过滤器。在对等节点上,布隆过滤器与每个传入交易进行匹配。完整节点会针对所有这些组件检查布隆过滤器,以寻找匹配,包括:

  • 交易ID
  • 每个交易输出脚本中的数据组件(脚本中的每个密钥和哈希)
  • 每个交易输入
  • 每个输入签名数据组件(或见证脚本)

通过检查所有这些组件,布隆过滤器可以用于匹配公钥哈希、脚本、OP_RETURN值、签名中的公钥,或者智能合约或复杂脚本的任何未来组件。

建立了过滤器后,对等节点将对每个交易输出进行布隆过滤器测试。只有与过滤器匹配的交易才会发送给客户端。

作为对客户端的getdata消息的响应,对等节点将发送一个merkleblock消息,其中包含与过滤器匹配的块头以及每个匹配交易的merkle路径(见“默克尔树”第252页)。然后,对等节点还会发送包含由过滤器匹配的交易的tx消息。

当完整节点发送交易给轻量级客户端时,轻量级客户端会丢弃任何误报,并使用正确匹配的交易来更新其UTXO集合和钱包余额。当它更新其自己的UTXO集合视图时,它还会修改布隆过滤器,以匹配任何引用它刚刚找到的UTXO的未来交易。然后,完整节点使用新的布隆过滤器来匹配新的交易,整个过程重复进行。

客户端设置布隆过滤器可以通过发送filteradd消息来交互式地添加模式到过滤器中。要清除布隆过滤器,客户端可以发送filterclear消息。因为不可能从布隆过滤器中删除模式,所以如果不再需要某个模式,则客户端必须清除并重新发送新的布隆过滤器。

轻量级客户端使用的网络协议和布隆过滤器机制在BIP37中定义。

不幸的是,在部署了布隆过滤器之后,很明显它们并没有提供太多的隐私。从对等节点接收到布隆过滤器的完整节点可以将该过滤器应用于整个区块链,以找到所有客户端的交易(以及误报)。然后,它可以查找交易之间的模式和关系。随机选择的误报交易不太可能具有从输出到输入的父子关系,但是来自用户钱包的交易很可能具有该关系。如果所有相关交易具有某些特征,例如至少一个P2PKH输出,则不具备该特征的交易可以假定不属于该钱包。

还发现,特殊构造的过滤器可能会迫使处理它们的完整节点执行大量工作,这可能导致拒绝服务攻击。

由于这两个原因,Bitcoin Core最终限制了对布隆过滤器的支持,仅允许节点运营者明确允许的IP地址上的客户端使用。

这意味着需要一种替代方法来帮助轻量级客户端找到它们的交易。

紧凑块过滤器

2016年,一位匿名开发者在Bitcoin-Dev邮件列表上提出了一个逆向布隆过滤器过程的想法。根据BIP37布隆过滤器,每个客户端将它们的地址哈希为一个布隆过滤器,而节点则对每个交易的部分进行哈希以尝试匹配该过滤器。在新提议中,节点将每个区块中的交易部分进行哈希以创建一个布隆过滤器,而客户端则对它们的地址进行哈希以尝试匹配该过滤器。如果客户端找到匹配项,它们将下载整个区块。

注意: 尽管名称相似,但BIP152紧凑块和BIP157/158紧凑块过滤器是无关的。 {% endhint %}

这使得节点能够为每个区块创建一个单一的过滤器,并将其保存到磁盘上并重复提供,从而消除了BIP37的拒绝服务漏洞。客户端不向完整节点提供有关其过去或未来地址的任何信息。它们只下载可能包含数千个并非由客户端创建的交易的区块。它们甚至可以从不同的对等体下载每个匹配的区块,从而使完整节点更难以连接属于单个客户端的多个区块中的交易。

这种服务器生成的过滤器并不提供完美的隐私保护;它仍然给完整节点带来一些成本(并且需要轻量级客户端为区块下载使用更多带宽),而且这些过滤器只能用于已确认的交易(而非未确认的交易)。然而,它比BIP37客户端请求的布隆过滤器更加隐私和可靠。

在描述了基于布隆过滤器的原始想法之后,开发者们意识到了一种更好的服务器生成的过滤器的数据结构,称为Golomb-Rice编码集(GCS)。

Golomb-Rice编码集(GCS)

假设Alice想要将一组数字发送给Bob。简单的方法是直接将整个数字列表发送给他:

849

653

476

900

379

但是有一种更有效率的方法。首先,Alice将列表按数字顺序排列:

379

476

653

849

900

然后,Alice发送第一个数字。对于剩余的数字,她发送该数字与前一个数字的差异。例如,对于第二个数字,她发送 97(476 - 379);对于第三个数字,她发送 177(653 - 476);依此类推:

379

97

177

196

51

我们可以看到,有序列表中两个数字之间的差异产生的数字比原始数字要短。收到这个列表后,Bob可以通过将每个数字与其前一个数字相加来重建原始列表。这意味着我们节省了空间而没有丢失任何信息,这称为无损编码。 如果我们在固定值范围内随机选择数字,那么我们选择的数字越多,平均差异(均值)的大小就越小。这意味着我们需要传输的数据量并不像列表长度增加得那么快(直到某个点)。 更有用的是,列表中随机选择的数字的长度自然倾向于较小的长度。考虑从1到6选择两个随机数;这相当于掷两个骰子。两个骰子有36个不同的组合:

1 1 1 2 1 3 1 4 1 5 1 6

2 1 2 2 2 3 2 4 2 5 2 6

3 1 3 2 3 3 3 4 3 5 3 6

4 1 4 2 4 3 4 4 4 5 4 6

5 1 5 2 5 3 5 4 5 5 5 6

6 1 6 2 6 3 6 4 6 5 6 6

让我们找到两个数字中较大的数字与较小的数字之间的差异:

0 1 2 3 4 5

1 0 1 2 3 4

2 1 0 1 2 3

3 2 1 0 1 2

4 3 2 1 0 1

5 4 3 2 1 0

如果我们统计每个差异发生的频率,我们会发现较小的差异比较大的差异更有可能发生:

差异 频率
0 6
1 10
2 8
3 6
4 4
5 2

如果我们知道可能需要存储大数(因为即使它们很少,大的差异也可能发生),但通常需要存储小数,我们可以使用一种系统对每个数字进行编码,对小数使用较少的空间,并为大数提供额外的空间。从平均意义上讲,这种系统的性能会比对每个数字使用相同数量的空间好。

Golomb 编码提供了这种功能。Rice 编码是 Golomb 编码的一个子集,在某些情况下更方便使用,包括 Bitcoin 块过滤器的应用。

要包含在区块过滤器中的数据

我们的主要目标是允许钱包确定一个区块是否包含影响该钱包的交易。为了使钱包更有效,它需要了解两种类型的信息:

  1. 当它收到钱时

具体来说,当一个交易输出包含钱包控制的脚本(例如通过控制授权的私钥)时。

  1. 当它花费钱时

具体来说,当一个交易输入引用钱包控制的先前交易输出时。

在设计紧凑型区块过滤器时的一个次要目标是允许接收过滤器的钱包验证它是否从对等方接收到准确的过滤器。例如,如果钱包下载了生成过滤器的区块,那么它可以生成自己的过滤器。然后,它可以将自己的过滤器与对等方的过滤器进行比较,并验证它们是否相同,从而证明对等方生成了准确的过滤器。

为了实现主要目标和次要目标,过滤器需要引用两种类型的信息:

  • 每个区块中每个交易输出的脚本。
  • 每个区块中每个交易输入的引用点。

紧凑型区块过滤器的早期设计包含了这两种信息,但后来意识到,如果我们牺牲次要目标,就可以更有效地实现主要目标。在新设计中,区块过滤器仍然引用两种类型的信息,但它们会更加相关:

  • 与之前相同,每个区块中每个交易输出的脚本。
  • 在更改中,它还将引用每个区块中每个交易输入引用的输出脚本。换句话说,正在花费的输出脚本。

这有几个优点。首先,这意味着钱包不需要跟踪引用点;它们可以只扫描它们期望收到资金的输出脚本。其次,任何时候一个区块中的后续交易花费了同一个区块中较早交易的输出,它们都将引用相同的输出脚本。在紧凑型区块过滤器中,对同一个输出脚本的多次引用是多余的,因此可以删除多余的副本,从而缩小过滤器的大小。

当全节点验证一个区块时,它们需要访问该区块中当前交易输出和被引用的先前区块中的交易输出的输出脚本,以便能够在这个简化模型中构建紧凑型区块过滤器。但是,一个区块本身不包含之前区块中的交易输出的输出脚本,因此没有方便的方式让客户端验证区块过滤器是否正确构建。然而,有一种替代方法可以帮助客户端检测对等方是否在说谎:从多个对等方获取相同的过滤器

从多个对等方下载区块过滤器

一个对等方可以向钱包提供一个不准确的过滤器。有两种方式可以创建不准确的过滤器。对等方可以创建一个引用实际上不存在于相关区块中的交易的过滤器(假阳性)。或者,对等方可以创建一个不引用实际上出现在相关区块中的交易的过滤器(假阴性)。

对不准确过滤器的第一种防护措施是让客户端从多个对等方获取过滤器。BIP157协议允许客户端仅下载一个短短的32字节的对过滤器的承诺,以确定每个对等方是否正在广告与客户端的所有其他对等方相同的过滤器。这样,客户端只需消耗少量带宽来查询许多不同对等方的过滤器,如果所有这些对等方都同意的话。

如果两个或更多不同的对等方对同一区块有不同的过滤器,那么客户端可以下载所有这些过滤器。然后,客户端还可以下载相关的区块。如果区块包含与钱包相关的任何交易,但不包含在其中的任何一个过滤器中,那么钱包就可以确定创建该过滤器的对等方是不准确的——Golomb-Rice编码集始终会包含一个潜在匹配。

或者,如果区块不包含过滤器中可能与钱包匹配的交易,这并不能证明该过滤器是不准确的。为了最小化GCS的大小,我们允许一定数量的假阳性。钱包可以继续从对等方下载附加的过滤器,无论是随机下载还是在它们指示匹配时,然后跟踪客户端的假阳性率。如果它与过滤器设计时所使用的假阳性率明显不同,那么钱包可以停止使用该对等方。在大多数情况下,不准确的过滤器的唯一后果就是钱包使用的带宽比预期更多。

使用有损编码来减少带宽

我们希望在一个区块中传输的交易数据是输出脚本。输出脚本的长度各不相同,并且遵循不同的模式,这意味着它们之间的差异并不会像我们期望的那样均匀分布。然而,在本书的许多地方已经提到,我们可以使用哈希函数来创建对某些数据的承诺,并且还可以生成类似于随机选择的数字。在本书的其他地方,我们已经使用了一个具有密码学安全性的哈希函数,它提供了关于其承诺强度和其输出与随机性之间的不可区分性的保证。然而,还有一些更快速和更可配置的非密码学哈希函数,例如我们将用于紧凑块过滤器的SipHash函数。

所使用算法的详细信息在BIP158中有描述,但其要点是使用SipHash和一些算术运算将每个输出脚本简化为一个64位的承诺。你可以将其视为将一组大数字截断为较短数字的过程,这个过程会丢失数据(因此称为有损编码)。通过丢失一些信息,我们就不需要在后续存储那么多信息,从而节省空间。在这种情况下,我们从典型的长度为160位或更长的输出脚本减少到仅仅64位。

使用紧凑块过滤器

每个区块中对输出脚本的每个承诺的64位值被排序,重复的条目被移除,并且通过查找每个条目之间的差值(增量)来构建GCS。然后,对等方将该紧凑块过滤器分发给它们的客户(如钱包)。

客户端使用这些增量来重建原始的承诺。客户端,比如钱包,还会对其监视的所有输出脚本进行相同方式的承诺生成,就像BIP158那样。它检查它生成的任何承诺是否与过滤器中的承诺匹配。

回想一下,紧凑块过滤器失真类似于截断数字的情况。想象一个客户端正在寻找一个包含数字123456的区块,而一个准确(但失真)的紧凑块过滤器包含数字1234。当一个客户端看到1234时,它将下载相关的区块。准确的过滤器包含1234的情况下,客户端学习到包含123456的区块的概率是100%,称为真正的正匹配。然而,该区块可能还包含123400、123401或近百个其他不符合客户端期望的条目(在这个例子中),称为假正匹配。

一个100%的真正匹配率是很好的。这意味着一个钱包可以依赖紧凑块过滤器来找到影响该钱包的每个交易。非零的假正匹配率意味着钱包将下载一些对其无兴趣的区块。这的主要后果是客户端会使用额外的带宽,这不是一个很大的问题。BIP158紧凑块过滤器的实际假正匹配率非常低,因此这不是一个主要问题。假正匹配率也可以帮助提高客户端的隐私,就像布隆过滤器一样,尽管任何想要最佳隐私的人仍然应该使用自己的完整节点。

长期来看,一些开发者主张让区块对该区块的过滤器进行承诺,最可能的方案是每个Coinbase交易对该区块的过滤器进行承诺。完整节点将为每个区块计算过滤器,并仅在包含准确承诺的情况下接受该区块。这将允许轻量级客户端下载一个80字节的区块头、一个(通常很小的)Coinbase交易和该区块的过滤器,以获得强有力的证据证明该过滤器是准确的。

轻量级客户端与隐私

轻量级客户端的隐私性不如完整节点。完整节点会下载所有的交易,因此不会透露有关是否在其钱包中使用某些地址的信息。轻量级客户端只会下载与其钱包相关的交易。

布隆过滤器和紧凑块过滤器是减少隐私损失的方法。没有它们,轻量级客户端将不得不明确列出其感兴趣的地址,从而造成严重的隐私泄露。然而,即使使用了过滤器,一个监视轻量级客户端流量的对手或直接连接到其作为P2P网络中的节点,也可能随着时间的推移收集到足够的信息来了解轻量级客户端的钱包中的地址。

加密和认证连接

大多数新用户认为比特币节点的网络通信是加密的。事实上,比特币的原始实现完全以明文通信,截至撰写本文时,现代的比特币核心实现也是如此。

为了增加比特币P2P网络的隐私和安全性,有一种解决方案提供了通信加密:Tor传输。

Tor是一项软件项目和网络,通过随机的网络路径提供数据的加密和封装,从而实现匿名性、不可追踪性和隐私性。

比特币核心提供了几种配置选项,允许你在Tor网络上传输比特币节点的流量。此外,比特币核心还可以提供Tor隐藏服务,允许其他Tor节点通过Tor直接连接到你的节点。

从比特币核心版本0.12开始,如果节点能够连接到本地Tor服务,它将自动提供一个隐藏的Tor服务。如果你已经安装了Tor,并且比特币核心进程以具有足够权限访问Tor身份验证cookie的用户身份运行,它应该会自动工作。使用调试标志来启用比特币核心的Tor服务调试,像这样:

$ bitcoind --daemon --debug=tor

你应该在日志中看到“tor: ADD_ONION successful”,表示比特币核心已将一个隐藏服务添加到Tor网络中。

你可以在比特币核心文档(docs/tor.md)和各种在线教程中找到有关将比特币核心作为Tor隐藏服务运行的更多说明。

内存池和孤立池

几乎每个比特币网络上的节点都维护着一个临时的未确认交易列表,称为内存池(mempool)。节点使用这个池来跟踪已知于网络但尚未包含在区块链中的交易,称为未确认交易。

随着未确认交易的接收和验证,它们被添加到内存池,并被中继到相邻节点以在网络上传播。

一些节点实现还维护一个单独的孤立交易池。如果交易的输入引用了尚未知晓的交易,比如缺少的父交易,那么孤立交易将临时存储在孤立池中,直到父交易到来。

当交易被添加到内存池时,会检查孤立池中是否有任何引用该交易输出(其子交易)的孤立交易。任何匹配的孤立交易都将被验证。如果有效,它们将从孤立池中移除,并添加到内存池中,完成从父交易开始的链条。随着新添加的交易不再是孤立交易,这个过程会递归地重复,查找任何更进一步的后代,直到不再找到后代。通过这个过程,父交易的到来触发了整个链条的相互依赖交易的级联重构,将孤立交易与其父交易一直重组到链条的底部。

一些比特币实现还维护一个未花费交易输出(UTXO)数据库,这是区块链上所有未花费输出的集合。这代表了与内存池不同的一组数据。与内存池和孤立池不同,UTXO数据库包含了数百万条未花费交易输出的条目,从创世区块一直到现在所有尚未花费的输出。UTXO数据库以表格形式存储在持久性存储中。

虽然内存池和孤立池代表了单个节点的局部视角,可能会因节点的启动或重新启动时间的不同而变化,但UTXO数据库代表了网络的累积共识,因此在节点之间通常不会变化。

现在我们对节点和客户端用于在比特币网络上传输数据的许多数据类型和结构有了一定的了解,现在是时候来看看负责保持网络安全和运行的软件了。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论