ZYCORD 文档
简体中文
Zycord文档钱包

钱包

密钥、地址和发送 — 外加这个协议上每一个钱包都必须满足的行为契约。每一条规则都描述了一种协议所允许的亏钱方式,因而也是钱包必须加以阻止的方式。

这些命令#

zcd wallet new     --out KEYFILE                        # create an encrypted key file
zcd wallet address --key KEYFILE                        # show an address
zcd wallet balance --key KEYFILE                        # ask a node for a balance
zcd wallet send    --key KEYFILE --to ADDR --amount N
zcd wallet sweep   --key KEYFILE --to ADDR --one-shot
zcd wallet retire  --key KEYFILE [--addr ADDR]
zcd ui             --key KEYFILE                        # the same wallet in a browser

图形钱包不是第二个实现。zcd wallet、zcd ui 和桌面应用是同一个 wallet/session 之上的三个界面:每一个都在那里构建自己的凭证,而各项规则检查 就在那次调用内部运行。因此图形钱包在结构上就不可能比 CLI 更宽松 — 不是因为写它的人足够小心,而是因为根本没有第二条代码 路径能让它更宽松。

两种地址#

版本种类用于可重复使用?
0x01一次性一笔预期中的付款。每收到一笔付款就派生一个全新的。不可以。一次借记就会永久烧毁它。
0x02持久商户、捐赠地址、热钱包、挖矿收益支付。可以,永久有效。永远不会进入已花费注册表。

一次性与持久之别,是一项以地址中的一个位呈现出来的钱包策略,而不是两套 账本。钱包对每收到的一笔付款默认使用 0x01,因为正是这一点让一笔 付款与下一笔无法被关联起来;想要一个账户的人则使用 0x02。

八条规则#

下面每一条规则都描述了一种协议 允许的亏钱或亏手续费的方式 — 这是刻意的,因为另一种做法 更糟 — 因而也是钱包必须加以阻止的方式。参考钱包实现了 它们。它不只是把它们写进文档。

规则 1 — 整格归集#

从一个 0x01 地址花费时,请把全部余额都转走 — 转给收款方,以及转给一个你自己控制的找零地址。该凭证会携带那个地址的一个 MARK_SPENT,而它一旦应用,该地址之下的每一次读取和写入 都将永久失败。不会有第二笔交易。

链现在对此有所缓和:一个被烧毁的地址仍然持有的任何东西,都会在提交时转入该凭证 自己的 RefundTo 存储单元 — 既包括该地址的原生余额,也包括该凭证自己 点名的每一个存储单元。所以扫得不干净不再会毁掉那些零头;它会把它们送到 你的找零地址。

随之而来的义务

一份凭证只有一个 RefundTo,却可能烧毁分属若干签名者的一次性地址, 所以你那个存储单元下的余量可能被送到他们那里。 绝不要联署一份既烧毁你的某个一次性地址、又退款到一个你并不控制的地址的凭证。

这条规则救不了什么:一笔存在于该凭证从未提及的某种资产中的余额。 某个地址之下有哪些资产,无法从一个槽位推导出来,所以要触及一个未被点名的资产, 就意味着要在唯一不能并行的那个阶段扫描整张存储单元表。请在烧毁该地址的那份凭证中 点名每一种资产。

规则 2 — RefundTo 必须是一个你仍然能用的地址#

它必须点名一个你所控制的持久地址,或者一个这份凭证不会烧毁的全新 一次性地址。把余款结算进一个已被烧毁的存储单元会让它搁浅:折叠会烧掉它,而不是把它写进 一个谁也读不了的存储单元,并且会把它报告为 refund_burned 而非 refunded,好让一个在对账的钱包能够 分辨出来。

规则 3 — 一个地址,一笔预期付款#

两项义务,双方各一项:

  • 收款方。对你预期的每一笔付款派生一个全新的 0x01 地址。绝不要把同一个公布两次。不要归集或退役一个你在最近 ttl_max 个区块内披露过的地址,除非你接受一笔在途发往它的付款会被跳过并向其发送方计费。
  • 发送方。拒绝向一个你能看出已被贷记过或已被花费过的一次性地址付款。如果一位收款方第二次把某个地址交给你,请把它当成一个错误,而不是一种便利。

对于任何会被付款不止一次的东西 — 一位商户、一个捐赠地址、一个挖矿 收益地址 — 请使用持久(0x02)地址。这是唯一一条 把这份风险敞口消除掉、而不只是收窄的规则。

规则 4 — 依赖链:要么确认,要么接受风险#

对每一份依赖于前一份的凭证,请把 Seq 递增。在同一个 区块内,折叠按 Seq 顺序提交某个签名者的各份凭证,所以同一区块内的依赖链 是安全的。跨区块则不然:只有在 Seq = n 确认之后再广播 Seq = n+1, 否则就要接受那份依赖者可能单独提交并被跳过。这不是协议缺陷 — 签名者通过签名就接受了过时风险。

规则 5 — 上限设得慷慨,优先费设得诚实#

每个市场都收取两个价格。上限是一道偿付能力边界:一旦基础 费越过它,该凭证就无法被打包,必须重新签名。优先费 才是矿工实际拿到的钱。

  • 把上限调高在费用上分文不费 — 这道安全缓冲是免费的。
  • 但保证金会预留 gas × max,所以上限决定了一次折叠步骤会锁住多少余额。请按实际可用的余额来设定它的大小。
  • 一份 TTL 较长的凭证比一份 TTL 较短的需要更多余量,因为基础费有更多的区块可供其变动。

小额余额的逃生舱口是缩小那个窗口,而不是缩小那份 安全:用较短的 TTL 签名,到期后重新签名,以延迟换取更少的资金锁定。错误的 做法是保留长 TTL 而调小上限,那会让这份凭证恰恰在市场发生变动时变得可能搁浅 — 而那正是这道缓冲当初存在的理由。

规则 6 — 按规范顺序排列各项移动#

协议不对一次转账的各项移动强加顺序,所以在没有规范顺序的情况下,同一笔逻辑付款的 一次重试会产出一个不同的 id,钱包也就无法分辨“已经发过”和 “发了两次”。wallet.Transfer 按资产、来源、去向,然后按金额排序,这样一次 重试就能重现出同一个 id。

一次重试不必重现出同一个签名:id 的原像排除了 签名列表,所以一次以新 nonce 重新签名的重试仍是同一个 id,而网络会把它作为重复项拒绝掉。 幂等性是协议提供的一项性质,面向每一个 钱包,而不只是面向那些小心的钱包。

规则 7 — 绝不把种子交出去#

种子就是密钥。任何读到它的人,都拥有它那两个地址所持有的一切 — 一次性的和持久的,它们在链上互不相关,却派生自同一把密钥。

密钥文件始终是加密的:对口令使用 Argon2id,对种子使用 AES-256-GCM, 两者的参数都存放在文件里,好让一个被这个二进制文件挡在门外的用户 能用任何一门语言的标准库把它恢复出来。没有任何标志可以把它写成未加密的。 口令是从终端无回显地读入的,绝不从某个标志读入 — 一个 出现在命令行上的口令,就在 shell 历史里,也在进程表里。

写入同时保有两项性质,而两者说的是同一句话 — 丢掉其中之一 就是丢钱:

  • 绝不无声覆写。目标位置上已有的密钥文件会原封不动,而写入会失败。
  • 绝不留下一个写了一半的。种子先写到同一目录下的一个临时文件里,在那里被 fsync,然后才在一次文件系统操作中以其最终名字发布出来。

在 CLI 所测试过的每一种文件系统上,这两点都成立,包括 FAT32 和 exFAT — 而它们正是一个冷存储 USB 备份最可能用到的格式。

放在 FAT 格式化的 U 盘上的加密密钥文件,除了它的口令之外没有任何东西在保护它

FAT32 和 exFAT 没有 Unix 权限位,所以密钥文件被创建时所带的那个 0600 在它们上面存活不下来 — 由挂载选项说了算。在 Linux 上,默认的 fmask 通常会让它对所有人可读;macOS 以 noowners 挂载这类卷, 它会报告 0700 却什么也不强制执行。请据此选择口令。 它们也没有日志,所以如果一次发布在 FAT 上被中断并报告失败, 请在重新运行之前先检查目标位置。

规则 8 — 拒绝网络将要拒绝的东西#

一个为网络只能丢弃的东西报告成功的钱包,等于把这次失败挪到了 用户永远不会去看的地方。这有两种形态,都是在实际使用中发现的:

  • 没有哪个对等方能解码的字节。编解码器本身就是一个权威,它有一些验证器并未重述的规则。构建器现在直接断言这项性质:无论它产出什么,解码器都能接受。
  • 一笔折叠只能跳过的转账。一笔超出来源余额的转账并不违反任何规则,所以每个节点都会接纳它 — 而且也没有什么会去拒绝一个仍然把它打包进去的生产者,到那时折叠会以 skip_fee 把它了结,这笔钱从保证金里烧掉,谁也拿不到。糟糕的情况不是“没有效果”,而是“费用被烧掉了,而价值根本没动”。

zcd wallet send --force 绕过的只是那一次拒绝,别的一概不绕, 因为一笔预期会在 TTL 窗口内落定的保证金,会让同一份凭证得以应用,而 钱包看不到未来。它无法把一份无效的凭证变得有效;它只能提交一份 可能被跳过的有效凭证 — 而一次跳过并不是免费的。

信任一个不是你自己运行的节点#

zcd 和钱包都不是全节点。它们相信节点告诉它们的东西,而一个 通过 RPC 与一个自己并不验证的节点对话的 CLI,就必须对那个节点的答复 信任某些东西 — 这不是一个待修的缺陷,这就是钱包的本质。有三项缓解措施, 各自针对一种具体的谎言:

缓解措施它能抓住什么
--devnet / --testnet / --params由运营者断言他们打算为哪个网络签名,而被问到的每一个节点都会与那项断言相核对。网络身份绝不会仅从一个节点那里取得。这一点同样适用于 balance,而不只适用于那些会签名的命令。
--confirm-rpc NODE指定第二个独立的节点。每个地址的余额以及已花费标志都必须由两者报告得完全一致,之后才会继续。一个 chain_id 不一致的节点会被拒绝,而一个指向 --rpc 已经指定的那个端点的 --confirm-rpc 会被直接拒绝 — 一个与自己交叉核对的节点,总是与自己一致。
打字确认在提交之前,zcd wallet sweep 会打印出确切的数字,并要求你输入 sweep。--yes 只跳过这个提示,绝不跳过它上面的那些检查。

钱包实现检查清单#

如果你正在为这个协议编写一个钱包,这就是那份契约:

  • 从 0x01 花费时要转走全部余额,并把保证金预留计入其中 — 即使在完全没有任何移动的程序上也是如此。
  • RETIRE 会拒绝一个仍然持有余额的目标。
  • 节点对某笔余额的报告不被当作不容置疑:网络身份由运营者断言,第二个来源可供交叉核对,而在一次不可逆的归集之前会先确认确切的数字。
  • RefundTo 在签名之前会被验证为持久地址或全新地址。
  • 收款时为每一笔预期的付款派生一个全新地址;发送时拒绝一个已被贷记过或已被花费过的一次性地址。
  • 商户地址和收益地址默认为 0x02。
  • 每有一份依赖凭证就把 Seq 递增,而钱包在广播下一份之前会等待确认 — 否则就要明说自己没有等。
  • 费用上限是按可用余额和 TTL 定出来的,而不是硬编码的。
  • 各项移动按规范顺序排序,这样一次重试就能重现出同一个凭证 id。
  • 密钥文件是加密的,写入是持久的且不会无声覆写,而口令绝不会出现在某个标志里。
  • 绝不签署任何其自身编码无法被解码回来的东西,也绝不提交任何来源余额覆盖不了的东西 — 而任何覆盖开关都要被点名、范围狭窄,并在预览中被报告出来。