架构
白皮书的工程篇伴读 — 参考节点如何实现这套设计,以及其背后的各项决定。是解释性的,不是规范性的。
协议是参数文件与黄金 向量,外加各项具名规则,无论它们在何处被定义; 线路规范承载着对等层的各项要求。本 页解释那个接口面,并记录其背后的各项决定。凡是它与规范性接口不一致之处, 以规范性接口为准,而本文会被更正。 完整的工程篇伴读是 docs/ARCHITECTURE.md。
两个谓词,两个引擎#
一份凭证从头到尾的一生:
wallet network every full node
+------------+ gossip +--------------+ +---------------------+
| build cert | ---------> | cert topic | ------------> | STATELESS PIPELINE |
| (reads, | | (TLS gossip) | | V1..V8, batch sigs, |
| writes, | +--------------+ | native re-exec |
| sigs, seq, | | -> mark VALID |
| deposit) | +----------+----------+
+------------+ | mempool
v
miner (any node) +---------------------+
+-------------------+ block | FOLD (sequential) |
| assemble ordered | --------> | F-rules per cert: |
| hash list + bodies| gossip | APPLY / SKIP / DROP |
| + RandomX solve | | -> new state |
+-------------------+ +---------------------+
有效性是无状态且可并行的:它在每个节点上对每份凭证运行一次, 在区块之前进行,且与区块无关。可应用性是有状态且串行的:它 在凭证被提交的位置上、在折叠内部运行。矿工不运行任何执行;它 对自己已经见过被验证通过的那些哈希进行排序,并求解工作量证明。折叠是一个在内存工作集上的紧凑循环 — 比较、相加、写入。
工程原则#
| 原则 | |
|---|---|
| P1 | 折叠是神圣的。状态转换函数住在一个纯粹的包里,没有 I/O,没有时钟,没有 goroutine,没有 map 遍历,也没有浮点数。它是唯一一处其缺陷在事后无法修补的代码。节点里的其他一切都是可替换的管道。 |
| P2 | 确定性优先于性能。任何有不确定性风险的优化,在共识代码中一律被否决。性能属于无状态流水线,在那里它是安全的。 |
| P3 | 没有管理员密钥,没有特权 RPC。不存在任何一条代码路径,能让某把密钥去暂停、升级、铸币或重组。如果它不在折叠规则里,它就不存在。 |
| P4 | 共识面要小。core/ 不引入标准库以外的任何东西。节点的其余部分可以使用生态库;核心不可以。 |
| P5 | 规范优先。黄金向量就是协议。这份 Go 代码是一个参考实现;一个能通过这些向量的独立实现是对等实现,不是分叉。正是这一点使得维护者最终可以是无名之辈。 |
| P6 | 从 v0.1 起即可复现。钉死的 Go 工具链、-trimpath、钉死的依赖。信任从二进制文件转移到代码上,而这是一个匿名作者所能提供的唯一一种信任。 |
| P7 | 一个二进制文件,由高度把关。机器、抵押相关操作和定序器注册都编译进每一个发行版,并在其启用高度之下被拒绝 — Validate 要求 h1_vm ≥ h1_bond,且要求 h1_vm 落在一个纪元边界上。一个时代之所以到来,是因为链走到了某个数字,绝不是因为有人请求运营者升级。 |
P3 是一位怀疑的读者最该用力去按的一条,而国库正是该按下去的地方: 创世区块里根本没有密钥,也没有任何花费路径,所以没有什么特权可供 持有、委托或窃取。Era 2 的那套 5 选 3 法定人数是由一次未来的硬分叉钉定的 — 与任何其他共识变更是同一套社会机制,要经受同样的否决, 而且即便如此,它也只能移动恰好一个存储单元。一套只有在网络同意把它写进去时才会出现、 出现之后只掌管一个存储单元的法定人数,是一条花费规则。而管理员密钥是那种在任何人表示同意之前就已存在、 且触达一切的东西。
密码学原语#
| 角色 | 选择 | 为什么 |
|---|---|---|
| 签名 | Ed25519 | 支持批量验证(通往 GPU 的入口匝道),没有可锻性,密钥极小。在创世时就钉死了严格规则:公钥和 R 必须采用规范编码,公钥必须无挠且不属于低阶,验证必须是无余因子的。 |
| 哈希 | BLAKE3 | 凭证 id 与区块 id、地址、状态根。快到足以按转发线速做哈希;对纪元状态根而言又对并行友好。 |
| 工作量证明 | RandomX | 为 CPU 优化。它是整棵代码树里唯一的 cgo,藏在一个构建标签之后,不带该标签的构建里根本没有它。pow_engine 位于共识根之内,所以一个持有错误引擎的二进制文件会拒绝启动,而不是去接受错误的证明。 |
| 域分离 | 强制 | 每一次哈希都是 blake3(tag ‖ payload)。签名同时覆盖链 id 和共识根,这就同时消灭了跨网络的重放和跨同一网络两次化身的重放。 |
正是拒绝挠元素让批量路径变得安全。一把混合阶的密钥并不是 低阶的,所以没有哪份封禁名单能拦住它,而它恰恰是带余因子的批量验证器与无余因子的单次验证器 发生分歧的地方。当密钥和 R 都落在素数阶子群里时,两者 可被证明是等价的 — 因此批量验证器可以带余因子,前提是它在批处理之前 施加同样的编码规则和挠规则。那份义务是这项选择的代价,而批量验证器还不存在。
规范编码与各类标识符#
所有共识对象都是 SSZ 容器:单一的规范字节编码,没有 map 顺序问题,没有可选字段的歧义,有便于低成本部分解析的固定偏移量,以及原生的 merkle 化。
cert_id = blake3("zcd/certid/v1" || ssz(certificate with an empty signature list))
cert_exemplar = blake3("zcd/cert/v1" || ssz(certificate))
block_id = blake3("zcd/block/v1" || ssz(header))
前两者是回答不同问题的不同摘要,而一个在该用其中之一的地方用了另一个的 实现,在金钱意义上是坏的。id 回答的是 这份授权是否已被计费;实例字节的哈希回答的是这些字节是否证明了 它。两者从不共用一个键。
签名位于 id 的原像之外,因为签名是一次随机化的展示: 签名者挑选 nonce,每一个 nonce 都会对同一个消息体产出另一个同样有效、同样完美规范的签名, 而没有哪个验证者能核查用的是哪一个。倘若它们在里面,一份授权就会 拥有无穷多个 id,每一个都可计费,每一个都能由其所需签名者中任意一位 越过其他人的权限而单独产出。
一个位于 id 之外的出价,会是一个任何位于传输途中的人都能改写的出价 — 被抬高以便通过基础费 烧掉签名者的余额,或者被清零以便让这份凭证进不了任何区块。一份 凭证付出什么,是它的签名者所授权内容的一部分,所以它要被哈希,也要被签名。日后一次 “为了转发灵活性而把费用移出被签名的消息体”的编码变更,看起来会像 一次优化,而它会是一条盗窃通道。
随之而来的规则是:解析,而不要验证两遍。解码本身就强制施行规范 形式,所以一个被解码出来的对象在构造上就是结构合法的,而各规则引擎从不 重新核查形状。
存储单元模型#
存储单元是一个槽位上的值。存储单元的值是以大端序存放在 32 字节中的 无符号 256 位整数。不存在的存储单元读出来是零 — 零就是不存在,而这是一项 共识要求,而不是实现上的图省事:它让状态根成为状态的函数, 而不是产生出该状态的那段历史的函数。
另外,协议还保有一份已花费地址注册表:一个由签名权限 已被烧毁的一次性地址构成的、永久性的共识集合。
Addr = version || blake3("zcd/addr/v1" || version || payload)[:31]
| 版本 | 种类 | 借记授权 |
|---|---|---|
0x01 | 一次性用户地址 | 所有者签名;任何进行借记的凭证还必须携带一个显式的 MARK_SPENT。它一旦应用,该地址之下的每一次读取和写入都将永远失败。 |
0x02 | 持久用户地址 | 所有者签名;可永久重复使用。永远不会进入已花费注册表。 |
0x03 | 资产 | 由该资产不可变的权限存储单元管辖。 |
0x00 | 协议 | 仅折叠可用:纪元信标、基础费存储单元、coinbase 成熟环、国库存储单元。 |
0x04 | 保留 — 隐藏值存储单元(Era S) | 在 Era 0 中不可达。 |
0x04 是现在就保留下来,而不是日后再分配,因为一个隐藏
值必须凭其地址就能与普通值区分开:一个 Pedersen 承诺
和一个 256 位余额都是 32 字节,所以一个因错误或恶意而瞄准某个承诺槽位的带守卫增量,
会让折叠把一个整数加到一个曲线点编码上 — 这种算术
能通过每一项检查,并留下一个谁也永远花不掉的存储单元。这张表在创世时就被冻结了,
所以这个字节在此被占下并留作不可达。
注册表条目从不被压缩。已花费地址之下的存储单元值 可以在回滚视界之后被剪除,但那条把该地址记录为已花费的条目,正是 阻止它被复活的东西。它是只追加的共识状态,每个地址约 33 字节, 永远保留 — 这个协议诚实承认的尚未解决的问题,在结构上与每一种 作废符集合设计共有。
无状态有效性,以及计费法则#
V 规则在每一份凭证上运行,是并行的,发生在进入内存池之前以及区块 验证期间,且需要零状态:规范形式与链 id;每一个签名都要在 签名根之上验证通过;授权可仅凭该凭证本身推导出来;所声明的读取要等于 程序推导出的结果;以及退款去向要与该凭证自己所烧掉的东西相核对。
这个系统的计费法则只有一句话,而它就是要牢牢抓住的那件事:
一个签名,至多一次计费,且绝不在其签名者无从回避的位置上计费。
本规范为白皮书的词汇表添了一个词:丢弃,一种不计费的 非事件。一份凭证到达应用阶段时其保证金已被消耗,它会被丢弃 — 不计费,不标记为已见,可以针对一笔新鲜的保证金自由重新提交 — 因此诚实的用户不会 因为自己那个保证金存储单元上的竞争而损失任何东西。
仓库布局#
zycord/
spec/ parameter sets, golden vectors, library images <- THE PROTOCOL
core/ consensus-critical; standard library only (P4)
types/ crypto/ ssz/ u256/ state/ validity/ fold/ params/ genesis/
cevm/ the certificate-adapted EVM; vendored interpreter, pure Go
stdlib/ the pre-deployed library, its addresses and code hashes
pow/randomx/ the mainnet engine: vendored C++, cgo, behind a build tag
node/ storage/ chain/ verify/ mempool/ miner/ p2p/ sync/ rpc/ stratum/
wallet/ key management, certificate builders (reference; not consensus)
contracts/ the reference contracts, in Solidity
sim/ simulator, fuzz harnesses, differential refold, chaos soak
cmd/ zycordd, zcd
desktop/ the wallet in a native window — a separate Go module
docs/ architecture, protocol, operating guide, whitepaper
依赖箭头只朝内指 — node → core、
wallet → core,绝不反向 — 而 core/ 内部的任何东西
都不会伸到 core/ 和标准库之外。有一个例外,它被点名并被强制执行:
core/pow/randomx 是整棵代码树里唯一的 cgo,而它只在那个构建
标签之下才会编译,所以每一个不带该标签的构建仍然是只依赖标准库的,也不需要 C 工具链。CI
会运行强制执行这一点的检查,因为第三方检查是靠 grep 模块路径来做的,而 cgo 一个模块路径都没有
— 这曾让这条规则在被补上之前无人执行。
它是怎么被测试的#
- 黄金向量。每一条折叠规则、区块规则和有效性规则都有正例和反例,形式为
(pre-state, block) → (post-state | invalid, outcomes, fees)。这套测试集就是面向独立实现的兼容性契约。 - 骚扰攻击测试集。重新打包已应用和已跳过的凭证、过期后打包、出价不足时打包、在提议者洗牌下的依赖链、烧毁与退款的循环、第三方贷记风暴、铸造上限的边界竞争。
- 基于性质的测试。在提议者顺序置换下的折叠确定性、增量的可交换性、ABA 容忍度,以及守恒 — 其中包括在途保证金、成熟环和国库存储单元,因为一项遗漏了国库的守恒检查,会把每一个区块都报告成在凭空创造价值。
- 差分测试。一个刻意写得朴素的第二份折叠实现,为显而易见而非为速度而写,与真正那份对拍模糊测试。出现分歧就是发布的阻断项。
- 对抗性模拟。跳过风暴、保证金抽干竞争、往里塞丢弃项的矿工、重组酷刑、时间戳操纵、轻量日蚀转发场景。场景配置都已提交入库;每次运行都可由种子复现。
- 并发,且是刻意的。每一个被不止一个 goroutine 触及的组件,都有一个本身就是并发的测试,其形态与进程实际使用的形态一致。在一个单 goroutine 的测试集上跑
-race什么也测不到,却会报告成功。 - 混沌浸泡测试。真实的节点进程通过真实的套接字,经由一个注入延迟、抖动、丢包和分区的代理进行通信,同时节点会被随机地以
SIGKILL杀掉。正是这个面找出了整套-race测试集都漏掉的那个数据竞争。
创世是一件产物,不是一场仪式#
zcd genesis 会产出创世区块 — 空状态、已初始化的信标存储单元、
空的已花费注册表、没有任何形式的分配 — 以及它的 id。已宣布的
发布会提前数周就对代码标签、参数哈希、创世 id 和
发布时间作出承诺。任何人都可以在任何机器上、从源代码出发,用毫秒级的时间把这四样全部重建出来。
除此之外没有别的东西需要信任。