diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 1727c2a4204c..5ac7be1b50de 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -199,9 +199,14 @@ func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitC } // Implements ABCI. +// Delegates to CommitMultiStore if it implements Queryable func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) { - // TODO: See app/query.go - return + queryable, ok := app.cms.(sdk.Queryable) + if !ok { + msg := "application doesn't support queries" + return sdk.ErrUnknownRequest(msg).Result().ToQuery() + } + return queryable.Query(req) } // Implements ABCI. diff --git a/store/cachemultistore.go b/store/cachemultistore.go index b53899b3d5c6..b1a7548811ea 100644 --- a/store/cachemultistore.go +++ b/store/cachemultistore.go @@ -10,14 +10,18 @@ import ( // cacheMultiStore holds many cache-wrapped stores. // Implements MultiStore. type cacheMultiStore struct { - db CacheKVStore - stores map[StoreKey]CacheWrap + db CacheKVStore + stores map[StoreKey]CacheWrap + keysByName map[string]StoreKey } +var _ CacheMultiStore = cacheMultiStore{} + func newCacheMultiStoreFromRMS(rms *rootMultiStore) cacheMultiStore { cms := cacheMultiStore{ - db: NewCacheKVStore(dbStoreAdapter{rms.db}), - stores: make(map[StoreKey]CacheWrap, len(rms.stores)), + db: NewCacheKVStore(dbStoreAdapter{rms.db}), + stores: make(map[StoreKey]CacheWrap, len(rms.stores)), + keysByName: rms.keysByName, } for key, store := range rms.stores { cms.stores[key] = store.CacheWrap() diff --git a/store/iavlstore.go b/store/iavlstore.go index 59b520954805..52deef367a40 100644 --- a/store/iavlstore.go +++ b/store/iavlstore.go @@ -4,6 +4,7 @@ import ( "fmt" "sync" + abci "github.com/tendermint/abci/types" "github.com/tendermint/iavl" cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" @@ -31,6 +32,7 @@ func LoadIAVLStore(db dbm.DB, id CommitID) (CommitStore, error) { var _ KVStore = (*iavlStore)(nil) var _ CommitStore = (*iavlStore)(nil) +var _ Queryable = (*iavlStore)(nil) // iavlStore Implements KVStore and CommitStore. type iavlStore struct { @@ -123,6 +125,55 @@ func (st *iavlStore) ReverseIterator(start, end []byte) Iterator { return newIAVLIterator(st.tree.Tree(), start, end, false) } +// Query implements ABCI interface, allows queries +// +// by default we will return from (latest height -1), +// as we will have merkle proofs immediately (header height = data height + 1) +// If latest-1 is not present, use latest (which must be present) +// if you care to have the latest data to see a tx results, you must +// explicitly set the height you want to see +func (st *iavlStore) Query(req abci.RequestQuery) (res abci.ResponseQuery) { + if len(req.Data) == 0 { + msg := "Query cannot be zero length" + return sdk.ErrTxParse(msg).Result().ToQuery() + } + + tree := st.tree + height := req.Height + if height == 0 { + latest := tree.Version64() + if tree.VersionExists(latest - 1) { + height = latest - 1 + } else { + height = latest + } + } + // store the height we chose in the response + res.Height = height + + switch req.Path { + case "/store", "/key": // Get by key + key := req.Data // Data holds the key bytes + res.Key = key + if req.Prove { + value, proof, err := tree.GetVersionedWithProof(key, height) + if err != nil { + res.Log = err.Error() + break + } + res.Value = value + res.Proof = proof.Bytes() + } else { + _, res.Value = tree.GetVersioned(key, height) + } + + default: + msg := fmt.Sprintf("Unexpected Query path: %v", req.Path) + return sdk.ErrUnknownRequest(msg).Result().ToQuery() + } + return +} + //---------------------------------------- // Implements Iterator. diff --git a/store/iavlstore_test.go b/store/iavlstore_test.go index 80731c24ee3a..c426e4d8a8a5 100644 --- a/store/iavlstore_test.go +++ b/store/iavlstore_test.go @@ -5,10 +5,12 @@ import ( "github.com/stretchr/testify/assert" + abci "github.com/tendermint/abci/types" + "github.com/tendermint/iavl" cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" - "github.com/tendermint/iavl" + sdk "github.com/cosmos/cosmos-sdk/types" ) var ( @@ -79,3 +81,63 @@ func TestIAVLIterator(t *testing.T) { i += 1 } } + +func TestIAVLStoreQuery(t *testing.T) { + db := dbm.NewMemDB() + tree := iavl.NewVersionedTree(db, cacheSize) + iavlStore := newIAVLStore(tree, numHistory) + + k, v := []byte("wind"), []byte("blows") + k2, v2 := []byte("water"), []byte("flows") + v3 := []byte("is cold") + // k3, v3 := []byte("earth"), []byte("soes") + // k4, v4 := []byte("fire"), []byte("woes") + + cid := iavlStore.Commit() + ver := cid.Version + query := abci.RequestQuery{Path: "/key", Data: k, Height: ver} + + // set data without commit, doesn't show up + iavlStore.Set(k, v) + qres := iavlStore.Query(query) + assert.Equal(t, uint32(sdk.CodeOK), qres.Code) + assert.Nil(t, qres.Value) + + // commit it, but still don't see on old version + cid = iavlStore.Commit() + qres = iavlStore.Query(query) + assert.Equal(t, uint32(sdk.CodeOK), qres.Code) + assert.Nil(t, qres.Value) + + // but yes on the new version + query.Height = cid.Version + qres = iavlStore.Query(query) + assert.Equal(t, uint32(sdk.CodeOK), qres.Code) + assert.Equal(t, v, qres.Value) + + // modify + iavlStore.Set(k2, v2) + iavlStore.Set(k, v3) + cid = iavlStore.Commit() + + // query will return old values, as height is fixed + qres = iavlStore.Query(query) + assert.Equal(t, uint32(sdk.CodeOK), qres.Code) + assert.Equal(t, v, qres.Value) + + // update to latest in the query and we are happy + query.Height = cid.Version + qres = iavlStore.Query(query) + assert.Equal(t, uint32(sdk.CodeOK), qres.Code) + assert.Equal(t, v3, qres.Value) + query2 := abci.RequestQuery{Path: "/key", Data: k2, Height: cid.Version} + qres = iavlStore.Query(query2) + assert.Equal(t, uint32(sdk.CodeOK), qres.Code) + assert.Equal(t, v2, qres.Value) + + // default (height 0) will show latest -1 + query0 := abci.RequestQuery{Path: "/store", Data: k} + qres = iavlStore.Query(query0) + assert.Equal(t, uint32(sdk.CodeOK), qres.Code) + assert.Equal(t, v, qres.Value) +} diff --git a/store/rootmultistore.go b/store/rootmultistore.go index f964a1fd5ba8..dd6361ea1b92 100644 --- a/store/rootmultistore.go +++ b/store/rootmultistore.go @@ -2,10 +2,13 @@ package store import ( "fmt" + "strings" + "golang.org/x/crypto/ripemd160" + + abci "github.com/tendermint/abci/types" dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/merkle" - "golang.org/x/crypto/ripemd160" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -24,15 +27,18 @@ type rootMultiStore struct { lastCommitID CommitID storesParams map[StoreKey]storeParams stores map[StoreKey]CommitStore + keysByName map[string]StoreKey } var _ CommitMultiStore = (*rootMultiStore)(nil) +var _ Queryable = (*rootMultiStore)(nil) func NewCommitMultiStore(db dbm.DB) *rootMultiStore { return &rootMultiStore{ db: db, storesParams: make(map[StoreKey]storeParams), stores: make(map[StoreKey]CommitStore), + keysByName: make(map[string]StoreKey), } } @@ -53,6 +59,7 @@ func (rs *rootMultiStore) MountStoreWithDB(key StoreKey, typ StoreType, db dbm.D db: db, typ: typ, } + rs.keysByName[key.Name()] = key } // Implements CommitMultiStore. @@ -169,6 +176,66 @@ func (rs *rootMultiStore) GetKVStore(key StoreKey) KVStore { return rs.stores[key].(KVStore) } +// getStoreByName will first convert the original name to +// a special key, before looking up the CommitStore. +// This is not exposed to the extensions (which will need the +// StoreKey), but is useful in main, and particularly app.Query, +// in order to convert human strings into CommitStores. +func (rs *rootMultiStore) getStoreByName(name string) Store { + key := rs.keysByName[name] + if key == nil { + return nil + } + return rs.stores[key] +} + +//---------------------- Query ------------------ + +// Query calls substore.Query with the same `req` where `req.Path` is +// modified to remove the substore prefix. +// Ie. `req.Path` here is `//`, and trimmed to `/` for the substore. +// TODO: add proof for `multistore -> substore`. +func (rs *rootMultiStore) Query(req abci.RequestQuery) abci.ResponseQuery { + // Query just routes this to a substore. + path := req.Path + storeName, subpath, err := parsePath(path) + if err != nil { + return err.Result().ToQuery() + } + + store := rs.getStoreByName(storeName) + if store == nil { + msg := fmt.Sprintf("no such store: %s", storeName) + return sdk.ErrUnknownRequest(msg).Result().ToQuery() + } + queryable, ok := store.(Queryable) + if !ok { + msg := fmt.Sprintf("store %s doesn't support queries", storeName) + return sdk.ErrUnknownRequest(msg).Result().ToQuery() + } + + // trim the path and make the query + req.Path = subpath + res := queryable.Query(req) + return res +} + +// parsePath expects a format like /[/] +// Must start with /, subpath may be empty +// Returns error if it doesn't start with / +func parsePath(path string) (storeName string, subpath string, err sdk.Error) { + if !strings.HasPrefix(path, "/") { + err = sdk.ErrUnknownRequest(fmt.Sprintf("invalid path: %s", path)) + return + } + paths := strings.SplitN(path[1:], "/", 2) + storeName = paths[0] + if len(paths) == 2 { + subpath = "/" + paths[1] + } + return +} + //---------------------------------------- func (rs *rootMultiStore) loadCommitStoreFromParams(id CommitID, params storeParams) (store CommitStore, err error) { diff --git a/store/rootmultistore_test.go b/store/rootmultistore_test.go index 99e905062888..333f0f5af024 100644 --- a/store/rootmultistore_test.go +++ b/store/rootmultistore_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + abci "github.com/tendermint/abci/types" dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/merkle" @@ -20,6 +21,14 @@ func TestMultistoreCommitLoad(t *testing.T) { commitID := CommitID{} checkStore(t, store, commitID, commitID) + // make sure we can get stores by name + s1 := store.getStoreByName("store1") + assert.NotNil(t, s1) + s3 := store.getStoreByName("store3") + assert.NotNil(t, s3) + s77 := store.getStoreByName("store77") + assert.Nil(t, s77) + // make a few commits and check them nCommits := int64(3) for i := int64(0); i < nCommits; i++ { @@ -62,6 +71,89 @@ func TestMultistoreCommitLoad(t *testing.T) { checkStore(t, store, commitID, commitID) } +func TestParsePath(t *testing.T) { + _, _, err := parsePath("foo") + assert.Error(t, err) + + store, subpath, err := parsePath("/foo") + assert.NoError(t, err) + assert.Equal(t, store, "foo") + assert.Equal(t, subpath, "") + + store, subpath, err = parsePath("/fizz/bang/baz") + assert.NoError(t, err) + assert.Equal(t, store, "fizz") + assert.Equal(t, subpath, "/bang/baz") + + substore, subsubpath, err := parsePath(subpath) + assert.NoError(t, err) + assert.Equal(t, substore, "bang") + assert.Equal(t, subsubpath, "/baz") + +} + +func TestMultiStoreQuery(t *testing.T) { + db := dbm.NewMemDB() + multi := newMultiStoreWithMounts(db) + err := multi.LoadLatestVersion() + assert.Nil(t, err) + + k, v := []byte("wind"), []byte("blows") + k2, v2 := []byte("water"), []byte("flows") + // v3 := []byte("is cold") + + cid := multi.Commit() + + // make sure we can get by name + garbage := multi.getStoreByName("bad-name") + assert.Nil(t, garbage) + + // set and commit data in one store + store1 := multi.getStoreByName("store1").(KVStore) + store1.Set(k, v) + + // and another + store2 := multi.getStoreByName("store2").(KVStore) + store2.Set(k2, v2) + + // commit the multistore + cid = multi.Commit() + ver := cid.Version + + // bad path + query := abci.RequestQuery{Path: "/key", Data: k, Height: ver} + qres := multi.Query(query) + assert.Equal(t, uint32(sdk.CodeUnknownRequest), qres.Code) + + query.Path = "h897fy32890rf63296r92" + qres = multi.Query(query) + assert.Equal(t, uint32(sdk.CodeUnknownRequest), qres.Code) + + // invalid store name + query.Path = "/garbage/key" + qres = multi.Query(query) + assert.Equal(t, uint32(sdk.CodeUnknownRequest), qres.Code) + + // valid query with data + query.Path = "/store1/key" + qres = multi.Query(query) + assert.Equal(t, uint32(sdk.CodeOK), qres.Code) + assert.Equal(t, v, qres.Value) + + // valid but empty + query.Path = "/store2/key" + query.Prove = true + qres = multi.Query(query) + assert.Equal(t, uint32(sdk.CodeOK), qres.Code) + assert.Nil(t, qres.Value) + + // store2 data + query.Data = k2 + qres = multi.Query(query) + assert.Equal(t, uint32(sdk.CodeOK), qres.Code) + assert.Equal(t, v2, qres.Value) +} + //----------------------------------------------------------------------- // utils diff --git a/store/types.go b/store/types.go index e6946fb4d5a1..ec842d9cf813 100644 --- a/store/types.go +++ b/store/types.go @@ -19,3 +19,4 @@ type CacheWrap = types.CacheWrap type CommitID = types.CommitID type StoreKey = types.StoreKey type StoreType = types.StoreType +type Queryable = types.Queryable diff --git a/types/errors.go b/types/errors.go index 3f93045f693b..a878149313e5 100644 --- a/types/errors.go +++ b/types/errors.go @@ -2,8 +2,9 @@ package types import ( "fmt" - "github.com/tendermint/go-crypto" "runtime" + + "github.com/tendermint/go-crypto" ) type CodeType uint32 diff --git a/types/result.go b/types/result.go index 412a9778defc..c1afec00ce03 100644 --- a/types/result.go +++ b/types/result.go @@ -38,3 +38,11 @@ type Result struct { func (res Result) IsOK() bool { return res.Code.IsOK() } + +// ToQuery allows us to return sdk.Error.Result() in query responses +func (res Result) ToQuery() abci.ResponseQuery { + return abci.ResponseQuery{ + Code: uint32(res.Code), + Log: res.Log, + } +} diff --git a/types/store.go b/types/store.go index f9e8c912a30d..6802a4bf1077 100644 --- a/types/store.go +++ b/types/store.go @@ -3,6 +3,7 @@ package types import ( "fmt" + abci "github.com/tendermint/abci/types" dbm "github.com/tendermint/tmlibs/db" ) @@ -25,6 +26,14 @@ type CommitStore interface { Store } +// Queryable allows a Store to expose internal state to the abci.Query +// interface. Multistore can route requests to the proper Store. +// +// This is an optional, but useful extension to any CommitStore +type Queryable interface { + Query(abci.RequestQuery) abci.ResponseQuery +} + //---------------------------------------- // MultiStore