Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

R4R: keeper custom queries #1918

Merged
merged 11 commits into from
Aug 22, 2018
1 change: 1 addition & 0 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ FEATURES
* Gaia

* SDK
* [querier] added custom querier functionality, so ABCI query requests can be handled by keepers

* Tendermint

Expand Down
60 changes: 46 additions & 14 deletions baseapp/baseapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,14 @@ const (
// BaseApp reflects the ABCI application implementation.
type BaseApp struct {
// initialized on creation
Logger log.Logger
name string // application name from abci.Info
db dbm.DB // common DB backend
cms sdk.CommitMultiStore // Main (uncached) state
router Router // handle any kind of message
codespacer *sdk.Codespacer // handle module codespacing
txDecoder sdk.TxDecoder // unmarshal []byte into sdk.Tx
Logger log.Logger
name string // application name from abci.Info
db dbm.DB // common DB backend
cms sdk.CommitMultiStore // Main (uncached) state
router Router // handle any kind of message
queryRouter QueryRouter // router for redirecting query calls
codespacer *sdk.Codespacer // handle module codespacing
txDecoder sdk.TxDecoder // unmarshal []byte into sdk.Tx

anteHandler sdk.AnteHandler // ante handler for fee and auth

Expand Down Expand Up @@ -84,13 +85,14 @@ var _ abci.Application = (*BaseApp)(nil)
// Accepts variable number of option functions, which act on the BaseApp to set configuration choices
func NewBaseApp(name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecoder, options ...func(*BaseApp)) *BaseApp {
app := &BaseApp{
Logger: logger,
name: name,
db: db,
cms: store.NewCommitMultiStore(db),
router: NewRouter(),
codespacer: sdk.NewCodespacer(),
txDecoder: txDecoder,
Logger: logger,
name: name,
db: db,
cms: store.NewCommitMultiStore(db),
router: NewRouter(),
queryRouter: NewQueryRouter(),
codespacer: sdk.NewCodespacer(),
txDecoder: txDecoder,
}

// Register the undefined & root codespaces, which should not be used by
Expand Down Expand Up @@ -266,6 +268,7 @@ func (app *BaseApp) FilterPeerByPubKey(info string) abci.ResponseQuery {
return abci.ResponseQuery{}
}

// Splits a string path using the delimter '/'. i.e. "this/is/funny" becomes []string{"this", "is", "funny"}
func splitPath(requestPath string) (path []string) {
path = strings.Split(requestPath, "/")
// first element is empty string
Expand All @@ -291,6 +294,8 @@ func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) {
return handleQueryStore(app, path, req)
case "p2p":
return handleQueryP2P(app, path, req)
case "custom":
return handleQueryCustom(app, path, req)
}

msg := "unknown query path"
Expand Down Expand Up @@ -362,6 +367,33 @@ func handleQueryP2P(app *BaseApp, path []string, req abci.RequestQuery) (res abc
return sdk.ErrUnknownRequest(msg).QueryResult()
}

func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) {
// path[0] should be "custom" because "/custom" prefix is required for keeper queries.
// the queryRouter routes using path[1]. For example, in the path "custom/gov/proposal", queryRouter routes using "gov"
if path[1] == "" {
sdk.ErrUnknownRequest("No route for custom query specified").QueryResult()
}
querier := app.queryRouter.Route(path[1])
if querier == nil {
sdk.ErrUnknownRequest(fmt.Sprintf("no custom querier found for route %s", path[1])).QueryResult()
}

ctx := sdk.NewContext(app.cms.CacheMultiStore(), app.checkState.ctx.BlockHeader(), true, app.Logger)
// Passes the rest of the path as an argument to the querier.
// For example, in the path "custom/gov/proposal/test", the gov querier gets []string{"proposal", "test"} as the path
resBytes, err := querier(ctx, path[2:], req)
if err != nil {
return abci.ResponseQuery{
Code: uint32(err.ABCICode()),
Copy link
Contributor

@jaekwon jaekwon Aug 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a ResponseQuery.Log where we can put the err.String() in? That's for nondeterministic debug info like this.

Log: err.ABCILog(),
}
}
return abci.ResponseQuery{
Code: uint32(sdk.ABCICodeOK),
Value: resBytes,
}
}

// BeginBlock implements the ABCI application interface.
func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) (res abci.ResponseBeginBlock) {
if app.cms.TracingEnabled() {
Expand Down
41 changes: 41 additions & 0 deletions baseapp/queryrouter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package baseapp

import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

// QueryRouter provides queryables for each query path.
type QueryRouter interface {
AddRoute(r string, h sdk.Querier) (rtr QueryRouter)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h - handler, right?

what's rtr?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

router I think

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the rtr is fine personally

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd change r --> route and h --> handler

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine IMO

Route(path string) (h sdk.Querier)
}

type queryrouter struct {
routes map[string]sdk.Querier
}

// nolint
// NewRouter - create new router
// TODO either make Function unexported or make return type (router) Exported
func NewQueryRouter() *queryrouter {
return &queryrouter{
routes: map[string]sdk.Querier{},
}
}

// AddRoute - Adds an sdk.Querier to the route provided. Panics on duplicate
func (rtr *queryrouter) AddRoute(r string, q sdk.Querier) QueryRouter {
if !isAlphaNumeric(r) {
panic("route expressions can only contain alphanumeric characters")
}
if rtr.routes[r] != nil {
panic("route has already been initialized")
}
rtr.routes[r] = q
return rtr
}

// Returns the sdk.Querier for a certain route path
func (rtr *queryrouter) Route(path string) (h sdk.Querier) {
return rtr.routes[path]
}
3 changes: 3 additions & 0 deletions baseapp/setters.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ func (app *BaseApp) Router() Router {
}
return app.router
}
func (app *BaseApp) QueryRouter() QueryRouter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does sealing do on the baseapp and do we need to seal the query router as well? anything else we're missing? docs would be helpful!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why we have the seal it. I just saw that the router was sealed, so I sealed the QueryRouter as well

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think we should restrict sealing to fields that actually need to be sealed for clarity, otherwise readers of the code will have a hard time figuring out what our intent was (as apparently we ourselves have a hard time figuring out already!). As I understand, this is just defensive coding - I believe @mossid wrote the original PR - and we only need to do it for write-once fields or fields that should not be read from after a certain event in the app lifecycle.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, don't we want to query the QueryRouter after the app is sealed?

return app.queryRouter
}
func (app *BaseApp) Seal() { app.sealed = true }
func (app *BaseApp) IsSealed() bool { return app.sealed }
func (app *BaseApp) enforceSeal() {
Expand Down
5 changes: 5 additions & 0 deletions client/context/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ func (ctx CLIContext) Query(path string) (res []byte, err error) {
return ctx.query(path, nil)
}

// Query information about the connected node with a data payload
func (ctx CLIContext) QueryWithData(path string, data []byte) (res []byte, err error) {
return ctx.query(path, data)
}

// QueryStore performs a query from a Tendermint node with the provided key and
// store name.
func (ctx CLIContext) QueryStore(key cmn.HexBytes, storeName string) (res []byte, err error) {
Expand Down
3 changes: 3 additions & 0 deletions cmd/gaia/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ func NewGaiaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio
AddRoute("slashing", slashing.NewHandler(app.slashingKeeper)).
AddRoute("gov", gov.NewHandler(app.govKeeper))

app.QueryRouter().
AddRoute("gov", gov.NewQuerier(app.govKeeper))

// initialize BaseApp
app.SetInitChainer(app.initChainer)
app.SetBeginBlocker(app.BeginBlocker)
Expand Down
2 changes: 1 addition & 1 deletion docs/sdk/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

The Cosmos SDK has all the necessary pre-built modules to add functionality on top of a `BaseApp`, which is the template to build a blockchain dApp in Cosmos. In this context, a `module` is a fundamental unit in the Cosmos SDK.

Each module is an extension of the `BaseApp`'s functionalities that defines transactions, handles application state and manages the state transition logic. Each module also contains handlers for messages and transactions, as well as REST and CLI for secure user interactions.
Each module is an extension of the `BaseApp`'s functionalities that defines transactions, handles application state and manages the state transition logic. Each module also contains handlers for messages and transactions, queriers for handling query requests, as well as REST and CLI for secure user interactions.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a more detailed description on how Queriers work. Otherwise please submit a separate issue

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Some of the most important modules in the SDK are:

Expand Down
35 changes: 35 additions & 0 deletions types/account.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package types

import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
Expand Down Expand Up @@ -108,6 +109,23 @@ func (bz AccAddress) Format(s fmt.State, verb rune) {
}
}

// Returns boolean for whether two AccAddresses are Equal
func (bz AccAddress) Equals(bz2 AccAddress) bool {
if bz.Empty() && bz2.Empty() {
return true
}
return bytes.Compare(bz.Bytes(), bz2.Bytes()) == 0
}

// Returns boolean for whether an AccAddress is empty
func (bz AccAddress) Empty() bool {
if bz == nil {
return true
}
bz2 := AccAddress{}
return bytes.Compare(bz.Bytes(), bz2.Bytes()) == 0
}

//__________________________________________________________

// AccAddress a wrapper around bytes meant to represent a validator address
Expand Down Expand Up @@ -192,6 +210,23 @@ func (bz ValAddress) Format(s fmt.State, verb rune) {
}
}

// Returns boolean for whether two ValAddresses are Equal
func (bz ValAddress) Equals(bz2 ValAddress) bool {
if bz.Empty() && bz2.Empty() {
return true
}
return bytes.Compare(bz.Bytes(), bz2.Bytes()) == 0
}

// Returns boolean for whether an AccAddress is empty
func (bz ValAddress) Empty() bool {
if bz == nil {
return true
}
bz2 := ValAddress{}
return bytes.Compare(bz.Bytes(), bz2.Bytes()) == 0
}

// Bech32ifyAccPub takes AccountPubKey and returns the bech32 encoded string
func Bech32ifyAccPub(pub crypto.PubKey) (string, error) {
return bech32.ConvertAndEncode(Bech32PrefixAccPub, pub.Bytes())
Expand Down
6 changes: 6 additions & 0 deletions types/queryable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package types

import abci "github.com/tendermint/tendermint/abci/types"

// Type for querier functions on keepers to implement to handle custom queries
type Querier = func(ctx Context, path []string, req abci.RequestQuery) (res []byte, err Error)
Loading