深入解析:LIBP2P

libp2p:从 PeerId 到去中心化打洞的模块化网络栈

每个 P2P 项目过去都要各自重新发明一套网络层(给节点命名、找到它们、穿透 NAT),而且大多做得并不好。libp2p 把这些关切拆分成一个个可替换的模块,成为 IPFS、以太坊共识层、Filecoin 与 Polkadot 共用的底层基座。本文从 PeerId 与自描述的 multiaddr 讲起,途经传输、安全与 pubsub,最后把重点放在真正的收获上:AutoNAT、Circuit Relay v2 与 DCUtR 如何仅凭其他节点、无需任何 STUN 或 TURN 服务器,就让节点彼此穿透 NAT。

libp2p 是什么

几乎每一个 P2P 应用最终都要面对同一批问题:如何给一个节点命名、如何找到它、如何在中间挡着 NAT 的情况下与它建立连接、如何为这条连接加密、又如何把它切分成多条逻辑流。多年以来,每个项目都是从零开始各自解决这些问题,而且大多解决得并不好。用 libp2p 开发者自己的话说,既有方案往往「文档匮乏、许可证苛刻、代码过时、找不到维护者……或是与特定用例死死绑定、无法升级」。

libp2p("library peer-to-peer")最初诞生于星际文件系统(IPFS)项目内部,作为 IPFS 自己的线路协议,后来才独立成为一套通用的网络栈。其核心思路是不再把节点发现、传输、安全与多路复用当成一个整体协议,而是把每一项关切都暴露成一个可替换的模块:同一个应用既可以跑在 TCP 之上,也可以跑在 WebRTC 之上;既可以用 Noise 认证,也可以用 TLS 认证;既可以通过 DHT 发现节点,也可以用局域网 mDNS,业务逻辑本身无需为此改动一行。

正是这种模块化,让 libp2p 出现在了远超其出身的 IPFS 家族之外的地方。以太坊的权益证明共识层跑在它之上:Lighthouse 与 Prysm 两个客户端都用 libp2p 做节点组网。Filecoin 这个「规模最大的去中心化存储网络」,其大规模场景下的可靠性也依赖于它。Polkadot 把它作为 Substrate 架构的一部分加以利用,而从 Algorand 到 Starknet、Uniswap、Base 等一系列项目,都把它列入自己的网络基础设施。曾经只是一个项目自用的线路格式,如今已成为 P2P 世界共享的连接组织。

项目如何使用 libp2p
IPFS最初的创造者,至今仍是参照实现,用于在数十万节点间进行去中心化内容分发
以太坊(共识层)为权益证明客户端(Lighthouse、Prysm)提供节点组网能力
Filecoin支撑这一规模最大的去中心化存储网络的网络性能与可靠性
Polkadot作为基于 Substrate 的架构的一部分
Algorand正在借助它摆脱对中心化中继节点的依赖

身份:PeerId 与公钥

PeerId
节点与其公钥之间一条可验证的链接,概念上讲,它就是该公钥的一个加密哈希。由于这个哈希直接与公钥绑定,收到安全信道握手的一方可以核对:用来建立该信道的公钥,与 PeerId 所声称的公钥是否一致。
multihash
一种紧凑的、自描述的哈希二进制格式,libp2p 的规范用它来编码 PeerId:在哈希字节之前加上一个标识该哈希函数的代码前缀。

生成规则刻意为小尺寸密钥设计得很「便宜」:序列化后不超过 42 字节的公钥会被直接嵌入一个「identity」multihash,完全不做哈希运算;超过 42 字节的公钥则先经 SHA-256 哈希,再包装进 multihash。每个实现都必须支持 Ed25519;RSA 属于「应当支持」的级别,主要是为了与 IPFS DHT 既有的引导节点保持互操作;Secp256k1 与 ECDSA 则是可选的附加项,支持程度参差不齐。

PeerId 有两种合法的文本表示形式:旧式写法是一个裸的 multihash,用 base58btc 编码且不带前缀(经过哈希的密钥以 Qm... 开头,identity 编码的 Ed25519 密钥则以 1... 开头);较新的写法则把 multihash 包进一个使用 libp2p-key multicodec 的 CIDv1 中,以 base32 编码(以 bafz... 开头)。实现必须能同时解析这两种形式。PeerId 会以 /p2p/<PeerId> 这样一段的形式嵌入 multiaddr(这一点会在下一节展开);旧式的 /ipfs/ 前缀在改名之前表达的是同一个意思。

multiaddr:会自我描述的地址

传统的地址(host:port)一旦你连上去,完全无法告诉你接下来该讲哪种协议。libp2p 给出的答案是 multiaddr:一种把每一层寻址信息都编码进同一条自描述路径中的约定。/ip4/198.51.100.0/tcp/4242 从左往右读,就是一组待执行的指令:先,抵达这台 IPv4 主机;然后,打开这个 TCP 端口。每一段本身都是一个合法的 multiaddr,把一个包进另一个的做法叫作封装(encapsulation);反过来剥掉一层则叫解封装(decapsulation)

由于每一层都写得明明白白,一个字符串就能携带一个节点完整的可拨号身份:/ip4/198.51.100.0/tcp/4242/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N 精确地说明了要连接哪台主机、哪个端口,以及对端应当是哪个 PeerId。更新的传输方式也用同样的方式折叠进加密材料:一个面向浏览器的 WebRTC 地址看起来像 /ip4/1.2.3.4/udp/1234/webrtc/certhash/<hash>/p2p/<peer-id>,其中嵌入了一个证书哈希,让浏览器无需可信 CA 也能验证一份自签名证书。一条经中继的连接则把两个 PeerId 串在一起:/ip4/198.51.100.0/tcp/4242/p2p/QmRelay/p2p-circuit/p2p/QmRelayedPeer

连接的分层:传输、安全、多路复用

libp2p 坚持做到传输无关:是用 TCP、QUIC、WebSocket、WebRTC 还是 WebTransport,完全交给应用决定,同一个节点甚至可以同时在多种传输上监听。无论走哪种传输,每条连接都会经过同一套概念上的栈:先协商出一条安全信道,然后(除非该传输本身已自带)在其上叠加一个流多路复用器。

QUIC 是唯一能省掉一步的传输方式。它由 IETF 标准化为 RFC 9000(传输层)、RFC 9001(TLS 集成)与 RFC 9002(丢包检测),前身是 2014 年 Google/Chrome 内部的一项实验。QUIC 把常开的加密与原生的流多路复用直接打包进了传输层本身。libp2p 索性直接复用这两者,而不是在其上再叠加自己的安全握手与多路复用器。一份自签名证书携带 PeerId,验证逻辑与 libp2p 的 TLS 安全模块共用同一套握手逻辑,因此在 QUIC 之上建立一条 libp2p 连接只需一个往返。go-libp2p 最早实现的是标准化之前的 draft-29 草案(multiaddr 代码 quic),后来又加入了 RFC 9000(multiaddr 代码 quic-v1);这两者在实际网络中至今仍需区分。

在传输本身不提供安全能力的场合,libp2p 会协商以下两种安全信道协议之一:

Noise(/noiseTLS 1.3(/tls/1.0.0
握手方式XX 模式,Noise_XX_25519_ChaChaPoly_SHA256永不低于 TLS 1.3(RFC 8446)
节点认证静态 Noise 密钥对由 libp2p 身份密钥签名,携带在握手载荷中主机密钥嵌入自签名证书内的「libp2p Public Key Extension」
客户端认证不适用(结构上天然双向)服务端必须要求客户端证书
提前协商自定义扩展注册表(Noise 本身没有原生的扩展机制)标准 ALPN(RFC 7301)

这两种协议都支持提前协商多路复用器:把流多路复用器的选择折叠进安全握手载荷本身,省下单独协商本要花费的那一次往返。截至目前,这项优化只在 go-libp2p 中实现,且仅用于那些本身不像 QUIC 一样自带原生多路复用能力的传输(TCP、WebSocket)。

在多路复用这一侧,libp2p 支持两种多路复用器:yamux(协议 ID /yamux/1.0.0),由 Hashicorp 设计,具备真正的流量控制,接收方可以通过基于偏移量的背压机制来限制发送方的速度;以及 mplex,一种更早期、更简单的设计,没有流量控制,也没有对节点可开启流数量的上限。规范对这一权衡说得很直白:yamux 应当优先于 mplex,mplex 目前正逐步被弃用,仅保留用于向后兼容。原生自带多路复用能力的传输(QUIC、WebTransport、WebRTC)则完全跳过这一协商步骤。

正是这种原生多路复用,使得 WebTransport 的连接成本远低于普通 WebSocket。一条标准的基于 WebSocket 的 libp2p 连接要依次叠加 TCP 握手(1 个 RTT)、TLS 1.3 握手(1 个 RTT)、WebSocket 升级(1 个 RTT),再加上两轮 multistream 协商与安全握手本身:在任何应用数据传输之前,共需六个往返。而基于 QUIC 构建的 WebTransport,把这一切压缩为一次 QUIC 握手、一次 WebTransport 握手,以及一次合并了 multistream 与 Noise 的 libp2p 握手:总共三个往返,成本减半。

技术栈数据开始流动前的往返次数
TCP + TLS 1.3 + WebSocket 升级 + multistream-select + 安全握手6 RTT
QUIC + WebTransport 握手 + libp2p 握手(multistream + Noise)3 RTT
仅 QUIC(原生安全与多路复用,无需额外协商)1 RTT

就协议达成一致:multistream-select 与 identify

两个节点一旦共享了一条安全的、多路复用的连接,它们仍需就任意给定的一条流上具体讲哪种应用协议达成一致。这正是 multistream-select 的职责,一次轻量级协商:发起拨号的一方先发送 /multistream/1.0.0 头,紧接着发送自己想要的协议 ID(比如 /ipfs/id/1.0.0);如果监听方支持该协议,就把这个 ID 原样回传以示接受;如果不支持,则回复 na("not available"),拨号方可以换一个备用协议 ID 重试。每一个 libp2p 协议都用一个类似路径、带版本号的字符串来标识自己,例如 /my-app/amazing-protocol/1.0.1

协议协议 ID
Ping/ipfs/ping/1.0.0
Identify/ipfs/id/1.0.0
Identify Push/ipfs/id/push/1.0.0
Noise/noise
TLS 1.3/tls/1.0.0
yamux/yamux/1.0.0
Circuit Relay v2(hop)/libp2p/circuit/relay/0.2.0/hop
DCUtR/libp2p/dcutr
gossipsub v1.1/meshsub/1.1.0
Rendezvous/rendezvous/1.0.0
Kademlia DHT通常称作 /ipfs/kad/1.0.0(go-libp2p-kad-dht 的实现是从一个可配置前缀加上 /kad/1.0.0 拼出来的)

大多数节点之间第一个要讲的协议是 identify:交换公钥、监听地址与支持的协议 ID。这次交换里藏着一个日后会变得极其重要的字段:observedAddr,也就是响应方实际观测到这条连接是从哪个地址发起的。一个身处 NAT 之后的节点,仅凭查看自己的网络接口是无从得知自己面向公网的地址的;observedAddr 正是它借由询问别人「你看到的是什么」来获知这一点的途径。正是这一个字段,成了下文整套 NAT 穿透体系赖以生长的种子。一个配套协议 identify/push 让节点可以主动向所有已知对端广播一份更新过的 Identify 消息,一旦该节点得知了一个新的中继地址或公网地址,就可以立即通知出去,网络的其余部分无需再重新询问一遍。

发现节点:Kademlia DHT、mDNS、rendezvous、bootstrap

光知道一个协议 ID 并没有用,除非你还知道要跟谁开一条流。libp2p 把「发现节点」(让别人找到自己)与「路由到节点」(找到某个具体目标)视为相关但不同的两个问题,并用若干可互操作的机制分别应对。

在互联网规模下,主力机制是一个 Kademlia 的 DHT 实现,其思路借鉴自 S/Kademlia、Coral 以及 BitTorrent 自己的 DHT。两个键之间的距离定义为 XOR(sha256(key1), sha256(key2)),落在一个 256 位的 SHA-256 键空间中;路由表力求为每个前缀长度对应的桶维持 k = 20 个节点(复制参数),单次查找会并行发出 α = 10 个请求。公网可直接触达的节点运行在服务端模式下,对外宣告 Kademlia 协议并接受入站流;受限于 NAT 或资源的节点则运行在客户端模式下,可以参与查找,但永远不会被别的节点加入自己的路由表。节点通过周期性地查找随机的 PeerId(包括查找自己),并把查找过程中发现的一切都并入自己的路由表,来完成引导(bootstrap)。

对于同一局域网内的节点,mDNS(RFC 6762)完全不需要任何配置:一个节点广播一次查询,同一网段上的任何节点都会用自己的 multiaddr 应答。

Rendezvous 走的是完全不同的路子,它是联邦式而非分布式的,这意味着单个 rendezvous 节点有可能成为瓶颈或单点故障,而这恰恰是 DHT 与 gossipsub 刻意要规避的问题。节点在某个 rendezvous 点上以某个命名空间下的身份 REGISTER(注册)自己,默认注册有效期为两小时,硬上限为 72 小时,走的是协议 ID /rendezvous/1.0.0。它常被用来为浏览器节点引导发现中继电路,或是为某个 pubsub 主题引导发现订阅者。

最后,最朴素的 bootstrap 列表(一份写死或配置好的已知节点集合,在启动时逐一拨号)依旧是让一个全新节点获得最初几条连接、进而接入上述任何一种机制的最简单方式。

传播:gossipsub v1.1

发现回答的是「外面都有谁」;pubsub 回答的是「在不做广播的前提下,如何把一件事告诉所有人」。libp2p 用于此的流言协议gossipsub,它为每个主题维护的覆盖网络包含两种对等关系:一种是稀疏的全消息网格(mesh),目标度数为 D = 6(容许范围 4–12),完整的消息经此转发;另一种是更密集的仅元数据对等关系,纯粹用来八卦「哪个节点已经见过哪些消息」。每秒一次的心跳驱动网格维护:把仅元数据的对等节点提升(graft)进全网格,或在网格超员时把全网格节点降级(prune)回仅元数据状态。IHAVE 用来宣告最近见过的消息 ID;IWANT 用来请求自己确实缺失的那些消息。向一个节点尚未订阅的主题发布消息时,会退回到 fanout(扇出) 机制:为每个主题记住六个随机选中的对等节点用于投递,若某个主题两分钟内都没有新消息发布,这份记忆就会被遗忘。

v1.1 版本在这套基础设计之上叠加了若干出于安全考虑的扩展:显式配对协议,供运营者在评分系统之外建立无条件、始终在线的连接;剪枝(prune)时的对等节点交换(PX),为被剪掉的节点提供替代候选,而不是直接把它扔掉;洪泛式发布,无视订阅关系,把自己发出的消息发给所有评分足够高的已连接节点,用作抵御 eclipse 攻击的一道防线;自适应流言传播,由一个 0.25 的流言系数调节,其三轮传播大约给任意一个节点留下约 58%(1 − (3/4)³)的概率能收到关于某条新消息的流言通知;以及出向网格配额D_out),保证网格中至少存在一定数量由自己主动发起、而非被动接受的连接,专门用来削弱那些试图占满受害者入站连接槽位的女巫攻击。

每个节点都会在本地为其余每个节点打分(分数从不对外共享),由分主题的行为(在网格中停留的时长、首次消息投递、消息投递率、无效消息)与全局行为(IP 同址情况、显式的行为惩罚)加权组合而成。低于 GraylistThreshold 的节点,其消息会被直接忽略;一个健康网格的中位数分数一旦跌破 OpportunisticGraftThreshold,就会触发机会性提升(opportunistic grafting),把至少两个评分更高的节点拉进网格,这一检查大约每分钟进行一次。正是这套评分系统,把「所有人对所有人都八卦」变成了一种能在对抗性节点存在的情况下依然存活的机制,而不是任由单个恶意节点悄悄把它饿死。

NAT 穿透体系

互联网上的大多数节点都身处 NAT 或防火墙之后,无法被未经邀请的入站连接触达。这正是 NAT 穿透与 WebRTC 借助 STUN 和 TURN 解决的同一个根本问题。STUN 与 TURN 确实好用,但它们的运作方式依赖于专用的、由中心统一运营的服务器:STUN 服务器存在的唯一目的,就是从外部告诉客户端自己的公网地址长什么样;TURN 服务器存在的唯一目的,则是在找不到直连路径时充当流量中继。libp2p 的这套体系解决的是完全相同的问题,却完全不需要搭建任何专用基础设施:每一个角色都由网络中已有的普通节点来充当。

这种对应关系是直接的:STUN 告诉你自己的反射地址,AutoNAT 对应的做法是请求其他 libp2p 节点回拨给你,并报告它们看到了什么。TURN 在别无他法时中继你的流量,Circuit Relay v2 对应的做法是让其他节点自愿充当中继。ICE 负责协调两个身处 NAT 之后的节点同时发起连接尝试,DCUtR(用它所依据的那篇同行评审论文的说法,就是「去中心化打洞」)对应的做法是把同样的协调过程搬到一条既有的中继连接之上运行,而不需要专门的信令服务器,除了能连上一个引导节点之外,不需要任何关于网络的先验知识。

AutoNAT:判断自己是否可被触达

AutoNAT v1/libp2p/autonat/1.0.0)解决的是一个简单却至关重要的问题:节点自己没有内建的手段来判断自己是公网可达,还是隐藏在 NAT 之后。它会请求其他节点回拨自己,发送一条列出候选地址的 DIAL 消息,收到一条带有状态(OKE_DIAL_ERRORE_DIAL_REFUSED)的 DIAL_RESPONSE。为了防止这个协议被滥用成放大攻击的工具,执行回拨的节点只能拨打自己观测到请求实际来源的那个地址,并且必须拒绝经由中继连接抵达的请求。一旦超过三个节点报告回拨成功,一个节点便可以合理地断定自己是公网可达的;超过三次失败,则断定自己身处私有网络之后。

AutoNAT v2 从两个方向做了改进。首先,它逐个地址地验证,而不是把一个节点的所有地址捆在一起给出单一结论,这一点很有用,因为一个节点可能同时拥有多个地址(不同传输、不同网络接口),而这些地址的可达性可能各不相同。其次,它加入了真正的验证机制:在一个 DialBack 步骤中交换的随机数(nonce),让请求方能够证明回拨节点确实连接到了它所声称的地址;同时它现在也允许拨打一个与观测到的源 IP 不同的地址(这是 v1 完全禁止的),但要为这一特权支付一笔抗放大攻击的代价:请求方必须先以 4,096 字节为单位、上传 30,000 至 100,000 字节的填充数据,回拨才会继续进行。

Circuit Relay v2:带限制的中继

Circuit Relay v2 拆分成两个协议:hop/libp2p/circuit/relay/0.2.0/hop,用于客户端与它想使用的中继之间)和 stop/libp2p/circuit/relay/0.2.0/stop,用于中继与被联系的对端之间)。一个预计需要中继的客户端会经 hop 发送一条 RESERVE 消息;接受请求的中继会返回一份带签名的凭证、一个过期时间,以及可选的、针对它愿意承载的任何连接的时长与流量上限。这份预留必须在过期之前刷新,而且它只在客户端本身持续与该中继保持连接的期间才有效。

这些限制之所以重要,是因为它们体现了一个刻意的设计取舍。Circuit Relay v1 完全没有预留机制,这使得中继可能被任何人愿意经它转发的任意流量压垮。v2 强制的预留机制、以及可选的时长/流量上限,把中继变成了一座用来启动打洞尝试的临时桥梁,而非直连的永久替代品,不管网络如何增长,中继运营者的带宽账单都能保持在可控范围内。一个完整的中继 multiaddr 看起来像 /ip4/198.51.100.0/tcp/55555/p2p/QmRelay/p2p-circuit/p2p/QmAlice。中继自己宣告地址时不带 /p2p-circuit 后缀,而是由客户端在构造通往某个具体节点的中继路径时自行追加上去。

DCUtR:不用信令服务器也能打洞

在一条已经建立、且本身就已经由常规的 Noise/TLS 握手做到端到端加密的中继连接之上,DCUtR/libp2p/dcutr)把这条连接本身当作信令通道,来协调一次同步的直接拨号尝试。这套消息交换刻意做得很精简:一条 CONNECT 消息携带双方各自观测到的地址与预测地址,一条 SYNC 消息则触发那次定时的、同步的拨号本身。

真正让打洞成功落地的是其中的时序技巧。接收到入站中继连接的一方(记作 B)先发出第一条 CONNECT 并启动一个计时器;当 A 的 CONNECT 回复到达时,B 停止计时器,此时便得到了一个往返时延(RTT)的测量值。B 会等待这个 RTT 的一半(作为单向网络延迟的粗略估计)再发送 SYNC;这样一来,当 A 在收到 SYNC 的那一刻立即拨号、B 在自己 RTT/2 的计时器到期的那一刻立即拨号时,两次拨号尝试会落在大致相同的时刻,这正是 NAT 打洞真正依赖的东西。

从这里起,TCP 与 QUIC 就分道扬镳了。在 TCP 上,双方都在各自同步好的那一刻发起一次出站 socket 连接,依靠 TCP 同时打开(simultaneous open)一举完成握手;对于叠加在其上的任何东西而言,A 被当作客户端、B 被当作服务端。在 QUIC 上,A 仍然在收到 SYNC 时立即拨号,但 B 并不直接拨号,而是每隔 10 到 200 毫秒向 A 的地址发射装满随机字节的 UDP 包,制造刚好足够撑开一个 NAT 映射的流量,而不去尝试真正走通一次 QUIC 握手,直到 A 真正的连接尝试成功穿透为止。若第一次尝试失败,入站一方最多再重试两次(总共三次尝试)。

综合起来,完整的流程如下:Identify 让双方各自得知自己被观测到的地址 → AutoNAT 告诉它这个地址从外部看是否真的可达 → 若为私有,则(通常经由 DHT)发现一个中继,并拿到一份 Circuit Relay v2 预留 → 一条初始连接经这个中继抵达目标 → DCUtR 在这条中继信道上跑一轮 CONNECT/SYNC 交换,触发一次同步的直接拨号尝试 → 成功则把连接升级为直连路径并放弃中继;失败则把这条中继连接留作后备。

在真实世界中测量:punchr 项目

设计意图是一回事;Protocol Labs 用一套名为 punchr 的自研工具、在 2022 年 12 月到 2023 年 1 月间开展的测量活动,才真正告诉我们去中心化打洞在互联网规模下究竟能不能行得通。这些数字来自大约 440 万次打洞尝试,覆盖超过 8.5 万个不同网络,遍及 167 个国家

最核心的数字是:一个节点一旦度过了更早的阶段(拿到可用的观测地址、拿到中继预留),打洞本身的成功率是 70% ± 7.1%。不过这是一个条件概率:大约 29% 的全部尝试根本没能走到打洞这一步,就已经因地址发现或中继预留阶段的失败而提前出局。而当打洞真的成功时,它几乎全都是一次成功:97.6% 在第一次尝试就成功,DCUtR 内建的重试机制只贡献了剩下的 2.4%。

TCP 与 QUIC 的原始成功率没有统计学意义上的显著差异(两者都在 70% 左右),不过 QUIC 在 IPv4 上表现出更小的波动;而当两种传输被同时拿来赛跑时,QUIC 有 81% 的概率率先建立起直连。有点反直觉的是,往返时延对结果几乎没有可测量的影响,该研究发现打洞成功率「不依赖于往返时延」;而 IPv6 的表现相对 IPv4 出人意料地差,尽管现有数据没能对这一差异做出更细的拆解。与此前两项学术基准相比(一项 2011 年的研究报告约 64% 的成功率,一项规模更小、针对 93 个家用 NAT 的 2005 年研究报告 88% 的成功率),本次活动测得的 70% 来自一个规模大出好几个数量级、地理代表性也远胜以往的数据集。

唯一可靠地会失败的穿透场景,是节点身处对称型 NAT 之后,它会把每一次出站连接映射到一个不同、实质上不可预测的外部端口。DCUtR 的协调协议在这里并没有从根本上失灵,真正的障碍在于对端需要猜中的那个端口本身无法被提前预测。打洞失败时,连接会直接继续沿着它原本就在用的 Circuit Relay v2 路径运行;中继不仅仅是通向打洞尝试的一座桥梁,也是那些打洞永远够不到的场景的安全网。

libp2p 作为 P2P 的公共基座

从任意一层抽身回望,同一种模式反复出现:身份是一个哈希后的公钥,而非一个地址;地址是自描述的,而非预先假定的;传输、安全与多路复用全都是协商出来的,而非固定写死的;就连穿透 NAT 的机制本身,也是由普通节点搭建而成,而非依赖专用服务器。这些做法没有一项是 libp2p 独有的:DHT流言协议与 NAT 穿透各自都是被充分研究过的独立领域。但把它们统统包裹在一套一致的、可替换的模块接口之后,一个最初为某个文件共享项目打造的线路协议,最终成了一条区块链共识客户端、一个存储网络、乃至一种浏览器传输方式共同依赖的网络层,而这一切,都不需要它们之间就模块以外的任何事情达成一致。

返回首页