5.BTC-网络

简单,鲁棒而不是高效

总体协议

BTC在应用层以Bitcion Block Chain为总和,运行着一个Bitcoin P2P Protocol。在网络层上,节点之间使用TCP通讯,整体上是一个P2P Overlay Network。

具体上,应用层规定了一系列命令和消息,例如:

  • version / verack:当两个节点初次建立连接时,它们会交换 version 消息来介绍自己(比如软件版本、区块高度等),然后用 verack 消息来确认连接成功。这是“握手”过程。
  • getaddr / addr:节点可以用 getaddr 向对方请求已知的其他活跃节点地址列表,对方则用 addr 消息回复。这是节点发现和网络拓扑维护的关键。
  • inv (Inventory):当一个节点有新的交易或区块时,它不会直接发送完整数据,而是先发送一个 inv 消息,告诉对方它拥有哪些新东西的“清单”(用哈希值表示)。
  • getdata:如果接收方节点发现清单里的东西是自己没有的,它就会发送 getdata 消息,根据哈希值请求具体的交易或区块数据。
  • tx / block:发送方在收到 getdata 请求后,用 tx 消息发送交易数据,或用 block 消息发送区块数据。

加入网络

从原理上,加入BTC网络分为以下四个步骤:

寻找第一个联系人 (通过种子节点)

一个全新的比特币节点(比如您刚在电脑上安装的Bitcoin Core客户端),启动时是“孤独”的,它不知道网络中任何其他节点的存在。它的首要任务就是找到第一个可以与之通信的“邻居”。

这就是种子节点 (Seed Node) 发挥作用的地方。

  • 种子节点
    • 不是一种特殊类型的节点。是一个普通的、由社区志愿者运行的、长期稳定在线的比特币全节点
    • 它的特殊之处在于,它的域名地址 (Domain Name)被硬编码到了比特币客户端软件的源代码里。
    • 是比特币软件出厂时,内置的一份“永久有效的、可信赖的联系人黄页”**。
  • 加入步骤
    1. 您的新节点启动后,会查询这份内置的“黄页”(比如查询域名 seed.bitcoin.sipa.be)。
    2. 它会向这个域名发起一个 DNS查询
    3. 这个域名背后的DNS服务器经过特殊配置,它不会只返回一个IP地址,而是会返回一个随机的、包含了多个当前活跃且可靠的比特币节点IP地址的列表

新节点就获得了第一批可以尝试联系的“潜在邻居”的IP地址。在实际中,这个邻居不是地理位置上相近的邻居的距离。在寻找邻居的过程中,会使用叫做 asmap 的技术,其目标是增加网络服务商的多样性。客户端会尝试从不同的运营商和ISP中选择邻居节点。它会避免将自己所有的8个出站连接都建立到同一个ISP的节点上。

建立连接并握手

节点有了一份IP地址列表后,它会选择其中的一个,尝试与之建立一个底层的 TCP连接(默认通过端口8333)。

一旦TCP连接成功建立,比特币应用层的P2P协议握手开始:

  1. 发送 version 消息:您的节点会向对方发送一条 version 消息。内容包括:
    • 自己的协议版本号。
    • 自己提供的服务类型。
    • 当前的时间戳。
    • 自己当前的区块高度(对于新节点来说是0)。
    • ...等等。
  2. 接收 version 和发送 verack 消息
    • 对方节点收到您的 version 消息后,如果它接受连接,也会回复一条它自己的 version 消息。
    • 紧接着,对方会发送一条 verack (Version Acknowledgment) 消息,意思是ACK确认通讯。
    • 您的节点收到对方的versionverack后,也会发送一个verack给对方。

当这个 version/verack 的双向交换完成之后,两个节点之间的比特币P2P通信链路就正式建立起来了。它们成为了网络中的对等节点 (Peers)

交换通讯录以发现更多邻居

现在您的节点已经有了一个或几个邻居,但为了更好地融入网络并提高连接的稳定性,它需要发现更多的节点。

  1. 节点会向已经连接的邻居发送一条 getaddr 消息,意思是把你通讯录里知道的其他节点地址给我一些”。
  2. 邻居节点会回复一条 addr 消息,其中包含了它所知道的一系列其他节点的IP地址和端口号。

建立更多连接并同步数据

  1. 您的节点从收到的 addr 消息中,挑选新的IP地址,重复第二步的“握手”过程,与其他节点建立更多的连接(通常一个全节点会维持8个左右的对外连接)。
  2. 一旦连接稳定,节点就会开始通过 getheaders, getblocks 等消息,向它的邻居们请求区块数据,开始漫长的区块链同步过程。同时,它也会通过 inv, getdata, tx 等消息,参与到全网新交易和新区块的实时广播中。

离开网络

情况一:正常关闭

这种情况发生在您手动关闭Bitcoin Core客户端或钱包软件时。

退出的节点 (节点A) 的过程:

  1. 用户点击“关闭”按钮。
  2. 节点A的应用程序会向其操作系统下达指令:“关闭所有正在监听和已建立的网络连接。”
  3. 节点A的操作系统会为其每一个活跃的TCP连接,启动一个标准的TCP连接终止“四次挥手”过程。它会向每一个与之相连的邻居节点(比如节点B)发送一个FIN(Finish)数据包。

其他节点 (以节点B为例) 的反应过程:

  1. 节点B的操作系统收到了来自节点A的FIN数据包。
  2. 操作系统立刻识别出这是一个正常的、有意图的连接关闭请求。
  3. 操作系统会通知正在运行的比特币软件节点A主动关闭连接。
  4. 节点B的比特币软件执行以下操作:
    • 更新状态:将节点A从其“当前活跃邻居列表”中移除。
    • 保留地址:它可能会保留节点A的IP地址在其“已知节点地址池”中,以便将来可能再次尝试连接。
    • 补充连接:软件会检查自己当前的对外连接数是否低于目标值(比如默认是8个)。如果因为节点A的离开导致连接数变成了7个,它会自动从“已知节点地址池”中挑选一个新的地址,尝试与其建立连接,以将连接数恢复到8个。

情况二:“突然消失”(意外断开)

这种情况发生在节点A的电脑突然断电、网络中断或软件崩溃时。

退出的节点 (节点A) 的过程:

  • 瞬间消失。

其他节点 (以节点B为例) 的反应过程: 节点B无法立即知道A已经消失了,它需要通过“超时”机制来发现这一点。

  1. 等待与无响应:节点B的TCP连接处于打开状态,但它长时间没有收到来自节点A的任何数据。
  2. 触发超时机制:有几种方式可以发现连接已经死亡:
    • 应用层 ping/pong:比特币P2P协议有自己的ping消息。节点B会周期性地向它的邻居(包括A)发送ping。如果在指定的时间内没有收到A回复的pong消息,B就会认为A已经无响应。
    • TCP Keep-Alive:更底层的TCP协议自身也有“保活”机制。如果连接长时间空闲,操作系统可能会发送探测包。如果连续几次探测都没有得到A的回应,操作系统就会判定该连接已失效。
  3. 识别连接中断:无论是哪种超时机制被触发,最终结果都是节点B的操作系统或比特币软件认定“与节点A的连接已经中断/死亡”。
  4. 执行与情况一相同的后续操作
    • 节点B的比特币软件将节点A从“当前活跃邻居列表”中移除。
    • 检查并补充新的对外连接,以维持网络连接的健壮性。

传递信息

BTC中,不同节点之间适用泛滥(Flooding)过程来实现数据交换。它通过一个“宣告-请求-发送”的机制来节省带宽,避免网络中充斥着大量重复的数据。这个机制的核心是inv(Inventory/清单)消息。

首先,我们需要了解在这个过程中扮演关键角色的四种P2P消息:

  • inv (Inventory - 清单):一个节点用来告诉它的邻居:“我这里有这些新东西(用哈希值列表表示)。”
  • getdata (Get Data - 请求数据):一个节点对它的邻居说:“你清单里的这些东西我没有,请把它们的完整数据发给我。”
  • tx (Transaction - 交易):一个节点回应getdata请求,发送完整的交易数据。
  • block (Block - 区块):一个节点回应getdata请求,发送完整的区块数据。

假设用户Alice创建了一笔新交易,并将其广播出去。

第一步:发起与首次“宣告”

  1. 创建交易:Alice的钱包节点(我们称之为节点A)创建并签名了一笔交易。
  2. 准备清单:节点A将这笔新交易的哈希值(即TxID)放进一个inv消息的“清单”中。
  3. 发送清单:节点A向所有与之相连的邻居节点(比如节点B、C、D)发送这条inv消息。
    • 注意:此时发送的只是一个几十字节的哈希值,而不是几百字节的完整交易数据。

第二步:邻居的反应与“请求”

  1. 接收清单节点B收到了来自节点A的inv消息。
  2. 检查自身数据:节点B会立刻检查自己的“内存池”(Mempool,待确认交易的集合),看看自己是否已经拥有这个TxID的交易。
  3. 发起请求:节点B发现自己没有这笔交易,因此它知道这是自己需要的新数据。于是,节点B会向节点A(那个最先通知它的节点)发送一条getdata消息,请求这个TxID对应的完整交易。

第三步:发送完整数据

  1. 响应请求节点A收到了来自节点B的getdata请求。
  2. 发送交易:节点A随即发送一条tx消息给节点B,这条消息里包含了那笔交易的全部细节(输入、输出、脚本、签名等)。

第四步:验证与再次“宣告”

  1. 接收并验证节点B收到了完整的tx数据后,会立即对其进行全面的验证(检查签名、防止双花等)。
  2. 验证通过后,继续传播:一旦验证通过,节点B确认这是一笔有效的、新的交易。现在,节点B也变成了这笔交易的拥有者
  3. 新的宣告:节点B会向它自己的所有邻居除了把它告诉自己的节点A)发送包含这个新TxID的inv消息。

第五步:连锁反应

网络中的其他节点(比如E、F、G)收到来自节点B的inv消息后,会重复第二步至第四步的过程。

这个“宣告(inv) → 请求(getdata) → 发送(tx/block)”的循环会像涟漪一样迅速扩散,最终使得全网绝大多数节点都接收并验证了这笔交易。一个新区块的传播过程与此完全相同,只是最后一步发送的是block消息而不是tx消息。如果节点受到了一个新的区块,并且这个区块里面有已经存储的交易,那么还需要删掉这个交易。比特币的核心开发者在2015年对这个传播机制进行了一次重要的优化。他们引入了一种叫做“扩散”(Diffusion)的模式,它本质上是Flooding的一个变种。变化如下:

  • 工作方式:节点在收到新信息后,不会立即转发给所有邻居,而是会为每一个邻居设置一个随机的、很短的延迟时间,然后再逐一发送。
  • 效果:这种随机延迟模糊了信息传播的清晰“波纹”,使得通过时间分析来定位源头变得更加困难,从而提高了用户的隐私性。

对双重支付问题的解决

我们先设定一个具体的场景:

  • 用户A (Alice) 拥有一个包含1 BTC的UTXO,我们称之为 UTXO_X
  • 冲突交易1 (Tx1):Alice创建了一笔交易,花费UTXO_X,向B(Bob)支付1 BTC。
  • 冲突交易2 (Tx2):几乎在同一时间,Alice又创建了第二笔交易,花费同一个UTXO_X,向C(Carol)支付1 BTC。
  • Alice的操作:她将Tx1广播到网络的一部分节点,同时将Tx2广播到网络的另一部分节点,试图制造混乱。

第一阶段:节点的即时反应(Mempool中的短期竞赛)

当冲突交易刚进入网络时,不同的节点会看到不同的情况,导致网络进入一个短暂的、不一致的状态。

  1. “先到先得”原则 (First-Seen Rule)
    • 大多数比特币节点的默认行为遵循“先到先得”的原则。当一个节点收到一笔有效的交易后,会将其放入自己的内存池 (Mempool) 中。
    • 如果它稍后收到另一笔与内存池中已有交易相冲突的交易(即花费了同一个UTXO),它会简单地忽略并丢弃这第二笔交易。
  2. 网络状态分裂
    • 由于Alice的广播策略,网络中的节点会分裂成两个阵营:
      • 阵营1:一些节点先收到了 Tx1 (A→B)。它们会将Tx1放入自己的内存池,并拒绝之后到达的Tx2。
      • 阵营2:另一些节点则先收到了 Tx2 (A→C)。它们会将Tx2放入自己的内存池,并拒绝之后到达的Tx1。
  3. 此阶段的结论
    • 在这个阶段,两笔交易都处于“0次确认”状态
    • 整个网络的内存池状态是不一致的。没有人能100%确定哪笔交易最终会“胜出”。对于Bob和Carol来说,此时收到的钱都是极不可靠的。

一个重要的例外:手续费替代 (Replace-by-Fee, RBF)

值得一提的是,现代比特币交易中有一个叫做RBF的特性。如果Alice在发送Tx1时标记了它“可被替换”,那么她可以在发送Tx2时附带一个更高的手续费。支持RBF的节点在看到手续费更高的Tx2时,会主动丢弃掉Tx1,并将Tx2放入内存池。这是“价高者得”的原则,但我们先以简单的“先到先得”来理解主流程。

第二阶段:矿工的最终裁决(区块链上的永久解决)

内存池中的混乱只是暂时的。哪个交易能被永久记录下来,拥有最终决定权的是矿工

  1. 矿工的选择
    • 全球的矿工在准备创建新区块时,会从各自的内存池中挑选交易。
    • 属于“阵营1”的矿工,他们的内存池里只有Tx1,所以他们会将Tx1打包进自己的候选区块。
    • 属于“阵营2”的矿工,他们的内存池里只有Tx2,所以他们会将Tx2打包进候选区块。
  2. 新区块被寻找出的时刻
    • 假设一个属于“阵营1”的矿工(我们称他为矿工M)率先解决了工作量证明难题,成功挖出了一个新的区块。
    • 这个新区块中,包含了Tx1 (A→B)
    • 矿工M立刻将这个新区块广播到全网。
  3. 整个网络的反应过程
    • 所有节点(包括“阵营1”和“阵营2”的节点)都收到了这个新挖出的区块。
    • 验证区块:所有节点都会验证这个新区块的合法性。它们发现区块本身是有效的,并且其中包含的Tx1也是一笔有效的交易。
    • 接受事实,更新账本:根据最长链共识规则,所有节点都会接受这个新区块,并将其链接到自己的本地区块链副本上。
    • 清理内存池:在接受新区块的过程中,节点会执行一个关键操作:它们会检查自己内存池中所有的交易,将任何与新区块中已确认交易相冲突的交易全部清除
    • 最终结果
      • Tx1 (A→B) 现在获得了“1次确认”,被正式写入了不可篡改的公共账本。
      • 所有之前持有 Tx2 (A→C) 的“阵营2”节点,在验证新区块时发现,Tx1已经花费了UTXO_X。因此,它们内存池里的Tx2现在变得明确无效了。这些节点会立即将Tx2从自己的内存池中删除。

不可能结论

在分布式共识领域,有两个非常著名的“不可能结论”,它们从理论上为分布式系统的设计划定了边界,深刻地影响了包括区块链在内的所有分布式系统的架构。

这两个结论分别是 FLP 不可能定理CAP 定理

1. FLP 不可能定理 (FLP Impossibility Theorem)

由 Fischer、Lynch 和 Paterson 三位科学家在1985年提出。

结论一句话概括:

在一个允许节点(进程)发生故障的异步分布式系统中,不存在一个确定性的共识算法,能够保证所有节点在有限时间内达成一致。

它意味着完美的共识协议是不存在的。让我们来拆解一下它的前提条件和结论:

前提条件(满足以下所有条件):

  1. 异步系统 (Asynchronous System):这是最关键的前提。异步系统意味着网络消息的延迟是没有上限的。你发送一条消息,它可能很快到达,也可能花费极长的时间才到达,甚至比你想象的还要久。你无法通过“超时”来判断一个节点是宕机了,还是仅仅是它的消息在网络上“堵车”了。这非常符合互联网的真实情况。
  2. 允许故障 (Faults May Occur):系统中哪怕只允许一个节点发生崩溃故障(Crash Failure),即节点可能在任何时候突然停止工作。
  3. 确定性算法 (Deterministic Algorithm):算法的下一步行为完全由其当前状态和接收到的消息决定,不涉及任何随机性(比如抛硬币)。

结论的含义:

FLP 定理证明,只要上述三个条件同时满足,你设计的任何共识算法都无法保证“活性”(Liveness)

  • 安全性 (Safety):算法保证所有节点最终达成的值是相同的,不会出现分歧。
  • 活性 (Liveness):算法保证所有正常的节点最终一定能做出决定,而不是永远地等待下去。

FLP 定理指出,你设计的算法可以保证“安全性”,但它可能会在某些极端情况下(例如,一个关键节点的消息被网络无限延迟),导致整个系统陷入无限等待的“死循环”,永远也达不成最终共识。

2. CAP 定理 (CAP Theorem)

CAP 定理由科学家 Eric Brewer 在2000年提出,它更侧重于分布式数据系统(如数据库、存储系统)在设计时必须做出的权衡。它也被称为“布鲁尔定理”。

结论一句话概括:

对于一个分布式数据系统,一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)这三个特性,最多只能同时满足其中两个。

这是一个著名的“不可能三角”。

三个核心特性:

  1. 一致性 (Consistency)
    • 含义:任何读操作,总能读到最近一次写入的数据。所有节点在同一时刻看到的数据是完全一致的。
    • 类比:你在银行A网点存了100元,要保证在同一秒钟,你去银行B网点查询时,余额必须已经更新。
  2. 可用性 (Availability)
    • 含义:任何来自客户端的请求,总能得到一个(非错误的)响应,系统服务一直处于可用状态。但不保证这个响应返回的是最新的数据。
    • 类比:你去任何一个银行网点查询余额,它总会给你一个答复(比如告诉你余额是昨天的结算结果),而不会告诉你“系统繁忙,请稍后再试”。
  3. 分区容错性 (Partition Tolerance)
    • 含义:系统能够容忍网络分区故障。网络分区指的是,由于网络故障,系统中的节点被分成了两个或多个无法相互通信的“孤岛”。系统在这种情況下仍能继续运行。
    • 在现代分布式系统中,分区容错性通常被认为是必须保证的,因为网络故障是常态。所以,设计的权衡往往是在 C 和 A 之间进行。

设计的权衡 (P 必须保证):

  • 选择 CP (Consistency + Partition Tolerance)
    • 当网络发生分区时,为了保证数据的一致性,系统会选择牺牲“可用性”
    • 例子:一个分区中的节点,为了避免返回可能过时的数据,会拒绝服务,直到网络恢复,它能与包含最新数据的节点同步为止。很多分布式数据库、银行系统会选择CP。
  • 选择 AP (Availability + Partition Tolerance)
    • 当网络发生分区时,为了保证每个节点都能响应请求(高可用),系统会选择牺牲“一致性”
    • 例子:一个分区中的节点,即使无法与主节点通信,它也会返回自己本地存储的、可能是旧版本的数据,以确保服务不中断。很多强调用户体验的社交网络、电商系统会选择AP,并采用“最终一致性”模型。

BTC对于不可能结论的绕过

FLP

FLP 的结论:在一个异步网络中,一个确定性的算法,无法保证活性

比特币的公共P2P网络是一个典型的异步网络,消息延迟没有上限。它主要通过以下两点:

  1. 它的共识核心是“概率性”的,而非确定性的 (It is not deterministic) FLP定理只适用于“确定性”算法。而比特币的核心——工作量证明(Proof of Work, PoW)——本质上是一个概率
    • PoW是随机性的:谁能找到下一个区块,不是由一个预设的程序决定的,而是看谁先通过大量的哈希计算,“随机”地碰上一个符合要求的解。这个过程充满了不确定性。
    • 结论依赖概率:一个区块被认为是“最终确认”的,也是基于概率。当一个区块后面又链接了6个新区块时,我们认为它被篡改的可能性已经小到可以忽略不计,从而在“概率上”达成了共识。
  2. 它不提供严格的“活性”保证 (It does not guarantee Liveness) FLP定理中的“活性”指的是系统必须在有限时间内给出最终结果。比特币不做出这样的承诺
    • 出块时间是期望值:比特币网络的目标是平均每10分钟出一个块,但这只是一个统计上的期望值。由于挖矿的随机性,下一个区块可能在1分钟后出现,也可能在1个小时后才出现,理论上甚至可能永远不出现(尽管概率极小)。
    • 交易确认没有时间上限:你的交易被打包进区块所需的时间也没有绝对的上限。如果网络拥堵且你给的交易费很低,你的交易可能会在内存池(mempool)里待上非常久。

CAP

CAP 的结论:在分区容错性(P)必须保证的前提下,系统必须在一致性(C)可用性(A)**之间做出选择。

比特币作为一个全球化的、去中心化的P2P网络,网络分区(比如因为防火墙或海底光缆故障,导致一部分网络暂时与其他部分失联)是必然会发生的。因此,它必须保证分区容错性(P)

那么,它在一致性(C)和可用性(A)之间是如何选择的呢?

比特币选择成为一个 AP 系统 (Availability + Partition Tolerance),并追求“最终一致性”。

  1. 优先选择“可用性”(A)
    • 只要你连接到比特币网络中的任何一个节点,这个网络几乎永远是“可用”的。你可以随时创建并广播一笔交易,节点会接收你的交易并将其放入内存池。
    • 即使在网络分区期间,你所在“孤岛”里的节点仍然是可用的,它们会继续接收交易,甚至可能自己独立地挖矿。
  2. 牺牲了“强一致性”(C)
    • 强一致性要求任何时刻所有节点的数据都必须完全一样。比特币做不到这一点。
    • 临时分叉 (Temporary Forks):由于网络延迟,在地球两端的两个矿工可能在几乎同一时间都挖出了一个新的区块(比如区块高度#800000)。这时,网络中就暂时出现了两条平行的、都合法的链。一部分网络看到了矿工A的区块,另一部分看到了矿工B的区块。在这一刻,系统的数据是不一致的。
    • 最终一致性 (Eventual Consistency):比特币通过“最长链原则”来解决这个问题。网络会自发地在其中一条链上继续延伸,最终这条链会变得比另一条更长,所有诚实的节点都会抛弃那条较短的链,转向最长的链。这样,整个网络最终会回到一致的状态,但这个过程需要时间。

实际影响:这就是为什么我们在进行大额比特币交易时,通常需要等待6个区块确认。等待6个区块(约1小时),就是为了确保交易所在的链已经成为了无可争议的“最长链”,被篡改或抛弃的风险已经降到了极低的水平。这个等待的过程,正是为“最终一致性”付出的时间成本。