github.com/cosmos/cosmos-sdk@v0.50.10/docs/architecture/adr-003-dynamic-capability-store.md (about) 1 # ADR 3: Dynamic Capability Store 2 3 ## Changelog 4 5 * 12 December 2019: Initial version 6 * 02 April 2020: Memory Store Revisions 7 8 ## Context 9 10 Full implementation of the [IBC specification](https://github.com/cosmos/ibc) requires the ability to create and authenticate object-capability keys at runtime (i.e., during transaction execution), 11 as described in [ICS 5](https://github.com/cosmos/ibc/tree/master/spec/core/ics-005-port-allocation#technical-specification). In the IBC specification, capability keys are created for each newly initialised 12 port & channel, and are used to authenticate future usage of the port or channel. Since channels and potentially ports can be initialised during transaction execution, the state machine must be able to create 13 object-capability keys at this time. 14 15 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](https://github.com/cosmos/gaia/blob/dcbddd9f04b3086c0ad07ee65de16e7adedc7da4/app/app.go#L132)) 16 and passed to Keepers as fixed arguments ([example](https://github.com/cosmos/gaia/blob/dcbddd9f04b3086c0ad07ee65de16e7adedc7da4/app/app.go#L160)). Keepers cannot create or store capability keys during transaction execution — although they could call `NewKVStoreKey` and take the memory address 17 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). 18 19 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. 20 This ADR proposes such an interface & mechanism. 21 22 ## Decision 23 24 The Cosmos SDK will include a new `CapabilityKeeper` abstraction, which is responsible for provisioning, 25 tracking, and authenticating capabilities at runtime. During application initialisation in `app.go`, 26 the `CapabilityKeeper` will be hooked up to modules through unique function references 27 (by calling `ScopeToModule`, defined below) so that it can identify the calling module when later 28 invoked. 29 30 When the initial state is loaded from disk, the `CapabilityKeeper`'s `Initialise` function will create 31 new capability keys for all previously allocated capability identifiers (allocated during execution of 32 past transactions and assigned to particular modes), and keep them in a memory-only store while the 33 chain is running. 34 35 The `CapabilityKeeper` will include a persistent `KVStore`, a `MemoryStore`, and an in-memory map. 36 The persistent `KVStore` tracks which capability is owned by which modules. 37 The `MemoryStore` stores a forward mapping that map from module name, capability tuples to capability names and 38 a reverse mapping that map from module name, capability name to the capability index. 39 Since we cannot marshal the capability into a `KVStore` and unmarshal without changing the memory location of the capability, 40 the reverse mapping in the KVStore will simply map to an index. This index can then be used as a key in the ephemeral 41 go-map to retrieve the capability at the original memory location. 42 43 The `CapabilityKeeper` will define the following types & functions: 44 45 The `Capability` is similar to `StoreKey`, but has a globally unique `Index()` instead of 46 a name. A `String()` method is provided for debugging. 47 48 A `Capability` is simply a struct, the address of which is taken for the actual capability. 49 50 ```go 51 type Capability struct { 52 index uint64 53 } 54 ``` 55 56 A `CapabilityKeeper` contains a persistent store key, memory store key, and mapping of allocated module names. 57 58 ```go 59 type CapabilityKeeper struct { 60 persistentKey StoreKey 61 memKey StoreKey 62 capMap map[uint64]*Capability 63 moduleNames map[string]interface{} 64 sealed bool 65 } 66 ``` 67 68 The `CapabilityKeeper` provides the ability to create *scoped* sub-keepers which are tied to a 69 particular module name. These `ScopedCapabilityKeeper`s must be created at application initialisation 70 and passed to modules, which can then use them to claim capabilities they receive and retrieve 71 capabilities which they own by name, in addition to creating new capabilities & authenticating capabilities 72 passed by other modules. 73 74 ```go 75 type ScopedCapabilityKeeper struct { 76 persistentKey StoreKey 77 memKey StoreKey 78 capMap map[uint64]*Capability 79 moduleName string 80 } 81 ``` 82 83 `ScopeToModule` is used to create a scoped sub-keeper with a particular name, which must be unique. 84 It MUST be called before `InitialiseAndSeal`. 85 86 ```go 87 func (ck CapabilityKeeper) ScopeToModule(moduleName string) ScopedCapabilityKeeper { 88 if k.sealed { 89 panic("cannot scope to module via a sealed capability keeper") 90 } 91 92 if _, ok := k.scopedModules[moduleName]; ok { 93 panic(fmt.Sprintf("cannot create multiple scoped keepers for the same module name: %s", moduleName)) 94 } 95 96 k.scopedModules[moduleName] = struct{}{} 97 98 return ScopedKeeper{ 99 cdc: k.cdc, 100 storeKey: k.storeKey, 101 memKey: k.memKey, 102 capMap: k.capMap, 103 module: moduleName, 104 } 105 } 106 ``` 107 108 `InitialiseAndSeal` MUST be called exactly once, after loading the initial state and creating all 109 necessary `ScopedCapabilityKeeper`s, in order to populate the memory store with newly-created 110 capability keys in accordance with the keys previously claimed by particular modules and prevent the 111 creation of any new `ScopedCapabilityKeeper`s. 112 113 ```go 114 func (ck CapabilityKeeper) InitialiseAndSeal(ctx Context) { 115 if ck.sealed { 116 panic("capability keeper is sealed") 117 } 118 119 persistentStore := ctx.KVStore(ck.persistentKey) 120 map := ctx.KVStore(ck.memKey) 121 122 // initialise memory store for all names in persistent store 123 for index, value := range persistentStore.Iter() { 124 capability = &CapabilityKey{index: index} 125 126 for moduleAndCapability := range value { 127 moduleName, capabilityName := moduleAndCapability.Split("/") 128 memStore.Set(moduleName + "/fwd/" + capability, capabilityName) 129 memStore.Set(moduleName + "/rev/" + capabilityName, index) 130 131 ck.capMap[index] = capability 132 } 133 } 134 135 ck.sealed = true 136 } 137 ``` 138 139 `NewCapability` can be called by any module to create a new unique, unforgeable object-capability 140 reference. The newly created capability is automatically persisted; the calling module need not 141 call `ClaimCapability`. 142 143 ```go 144 func (sck ScopedCapabilityKeeper) NewCapability(ctx Context, name string) (Capability, error) { 145 // check name not taken in memory store 146 if capStore.Get("rev/" + name) != nil { 147 return nil, errors.New("name already taken") 148 } 149 150 // fetch the current index 151 index := persistentStore.Get("index") 152 153 // create a new capability 154 capability := &CapabilityKey{index: index} 155 156 // set persistent store 157 persistentStore.Set(index, Set.singleton(sck.moduleName + "/" + name)) 158 159 // update the index 160 index++ 161 persistentStore.Set("index", index) 162 163 // set forward mapping in memory store from capability to name 164 memStore.Set(sck.moduleName + "/fwd/" + capability, name) 165 166 // set reverse mapping in memory store from name to index 167 memStore.Set(sck.moduleName + "/rev/" + name, index) 168 169 // set the in-memory mapping from index to capability pointer 170 capMap[index] = capability 171 172 // return the newly created capability 173 return capability 174 } 175 ``` 176 177 `AuthenticateCapability` can be called by any module to check that a capability 178 does in fact correspond to a particular name (the name can be untrusted user input) 179 with which the calling module previously associated it. 180 181 ```go 182 func (sck ScopedCapabilityKeeper) AuthenticateCapability(name string, capability Capability) bool { 183 // return whether forward mapping in memory store matches name 184 return memStore.Get(sck.moduleName + "/fwd/" + capability) === name 185 } 186 ``` 187 188 `ClaimCapability` allows a module to claim a capability key which it has received from another module 189 so that future `GetCapability` calls will succeed. 190 191 `ClaimCapability` MUST be called if a module which receives a capability wishes to access it by name 192 in the future. Capabilities are multi-owner, so if multiple modules have a single `Capability` reference, 193 they will all own it. 194 195 ```go 196 func (sck ScopedCapabilityKeeper) ClaimCapability(ctx Context, capability Capability, name string) error { 197 persistentStore := ctx.KVStore(sck.persistentKey) 198 199 // set forward mapping in memory store from capability to name 200 memStore.Set(sck.moduleName + "/fwd/" + capability, name) 201 202 // set reverse mapping in memory store from name to capability 203 memStore.Set(sck.moduleName + "/rev/" + name, capability) 204 205 // update owner set in persistent store 206 owners := persistentStore.Get(capability.Index()) 207 owners.add(sck.moduleName + "/" + name) 208 persistentStore.Set(capability.Index(), owners) 209 } 210 ``` 211 212 `GetCapability` allows a module to fetch a capability which it has previously claimed by name. 213 The module is not allowed to retrieve capabilities which it does not own. 214 215 ```go 216 func (sck ScopedCapabilityKeeper) GetCapability(ctx Context, name string) (Capability, error) { 217 // fetch the index of capability using reverse mapping in memstore 218 index := memStore.Get(sck.moduleName + "/rev/" + name) 219 220 // fetch capability from go-map using index 221 capability := capMap[index] 222 223 // return the capability 224 return capability 225 } 226 ``` 227 228 `ReleaseCapability` allows a module to release a capability which it had previously claimed. If no 229 more owners exist, the capability will be deleted globally. 230 231 ```go 232 func (sck ScopedCapabilityKeeper) ReleaseCapability(ctx Context, capability Capability) err { 233 persistentStore := ctx.KVStore(sck.persistentKey) 234 235 name := capStore.Get(sck.moduleName + "/fwd/" + capability) 236 if name == nil { 237 return error("capability not owned by module") 238 } 239 240 // delete forward mapping in memory store 241 memoryStore.Delete(sck.moduleName + "/fwd/" + capability, name) 242 243 // delete reverse mapping in memory store 244 memoryStore.Delete(sck.moduleName + "/rev/" + name, capability) 245 246 // update owner set in persistent store 247 owners := persistentStore.Get(capability.Index()) 248 owners.remove(sck.moduleName + "/" + name) 249 if owners.size() > 0 { 250 // there are still other owners, keep the capability around 251 persistentStore.Set(capability.Index(), owners) 252 } else { 253 // no more owners, delete the capability 254 persistentStore.Delete(capability.Index()) 255 delete(capMap[capability.Index()]) 256 } 257 } 258 ``` 259 260 ### Usage patterns 261 262 #### Initialisation 263 264 Any modules which use dynamic capabilities must be provided a `ScopedCapabilityKeeper` in `app.go`: 265 266 ```go 267 ck := NewCapabilityKeeper(persistentKey, memoryKey) 268 mod1Keeper := NewMod1Keeper(ck.ScopeToModule("mod1"), ....) 269 mod2Keeper := NewMod2Keeper(ck.ScopeToModule("mod2"), ....) 270 271 // other initialisation logic ... 272 273 // load initial state... 274 275 ck.InitialiseAndSeal(initialContext) 276 ``` 277 278 #### Creating, passing, claiming and using capabilities 279 280 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: 281 282 Module 1 would have the following code: 283 284 ```go 285 capability := scopedCapabilityKeeper.NewCapability(ctx, "resourceABC") 286 mod2Keeper.SomeFunction(ctx, capability, args...) 287 ``` 288 289 `SomeFunction`, running in module 2, could then claim the capability: 290 291 ```go 292 func (k Mod2Keeper) SomeFunction(ctx Context, capability Capability) { 293 k.sck.ClaimCapability(ctx, capability, "resourceABC") 294 // other logic... 295 } 296 ``` 297 298 Later on, module 2 can retrieve that capability by name and pass it to module 1, which will authenticate it against the resource: 299 300 ```go 301 func (k Mod2Keeper) SomeOtherFunction(ctx Context, name string) { 302 capability := k.sck.GetCapability(ctx, name) 303 mod1.UseResource(ctx, capability, "resourceABC") 304 } 305 ``` 306 307 Module 1 will then check that this capability key is authenticated to use the resource before allowing module 2 to use it: 308 309 ```go 310 func (k Mod1Keeper) UseResource(ctx Context, capability Capability, resource string) { 311 if !k.sck.AuthenticateCapability(name, capability) { 312 return errors.New("unauthenticated") 313 } 314 // do something with the resource 315 } 316 ``` 317 318 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 319 (in which case module 1, module 2, and module 3 would all be able to use this capability). 320 321 ## Status 322 323 Proposed. 324 325 ## Consequences 326 327 ### Positive 328 329 * Dynamic capability support. 330 * 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. 331 332 ### Negative 333 334 * Requires an additional keeper. 335 * Some overlap with existing `StoreKey` system (in the future they could be combined, since this is a superset functionality-wise). 336 * 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 337 338 ### Neutral 339 340 (none known) 341 342 ## References 343 344 * [Original discussion](https://github.com/cosmos/cosmos-sdk/pull/5230#discussion_r343978513)