Cosmos SDK ADR-033 解读:基于 Protobuf 的模块间通信(Inter-Module Communication)设计与安全模型
区块链【免费下载链接】cosmos-sdkFramework for building performant, customizable blockchains with native interoperability项目地址https://gitcode.com/gh_mirrors/co/cosmos-sdk点击查看免费下载导读本文深入解读 Cosmos SDK 架构决策记录 ADR-033Protobuf-based Inter-Module Communication状态为 Proposed 提案介绍如何基于 ADR-021Protobuf 查询编码与 ADR-031Msg Service已生成的Query/MsggRPC 服务定义构建一套带权限控制的模块间通信Inter-Module CommunicationIMC框架。读完本文你将掌握现有 Keeper 范式的权限缺陷、ModuleKey/ModuleID的对象能力OCAP设计、Invoker闭包的安全原理、Configurator的服务注册与依赖声明RequireServer、内部方法InternalServer的隔离思路以及该提案对 Cosmos SDK v1.0 模块生态稳定性的意义。背景为什么需要模块间通信框架威胁模型模块生态中可能存在恶意模块在 对象能力模型文档 中Cosmos SDK 明确了自身的威胁模型我们假设一个蓬勃发展的 Cosmos SDK 模块生态系统中会存在有缺陷或恶意的模块。OCAP 模型的两条核心规则是对象 A 只有持有指向 B 的引用才能向 B 发消息对象 A 只有通过接收包含引用的消息才能获得对 C 的引用。而 ADR-033 指出当前 Cosmos SDK 恰恰缺少一个真正实现的、可组合的模块对象能力系统这阻碍了模块生态的繁荣。提案归纳出两大原因缺少稳定的 v1.0 SDK 基线模块接口在版本之间频繁、有时甚至是剧烈地变化无法为模块开发者提供稳定基础缺少真正实现的对象能力/面向对象封装现有 Keeper 接口约束过弱导致重构不可避免。x/bank案例分析Keeper 权限过于宽泛ADR-033 以x/bankkeeper 为例说明了现状任何引用了它的模块几乎都拥有无限制的访问权。例如SetBalance方法允许调用方把任意账户余额设置为任意值甚至可以绕过正确的 supply 追踪。在当前仓库的 x/bank/keeper/keeper.go 中仍能看到这类能力的痕迹比如UncheckedSetBalance方法其注释明确写着设置某账户的 coin 余额且在BaseKeeper的公开接口Keeper中直接暴露。后续曾尝试用模块级 mint/staking/burning 权限实现某种 OCAP这些权限允许模块针对自己的账户进行铸币、销毁或委托以[]string数组形式存储在状态中的ModuleAccount类型上见 x/bank/keeper/keeper.go 中MintCoins对acc.HasPermission(authtypes.Minter)的检查。但 ADR-033 指出这种方案存在根本缺陷没有唯一的对象能力令牌只靠一个普通字符串控制访问。例如x/upgrade模块只需调用MintCoins(staking)就能替x/staking模块铸币所有能访问 keeper 方法的模块也都能访问SetBalance这使任何 OCAP 尝试失效连基本的面向对象封装都被打破。这正是该提案要解决的痛点用强类型、可鉴权的消息传递取代谁拿到 keeper 引用谁就能为所欲为的现状。决策以 Protobuf 服务为基础的模块间通信框架基于 ADR-021 与 ADR-031ADR-033 引入模块间通信框架用于安全的模块授权与 OCAP。当实现完成后它可作为在模块之间传递 keeper这一现有范式的替代方案被设计为 Cosmos SDK v1.0 的基础之一。需要注意的一个重要设计原则是该功能是启用式的模块可自行决定是否采用。迁移现有模块到新范式的提案需要单独讨论可能作为本 ADR 的修正案。新的 Keeper 范式ADR-021 引入了用 Protobuf service 定义 querier 的机制ADR-031 引入了用 Protobuf service 定义Msg的机制。Protobuf service 定义会生成一对 Go 接口——服务的客户端侧和服务端侧——以及若干辅助代码。以 bank 模块cosmos.bank.Msg/Send为例生成的接口大致如下package bank type MsgClient interface { Send(context.Context, *MsgSend, opts ...grpc.CallOption) (*MsgSendResponse, error) } type MsgServer interface { Send(context.Context, *MsgSend) (*MsgSendResponse, error) }ADR-021 与 ADR-031 说明了模块如何实现生成的QueryServer/MsgServer接口以取代旧式 querier 与Msghandler。本 ADR 则进一步说明模块如何利用生成的QueryClient/MsgClient接口向其他模块发起查询与发送Msg并提议以此取代现有 Keeper 范式。需要明确的是本 ADR并不要求创建新的 Protobuf 定义或服务——它复用的正是客户端已经用于模块间通信的同一套 proto 服务接口。在当前仓库中这类服务定义随处可见例如 proto/cosmos/bank/v1beta1/tx.proto 中的service Msg带有option (cosmos.msg.v1.service) true标记以及MsgSend消息通过option (cosmos.msg.v1.signer) from_address声明签名者字段见 proto/cosmos/bank/v1beta1/tx.proto。采用QueryClient/MsgClient方式相比直接暴露 keeper 有三大关键优势Protobuf 类型可用 buf 检查破坏性变更由于 Protobuf 的设计方式这能在允许向前演进的同时提供强向后兼容保证客户端/服务端接口分离允许在两者之间插入权限检查代码判断某模块是否被授权向另一模块发送指定Msg从而提供真正的对象能力系统模块间通信路由器成为回滚事务的天然位置任何模块到模块调用中的失败都会导致整个事务失败从而保证操作的原子性这正是当时的一个已知问题见 issue #8030。该机制还有附加收益通过代码生成减少样板代码允许用其他语言实现模块——无论是通过 CosmWasm 之类的 VM还是通过 gRPC 子进程。模块间通信的调用方式要使用 Protobuf 编译器生成的Client需要一个grpc.ClientConn接口实现。为此 ADR 引入新类型ModuleKey它实现了grpc.ClientConn接口。ModuleKey可被理解为模块账户对应的私钥其认证通过一个特殊的Invoker()函数提供下文详述。在常规流程中区块链用户外部客户端用自己的账户私钥签署包含Msg的交易Msg中他们被列为签名者每条消息通过Msg.GetSigners指定所需签名者认证检查由AnteHandler执行。本 ADR 扩展了这一过程允许模块出现在Msg.GetSigners中。当模块要触发另一模块中某个Msg的执行时它的ModuleKey充当发送者通过下文的ClientConn接口并被设为唯一签名者。需要强调这里不使用任何密码学签名。例如模块 A 可用自己的A.ModuleKey为/cosmos.bank.Msg/Send创建MsgSend对象MsgSend的校验会确保from账户此处即A.ModuleKey就是签名者。以下是一个假设的foo模块与x/bank交互的完整示例ADR 原文代码含原文档中的拼写保留以便对照package foo type FooMsgServer { // ... bankQuery bank.QueryClient bankMsg bank.MsgClient } func NewFooMsgServer(moduleKey RootModuleKey, ...) FooMsgServer { // ... return FooMsgServer { // ... modouleKey: moduleKey, bankQuery: bank.NewQueryClient(moduleKey), bankMsg: bank.NewMsgClient(moduleKey), } } func (foo *FooMsgServer) Bar(ctx context.Context, req *MsgBarRequest) (*MsgBarResponse, error) { balance, err : foo.bankQuery.Balance(bank.QueryBalanceRequest{Address: foo.moduleKey.Address(), Denom: foo}) ... res, err : foo.bankMsg.Send(ctx, bank.MsgSendRequest{FromAddress: fooMsgServer.moduleKey.Address(), ...}) ... }注意示例中bank.NewQueryClient(moduleKey)/bank.NewMsgClient(moduleKey)直接把ModuleKey当作grpc.ClientConn传入这正是模块作为发送者的体现——发出的查询与Msg都以模块自己的地址为来源并被鉴权。该设计还可扩展覆盖更细粒度的权限场景例如按 denom 前缀限制某些模块的铸币权ADR 中引用了 issue #7459 的讨论。ModuleKey与ModuleID模块的对象能力句柄概念私钥与公钥的对应关系ModuleKey可视为模块账户的私钥ModuleID是对应的公钥。依据 ADR-028模块可以拥有一个根模块账户root module account以及任意数量的子账户sub-accounts / derived accounts后者可用于不同资金池如 staking 池或托管账户如 group 账户。模块子账户类似于派生密钥有一个根密钥然后是某种派生路径。ModuleID是一个简单结构体包含模块名和可选的派生路径其地址基于 ADR-028 的AddressHash方法生成type ModuleID struct { ModuleName string Path []byte } func (key ModuleID) Address() []byte { return AddressHash(key.ModuleName, key.Path) }ADR-028 中模块地址的具体生成方式可参见 docs/architecture/adr-028-public-key-addresses.md模块账户地址类型为module通过address.Module(moduleName, derivationKeys...)生成当有派生密钥时使用Hash(module, []byte(moduleName) 0 key)再用Derive递归派生子-子账户无派生密钥时回退到 legacy 实现authtypes.NewModuleAddress。这解释了ModuleID中ModuleName Path与地址的对应关系。Invoker安全的访问入口除了生成ModuleID和地址ModuleKey还包含一个特殊的Invoker函数——这是安全模块间访问的关键。Invoker创建一个InvokeFn闭包用作grpc.ClientConn接口中的Invoke方法在底层它能将消息路由到合适的Msg和Queryhandler并对Msg执行相应安全检查。ADR 特别指出Go 不支持对函数闭包捕获变量进行反射因此恶意模块要绕过ModuleKey安全机制只能直接操纵内存——这使模块间访问比 keeper 更安全keeper 的私有成员变量可通过反射被篡改。两种ModuleKey类型为RootModuleKey与DerivedModuleKeytype Invoker func(callInfo CallInfo) func(ctx context.Context, request, response interface{}, opts ...interface{}) error type CallInfo { Method string Caller ModuleID } type RootModuleKey struct { moduleName string invoker Invoker } func (rm RootModuleKey) Derive(path []byte) DerivedModuleKey { /* ... */} type DerivedModuleKey struct { moduleName string path []byte invoker Invoker }模块可通过RootModuleKey.Derive(path []byte)方法获得DerivedModuleKey然后用该 key 从子账户认证Msg。示例如下package foo func (fooMsgServer *MsgServer) Bar(ctx context.Context, req *MsgBar) (*MsgBarResponse, error) { derivedKey : fooMsgServer.moduleKey.Derive(req.SomePath) bankMsgClient : bank.NewMsgClient(derivedKey) res, err : bankMsgClient.Balance(ctx, bank.MsgSend{FromAddress: derivedKey.Address(), ...}) ... }这样模块可以对根账户和任意数量的子账户获得带权限的访问并从这些账户发送已认证的Msg。Invoker的callInfo.Caller参数在底层用于区分不同模块账户无论哪种情况Invoker返回的函数只允许来自根模块账户或派生模块账户的Msg通过。Invoker本身根据传入的CallInfo返回一个函数闭包——这允许未来的客户端实现按方法类型缓存 invoke 函数避免哈希表查找的开销从而把该模块间通信方式的性能开销降到检查权限所需的最低限度。再次强调闭包只允许访问被授权的调用无论调用者如何冒用名称都接触不到其他任何东西。RootModuleKey实现grpc.ClientConn.Invoke的粗略示意如下func (key RootModuleKey) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...grpc.CallOption) error { f : key.invoker(CallInfo {Method: method, Caller: ModuleID {ModuleName: key.moduleName}}) return f(ctx, args, reply) }当前仓库中的演进形态ADR-033 的ModuleKey概念在仓库中已有演进形态core/intermodule包定义了模块间客户端的当前接口见 core/intermodule/client.gotype Client interface { grpc.ClientConnInterface // InvokerByMethod resolves an invoker for the provided method or returns an error. InvokerByMethod(method string) (Invoker, error) // InvokerByRequest resolves an invoker for the provided request type or returns an error. // This only works for Msgs as they are routed based on type name in transactions already. // For queries use InvokerByMethod instead. InvokerByRequest(request any) (Invoker, error) // DerivedClient returns an inter-module client for the ADR-028 derived // module address for the provided key. DerivedClient(key []byte) Client // Address is the ADR-028 address of this client against which messages will be authenticated. Address() []byte }其中Invoker被定义为已解析到具体方法路由的调用器见 core/intermodule/client.gotype Invoker func(ctx context.Context, request any, opts ...grpc.CallOption) (res any, err error)可以清晰地看到 ADR-033 思想在实现层面的落点grpc.ClientConnInterface对应 ADR 中的ModuleKey implements grpc.ClientConn、InvokerByMethod/InvokerByRequest对应Invoker按方法解析、DerivedClient对应Derive派生子账户 key、Address()对应 ADR-028 地址。注释中明确标注了 as specified in ADR-033。AppModule的装配与依赖声明ADR-031 引入了AppModule.RegisterService(Configurator)方法。为支持模块间通信ADR-033 扩展Configurator接口传入ModuleKey并允许模块用RequireServer()声明对其他模块的依赖type Configurator interface { MsgServer() grpc.Server QueryServer() grpc.Server ModuleKey() ModuleKey RequireServer(msgServer interface{}) }ModuleKey在RegisterService方法本身中传递给模块使RegisterServices成为配置模块服务的单一入口这有意带来另一个副作用大幅减少app.go中的样板代码。当前阶段ModuleKey将基于AppModuleBasic.Name()创建未来可能引入更灵活的系统。ModuleManager将在幕后处理模块账户的创建。由于模块之间不再直接相互访问模块可能出现未满足的依赖。为确保模块依赖在启动时得到解析需要Configurator.RequireServer方法ModuleManager会在应用启动前确认所有用RequireServer声明的依赖都能被解析。示例模块foo声明对x/bank的依赖package foo func (am AppModule) RegisterServices(cfg Configurator) { cfg.RequireServer((*bank.QueryServer)(nil)) cfg.RequireServer((*bank.MsgServer)(nil)) }与当前Configurator实现对照当前仓库中Configurator的实现在 types/module/configurator.go其注释明确写着它被设计为最终支持模块对象能力隔离如 https://github.com/cosmos/cosmos-sdk/issues/7093 所述——这正是 ADR-033 所提能力。当前接口提供MsgServer()、QueryServer()与RegisterMigration(...)其RegisterService实现会根据 Protobuf service 描述上是否存在cosmos.msg.v1扩展option (cosmos.msg.v1.service) true来决定把服务注册到 Msg 路由器还是 Query 路由器见 types/module/configurator.go。对应的路由器实现分别是 baseapp/msg_service_router.goMsgServiceRouter按消息类型 URL 路由到 handler并支持电路断路器装饰与 baseapp/grpcrouter.goGRPCQueryRouter将 ABCI Query 请求路由到 gRPC handler。它们是 ADR-021/031 服务注册的落地载体也是未来 IMC 路由器在现有架构中的对应位置。安全考量除了检查ModuleKey权限底层路由器基础设施还需要采取几项额外安全预防措施。递归与重入Recursion and Re-entry递归或重入的方法调用构成潜在安全威胁如果模块 A 调用模块 B而模块 B 在同一调用中又调用模块 A就会出问题。路由器系统应对此的一个基本方式是维护一个调用栈call stack防止同一模块在调用栈中被引用超过一次从而杜绝重入路由器中可以用map[string]interface{}表执行这一安全检查。查询QueriesCosmos SDK 中的查询通常是**无权限un-permissioned**的因此在采取基本预防措施的前提下允许一个模块查询另一个模块不会带来重大安全威胁。路由器系统需要采取的基本预防措施是确保传给查询方法的sdk.Context不允许写入 store。目前可以像BaseApp查询那样用CacheMultiStore实现对应实现可参考 store/cachemulti 与 baseapp 中的查询执行路径。内部方法Internal Methods模块专属但不对外暴露很多场景下我们希望模块能调用其他模块中不对外部客户端暴露的方法。为此ADR-033 在Configurator中增加InternalServer方法type Configurator interface { MsgServer() grpc.Server QueryServer() grpc.Server InternalServer() grpc.Server }经典例子是x/slashing的Slash必须调用x/staking的Slash但不想把x/staking的Slash暴露给最终用户和客户端。这一场景在当前仓库中依然存在x/slashingkeeper 的SlashWithInfractionReason直接委托给k.sk.SlashWithInfractionReason(...)见 x/slashing/keeper/keeper.go而x/staking的Slash实现在 x/staking/keeper/slash.go。这正好说明内部调用需要但外部不可见的需求真实存在。ADR 规定内部 Protobuf 服务将定义在对应模块 proto 包的internal.proto文件中注册到InternalServer的服务可被其他模块调用但外部客户端无法调用。内部专用方法的替代方案是 hooks/plugins 机制ADR 中引用了 issue #7459 的讨论。hooks/plugin 系统的更详细评估将在本 ADR 的后续跟进或单独 ADR 中展开。当前仓库中x/slashing的 Hooks 与StakingHooks即属于这类基于钩子的机制。授权Authorization默认情况下模块间路由器要求消息由GetSigners返回的第一个签名者发送。同时模块间路由器应接受授权中间件例如 ADR-030 提供的 authz 模块——这类中间件允许账户授权特定的模块账户代表自己执行操作。授权中间件还需要考虑授予某些模块对其他模块的admin级权限的需求这将在单独的 ADR 或本 ADR 的更新中处理。未来工作Future Work其他未来改进可能包括定制代码生成简化接口例如生成使用sdk.Context而非context.Context的代码优化模块间调用——例如在首次调用后缓存已解析的方法将StoreKey与ModuleKey合并为单一接口使模块拥有单一的 OCAP 句柄让模块间通信更高效的代码生成将ModuleKey的创建与AppModuleBasic.Name()解耦使应用可以覆盖根模块账户名模块间 hooks 与 plugins。备选方案对比MsgServices vsx/capabilityx/capability模块确实提供了可在 Cosmos SDK 中任何模块使用的对象能力实现甚至可用于模块间 OCAP见 issue #5931 的讨论。本 ADR 方案的优势主要在于与 Cosmos SDK 其他部分的集成方式Protobuf可借助接口的代码生成获得更好的开发者体验模块接口有版本化可用 buf 检查破坏性变更ADR-028 的子模块账户通用的Msg传递范式以及GetSigners指定签名者的方式。另外本方案是对 keepers 的完整替代可应用于所有模块间通信而x/capability方案#5931需要逐方法应用。影响评估Consequences向后兼容性本 ADR 旨在为模块间实现更强的长期兼容性铺路。短期内它很可能会导致某些过于宽松的Keeper接口被破坏或被Keeper接口的替代品整体取代。正面影响成为 keeper 的替代方案更容易形成稳定的模块间接口提供真正的模块间 OCAP改进模块开发者体验DevX——多位参与者曾在 2020 年 12 月 3 日的架构评审电话会议上提及为大幅简化的app.go奠定基础路由器可配置为对模块到模块调用强制实现原子事务。负面影响采用该方案的模块需要进行大量重构。中立影响原文未列出具体项。参考资料ADR-021: Protocol Buffer Query EncodingADR-031: Msg ServiceADR-028: Public Key AddressesADR-030: Authz Module草案相关讨论对象能力模型Object-Capability Model模块间客户端当前接口实现core/intermodulex/bank Keeper 接口权限现状Configurator 接口实现types/module赞分享区块链【免费下载链接】cosmos-sdkFramework for building performant, customizable blockchains with native interoperability项目地址https://gitcode.com/gh_mirrors/co/cosmos-sdk点击查看免费下载相关推荐Deeplearning4j 的 Java 9 模块化支持基于 moditect 的 module-info 集成实践ADR 0016 解读Deeplearning4j 的 Java 9 模块化支持基于 moditect 的 module info 集成实践ADR 0016 解读 本文基于仓深度学习人工智能机器学习分布式训练Cosmos SDK 共识安全审计9 大核心漏洞模式与检测实战基于 cosmos-vulnerability-scannerCosmos SDK 共识安全审计9 大核心漏洞模式与检测实战基于 cosmos vulnerability scanner 本指南系统梳理 cosmosAI 技能AI 插件应用安全网络安全AI 评测SIGN_MODE_TEXTUAL Value Renderers 完全指南Cosmos SDK ADR-050 Annex 1 解读SIGN_MODE_TEXTUAL Value Renderers 完全指南Cosmos SDK ADR 050 Annex 1 解读 导读 SIGN_MOD区块链创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考