diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 0373a199cf07..39e8817857da 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -195,6 +195,14 @@ func (app *BaseApp) MountTransientStores(keys map[string]*sdk.TransientStoreKey) } } +// MountMemoryStores mounts all in-memory KVStores with the BaseApp's internal +// commit multi-store. +func (app *BaseApp) MountMemoryStores(keys map[string]*sdk.MemoryStoreKey) { + for _, memKey := range keys { + app.MountStore(memKey, sdk.StoreTypeMemory) + } +} + // MountStoreWithDB mounts a store to the provided key in the BaseApp // multistore, using a specified DB. func (app *BaseApp) MountStoreWithDB(key sdk.StoreKey, typ sdk.StoreType, db dbm.DB) { diff --git a/docs/architecture/adr-003-dynamic-capability-store.md b/docs/architecture/adr-003-dynamic-capability-store.md index 7fd438fd931c..c1dbf4741f5c 100644 --- a/docs/architecture/adr-003-dynamic-capability-store.md +++ b/docs/architecture/adr-003-dynamic-capability-store.md @@ -171,8 +171,7 @@ func (sck ScopedCapabilityKeeper) ClaimCapability(ctx Context, capability Capabi } ``` -`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. If another module -claims a capability, the previously owning module will no longer be able to claim it. +`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. ```golang func (sck ScopedCapabilityKeeper) GetCapability(ctx Context, name string) (Capability, error) { diff --git a/simapp/app.go b/simapp/app.go index 0aea5d976589..8e73e752217f 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -18,6 +18,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/ante" "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/capability" "github.com/cosmos/cosmos-sdk/x/crisis" distr "github.com/cosmos/cosmos-sdk/x/distribution" "github.com/cosmos/cosmos-sdk/x/evidence" @@ -54,6 +55,7 @@ var ( supply.AppModuleBasic{}, genutil.AppModuleBasic{}, bank.AppModuleBasic{}, + capability.AppModuleBasic{}, staking.AppModuleBasic{}, mint.AppModuleBasic{}, distr.AppModuleBasic{}, @@ -98,27 +100,29 @@ type SimApp struct { invCheckPeriod uint // keys to access the substores - keys map[string]*sdk.KVStoreKey - tkeys map[string]*sdk.TransientStoreKey + keys map[string]*sdk.KVStoreKey + tkeys map[string]*sdk.TransientStoreKey + memKeys map[string]*sdk.MemoryStoreKey // subspaces subspaces map[string]params.Subspace // keepers - AccountKeeper auth.AccountKeeper - BankKeeper bank.Keeper - SupplyKeeper supply.Keeper - StakingKeeper staking.Keeper - SlashingKeeper slashing.Keeper - MintKeeper mint.Keeper - DistrKeeper distr.Keeper - GovKeeper gov.Keeper - CrisisKeeper crisis.Keeper - UpgradeKeeper upgrade.Keeper - ParamsKeeper params.Keeper - IBCKeeper ibc.Keeper - EvidenceKeeper evidence.Keeper - TransferKeeper transfer.Keeper + AccountKeeper auth.AccountKeeper + BankKeeper bank.Keeper + CapabilityKeeper *capability.Keeper + SupplyKeeper supply.Keeper + StakingKeeper staking.Keeper + SlashingKeeper slashing.Keeper + MintKeeper mint.Keeper + DistrKeeper distr.Keeper + GovKeeper gov.Keeper + CrisisKeeper crisis.Keeper + UpgradeKeeper upgrade.Keeper + ParamsKeeper params.Keeper + IBCKeeper ibc.Keeper + EvidenceKeeper evidence.Keeper + TransferKeeper transfer.Keeper // the module manager mm *module.Manager @@ -145,9 +149,10 @@ func NewSimApp( bam.MainStoreKey, auth.StoreKey, bank.StoreKey, staking.StoreKey, supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey, gov.StoreKey, params.StoreKey, ibc.StoreKey, upgrade.StoreKey, - evidence.StoreKey, transfer.StoreKey, + evidence.StoreKey, transfer.StoreKey, capability.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(params.TStoreKey) + memKeys := sdk.NewMemoryStoreKeys(capability.MemStoreKey) app := &SimApp{ BaseApp: bApp, @@ -155,6 +160,7 @@ func NewSimApp( invCheckPeriod: invCheckPeriod, keys: keys, tkeys: tkeys, + memKeys: memKeys, subspaces: make(map[string]params.Subspace), } @@ -177,6 +183,9 @@ func NewSimApp( app.BankKeeper = bank.NewBaseKeeper( appCodec, keys[bank.StoreKey], app.AccountKeeper, app.subspaces[bank.ModuleName], app.BlacklistedAccAddrs(), ) + app.CapabilityKeeper = capability.NewKeeper( + app.cdc, keys[capability.StoreKey], memKeys[capability.MemStoreKey], + ) app.SupplyKeeper = supply.NewKeeper( appCodec, keys[supply.StoreKey], app.AccountKeeper, app.BankKeeper, maccPerms, ) @@ -242,6 +251,7 @@ func NewSimApp( genutil.NewAppModule(app.AccountKeeper, app.StakingKeeper, app.BaseApp.DeliverTx), auth.NewAppModule(app.AccountKeeper, app.SupplyKeeper), bank.NewAppModule(app.BankKeeper, app.AccountKeeper), + capability.NewAppModule(*app.CapabilityKeeper), crisis.NewAppModule(&app.CrisisKeeper), supply.NewAppModule(app.SupplyKeeper, app.BankKeeper, app.AccountKeeper), gov.NewAppModule(app.GovKeeper, app.AccountKeeper, app.BankKeeper, app.SupplyKeeper), @@ -293,6 +303,7 @@ func NewSimApp( // initialize stores app.MountKVStores(keys) app.MountTransientStores(tkeys) + app.MountMemoryStores(memKeys) // initialize BaseApp app.SetInitChainer(app.InitChainer) @@ -312,6 +323,12 @@ func NewSimApp( } } + // Initialize and seal the capability keeper so all persistent capabilities + // are loaded in-memory and prevent any further modules from creating scoped + // sub-keepers. + ctx := app.BaseApp.NewContext(true, abci.Header{}) + app.CapabilityKeeper.InitializeAndSeal(ctx) + return app } @@ -382,6 +399,13 @@ func (app *SimApp) GetTKey(storeKey string) *sdk.TransientStoreKey { return app.tkeys[storeKey] } +// GetMemKey returns the MemoryStoreKey for the provided store key. +// +// NOTE: This is solely to be used for testing purposes. +func (app *SimApp) GetMemKey(storeKey string) *sdk.MemoryStoreKey { + return app.memKeys[storeKey] +} + // GetSubspace returns a param subspace for a given module name. // // NOTE: This is solely to be used for testing purposes. diff --git a/store/mem/mem_test.go b/store/mem/mem_test.go new file mode 100644 index 000000000000..cff4c37da7dc --- /dev/null +++ b/store/mem/mem_test.go @@ -0,0 +1,39 @@ +package mem_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/store/mem" + "github.com/cosmos/cosmos-sdk/store/types" +) + +func TestStore(t *testing.T) { + db := mem.NewStore() + key, value := []byte("key"), []byte("value") + + require.Equal(t, types.StoreTypeMemory, db.GetStoreType()) + + require.Nil(t, db.Get(key)) + db.Set(key, value) + require.Equal(t, value, db.Get(key)) + + newValue := []byte("newValue") + db.Set(key, newValue) + require.Equal(t, newValue, db.Get(key)) + + db.Delete(key) + require.Nil(t, db.Get(key)) +} + +func TestCommit(t *testing.T) { + db := mem.NewStore() + key, value := []byte("key"), []byte("value") + + db.Set(key, value) + id := db.Commit() + require.True(t, id.IsZero()) + require.True(t, db.LastCommitID().IsZero()) + require.Equal(t, value, db.Get(key)) +} diff --git a/store/mem/store.go b/store/mem/store.go new file mode 100644 index 000000000000..3a1cdd617a0a --- /dev/null +++ b/store/mem/store.go @@ -0,0 +1,53 @@ +package mem + +import ( + "io" + + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/store/cachekv" + "github.com/cosmos/cosmos-sdk/store/dbadapter" + "github.com/cosmos/cosmos-sdk/store/tracekv" + "github.com/cosmos/cosmos-sdk/store/types" +) + +var ( + _ types.KVStore = (*Store)(nil) + _ types.Committer = (*Store)(nil) +) + +// Store implements an in-memory only KVStore. Entries are persisted between +// commits and thus between blocks. State in Memory store is not committed as part of app state but maintained privately by each node +type Store struct { + dbadapter.Store +} + +func NewStore() *Store { + return NewStoreWithDB(dbm.NewMemDB()) +} + +func NewStoreWithDB(db *dbm.MemDB) *Store { // nolint: interfacer + return &Store{Store: dbadapter.Store{DB: db}} +} + +// GetStoreType returns the Store's type. +func (s Store) GetStoreType() types.StoreType { + return types.StoreTypeMemory +} + +// CacheWrap cache wraps the underlying store. +func (s Store) CacheWrap() types.CacheWrap { + return cachekv.NewStore(s) +} + +// CacheWrapWithTrace implements KVStore. +func (s Store) CacheWrapWithTrace(w io.Writer, tc types.TraceContext) types.CacheWrap { + return cachekv.NewStore(tracekv.NewStore(s, w, tc)) +} + +// Commit performs a no-op as entries are persistent between commitments. +func (s *Store) Commit() (id types.CommitID) { return } + +// nolint +func (s *Store) SetPruning(pruning types.PruningOptions) {} +func (s Store) LastCommitID() (id types.CommitID) { return } diff --git a/store/rootmulti/store.go b/store/rootmulti/store.go index c88657741617..bea5ee3e0146 100644 --- a/store/rootmulti/store.go +++ b/store/rootmulti/store.go @@ -16,6 +16,7 @@ import ( "github.com/cosmos/cosmos-sdk/store/cachemulti" "github.com/cosmos/cosmos-sdk/store/dbadapter" "github.com/cosmos/cosmos-sdk/store/iavl" + "github.com/cosmos/cosmos-sdk/store/mem" "github.com/cosmos/cosmos-sdk/store/tracekv" "github.com/cosmos/cosmos-sdk/store/transient" "github.com/cosmos/cosmos-sdk/store/types" @@ -169,12 +170,13 @@ func (rs *Store) loadVersion(ver int64, upgrades *types.StoreUpgrades) error { // load each Store (note this doesn't panic on unmounted keys now) var newStores = make(map[types.StoreKey]types.CommitKVStore) + for key, storeParams := range rs.storesParams { - // Load it store, err := rs.loadCommitStoreFromParams(key, rs.getCommitID(infos, key.Name()), storeParams) if err != nil { return errors.Wrap(err, "failed to load store") } + newStores[key] = store // If it was deleted, remove all data @@ -527,6 +529,13 @@ func (rs *Store) loadCommitStoreFromParams(key types.StoreKey, id types.CommitID return transient.NewStore(), nil + case types.StoreTypeMemory: + if _, ok := key.(*types.MemoryStoreKey); !ok { + return nil, fmt.Errorf("unexpected key type for a MemoryStoreKey; got: %s", key.String()) + } + + return mem.NewStore(), nil + default: panic(fmt.Sprintf("unrecognized store type %v", params.typ)) } diff --git a/store/types/store.go b/store/types/store.go index 72575baa6a89..cb0d3dd5c2ce 100644 --- a/store/types/store.go +++ b/store/types/store.go @@ -273,8 +273,30 @@ const ( StoreTypeDB StoreTypeIAVL StoreTypeTransient + StoreTypeMemory ) +func (st StoreType) String() string { + switch st { + case StoreTypeMulti: + return "StoreTypeMulti" + + case StoreTypeDB: + return "StoreTypeDB" + + case StoreTypeIAVL: + return "StoreTypeIAVL" + + case StoreTypeTransient: + return "StoreTypeTransient" + + case StoreTypeMemory: + return "StoreTypeMemory" + } + + return "unknown store type" +} + //---------------------------------------- // Keys for accessing substores @@ -333,6 +355,25 @@ func (key *TransientStoreKey) String() string { return fmt.Sprintf("TransientStoreKey{%p, %s}", key, key.name) } +// MemoryStoreKey defines a typed key to be used with an in-memory KVStore. +type MemoryStoreKey struct { + name string +} + +func NewMemoryStoreKey(name string) *MemoryStoreKey { + return &MemoryStoreKey{name: name} +} + +// Name returns the name of the MemoryStoreKey. +func (key *MemoryStoreKey) Name() string { + return key.name +} + +// String returns a stringified representation of the MemoryStoreKey. +func (key *MemoryStoreKey) String() string { + return fmt.Sprintf("MemoryStoreKey{%p, %s}", key, key.name) +} + //---------------------------------------- // key-value result for iterator queries diff --git a/types/store.go b/types/store.go index 6057526c80e9..884556cc6b26 100644 --- a/types/store.go +++ b/types/store.go @@ -76,6 +76,7 @@ const ( StoreTypeDB = types.StoreTypeDB StoreTypeIAVL = types.StoreTypeIAVL StoreTypeTransient = types.StoreTypeTransient + StoreTypeMemory = types.StoreTypeMemory ) // nolint - reexport @@ -84,6 +85,7 @@ type ( CapabilityKey = types.CapabilityKey KVStoreKey = types.KVStoreKey TransientStoreKey = types.TransientStoreKey + MemoryStoreKey = types.MemoryStoreKey ) // NewKVStoreKey returns a new pointer to a KVStoreKey. @@ -99,6 +101,7 @@ func NewKVStoreKeys(names ...string) map[string]*KVStoreKey { for _, name := range names { keys[name] = NewKVStoreKey(name) } + return keys } @@ -115,6 +118,18 @@ func NewTransientStoreKeys(names ...string) map[string]*TransientStoreKey { for _, name := range names { keys[name] = NewTransientStoreKey(name) } + + return keys +} + +// NewMemoryStoreKeys constructs a new map matching store key names to their +// respective MemoryStoreKey references. +func NewMemoryStoreKeys(names ...string) map[string]*MemoryStoreKey { + keys := make(map[string]*MemoryStoreKey) + for _, name := range names { + keys[name] = types.NewMemoryStoreKey(name) + } + return keys } diff --git a/types/utils.go b/types/utils.go index be33160261d5..f1ef6090b7f3 100644 --- a/types/utils.go +++ b/types/utils.go @@ -49,6 +49,16 @@ func Uint64ToBigEndian(i uint64) []byte { return b } +// BigEndianToUint64 returns an uint64 from big endian encoded bytes. If encoding +// is empty, zero is returned. +func BigEndianToUint64(bz []byte) uint64 { + if len(bz) == 0 { + return 0 + } + + return binary.BigEndian.Uint64(bz) +} + // Slight modification of the RFC3339Nano but it right pads all zeros and drops the time zone info const SortableTimeFormat = "2006-01-02T15:04:05.000000000" diff --git a/x/capability/alias.go b/x/capability/alias.go new file mode 100644 index 000000000000..3aa949677e50 --- /dev/null +++ b/x/capability/alias.go @@ -0,0 +1,41 @@ +package capability + +import ( + "github.com/cosmos/cosmos-sdk/x/capability/keeper" + "github.com/cosmos/cosmos-sdk/x/capability/types" +) + +// DONTCOVER + +// nolint +const ( + ModuleName = types.ModuleName + StoreKey = types.StoreKey + MemStoreKey = types.MemStoreKey +) + +// nolint +var ( + NewKeeper = keeper.NewKeeper + NewCapabilityKey = types.NewCapabilityKey + RevCapabilityKey = types.RevCapabilityKey + FwdCapabilityKey = types.FwdCapabilityKey + KeyIndex = types.KeyIndex + KeyPrefixIndexCapability = types.KeyPrefixIndexCapability + ErrCapabilityTaken = types.ErrCapabilityTaken + ErrOwnerClaimed = types.ErrOwnerClaimed + RegisterCodec = types.RegisterCodec + RegisterCapabilityTypeCodec = types.RegisterCapabilityTypeCodec + ModuleCdc = types.ModuleCdc + NewOwner = types.NewOwner + NewCapabilityOwners = types.NewCapabilityOwners +) + +// nolint +type ( + Keeper = keeper.Keeper + ScopedKeeper = keeper.ScopedKeeper + Capability = types.Capability + CapabilityKey = types.CapabilityKey + CapabilityOwners = types.CapabilityOwners +) diff --git a/x/capability/docs/README.md b/x/capability/docs/README.md new file mode 100644 index 000000000000..a280743d7edf --- /dev/null +++ b/x/capability/docs/README.md @@ -0,0 +1,89 @@ +# x/capability + +## Abstract + +`x/capability` is an implementation of a Cosmos SDK module, per [ADR 003](./../../../docs/architecture/adr-003-dynamic-capability-store.md), +that allows for provisioning, tracking, and authenticating multi-owner capabilities +at runtime. + +The keeper maintains two states: persistent and ephemeral in-memory. The persistent +store maintains a globally unique auto-incrementing index and a mapping from +capability index to a set of capability owners that are defined as a module and +capability name tuple. The in-memory ephemeral state keeps track of the actual +capabilities, represented as addresses in local memory, with both forward and reverse indexes. +The forward index maps module name and capability tuples to the capability name. The +reverse index maps between the module and capability name and the capability itself. + +The keeper allows the creation of "scoped" sub-keepers which are tied to a particular +module by name. Scoped keepers must be created at application initialization 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. A scoped keeper cannot escape its scope, +so a module cannot interfere with or inspect capabilities owned by other modules. + +The keeper provides no other core functionality that can be found in other modules +like queriers, REST and CLI handlers, and genesis state. + +## Initialization + +During application initialization, the keeper must be instantiated with a persistent +store key and an in-memory store key. + +```go +type App struct { + // ... + + capabilityKeeper *capability.Keeper +} + +func NewApp(...) *App { + // ... + + app.capabilityKeeper = capability.NewKeeper(codec, persistentStoreKey, memStoreKey) +} +``` + +After the keeper is created, it can be used to create scoped sub-keepers which +are passed to other modules that can create, authenticate, and claim capabilities. +After all the necessary scoped keepers are created and the state is loaded, the +main capability keeper must be initialized and sealed to populate the in-memory +state and to prevent further scoped keepers from being created. + +```go +func NewApp(...) *App { + // ... + + // Initialize and seal the capability keeper so all persistent capabilities + // are loaded in-memory and prevent any further modules from creating scoped + // sub-keepers. + ctx := app.BaseApp.NewContext(true, abci.Header{}) + app.capabilityKeeper.InitializeAndSeal(ctx) + + return app +} +``` + +## Capabilities + +Capabilities are multi-owner. A scoped keeper can create a capability via `NewCapability` +which creates a new unique, unforgeable object-capability reference. The newly +created capability is automatically persisted; the calling module need not call +`ClaimCapability`. Calling `NewCapability` will create the capability with the +calling module and name as a tuple to be treated the capabilities first owner. + +Capabilities can be claimed by other modules which add them as owners. `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. Again, capabilities are multi-owner, so if multiple modules have a +single Capability reference, they will all own it. If a module receives a capability +from another module but does not call `ClaimCapability`, it may use it in the executing +transaction but will not be able to access it afterwards. + +`AuthenticateCapability` can be called by any module to check that a capability +does in fact correspond to a particular name (the name can be un-trusted user input) +with which the calling module previously associated it. + +`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. diff --git a/x/capability/keeper/keeper.go b/x/capability/keeper/keeper.go new file mode 100644 index 000000000000..03bd897a3832 --- /dev/null +++ b/x/capability/keeper/keeper.go @@ -0,0 +1,246 @@ +package keeper + +import ( + "fmt" + + "github.com/tendermint/tendermint/libs/log" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/capability/types" +) + +type ( + // Keeper defines the capability module's keeper. It is responsible for provisioning, + // tracking, and authenticating capabilities at runtime. During application + // initialization, the keeper can be hooked up to modules through unique function + // references so that it can identify the calling module when later invoked. + // + // When the initial state is loaded from disk, the keeper allows the ability to + // 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 keeper allows the ability to create scoped sub-keepers which are tied to + // a single specific module. + Keeper struct { + cdc *codec.Codec + storeKey sdk.StoreKey + memKey sdk.StoreKey + scopedModules map[string]struct{} + sealed bool + } + + // ScopedKeeper defines a scoped sub-keeper which is tied to a single specific + // module provisioned by the capability keeper. Scoped keepers must be created + // at application initialization 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. + ScopedKeeper struct { + cdc *codec.Codec + storeKey sdk.StoreKey + memKey sdk.StoreKey + module string + } +) + +func NewKeeper(cdc *codec.Codec, storeKey, memKey sdk.StoreKey) *Keeper { + return &Keeper{ + cdc: cdc, + storeKey: storeKey, + memKey: memKey, + scopedModules: make(map[string]struct{}), + sealed: false, + } +} + +// ScopeToModule attempts to create and return a ScopedKeeper for a given module +// by name. It will panic if the keeper is already sealed or if the module name +// already has a ScopedKeeper. +func (k *Keeper) ScopeToModule(moduleName string) ScopedKeeper { + 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, + module: moduleName, + } +} + +// InitializeAndSeal loads all capabilities from the persistent KVStore into the +// in-memory store and seals the keeper to prevent further modules from creating +// a scoped keeper. InitializeAndSeal must be called once after the application +// state is loaded. +func (k *Keeper) InitializeAndSeal(ctx sdk.Context) { + if k.sealed { + panic("cannot initialize and seal an already sealed capability keeper") + } + + memStore := ctx.KVStore(k.memKey) + memStoreType := memStore.GetStoreType() + + if memStoreType != sdk.StoreTypeMemory { + panic(fmt.Sprintf("invalid memory store type; got %s, expected: %s", memStoreType, sdk.StoreTypeMemory)) + } + + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixIndexCapability) + iterator := sdk.KVStorePrefixIterator(prefixStore, nil) + + // initialize the in-memory store for all persisted capabilities + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + index := types.IndexFromKey(iterator.Key()) + cap := types.NewCapabilityKey(index) + + var capOwners *types.CapabilityOwners + k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &capOwners) + + for _, owner := range capOwners.Owners { + // Set the forward mapping between the module and capability tuple and the + // capability name in the in-memory store. + memStore.Set(types.FwdCapabilityKey(owner.Module, cap), []byte(owner.Name)) + + // Set the reverse mapping between the module and capability name and the + // capability in the in-memory store. + memStore.Set(types.RevCapabilityKey(owner.Module, owner.Name), k.cdc.MustMarshalBinaryBare(cap)) + } + } + + k.sealed = true +} + +// NewCapability attempts to create a new capability with a given name. If the +// capability already exists in the in-memory store, an error will be returned. +// Otherwise, a new capability is created with the current global unique index. +// The newly created capability has the scoped module name and capability name +// tuple set as the initial owner. Finally, the global index is incremented along +// with forward and reverse indexes set in the in-memory store. +// +// Note, namespacing is completely local, which is safe since records are prefixed +// with the module name and no two ScopedKeeper can have the same module name. +func (sk ScopedKeeper) NewCapability(ctx sdk.Context, name string) (types.Capability, error) { + store := ctx.KVStore(sk.storeKey) + memStore := ctx.KVStore(sk.memKey) + + if memStore.Has(types.RevCapabilityKey(sk.module, name)) { + return nil, sdkerrors.Wrapf(types.ErrCapabilityTaken, fmt.Sprintf("module: %s, name: %s", sk.module, name)) + } + + // create new capability with the current global index + index := types.IndexFromKey(store.Get(types.KeyIndex)) + cap := types.NewCapabilityKey(index) + + // update capability owner set + if err := sk.addOwner(ctx, cap, name); err != nil { + return nil, err + } + + // increment global index + store.Set(types.KeyIndex, types.IndexToKey(index+1)) + + // Set the forward mapping between the module and capability tuple and the + // capability name in the in-memory store. + memStore.Set(types.FwdCapabilityKey(sk.module, cap), []byte(name)) + + // Set the reverse mapping between the module and capability name and the + // capability in the in-memory store. + memStore.Set(types.RevCapabilityKey(sk.module, name), sk.cdc.MustMarshalBinaryBare(cap)) + + logger(ctx).Info("created new capability", "module", sk.module, "name", name) + return cap, nil +} + +// AuthenticateCapability attempts to authenticate a given capability and name +// from a caller. It allows for a caller to check that a capability does in fact +// correspond to a particular name. The scoped keeper will lookup the capability +// from the internal in-memory store and check against the provided name. It returns +// true upon success and false upon failure. +// +// Note, the capability's forward mapping is indexed by a string which should +// contain its unique memory reference. +func (sk ScopedKeeper) AuthenticateCapability(ctx sdk.Context, cap types.Capability, name string) bool { + memStore := ctx.KVStore(sk.memKey) + + bz := memStore.Get(types.FwdCapabilityKey(sk.module, cap)) + return string(bz) == name +} + +// ClaimCapability attempts to claim a given Capability. The provided name and +// the scoped module's name tuple are treated as the owner. It will attempt +// to add the owner to the persistent set of capability owners for the capability +// index. If the owner already exists, it will return an error. Otherwise, it will +// also set a forward and reverse index for the capability and capability name. +func (sk ScopedKeeper) ClaimCapability(ctx sdk.Context, cap types.Capability, name string) error { + // update capability owner set + if err := sk.addOwner(ctx, cap, name); err != nil { + return err + } + + memStore := ctx.KVStore(sk.memKey) + + // Set the forward mapping between the module and capability tuple and the + // capability name in the in-memory store. + memStore.Set(types.FwdCapabilityKey(sk.module, cap), []byte(name)) + + // Set the reverse mapping between the module and capability name and the + // capability in the in-memory store. + memStore.Set(types.RevCapabilityKey(sk.module, name), sk.cdc.MustMarshalBinaryBare(cap)) + + logger(ctx).Info("claimed capability", "module", sk.module, "name", name, "capability", cap.GetIndex()) + return nil +} + +// GetCapability allows a module to fetch a capability which it previously claimed +// by name. The module is not allowed to retrieve capabilities which it does not +// own. +func (sk ScopedKeeper) GetCapability(ctx sdk.Context, name string) (types.Capability, bool) { + memStore := ctx.KVStore(sk.memKey) + + bz := memStore.Get(types.RevCapabilityKey(sk.module, name)) + if len(bz) == 0 { + return nil, false + } + + var cap types.Capability + sk.cdc.MustUnmarshalBinaryBare(bz, &cap) + + return cap, true +} + +func (sk ScopedKeeper) addOwner(ctx sdk.Context, cap types.Capability, name string) error { + prefixStore := prefix.NewStore(ctx.KVStore(sk.storeKey), types.KeyPrefixIndexCapability) + indexKey := types.IndexToKey(cap.GetIndex()) + + var capOwners *types.CapabilityOwners + + bz := prefixStore.Get(indexKey) + if len(bz) == 0 { + capOwners = types.NewCapabilityOwners() + } else { + sk.cdc.MustUnmarshalBinaryBare(bz, &capOwners) + } + + if err := capOwners.Set(types.NewOwner(sk.module, name)); err != nil { + return err + } + + // update capability owner set + prefixStore.Set(indexKey, sk.cdc.MustMarshalBinaryBare(capOwners)) + return nil +} + +func logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} diff --git a/x/capability/keeper/keeper_test.go b/x/capability/keeper/keeper_test.go new file mode 100644 index 000000000000..dc52d9b96d5e --- /dev/null +++ b/x/capability/keeper/keeper_test.go @@ -0,0 +1,145 @@ +package keeper_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/capability" + "github.com/cosmos/cosmos-sdk/x/capability/keeper" + "github.com/cosmos/cosmos-sdk/x/capability/types" + "github.com/cosmos/cosmos-sdk/x/staking" +) + +type KeeperTestSuite struct { + suite.Suite + + ctx sdk.Context + keeper *keeper.Keeper + app *simapp.SimApp +} + +func (suite *KeeperTestSuite) SetupTest() { + checkTx := false + app := simapp.Setup(checkTx) + + // create new keeper so we can define custom scoping before init and seal + keeper := keeper.NewKeeper( + app.Codec(), app.GetKey(capability.StoreKey), app.GetMemKey(capability.MemStoreKey), + ) + + suite.ctx = app.BaseApp.NewContext(checkTx, abci.Header{Height: 1}) + suite.keeper = keeper + suite.app = app +} + +func (suite *KeeperTestSuite) TestInitializeAndSeal() { + sk := suite.keeper.ScopeToModule(bank.ModuleName) + + caps := make([]types.Capability, 5) + + for i := range caps { + cap, err := sk.NewCapability(suite.ctx, fmt.Sprintf("transfer-%d", i)) + suite.Require().NoError(err) + suite.Require().NotNil(cap) + suite.Require().Equal(uint64(i), cap.GetIndex()) + + caps[i] = cap + } + + suite.Require().NotPanics(func() { + suite.keeper.InitializeAndSeal(suite.ctx) + }) + + for i, cap := range caps { + got, ok := sk.GetCapability(suite.ctx, fmt.Sprintf("transfer-%d", i)) + suite.Require().True(ok) + suite.Require().Equal(cap, got) + suite.Require().Equal(uint64(i), got.GetIndex()) + } + + suite.Require().Panics(func() { + suite.keeper.InitializeAndSeal(suite.ctx) + }) + + suite.Require().Panics(func() { + _ = suite.keeper.ScopeToModule(staking.ModuleName) + }) +} + +func (suite *KeeperTestSuite) TestNewCapability() { + sk := suite.keeper.ScopeToModule(bank.ModuleName) + + cap, err := sk.NewCapability(suite.ctx, "transfer") + suite.Require().NoError(err) + suite.Require().NotNil(cap) + + got, ok := sk.GetCapability(suite.ctx, "transfer") + suite.Require().True(ok) + suite.Require().Equal(cap, got) + + got, ok = sk.GetCapability(suite.ctx, "invalid") + suite.Require().False(ok) + suite.Require().Nil(got) + + cap, err = sk.NewCapability(suite.ctx, "transfer") + suite.Require().Error(err) + suite.Require().Nil(cap) +} + +func (suite *KeeperTestSuite) TestAuthenticateCapability() { + sk1 := suite.keeper.ScopeToModule(bank.ModuleName) + sk2 := suite.keeper.ScopeToModule(staking.ModuleName) + + cap1, err := sk1.NewCapability(suite.ctx, "transfer") + suite.Require().NoError(err) + suite.Require().NotNil(cap1) + + forgedCap := types.NewCapabilityKey(0) // index should be the same index as the first capability + suite.Require().False(sk2.AuthenticateCapability(suite.ctx, forgedCap, "transfer")) + + cap2, err := sk2.NewCapability(suite.ctx, "bond") + suite.Require().NoError(err) + suite.Require().NotNil(cap2) + + suite.Require().True(sk1.AuthenticateCapability(suite.ctx, cap1, "transfer")) + suite.Require().False(sk1.AuthenticateCapability(suite.ctx, cap1, "invalid")) + suite.Require().False(sk1.AuthenticateCapability(suite.ctx, cap2, "transfer")) + + suite.Require().True(sk2.AuthenticateCapability(suite.ctx, cap2, "bond")) + suite.Require().False(sk2.AuthenticateCapability(suite.ctx, cap2, "invalid")) + suite.Require().False(sk2.AuthenticateCapability(suite.ctx, cap1, "bond")) + + badCap := types.NewCapabilityKey(100) + suite.Require().False(sk1.AuthenticateCapability(suite.ctx, badCap, "transfer")) + suite.Require().False(sk2.AuthenticateCapability(suite.ctx, badCap, "bond")) +} + +func (suite *KeeperTestSuite) TestClaimCapability() { + sk1 := suite.keeper.ScopeToModule(bank.ModuleName) + sk2 := suite.keeper.ScopeToModule(staking.ModuleName) + + cap, err := sk1.NewCapability(suite.ctx, "transfer") + suite.Require().NoError(err) + suite.Require().NotNil(cap) + + suite.Require().Error(sk1.ClaimCapability(suite.ctx, cap, "transfer")) + suite.Require().NoError(sk2.ClaimCapability(suite.ctx, cap, "transfer")) + + got, ok := sk1.GetCapability(suite.ctx, "transfer") + suite.Require().True(ok) + suite.Require().Equal(cap, got) + + got, ok = sk2.GetCapability(suite.ctx, "transfer") + suite.Require().True(ok) + suite.Require().Equal(cap, got) +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} diff --git a/x/capability/module.go b/x/capability/module.go new file mode 100644 index 000000000000..1ee89573c4b4 --- /dev/null +++ b/x/capability/module.go @@ -0,0 +1,117 @@ +package capability + +import ( + "encoding/json" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + abci "github.com/tendermint/tendermint/abci/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} + + // TODO: Enable simulation once concrete types are defined. + // _ module.AppModuleSimulation = AppModuleSimulation{} +) + +// ---------------------------------------------------------------------------- +// AppModuleBasic +// ---------------------------------------------------------------------------- + +// AppModuleBasic implements the AppModuleBasic interface for the capability module. +type AppModuleBasic struct { +} + +func NewAppModuleBasic() AppModuleBasic { + return AppModuleBasic{} +} + +// Name returns the capability module's name. +func (AppModuleBasic) Name() string { + return ModuleName +} + +// RegisterCodec registers the capability module's types to the provided codec. +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { + RegisterCodec(cdc) +} + +// DefaultGenesis returns the capability module's default genesis state. +func (AppModuleBasic) DefaultGenesis(_ codec.JSONMarshaler) json.RawMessage { return []byte("{}") } + +// ValidateGenesis performs genesis state validation for the capability module. +func (AppModuleBasic) ValidateGenesis(_ codec.JSONMarshaler, _ json.RawMessage) error { return nil } + +// RegisterRESTRoutes registers the capability module's REST service handlers. +func (a AppModuleBasic) RegisterRESTRoutes(_ context.CLIContext, _ *mux.Router) {} + +// GetTxCmd returns the capability module's root tx command. +func (a AppModuleBasic) GetTxCmd(_ *codec.Codec) *cobra.Command { return nil } + +// GetTxCmd returns the capability module's root query command. +func (AppModuleBasic) GetQueryCmd(_ *codec.Codec) *cobra.Command { return nil } + +// ---------------------------------------------------------------------------- +// AppModule +// ---------------------------------------------------------------------------- + +// AppModule implements the AppModule interface for the capability module. +type AppModule struct { + AppModuleBasic + + keeper Keeper +} + +func NewAppModule(keeper Keeper) AppModule { + return AppModule{ + AppModuleBasic: NewAppModuleBasic(), + keeper: keeper, + } +} + +// Name returns the capability module's name. +func (am AppModule) Name() string { + return am.AppModuleBasic.Name() +} + +// Route returns the capability module's message routing key. +func (AppModule) Route() string { return "" } + +// QuerierRoute returns the capability module's query routing key. +func (AppModule) QuerierRoute() string { return "" } + +// NewHandler returns the capability module's message Handler. +func (am AppModule) NewHandler() sdk.Handler { return nil } + +// NewQuerierHandler returns the capability module's Querier. +func (am AppModule) NewQuerierHandler() sdk.Querier { return nil } + +// RegisterInvariants registers the capability module's invariants. +func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// InitGenesis performs the capability module's genesis initialization It returns +// no validator updates. +func (am AppModule) InitGenesis(_ sdk.Context, _ codec.JSONMarshaler, _ json.RawMessage) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the capability module's exported genesis state as raw JSON bytes. +func (am AppModule) ExportGenesis(_ sdk.Context, cdc codec.JSONMarshaler) json.RawMessage { + return am.DefaultGenesis(cdc) +} + +// BeginBlock executes all ABCI BeginBlock logic respective to the capability module. +func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +// EndBlock executes all ABCI EndBlock logic respective to the capability module. It +// returns no validator updates. +func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/capability/types/codec.go b/x/capability/types/codec.go new file mode 100644 index 000000000000..7b483557f2ae --- /dev/null +++ b/x/capability/types/codec.go @@ -0,0 +1,28 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +// ModuleCdc defines the capability module's codec. The codec is not sealed as to +// allow other modules to register their concrete Capability types. +var ModuleCdc = codec.New() + +// RegisterCodec registers all the necessary types and interfaces for the +// capability module. +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterInterface((*Capability)(nil), nil) + cdc.RegisterConcrete(&CapabilityKey{}, "cosmos-sdk/CapabilityKey", nil) + cdc.RegisterConcrete(Owner{}, "cosmos-sdk/Owner", nil) + cdc.RegisterConcrete(&CapabilityOwners{}, "cosmos-sdk/CapabilityOwners", nil) +} + +// RegisterCapabilityTypeCodec registers an external concrete Capability type +// defined in another module for the internal ModuleCdc. +func RegisterCapabilityTypeCodec(o interface{}, name string) { + ModuleCdc.RegisterConcrete(o, name, nil) +} + +func init() { + RegisterCodec(ModuleCdc) +} diff --git a/x/capability/types/errors.go b/x/capability/types/errors.go new file mode 100644 index 000000000000..fbf718e9fecc --- /dev/null +++ b/x/capability/types/errors.go @@ -0,0 +1,13 @@ +package types + +// DONTCOVER + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// x/capability module sentinel errors +var ( + ErrCapabilityTaken = sdkerrors.Register(ModuleName, 2, "capability name already taken") + ErrOwnerClaimed = sdkerrors.Register(ModuleName, 3, "given owner already claimed capability") +) diff --git a/x/capability/types/keys.go b/x/capability/types/keys.go new file mode 100644 index 000000000000..76726015246b --- /dev/null +++ b/x/capability/types/keys.go @@ -0,0 +1,51 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // ModuleName defines the module name + ModuleName = "capability" + + // StoreKey defines the primary module store key + StoreKey = ModuleName + + // MemStoreKey defines the in-memory store key + MemStoreKey = "mem_capability" +) + +var ( + // KeyIndex defines the key that stores the current globally unique capability + // index. + KeyIndex = []byte("index") + + // KeyPrefixIndexCapability defines a key prefix that stores index to capability + // name mappings. + KeyPrefixIndexCapability = []byte("capability_index") +) + +// RevCapabilityKey returns a reverse lookup key for a given module and capability +// name. +func RevCapabilityKey(module, name string) []byte { + return []byte(fmt.Sprintf("%s/rev/%s", module, name)) +} + +// FwdCapabilityKey returns a forward lookup key for a given module and capability +// reference. +func FwdCapabilityKey(module string, cap Capability) []byte { + return []byte(fmt.Sprintf("%s/fwd/%p", module, cap)) +} + +// IndexToKey returns bytes to be used as a key for a given capability index. +func IndexToKey(index uint64) []byte { + return sdk.Uint64ToBigEndian(index) +} + +// IndexFromKey returns an index from a call to IndexToKey for a given capability +// index. +func IndexFromKey(key []byte) uint64 { + return sdk.BigEndianToUint64(key) +} diff --git a/x/capability/types/keys_test.go b/x/capability/types/keys_test.go new file mode 100644 index 000000000000..fb380888e90f --- /dev/null +++ b/x/capability/types/keys_test.go @@ -0,0 +1,29 @@ +package types_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/x/capability/types" +) + +func TestRevCapabilityKey(t *testing.T) { + expected := []byte("bank/rev/send") + require.Equal(t, expected, types.RevCapabilityKey("bank", "send")) +} + +func TestFwdCapabilityKey(t *testing.T) { + cap := types.NewCapabilityKey(23) + expected := []byte(fmt.Sprintf("bank/fwd/%p", cap)) + require.Equal(t, expected, types.FwdCapabilityKey("bank", cap)) +} + +func TestIndexToKey(t *testing.T) { + require.Equal(t, []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x5a}, types.IndexToKey(3162)) +} + +func TestIndexFromKey(t *testing.T) { + require.Equal(t, uint64(3162), types.IndexFromKey([]byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x5a})) +} diff --git a/x/capability/types/types.go b/x/capability/types/types.go new file mode 100644 index 000000000000..afa5b79b4e93 --- /dev/null +++ b/x/capability/types/types.go @@ -0,0 +1,93 @@ +package types + +import ( + "fmt" + "sort" + + "gopkg.in/yaml.v2" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +var _ Capability = (*CapabilityKey)(nil) + +// Capability defines the interface a capability must implement. The given +// capability must provide a GUID. +type Capability interface { + GetIndex() uint64 + String() string +} + +// CapabilityKey defines an implementation of a Capability. The index provided to +// a CapabilityKey must be globally unique. +type CapabilityKey struct { + Index uint64 `json:"index" yaml:"index"` +} + +// NewCapabilityKey returns a reference to a new CapabilityKey to be used as an +// actual capability. +func NewCapabilityKey(index uint64) Capability { + return &CapabilityKey{Index: index} +} + +// GetIndex returns the capability key's index. +func (ck *CapabilityKey) GetIndex() uint64 { + return ck.Index +} + +// String returns the string representation of a CapabilityKey. The string contains +// the CapabilityKey's memory reference as the string is to be used in a composite +// key and to authenticate capabilities. +func (ck *CapabilityKey) String() string { + return fmt.Sprintf("CapabilityKey{%p, %d}", ck, ck.Index) +} + +// Owner defines a single capability owner. An owner is defined by the name of +// capability and the module name. +type Owner struct { + Module string `json:"module" yaml:"module"` + Name string `json:"name" yaml:"name"` +} + +func NewOwner(module, name string) Owner { + return Owner{Module: module, Name: name} +} + +// Key returns a composite key for an Owner. +func (o Owner) Key() string { + return fmt.Sprintf("%s/%s", o.Module, o.Name) +} + +func (o Owner) String() string { + bz, _ := yaml.Marshal(o) + return string(bz) +} + +// CapabilityOwners defines a set of owners of a single Capability. The set of +// owners must be unique. +type CapabilityOwners struct { + Owners []Owner `json:"owners" yaml:"owners"` +} + +func NewCapabilityOwners() *CapabilityOwners { + return &CapabilityOwners{Owners: make([]Owner, 0)} +} + +// Set attempts to add a given owner to the CapabilityOwners. If the owner +// already exists, an error will be returned. Set runs in O(log n) average time +// and O(n) in the worst case. +func (co *CapabilityOwners) Set(owner Owner) error { + // find smallest index s.t. co.Owners[i] >= owner in O(log n) time + i := sort.Search(len(co.Owners), func(i int) bool { return co.Owners[i].Key() >= owner.Key() }) + if i < len(co.Owners) && co.Owners[i].Key() == owner.Key() { + // owner already exists at co.Owners[i] + return sdkerrors.Wrapf(ErrOwnerClaimed, owner.String()) + } + + // owner does not exist in the set of owners, so we insert at position i + co.Owners = append(co.Owners, Owner{}) // expand by 1 in amortized O(1) / O(n) worst case + copy(co.Owners[i+1:], co.Owners[i:]) + co.Owners[i] = owner + + return nil +} diff --git a/x/capability/types/types_test.go b/x/capability/types/types_test.go new file mode 100644 index 000000000000..0c1ba6e3d3bc --- /dev/null +++ b/x/capability/types/types_test.go @@ -0,0 +1,49 @@ +package types_test + +import ( + "fmt" + "sort" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/x/capability/types" +) + +func TestCapabilityKey(t *testing.T) { + idx := uint64(3162) + cap := types.NewCapabilityKey(idx) + require.Equal(t, idx, cap.GetIndex()) + require.Equal(t, fmt.Sprintf("CapabilityKey{%p, %d}", cap, idx), cap.String()) +} + +func TestOwner(t *testing.T) { + o := types.NewOwner("bank", "send") + require.Equal(t, "bank/send", o.Key()) + require.Equal(t, "module: bank\nname: send\n", o.String()) +} + +func TestCapabilityOwners(t *testing.T) { + co := types.NewCapabilityOwners() + + owners := make([]types.Owner, 1024) + for i := range owners { + var owner types.Owner + + if i%2 == 0 { + owner = types.NewOwner("bank", fmt.Sprintf("send-%d", i)) + } else { + owner = types.NewOwner("slashing", fmt.Sprintf("slash-%d", i)) + } + + owners[i] = owner + require.NoError(t, co.Set(owner)) + } + + sort.Slice(owners, func(i, j int) bool { return owners[i].Key() < owners[j].Key() }) + require.Equal(t, owners, co.Owners) + + for _, owner := range owners { + require.Error(t, co.Set(owner)) + } +}