运行节点
zycordd 的运营者指南。它所讲的协议是线路规范;本页讲的是如何运行它。
一句话版本#
zycordd --devnet --dir ./devnet
那就是一个全节点:它从创世区块起验证每一个区块,为对等节点提供服务,并在
127.0.0.1:9420 上响应一个只读的 RPC。要加入那个真正在运行的网络,请看
公共 testnet — 并注意它需要
-randomx 二进制文件。
节点没有什么#
节点进程中从来没有任何密钥材料。它不接受种子、不接受口令,也不接受密钥文件。
签名发生在 zcd 里,而节点唯一的写入
端点 — /submit — 不授予任何权限:一份提交上来的凭证,
会像一份来自陌生人的凭证那样被一模一样地验证。
同样,这里没有特权端点,因为根本没有什么可以享有特权。 没有哪把密钥可以暂停、升级、审查或铸币,所以也就没有什么调用需要暴露出来。
限流器以传输层的对端为键,而绝不以 X-Forwarded-For 为键:一个信任客户端自己设置的
请求头的限流器,是一个由客户端关掉的限流器。因此在代理后面,每一个
请求抵达时带的都是代理的地址,于是按客户端计算的限额就不再是按客户端的了,
而变成所有人共用的、每分钟 600 次的一个桶 — 而单独一个调用方就能把它替所有人清空。
这可以说比完全不限流还糟,而响应里对此只字未提:无论是自己造成的还是别人造成的,调用方拿到的都是一个平淡的 429。
只读接口#
任何从外部观察这条链的东西 — 区块浏览器、监控、索引器 — 都在这里读取。节点交出的是字节,绝不是解释。
| 端点 | 回答什么 |
|---|---|
/status、/head | 链尖的标识、高度、状态根 |
/block?height=N 或 ?id=0x… | 一个区块,JSON 格式,带 canonical、orphaned 和 confirmations |
/block?…&format=ssz | 同一个区块,规范 SSZ 字节形式,application/octet-stream |
/params | 生效中的参数集及其共识根 |
/cell、/balance | 链尖状态 |
/fees | 基础费,以及当前生效的弹性上限 |
/mempool | 池计数器;?limit=N 会附上最多 1000 个待处理 id,最小的在前 |
/network、/metrics | 连接形态与计数器 |
/submit | 唯一的写入,而且它什么权限也不授予 |
白皮书 §8.1 让区块的字节上限、gas 上限和凭证数上限成为
T(即串行目标)的函数,而 T 是由纪元控制器移动的共识状态。
因此 /params 只承载创世时的取值 —
block_byte_limit_genesis 及其同伴 — 它们是 T 的起点,以及它可以衰减回去的
下限,绝不是当前生效的那个上限。那些数字永远都是真实存在的,
所以把其中之一读作“区块大小上限”是一种无声的错误,误差最大可达 2.5 MB 的创世取值
与 8 MB 的容量墙之间的距离。/fees 提供的是实时的
T 以及由它导出的四个上限,这样这个推导就可以被核查,而不是被信任。
为什么字节要紧#
blake3("zcd/block/v1" ‖ header_bytes) 是区块 id,因此一个从自己所收到的内容
重新推导 id 的观察者,要么与网络一致,要么立刻就会发现不一致。这也是获得逐凭证结果的方式:
一份凭证究竟是被应用、被跳过并计费,还是被丢弃,都是在折叠内部算出来的,而且从不持久化,
因为在一个活在内存里的存储中,为每份凭证永久保留一行,不是一个节点可以被要求
背负的事。想要那些结果的观察者,就自己去折叠那个区块。
这里没有、将来也不会有什么#
- 不支持任意的状态遍历。没有地址列表,没有前缀扫描,没有富豪榜。
- 没有历史状态。
/cell和/balance读的是链尖;“高度 H 处的余额”意味着节点并不保留的那种留存历史。 - 没有管理、重建索引或调试调用,因为根本没有什么可以享有特权。
聚合属于观察方,由它在数据摄入时一次性算好,存进它自己的数据库。
状态码#
读取接口响应 GET 和 HEAD,并对其他所有动词以
405 拒绝。一个格式正确但答案为否的问题 — 一个链尚未
到达的高度、一个未知的 id、一个在重组中落败的区块的消息体 — 是 404;只有
格式错误的请求才是 400。一条节点写下过、如今却读不回来的记录是
500,不是 400:过错在节点的磁盘,而告诉调用方它的
请求格式有误,等于是在诱使它停止重试一个自始至终都格式正确的请求。一个轮询程序绝不应该
需要读散文才能分辨“不存在”与“出错”,或者分辨自己的缺陷与节点的缺陷。
重组#
按高度查找只对规范链作答。一个在重组中落败的区块会保留它的区块头
而失去它的消息体,所以它的 id 仍然可以解析,回来时会被标记为 orphaned,并且仍然
带着回溯到分叉点的父链接 — 这使得按 id 查找成为唯一
对重组安全的路径。
可达性,以及它为什么比看起来更要紧#
Zycord 不做 NAT 穿透。一个在可达端口上带 --listen 运行的节点,是别人可以
从中引导启动的地方,而这类节点的占比正是整个不做穿透的决定所依赖的那个可证伪条件。
# periphery: nothing to configure
zycordd --testnet --dir ./testnet
# core: bind a port, and make sure it is actually reachable
zycordd --testnet --dir ./testnet \
--listen 0.0.0.0:9421 --advertise <public-address>:9421
--advertise 会回退到 --listen,所以不带 --advertise 的 --listen
0.0.0.0:9421 会公告出 0.0.0.0:9421,而
没有人能拨打它。一个错误的地址会通过对等节点交换传播开来,并让每一个尝试它的节点白白付出一次拨号。
如果你无法做端口转发,那就不加 --listen,做一个边缘节点。
检查它是否奏效了#
curl -s localhost:9420/network
{"enabled":true,"peers":8,"listening":true,"inbound":5,"outbound":3,"reachable":true}
节点起来几分钟之后,listening: true 却 inbound: 0
意味着那个端口实际上并不可达:进程已经绑定并在等待,而什么也没有到来。这在其他任何视图里
看起来都很健康,这正是这个端点存在的原因。
从文件读取引导地址#
zycordd --testnet --dir ./testnet --peers-file peers.txt
每行一个地址;# 注释和空行会被忽略,而这份列表会与
网络的内置种子合并。--peers 接受同样的内容,形式是一个
逗号分隔的参数,而 --no-seeds 会去掉内置的那些,同时保留另外两者。
钱包界面,经由 ssh 隧道#
节点不持有密钥,将来也不会。钱包是一个独立的进程,而在服务器上它是
zcd ui:
zcd ui --key wallet.json --no-open
那会打印出一个 URL,并在 127.0.0.1:9430 上提供服务。它绑定回环地址,
并拒绝其他任何地址,没有可以覆盖它的标志。那个监听器背后的进程持有
一把已解锁的私钥,并以 URL 中的一个 bearer 令牌进行认证,这对于一个只有本机能打开的
套接字是够用的,而对其他任何情形都不够用。从别处访问它是 ssh 的活,而 ssh 干这个比这里能做的更好:
# on your own machine
ssh -L 9430:127.0.0.1:9430 <host>
然后打开服务器打印出来的那个 URL。令牌是搭在 URL 的片段里的,
所以它永远不会抵达服务器、日志或 Referer 头 — 请把这个 URL 当作
它本来就是的那个秘密来对待。它每次运行都是新的,而 Ctrl-C 会擦掉密钥并停止服务。
转发的本地一端不一定要是 9430 端口:这个界面核对的是
Host 头里的主机名,而刻意不核对端口。要紧的是主机名
— 是它挡住了 DNS 重绑定,而那才是针对一个在回环地址上的服务器的真正攻击,
因为你浏览器里的任何页面都能向 127.0.0.1 发请求,而它唯一
伪造不了的,就是它被导航到的那个名字。
zcd ui 前面放反向代理上面关于节点 RPC 的建议,说的是一个不持有密钥、也不授予任何权限的 接口。而这一个持有密钥。把它暴露出去,没有哪个版本是 好主意,而那条隧道只需要一个标志的代价。
还有另外两种值得知道的形态:
zcd ui --locked启动时不在终端里索要口令;改由浏览器来问。当终端是共享的或者被记录时很有用。zcd ui --lock-after 5m会缩短空闲上锁的时长,上锁会就地擦掉密钥 — 种子是被覆写,而不只是被解除引用。
对等身份与匿名性#
节点在每次启动时都生成一把全新的 Ed25519 对等密钥,并且从不把它写到 磁盘上。它不是从任何东西派生出来的 — 不是你的钱包,不是某个种子,也不是 这台机器上的其他任何东西。重启就会轮换它。
如果你运行节点的方式对你自己而言事关重大,那么让你被去匿名化的不是 那把密钥。是那个地址。一个你跑在可追溯到你本人的基础设施上的引导节点, 是一个再多的密钥卫生也修不好的去匿名化通道。
数据目录#
data/
chain/ blocks, state, and the write-ahead log
peers.json the persisted peer store
对等节点存储被刻意地持久化:一个每次重启后都从一张白纸开始的节点, 等于在给攻击者一次全新的机会去把它填满。删除它是安全的,但那会把这一点丢掉。
节点能在任意时刻被杀掉而幸存下来 — 那正是预写日志的用途, 而它是通过在网络混沌下随机杀死节点来测试的。它不能 在自己的数据目录被人从底下动过手脚后幸存:启动时它会重新计算状态根, 若与存下来的那个不符便拒绝运行。
选择一个网络#
| 标志 | 网络 | 链 id | 引擎 |
|---|---|---|---|
| (无) | mainnet | 1 | randomx-v1 |
--testnet | 公共 testnet | 2 | randomx-v1 |
--devnet | 本地 devnet | 1337 | 开发用引擎 |
这些参数内嵌在二进制文件里,而不是从某个路径读取的。一个公共网络的每一位
参与者都必须携带同样的字节,否则他们就不在同一个网络上,而一个由 --params 传入的文件
是一个会漂移的文件。不同的网络就是不同的网络:两者会在握手时断开,
而一个持有错误的链的数据目录会在启动时被拒绝,而不是被悄悄混在一起。
修复受损的数据目录#
zycordd 带有一条修复路径,用于处理一个让节点拒绝启动的
数据目录。先把节点停掉 — zycordd repair --dir ./data 会取走目录锁,
只要还有节点持有它就会拒绝运行 — 然后在动任何东西之前,用 --dry-run
问清楚发生了什么。只有一类损坏可以就地修复;其余的补救办法是重新同步,而告诉你
手上是哪一类的,正是这次试运行。