ZYCORD 文档
简体中文
Zycord文档线路协议

线路协议

Zycord 节点之间彼此说些什么,以及那些让一个对等协议不至于变成放大器的规则。这是一份通往规范文件的导读,不是它的替代品。

本页是一份导览,不是规范本身

规范是 spec/:参数文件和向量 语料。本页若与代码树不一致,以代码树为准 — 一个实现只要能复现创世 id 并通过那些向量,它就是合规的,本页写什么都不改变这一点。下面是一份关于各项要求都在哪里的地图。

传输#

先 TCP,再 TLS 1.3。对等身份是一把承载在自签名 X.509 证书中的 Ed25519 密钥,由双方各自出示。证书链不做验证 — 这里没有证书颁发机构,也没有什么可供其背书 — 所以验证要求恰好一份证书, 要求其公钥必须是 Ed25519,并把该密钥提取出来作为对方的身份。

由此获得的性质是与某把密钥的通道绑定,而不是授权:TLS 保证完成握手的那一方持有所出示身份对应的私钥,并保证数据流在传输途中无法被读取或修改。 它对那个对等方是否诚实只字未提。协议中没有任何东西是基于身份来授予权限的。

证书的有效期是固定的常量,而不是围绕当前时间的一个窗口,这是刻意的: 相对窗口会让证书是否被接受取决于时钟是否一致,而一个沿着时钟偏差分裂的网络, 是一个因与共识无关的原因而分裂的网络。

实现不得把任何其他层的密钥重用为对等 身份。节点在每次启动时都生成一把全新的,并且从不把它写到磁盘上。

分帧#

offset  size  field
0       4     length   uint32, little-endian - payload length, excluding this header
4       1     kind     uint8  - see below
5       n     payload
  • length 必须在任何分配之前与 MaxMessageBytes = 8 MiB 相核对。一个按所声称的长度进行分配的接收方,无论后面还做了什么,都存在一个远程内存耗尽缺陷。
  • kind 必须落在 [1, 9] 之内。零以及任何高于已知最大种类的值都是协议违规,而不是某个未知扩展:在协议版本 1 中没有向前兼容的逃生舱口,因为在握手时核对一个版本字段就已让它变得不必要。

消息种类#

取值名称方向载荷
1hello双向,首个握手
2certificategossipssz(Certificate)
3block-announcegossip区块头加上各凭证的 id
4get-block请求32 字节的区块 id ‖ u32 分块索引
5block响应ssz(Block) 的一个分块
6get-headers请求定位符
7headers响应一段连续的区块头
8get-peers请求空
9peers响应地址列表

共识对象没有任何网络专用的编码,正是这一点让 “我收到的东西的 id”成为一句可核查的陈述。

每一条入站消息都由其发送方付费#

这是规范文件中最值得完整读一遍的部分,因为对等协议通常正是在这里泄漏的。有两条规则管辖它。

成本排序规则#

工作按成本递增的顺序进行,而一条消息在其后那个昂贵步骤之前就被计费。 一个先验证、后计分的接收方等于造了一个放大器:发起来便宜的东西,正是核查起来昂贵的东西。

每一种结果都要计价#

不存在没有名字的 Free。每一种消息种类,与它可能产生的每一种 结果相交叉,都出现在一张带有分值的表里。一种没被计费就从表中漏过去的结果,正是这条规则存在所要防止的缺陷 — 而在实践中,它已经通过比“某一行没定价”更狭窄的路径被触达过:某个实现把一个格式错误的帧 从它唯一的计分器旁引开,结果只要还有一个请求未完成,那个帧就是免费的,而任何表里都没有多出一行。

提供服务同样是计量的。区块字节是唯一一种比索取它的请求大上几个数量级的响应, 所以除了请求计数之外,它们还自带一份字节预算。

连接管理,以及日蚀攻击防御#

这一节承载的实测失败案例比其他任何一节都多,而下面每一条要求之所以存在, 都是因为一个缺少它的实现在一次测量中被日蚀攻击了。

  • 出站目标的选择必须带有地址多样性,好让一个托管地址段无法填满一个节点的出站槽位。
  • 出站目标还必须按 gossip 来源加以限额。光有地址多样性是不够的,原因是算术上的,而不是判断上的:地址组是一个攻击者拥有的地址所具有的属性,而一个对等方声称的地址不过是它编出来的字节,而任意四个字节都是一个合法的 IPv4 主机。一个一个地址都没有的攻击者,只需一帧的代价,就能为每个字符串铸出一个全新的多样性组。它编不出来的,是这个声称所抵达时用的那条连接。
  • 一条入站连接所抵达的那个套接字不得成为出站目标。那是对方的来源地址 — 一个由它的操作系统挑选的临时端口,而不是它监听的东西 — 所以花在拨打它上的槽位,是花在一个不可能应答的地址上的槽位。
  • 这两条限额都必须针对一个节点所持有的连接来计数,而不是针对某一次选择调用。否则,一个把已连接的对等方排除在外的拨号循环,会在每一轮都把两份预算重新填满,于是这个限额只是让攻击者每份额度延迟一轮,而不是把它限制住。实测:一个报信方在四轮之内,先后占据了 8 个出站槽位中的 2 个、4 个、6 个、8 个。
  • 对等节点存储必须被持久化,而且必须有上界。一个每次重启后都从空白开始的节点,等于在每次重启时都给攻击者一次全新的机会;而一个带着攻击者那份名单启动的节点,等于把同一次机会永久地交给了它。
  • 一个有上界的存储不得因为已满而拒绝一个格式正确的地址;它要为之腾位。一个从未联系过的诚实地址,永远不会比一个从未联系过的编造地址更好,所以率先把存储填满的攻击者,就把它对之后提供的一切都锁死了 — 包括运营者自己的引导列表。
  • 腾位时选中谁,比腾位这件事本身更要紧。“从最大的那个群体里挑受害者”读起来像是洪水把自己冲走,而它只有在洪水就是存储里最大的那个东西时才成立。在一个从一个热心对等方引导启动的节点上,最大的群体就是那个对等方的地址簿。在一个按此排序的实现上实测:来自单一来源的 200 个编造地址挤掉了 200 个诚实地址,而攻击者分文未付。
  • 在无从区分的条目之间,最后的平局裁决不得是任何由散播消息的对等方所选择的东西 — 包括那个地址 — 而这一点适用于选择,正如它适用于腾位。在一个选择器最终落到地址字符串上的实现上实测:8 个诚实地址对 8 个编造地址,返回的是8 个全是编造的,而且十轮下来诚实的出站连接为零。

哪些东西是刻意缺席的#

缺席之物为什么
NAT 穿透代价已被明说,而重开条件是测出来的,不是假定的:公共 testnet 上可被拨入的节点占比,是重开这项决定的首要条件。
压缩在一条由攻击者控制的流上放一个压缩器,是一个攻击面,而换来的带宽节省,还没有人测量出对它的需求。
消息级签名传输层认证的是通道;共识对象自带它们自己的签名。第三层将会去认证转发方,而这不是任何决定所依赖的东西。
请求 id请求与响应按连接和顺序配对,所以同步在它自己的连接上运行 — 这样就少了一个状态机。若某个对等方不可被拨入,同步可以在一条既有的 gossip 连接上运行,届时必须按内容来匹配响应。
一套向前兼容的扩展机制协议版本 1 在握手时核对自己的版本,不匹配就断开。扩展将以协议版本 2 的形式到来。

一个凭证 id 没有覆盖什么#

值得在这里重述一遍,因为它决定了一条转发规则。一份凭证的 id 承诺的是它所 授权的东西,而绝不是它仅仅展示的东西,所以签名位于该 id 的原像之外。 这使得证据成为任何位于传输途中的人都能替换的东西:拿一份 凭证,把它的签名换成垃圾,然后传播出去 — id 相同,而实例字节此刻已然无效。

有两条规则堵上了它。区块要对它所携带的证据作出承诺,途径是一个建立在 实例字节哈希之上、而非建立在 id 之上的根。以及转发处理的是实例字节,不是 id: 一个收到某份实例字节、而其证据验证失败的节点,会丢弃那份实例字节, 而不牵连那个 id — 该 id 不会被标记,不会被缓存为无效,而稍后一份能通过验证的 实例字节会照常转发。一份被残害过的副本,让残害者付出发送它的带宽,而让那份凭证分文不损。

从零开始实现它#

一个能通过 黄金向量的独立实现是对等实现,不是分叉 — 这正是 以这种方式来制定规范的全部意义。请从 spec/README.md 开始了解共识对象,然后用 zcd vectors 核查你的成果。