变更日志

  • 2019 年 12 月 12 日:初始版本
  • 2020 年 4 月 2 日:内存存储修订

背景

完整实现 IBC 规范 需要能够在运行时(即事务执行期间)创建并认证对象能力键, 如 ICS 5 中所述。在 IBC 规范中,会为每个新初始化的 端口和通道创建能力键,并用于认证未来对该端口或通道的使用。由于通道以及可能的端口都可以在事务执行期间初始化,因此状态机必须能够在此时创建 对象能力键。 目前,Cosmos SDK 还不具备这一能力。对象能力键当前是 StoreKey 结构体的指针(内存地址),这些结构体在 app.go 中应用初始化时创建(示例), 并作为固定参数传递给 Keeper(示例)。Keeper 无法在事务执行期间创建或存储能力键,尽管它们可以调用 NewKVStoreKey 并获取 返回结构体的内存地址,但如果将其存储在 Merklised store 中,会导致共识故障,因为每台机器上的内存地址都不同(这是有意设计的;如果不是这样,这些键将是可预测的,也就无法充当对象能力)。 Keeper 需要一种方式来维护私有的 store key 映射,并且该映射可以在事务执行期间被修改;同时还需要一种合适的机制,在应用启动或重启时重新生成该映射中唯一的内存地址(能力键),以及一种在事务失败时回滚能力创建的机制。 本 ADR 提出了这样一套接口和机制。

决策

Cosmos SDK 将引入一个新的 CapabilityKeeper 抽象,负责在运行时分配、 跟踪和认证能力。在 app.go 中进行应用初始化期间, CapabilityKeeper 将通过唯一的函数引用接入各模块 (通过调用下文定义的 ScopeToModule),以便它在之后被调用时能够识别调用模块。 当从磁盘加载初始状态时,CapabilityKeeper 的 Initialize 函数将为所有先前已分配的能力标识符创建 新的能力键(这些标识符是在过去事务执行期间分配给特定模块的),并在链运行期间 将它们保存在仅驻留内存的存储中。 CapabilityKeeper 将包含一个持久化 KVStore、一个 MemoryStore,以及一个内存映射。 持久化 KVStore 用于跟踪某个能力归哪些模块所有。 MemoryStore 存储一个正向映射,即从模块名和能力元组映射到能力名称, 以及一个反向映射,即从模块名和能力名称映射到能力索引。 由于我们无法将能力序列化到 KVStore 中后再反序列化而不改变其内存位置, 因此 KVStore 中的反向映射只会映射到一个索引。随后可以使用该索引作为临时 go-map 中的键,从原始内存位置取回该能力。 CapabilityKeeper 将定义以下类型和函数: Capability 与 StoreKey 类似,但它使用全局唯一的 Index(),而不是 名称。同时提供 String() 方法用于调试。 Capability 本质上只是一个结构体,其地址会被作为实际的能力使用。
type Capability struct {
    index uint64
}
CapabilityKeeper 包含一个持久化 store key、一个 memory store key,以及已分配模块名的映射。
type CapabilityKeeper struct {
    persistentKey StoreKey
  memKey        StoreKey
  capMap        map[uint64]*Capability
  moduleNames   map[string]interface{
}

sealed        bool
}
CapabilityKeeper 提供创建带有作用域的子 keeper 的能力,这些 scoped 子 keeper 绑定到某个 特定模块名。这些 ScopedCapabilityKeeper 必须在应用初始化时创建 并传递给各模块,模块随后可以使用它们声明收到的能力,并按名称获取 自己拥有的能力;此外,它们还可以创建新的能力,并认证其他模块传入的能力。
type ScopedCapabilityKeeper struct {
    persistentKey StoreKey
  memKey        StoreKey
  capMap        map[uint64]*Capability
  moduleName    string
}
ScopeToModule 用于创建带有特定名称的作用域子 keeper,该名称必须唯一。 它必须在 InitializeAndSeal 之前调用。
func (ck CapabilityKeeper)

ScopeToModule(moduleName string)

ScopedCapabilityKeeper {
    if k.sealed {
    panic("cannot scope to module via a sealed capability keeper")
}
    if _, ok := k.scopedModules[moduleName]; ok {
    panic(fmt.Sprintf("cannot create multiple scoped keepers for the same module name: %s", moduleName))
}

k.scopedModules[moduleName] = struct{
}{
}

return ScopedKeeper{
    cdc:      k.cdc,
		storeKey: k.storeKey,
		memKey:   k.memKey,
		capMap:   k.capMap,
		module:   moduleName,
}
}
InitializeAndSeal 必须且只能调用一次,在加载初始状态并创建所有 所需的 ScopedCapabilityKeeper 之后调用,以便根据先前由特定模块声明的键, 使用新创建的能力键填充内存存储,并阻止继续创建新的 ScopedCapabilityKeeper。
func (ck CapabilityKeeper)

InitializeAndSeal(ctx Context) {
    if ck.sealed {
    panic("capability keeper is sealed")
}
    persistentStore := ctx.KVStore(ck.persistentKey)
    map := ctx.KVStore(ck.memKey)
  
  // initialise memory store for all names in persistent store
    for index, value := range persistentStore.Iter() {
    capability = &CapabilityKey{
    index: index
}
    for moduleAndCapability := range value {
    moduleName, capabilityName := moduleAndCapability.Split("/")

memStore.Set(moduleName + "/fwd/" + capability, capabilityName)

memStore.Set(moduleName + "/rev/" + capabilityName, index)

ck.capMap[index] = capability
}
 
}

ck.sealed = true
}
NewCapability 可由任何模块调用,用于创建一个新的、唯一且不可伪造的对象能力 引用。新创建的能力会被自动持久化;调用模块无需 调用 ClaimCapability。
func (sck ScopedCapabilityKeeper)

NewCapability(ctx Context, name string) (Capability, error) {
  // check name not taken in memory store
    if capStore.Get("rev/" + name) != nil {
    return nil, errors.New("name already taken")
}

  // fetch the current index
    index := persistentStore.Get("index")
  
  // create a new capability
    capability := &CapabilityKey{
    index: index
}
  
  // set persistent store
  persistentStore.Set(index, Set.singleton(sck.moduleName + "/" + name))
  
  // update the index
  index++
  persistentStore.Set("index", index)
  
  // set forward mapping in memory store from capability to name
  memStore.Set(sck.moduleName + "/fwd/" + capability, name)
  
  // set reverse mapping in memory store from name to index
  memStore.Set(sck.moduleName + "/rev/" + name, index)

  // set the in-memory mapping from index to capability pointer
  capMap[index] = capability
  
  // return the newly created capability
  return capability
}
AuthenticateCapability 可由任何模块调用,用于检查某个能力 是否确实对应某个特定名称(该名称可能来自不可信的用户输入), 且调用模块此前已将它与该名称关联。
func (sck ScopedCapabilityKeeper)

AuthenticateCapability(name string, capability Capability)

bool {
  // return whether forward mapping in memory store matches name
  return memStore.Get(sck.moduleName + "/fwd/" + capability) === name
}
ClaimCapability 允许模块声明一个从其他模块接收到的能力键, 从而使未来对 GetCapability 的调用能够成功。 如果一个接收到能力的模块希望未来能够按名称访问它, 则必须调用 ClaimCapability。能力支持多所有者,因此如果多个模块持有同一个 Capability 引用, 它们都会拥有该能力。
func (sck ScopedCapabilityKeeper)

ClaimCapability(ctx Context, capability Capability, name string)

error {
    persistentStore := ctx.KVStore(sck.persistentKey)

  // set forward mapping in memory store from capability to name
  memStore.Set(sck.moduleName + "/fwd/" + capability, name)

  // set reverse mapping in memory store from name to capability
  memStore.Set(sck.moduleName + "/rev/" + name, capability)

  // update owner set in persistent store
    owners := persistentStore.Get(capability.Index())

owners.add(sck.moduleName + "/" + name)

persistentStore.Set(capability.Index(), owners)
}
GetCapability 允许模块按名称获取其先前声明过的能力。 模块不允许获取自己并不拥有的能力。
func (sck ScopedCapabilityKeeper)

GetCapability(ctx Context, name string) (Capability, error) {
  // fetch the index of capability using reverse mapping in memstore
    index := memStore.Get(sck.moduleName + "/rev/" + name)

  // fetch capability from go-map using index
    capability := capMap[index]

  // return the capability
  return capability
}
ReleaseCapability 允许模块释放其先前声明过的某个能力。如果不再 存在任何所有者,则该能力将被全局删除。
func (sck ScopedCapabilityKeeper)

ReleaseCapability(ctx Context, capability Capability)

err {
    persistentStore := ctx.KVStore(sck.persistentKey)
    name := capStore.Get(sck.moduleName + "/fwd/" + capability)
    if name == nil {
    return error("capability not owned by module")
}

  // delete forward mapping in memory store
  memoryStore.Delete(sck.moduleName + "/fwd/" + capability, name)

  // delete reverse mapping in memory store
  memoryStore.Delete(sck.moduleName + "/rev/" + name, capability)

  // update owner set in persistent store
    owners := persistentStore.Get(capability.Index())

owners.remove(sck.moduleName + "/" + name)
    if owners.size() > 0 {
    // there are still other owners, keep the capability around
    persistentStore.Set(capability.Index(), owners)
}

else {
    // no more owners, delete the capability
    persistentStore.Delete(capability.Index())

delete(capMap[capability.Index()])
}
}

使用模式

初始化

任何使用动态能力的模块都必须在 app.go 中获得一个 ScopedCapabilityKeeper:
ck := NewCapabilityKeeper(persistentKey, memoryKey)

mod1Keeper := NewMod1Keeper(ck.ScopeToModule("mod1"), ....)

mod2Keeper := NewMod2Keeper(ck.ScopeToModule("mod2"), ....)

// other initialisation logic ...

// load initial state...

ck.InitializeAndSeal(initialContext)

创建、传递、声明和使用 capability

考虑这样一种情况:mod1 想要创建一个 capability,将其按名称与某个资源(例如 IBC 通道)关联,然后将其传递给 mod2,由后者稍后使用: 模块 1 的代码如下:
capability := scopedCapabilityKeeper.NewCapability(ctx, "resourceABC")

mod2Keeper.SomeFunction(ctx, capability, args...)
随后,在模块 2 中运行的 SomeFunction 可以声明该 capability:
func (k Mod2Keeper)

SomeFunction(ctx Context, capability Capability) {
    k.sck.ClaimCapability(ctx, capability, "resourceABC")
  // other logic...
}
之后,模块 2 可以按名称获取该 capability,并将其传递给模块 1,由模块 1 针对该资源进行认证:
func (k Mod2Keeper)

SomeOtherFunction(ctx Context, name string) {
    capability := k.sck.GetCapability(ctx, name)

mod1.UseResource(ctx, capability, "resourceABC")
}
然后,模块 1 会先检查这个 capability key 是否已通过认证、能够使用该资源,然后才允许模块 2 使用它:
func (k Mod1Keeper)

UseResource(ctx Context, capability Capability, resource string) {
    if !k.sck.AuthenticateCapability(name, capability) {
    return errors.New("unauthenticated")
}
  // do something with the resource
}
如果模块 2 将这个 capability key 传递给模块 3,那么模块 3 也可以声明它,并像模块 2 一样调用模块 1 (在这种情况下,模块 1、模块 2 和模块 3 都能够使用这个 capability)。

状态

提案中。

影响

正面

  • 支持动态 capability。
  • 允许 CapabilityKeeper 从 go-map 返回相同的 capability 指针,同时在交易失败时回滚对持久化 KVStore 和内存 MemoryStore 的任何写入。

负面

  • 需要额外增加一个 keeper。
  • 与现有的 StoreKey 系统有一些重叠(未来可以将两者合并,因为从功能上看这是一个超集)。
  • 反向映射需要额外增加一层间接性,因为 MemoryStore 必须映射到索引,然后再使用该索引作为 go map 中的 key 来获取实际的 capability

中性

(目前暂无)

参考


Changelog

  • 12 December 2019: Initial version
  • 02 April 2020: Memory Store Revisions

Context

Full implementation of the IBC specification requires the ability to create and authenticate object-capability keys at runtime (i.e., during transaction execution), as described in ICS 5. In the IBC specification, capability keys are created for each newly initialized port & channel, and are used to authenticate future usage of the port or channel. Since channels and potentially ports can be initialized during transaction execution, the state machine must be able to create object-capability keys at this time. At present, the Cosmos SDK does not have the ability to do this. Object-capability keys are currently pointers (memory addresses) of StoreKey structs created at application initialisation in app.go (example) and passed to Keepers as fixed arguments (example). Keepers cannot create or store capability keys during transaction execution — although they could call NewKVStoreKey and take the memory address of the returned struct, storing this in the Merklised store would result in a consensus fault, since the memory address will be different on each machine (this is intentional — were this not the case, the keys would be predictable and couldn’t serve as object capabilities). Keepers need a way to keep a private map of store keys which can be altered during transaction execution, along with a suitable mechanism for regenerating the unique memory addresses (capability keys) in this map whenever the application is started or restarted, along with a mechanism to revert capability creation on tx failure. This ADR proposes such an interface & mechanism.

Decision

The Cosmos SDK will include a new CapabilityKeeper abstraction, which is responsible for provisioning, tracking, and authenticating capabilities at runtime. During application initialisation in app.go, the CapabilityKeeper will be hooked up to modules through unique function references (by calling ScopeToModule, defined below) so that it can identify the calling module when later invoked. When the initial state is loaded from disk, the CapabilityKeeper’s Initialize function will create new capability keys for all previously allocated capability identifiers (allocated during execution of past transactions and assigned to particular modes), and keep them in a memory-only store while the chain is running. The CapabilityKeeper will include a persistent KVStore, a MemoryStore, and an in-memory map. The persistent KVStore tracks which capability is owned by which modules. The MemoryStore stores a forward mapping that map from module name, capability tuples to capability names and a reverse mapping that map from module name, capability name to the capability index. Since we cannot marshal the capability into a KVStore and unmarshal without changing the memory location of the capability, the reverse mapping in the KVStore will simply map to an index. This index can then be used as a key in the ephemeral go-map to retrieve the capability at the original memory location. The CapabilityKeeper will define the following types & functions: The Capability is similar to StoreKey, but has a globally unique Index() instead of a name. A String() method is provided for debugging. A Capability is simply a struct, the address of which is taken for the actual capability.
type Capability struct {
    index uint64
}
A CapabilityKeeper contains a persistent store key, memory store key, and mapping of allocated module names.
type CapabilityKeeper struct {
    persistentKey StoreKey
  memKey        StoreKey
  capMap        map[uint64]*Capability
  moduleNames   map[string]interface{
}

sealed        bool
}
The CapabilityKeeper provides the ability to create scoped sub-keepers which are tied to a particular module name. These ScopedCapabilityKeepers must be created at application initialisation and passed to modules, which can then use them to claim capabilities they receive and retrieve capabilities which they own by name, in addition to creating new capabilities & authenticating capabilities passed by other modules.
type ScopedCapabilityKeeper struct {
    persistentKey StoreKey
  memKey        StoreKey
  capMap        map[uint64]*Capability
  moduleName    string
}
ScopeToModule is used to create a scoped sub-keeper with a particular name, which must be unique. It MUST be called before InitializeAndSeal.
func (ck CapabilityKeeper)

ScopeToModule(moduleName string)

ScopedCapabilityKeeper {
    if k.sealed {
    panic("cannot scope to module via a sealed capability keeper")
}
    if _, ok := k.scopedModules[moduleName]; ok {
    panic(fmt.Sprintf("cannot create multiple scoped keepers for the same module name: %s", moduleName))
}

k.scopedModules[moduleName] = struct{
}{
}

return ScopedKeeper{
    cdc:      k.cdc,
		storeKey: k.storeKey,
		memKey:   k.memKey,
		capMap:   k.capMap,
		module:   moduleName,
}
}
InitializeAndSeal MUST be called exactly once, after loading the initial state and creating all necessary ScopedCapabilityKeepers, in order to populate the memory store with newly-created capability keys in accordance with the keys previously claimed by particular modules and prevent the creation of any new ScopedCapabilityKeepers.
func (ck CapabilityKeeper)

InitializeAndSeal(ctx Context) {
    if ck.sealed {
    panic("capability keeper is sealed")
}
    persistentStore := ctx.KVStore(ck.persistentKey)
    map := ctx.KVStore(ck.memKey)
  
  // initialise memory store for all names in persistent store
    for index, value := range persistentStore.Iter() {
    capability = &CapabilityKey{
    index: index
}
    for moduleAndCapability := range value {
    moduleName, capabilityName := moduleAndCapability.Split("/")

memStore.Set(moduleName + "/fwd/" + capability, capabilityName)

memStore.Set(moduleName + "/rev/" + capabilityName, index)

ck.capMap[index] = capability
}
 
}

ck.sealed = true
}
NewCapability can be called by any module to create a new unique, unforgeable object-capability reference. The newly created capability is automatically persisted; the calling module need not call ClaimCapability.
func (sck ScopedCapabilityKeeper)

NewCapability(ctx Context, name string) (Capability, error) {
  // check name not taken in memory store
    if capStore.Get("rev/" + name) != nil {
    return nil, errors.New("name already taken")
}

  // fetch the current index
    index := persistentStore.Get("index")
  
  // create a new capability
    capability := &CapabilityKey{
    index: index
}
  
  // set persistent store
  persistentStore.Set(index, Set.singleton(sck.moduleName + "/" + name))
  
  // update the index
  index++
  persistentStore.Set("index", index)
  
  // set forward mapping in memory store from capability to name
  memStore.Set(sck.moduleName + "/fwd/" + capability, name)
  
  // set reverse mapping in memory store from name to index
  memStore.Set(sck.moduleName + "/rev/" + name, index)

  // set the in-memory mapping from index to capability pointer
  capMap[index] = capability
  
  // return the newly created capability
  return capability
}
AuthenticateCapability can be called by any module to check that a capability does in fact correspond to a particular name (the name can be untrusted user input) with which the calling module previously associated it.
func (sck ScopedCapabilityKeeper)

AuthenticateCapability(name string, capability Capability)

bool {
  // return whether forward mapping in memory store matches name
  return memStore.Get(sck.moduleName + "/fwd/" + capability) === name
}
ClaimCapability allows a module to claim a capability key which it has received from another module so that future GetCapability calls will succeed. ClaimCapability MUST be called if a module which receives a capability wishes to access it by name in the future. Capabilities are multi-owner, so if multiple modules have a single Capability reference, they will all own it.
func (sck ScopedCapabilityKeeper)

ClaimCapability(ctx Context, capability Capability, name string)

error {
    persistentStore := ctx.KVStore(sck.persistentKey)

  // set forward mapping in memory store from capability to name
  memStore.Set(sck.moduleName + "/fwd/" + capability, name)

  // set reverse mapping in memory store from name to capability
  memStore.Set(sck.moduleName + "/rev/" + name, capability)

  // update owner set in persistent store
    owners := persistentStore.Get(capability.Index())

owners.add(sck.moduleName + "/" + name)

persistentStore.Set(capability.Index(), owners)
}
GetCapability allows a module to fetch a capability which it has previously claimed by name. The module is not allowed to retrieve capabilities which it does not own.
func (sck ScopedCapabilityKeeper)

GetCapability(ctx Context, name string) (Capability, error) {
  // fetch the index of capability using reverse mapping in memstore
    index := memStore.Get(sck.moduleName + "/rev/" + name)

  // fetch capability from go-map using index
    capability := capMap[index]

  // return the capability
  return capability
}
ReleaseCapability allows a module to release a capability which it had previously claimed. If no more owners exist, the capability will be deleted globally.
func (sck ScopedCapabilityKeeper)

ReleaseCapability(ctx Context, capability Capability)

err {
    persistentStore := ctx.KVStore(sck.persistentKey)
    name := capStore.Get(sck.moduleName + "/fwd/" + capability)
    if name == nil {
    return error("capability not owned by module")
}

  // delete forward mapping in memory store
  memoryStore.Delete(sck.moduleName + "/fwd/" + capability, name)

  // delete reverse mapping in memory store
  memoryStore.Delete(sck.moduleName + "/rev/" + name, capability)

  // update owner set in persistent store
    owners := persistentStore.Get(capability.Index())

owners.remove(sck.moduleName + "/" + name)
    if owners.size() > 0 {
    // there are still other owners, keep the capability around
    persistentStore.Set(capability.Index(), owners)
}

else {
    // no more owners, delete the capability
    persistentStore.Delete(capability.Index())

delete(capMap[capability.Index()])
}
}

Usage patterns

Initialisation

Any modules which use dynamic capabilities must be provided a ScopedCapabilityKeeper in app.go:
ck := NewCapabilityKeeper(persistentKey, memoryKey)

mod1Keeper := NewMod1Keeper(ck.ScopeToModule("mod1"), ....)

mod2Keeper := NewMod2Keeper(ck.ScopeToModule("mod2"), ....)

// other initialisation logic ...

// load initial state...

ck.InitializeAndSeal(initialContext)

Creating, passing, claiming and using capabilities

Consider the case where mod1 wants to create a capability, associate it with a resource (e.g. an IBC channel) by name, then pass it to mod2 which will use it later: Module 1 would have the following code:
capability := scopedCapabilityKeeper.NewCapability(ctx, "resourceABC")

mod2Keeper.SomeFunction(ctx, capability, args...)
SomeFunction, running in module 2, could then claim the capability:
func (k Mod2Keeper)

SomeFunction(ctx Context, capability Capability) {
    k.sck.ClaimCapability(ctx, capability, "resourceABC")
  // other logic...
}
Later on, module 2 can retrieve that capability by name and pass it to module 1, which will authenticate it against the resource:
func (k Mod2Keeper)

SomeOtherFunction(ctx Context, name string) {
    capability := k.sck.GetCapability(ctx, name)

mod1.UseResource(ctx, capability, "resourceABC")
}
Module 1 will then check that this capability key is authenticated to use the resource before allowing module 2 to use it:
func (k Mod1Keeper)

UseResource(ctx Context, capability Capability, resource string) {
    if !k.sck.AuthenticateCapability(name, capability) {
    return errors.New("unauthenticated")
}
  // do something with the resource
}
If module 2 passed the capability key to module 3, module 3 could then claim it and call module 1 just like module 2 did (in which case module 1, module 2, and module 3 would all be able to use this capability).

Status

Proposed.

Consequences

Positive

  • Dynamic capability support.
  • Allows CapabilityKeeper to return same capability pointer from go-map while reverting any writes to the persistent KVStore and in-memory MemoryStore on tx failure.

Negative

  • Requires an additional keeper.
  • Some overlap with existing StoreKey system (in the future they could be combined, since this is a superset functionality-wise).
  • Requires an extra level of indirection in the reverse mapping, since MemoryStore must map to index which must then be used as key in a go map to retrieve the actual capability

Neutral

(none known)

References