Cosmos SDK 围绕对象能力模型(ocap)构建,这是一种专为组合不受信任组件的系统设计的安全模型。 其威胁模型非常明确:一个繁荣的 Cosmos SDK 模块生态最终必然会包含有缺陷或恶意的模块。Ocap 会限制任意单个模块所能造成的破坏范围。

工作原理

该模型有两条规则:
  1. 只有在持有对另一个对象的引用时,一个对象才能向该对象发送消息。
  2. 一个对象只能通过接收消息的方式,获得对另一个对象的引用。
在实践中,这意味着:模块只能影响那些被明确授予访问权限的状态。如果你的模块没有被传入 bank keeper,那么你的模块就无法触碰余额,就这么简单。这里不存在可以直接访问的全局注册表。 这使安全分析变成局部问题。你无需阅读模块实现,只需查看它在连接时被授予了哪些引用,就可以审计它具备哪些能力。

指针与值

只传递模块实际需要的内容。如果你传递的是指针,就授予了写权限;如果你传递的是值,就授予了读权限。 下面这段代码违反了这一原则:将指针传给外部模块,会让它能够修改该账户:
account := &AppAccount{
    Address: pub.Address(),
    Coins:   sdk.Coins{sdk.NewInt64Coin("ATM", 100)},
}
sumValue := externalModule.ComputeSumValue(account) // can modify account
应改为传递副本:
sumValue := externalModule.ComputeSumValue(*account) // read-only

Keeper 接口

在 SDK 模块中,应用 ocap 最常见的位置是 keeper 边界。与其接收来自其他模块的具体 keeper 类型,不如定义一个精简接口,只包含你的模块实际会调用的方法。 例如,x/distribution 需要查询余额并发送代币,但它并不需要完整的 bank keeper。它定义了自己的接口:
// x/distribution/types/expected_keepers.go
type BankKeeper interface {
    GetAllBalances(ctx context.Context, addr sdk.AccAddress) sdk.Coins
    SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins
    SendCoinsFromModuleToModule(ctx context.Context, senderModule, recipientModule string, amt sdk.Coins) error
    SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
    SendCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
    BlockedAddr(addr sdk.AccAddress) bool
}
按惯例,这类接口放在 types/expected_keepers.go 中。这样做有两方面好处:一是接口明确记录了你的模块所需的跨模块访问能力;二是这种依赖在测试中很容易 mock。

Store 隔离

模块不会直接获得全局 multistore 的访问权限。相反,每个模块都会获得一个作用域限定在自身前缀下的 store.KVStoreService,因此它只能在该命名空间内读写。
type Keeper struct {
    storeService store.KVStoreService
    // ...
}
这意味着,一个模块 keeper 中的 bug 或恶意调用,无法读取或破坏其他模块的状态。这种作用域限制是在 store 层强制执行的,而不是依靠约定。

Authority

某些操作,例如更新参数、暂停模块、触发紧急操作,只应允许治理或其他受信任账户调用。SDK 通过在 keeper 中存储一个显式的 authority 字符串来处理这一点。
type Keeper struct {
    // the address capable of executing privileged messages,
    // typically the x/gov module account
    authority string
}
消息处理器在继续执行前,会先检查调用者是否与该地址一致:
if msg.Authority != k.authority {
    return nil, errors.Wrapf(sdkerrors.ErrUnauthorized, "expected %s, got %s", k.authority, msg.Authority)
}
authority 地址会在 app.go 中的连接阶段设置,且运行时不能更改。这就是将 ocap 应用于治理:特权能力本质上是一种引用,只有持有该引用的一方才能行使它。 可参考 simapp/app.go,了解在完整应用中 keeper 依赖和 authority 是如何连接的。 相关背景可参阅 Wikipedia 上关于 object-capability model 的文章。
The Cosmos SDK is built around the object-capability model (ocap) — a security model designed for systems that compose untrusted components. The threat model is explicit: a thriving ecosystem of Cosmos SDK modules will eventually include faulty or malicious ones. Ocap limits the damage any single module can do.

How it works

The model has two rules:
  1. An object can send a message to another object only if it holds a reference to it.
  2. An object can obtain a reference to another object only by receiving it through a message.
In practice: a module can only affect the state it has been explicitly handed access to. If the bank keeper was not passed to your module, your module cannot touch balances — full stop. There is no global registry to reach into. This makes security analysis local. You can audit what a module can do by looking at what references it was given at wiring time, without reading its implementation.

Pointer vs. value

Only pass what a module needs. If you pass a pointer, you grant write access. If you pass a value, you grant read access. This code violates the principle — passing a pointer to an external module grants it the ability to mutate the account:
account := &AppAccount{
    Address: pub.Address(),
    Coins:   sdk.Coins{sdk.NewInt64Coin("ATM", 100)},
}
sumValue := externalModule.ComputeSumValue(account) // can modify account
Pass a copy instead:
sumValue := externalModule.ComputeSumValue(*account) // read-only

Keeper interfaces

The most common place to apply ocap in SDK modules is at keeper boundaries. Instead of accepting a concrete keeper type from another module, define a narrow interface containing only the methods your module actually calls. For example, x/distribution needs to query balances and send coins, but it does not need the full bank keeper. It defines its own interface:
// x/distribution/types/expected_keepers.go
type BankKeeper interface {
    GetAllBalances(ctx context.Context, addr sdk.AccAddress) sdk.Coins
    SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins
    SendCoinsFromModuleToModule(ctx context.Context, senderModule, recipientModule string, amt sdk.Coins) error
    SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
    SendCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
    BlockedAddr(addr sdk.AccAddress) bool
}
By convention these live in types/expected_keepers.go. The benefit is twofold: the interface documents exactly what cross-module access your module requires, and it makes the dependency easy to mock in tests.

Store isolation

Modules do not receive direct access to the global multistore. Instead, each module gets a store.KVStoreService scoped to its own prefix — it can only read and write within that namespace.
type Keeper struct {
    storeService store.KVStoreService
    // ...
}
This means a bug or malicious call in one module’s keeper cannot read or corrupt another module’s state. The scoping is enforced at the store layer, not by convention.

Authority

Some operations — updating parameters, pausing a module, triggering emergency actions — should only be callable by governance or another trusted account. The SDK handles this with an explicit authority string stored in the keeper.
type Keeper struct {
    // the address capable of executing privileged messages,
    // typically the x/gov module account
    authority string
}
Message handlers check the caller against this address before proceeding:
if msg.Authority != k.authority {
    return nil, errors.Wrapf(sdkerrors.ErrUnauthorized, "expected %s, got %s", k.authority, msg.Authority)
}
The authority address is set at wiring time in app.go and cannot be changed at runtime. This is ocap applied to governance: privileged capability is a reference, and only the holder of that reference can exercise it. See simapp/app.go for how keeper dependencies and authorities are wired in a complete application. For background, see the Wikipedia article on object-capability model.