没有私钥也能持有代币:Cosmos SDK 模块账户的权限位与黑名单 图 1
没有私钥也能持有代币:Cosmos SDK 模块账户的权限位与黑名单 · 图 1

在 Cosmos 系的链上查账户余额,有时会看到一串带名字却没有公钥的账户:fee 模块、mint 模块、bonded_tokens_pool 各占一个地址。它们没有私钥,不可能有人签名,却能持有、铸造和销毁代币。本文按 Cosmos SDK 官方 x/auth 与 x/bank 模块文档说明这种模块账户靠什么约束:一份写在创世里的权限表、一段只能由链上代码调用的接口,以及被有意拉黑的黑名单地址。

没有密钥的账户怎么动钱

权限表与托管账户关系的抽象示意

模块账户要解决的是一个历史遗留问题。官方 x/bank 文档交代了演进:早期实现里,模块要持有代币就得把进入的代币烧毁、自己在内部记账,发送时再往目标账户里铸造。这种”模块自记一套账”的做法与银行模块的全局余额表对不上,出问题很难核对。现在的做法是模块账户作为标准账户参与同一张余额表,能像普通账户一样收发,只是永远没有私钥——文档里它的示例公钥字段就是空值,动钱的方式只有链上模块代码调用 keeper 接口。

这样设计换来两个好处。所有代币的守恒律在同一处可验证:模块持有的余额和外部余额放在同一本账里,总供应量不变式可以机械检查。动钱的路径也收敛到接口:模块要铸币,必须调用带权限校验的方法,而调用方是谁由模块名字硬绑定,不能由外部交易直接冒充。

权限表:minter、burner 与保留位

权限写在账户结构里。官方 x/auth 文档给出的示例账户展示了三件信息:账户名字、地址,以及一份权限清单。样例中名字为 transfer 的账户带 minter 与 burner 两项,bonded_tokens_pool 与 not_bonded_tokens_pool 带 burner 与 staking 两项。这些字符串对应的就是链上代码调用铸币、销毁接口时的准入判断:没有 minter 的模块调铸币会被直接拒绝,权限检查发生在状态变更之前。

需要说明两点边界。权限清单是能力清单而非策略引擎:它决定”这个模块能不能铸/烧”,不决定”什么条件下铸多少”,后者完全写在各模块自身逻辑里。另外文档里保留了为质押等专用场景预留的权限位,具体取值以所用 SDK 版本的源码为准,不同版本可能增删,本文不列全表。

谁能动模块的钱:接口与黑名单

模块之间搬钱通过明确的接口:从模块到账户、从账户到模块、模块到模块,各自对应一个方法名,没有”给模块地址转账”这条通用路径。x/bank 文档特意解释了配套的黑名单机制——有些地址(通常是模块账户)不应该被外部直接转入资金,一旦绕过预期规则收到钱,守恒不变式可能被破坏,严重时会导致网络停摆;把这类地址加进黑名单后,异常转入会直接报错,而不是让脏数据落进状态里。

对开发者的实际含义是:自建模块需要托管资金时,正确做法是注册一个模块账户并按最小必要申请权限,而不是用一个普通外部账户当金库;后者既需要保管密钥,也无法接入权限校验与黑名单保护。

官方 x/gov 文档提供了一个现成用例:提案押金在投票期间由治理模块账户托管(escrow),提案通过或被否决但未遭否决票时原路退还,被三分之一以上否决或从未进入投票期则从该账户销毁。托管、退还、销毁三条路径全部走模块账户接口,外部无法直接动用这笔钱。

核验路径与不确定项

想核对某条 Cosmos 链的实际配置,可在创世文件的 auth 部分查看模块账户的名字、地址与权限清单,再用 x/bank 查询接口逐项核对余额,并把黑名单地址列出来比对。哪些地址在黑名单、哪些权限被授予,都由该链的 genesis 与各模块实现决定,属于链上配置,本文不代各链写死。

风险提示:本文仅为机制解释,不构成投资建议;开发与运维判断请参照所用 Cosmos SDK 版本源码与官方文档。