From a68ef74b07c27062f2e5bf774d3ce6b77e9d6c4a Mon Sep 17 00:00:00 2001 From: Nick DeLuca Date: Mon, 29 Jun 2020 18:39:35 -0500 Subject: [PATCH] Query old blocks for proposals in CLI (#598) * move file to query.go (we are adding functionality so specific name doesn't fit anymore) * Add tx search for proposals in cli query proposal * add rest support, height support for rest api, and add go doc string * add in deadline calculation * update changelog Co-authored-by: Kevin Davis --- CHANGELOG.md | 5 +- x/committee/client/cli/query.go | 12 +- x/committee/client/common/query.go | 164 ++++++++++++++++++++ x/committee/client/common/query_proposer.go | 60 ------- x/committee/client/rest/query.go | 8 +- x/committee/keeper/querier.go | 13 +- x/committee/keeper/querier_test.go | 12 ++ x/committee/types/querier.go | 17 +- 8 files changed, 207 insertions(+), 84 deletions(-) create mode 100644 x/committee/client/common/query.go delete mode 100644 x/committee/client/common/query_proposer.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b996d02..0ab90f81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ [\#584](https://github.com/Kava-Labs/kava/pulls/584) Add REST client and CLI queries for `kavadist` module [\#578](https://github.com/Kava-Labs/kava/pulls/578) Add v0.3 compatible REST client that supports -``` + +```plaintext /v0_3/node_info /v0_3/auth/accounts/
/v0_3/ @@ -55,6 +56,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ /v0_3/distribution/delegators/
/rewards ``` +[\#598](https://github.com/Kava-Labs/kava/pulls/598) CLI and REST queries for committee proposals (ie `kvcli q committee proposal 1`) now query the historical state to return the proposal object before it was deleted from state + ## [v0.8.1](https://github.com/Kava-Labs/kava/releases/tag/v0.8.1) kava-3 Patch Release This version mitigates a memory leak in tendermint that was found prior to launching kava-3. It is fully compatible with v0.8.0 and is intended to replace that version as the canonical software version for upgrading the Kava mainnet from kava-2 to kava-3. Note that there are no breaking changes between the versions, but a safety check was added to this version to prevent starting the node with an unsafe configuration. diff --git a/x/committee/client/cli/query.go b/x/committee/client/cli/query.go index 8bda3128..c44170fe 100644 --- a/x/committee/client/cli/query.go +++ b/x/committee/client/cli/query.go @@ -130,20 +130,12 @@ func GetCmdQueryProposal(queryRoute string, cdc *codec.Codec) *cobra.Command { if err != nil { return fmt.Errorf("proposal-id %s not a valid uint", args[0]) } - bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID)) + + proposal, _, err := common.QueryProposalByID(cliCtx, cdc, queryRoute, proposalID) if err != nil { return err } - // Query - res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryProposal), bz) - if err != nil { - return err - } - - // Decode and print results - var proposal types.Proposal - cdc.MustUnmarshalJSON(res, &proposal) return cliCtx.PrintOutput(proposal) }, } diff --git a/x/committee/client/common/query.go b/x/committee/client/common/query.go new file mode 100644 index 00000000..89de2353 --- /dev/null +++ b/x/committee/client/common/query.go @@ -0,0 +1,164 @@ +package common + +import ( + "fmt" + "strings" + "time" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + + "github.com/kava-labs/kava/x/committee/types" +) + +// Note: QueryProposer is copied in from the gov module + +const ( + defaultPage = 1 + defaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19 +) + +// Proposer contains metadata of a governance proposal used for querying a proposer. +type Proposer struct { + ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"` + Proposer string `json:"proposer" yaml:"proposer"` +} + +// NewProposer returns a new Proposer given id and proposer +func NewProposer(proposalID uint64, proposer string) Proposer { + return Proposer{proposalID, proposer} +} + +func (p Proposer) String() string { + return fmt.Sprintf("Proposal with ID %d was proposed by %s", p.ProposalID, p.Proposer) +} + +// QueryProposer will query for a proposer of a governance proposal by ID. +func QueryProposer(cliCtx context.CLIContext, proposalID uint64) (Proposer, error) { + events := []string{ + fmt.Sprintf("%s.%s='%s'", sdk.EventTypeMessage, sdk.AttributeKeyAction, types.TypeMsgSubmitProposal), + fmt.Sprintf("%s.%s='%s'", types.EventTypeProposalSubmit, types.AttributeKeyProposalID, []byte(fmt.Sprintf("%d", proposalID))), + } + + // NOTE: SearchTxs is used to facilitate the txs query which does not currently + // support configurable pagination. + searchResult, err := utils.QueryTxsByEvents(cliCtx, events, defaultPage, defaultLimit) + if err != nil { + return Proposer{}, err + } + + for _, info := range searchResult.Txs { + for _, msg := range info.Tx.GetMsgs() { + // there should only be a single proposal under the given conditions + if msg.Type() == types.TypeMsgSubmitProposal { + subMsg := msg.(types.MsgSubmitProposal) + return NewProposer(proposalID, subMsg.Proposer.String()), nil + } + } + } + + return Proposer{}, fmt.Errorf("failed to find the proposer for proposalID %d", proposalID) +} + +// QueryProposalByID returns a proposal from state if present or fallbacks to searching old blocks +func QueryProposalByID(cliCtx context.CLIContext, cdc *codec.Codec, queryRoute string, proposalID uint64) (*types.Proposal, int64, error) { + bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID)) + if err != nil { + return nil, 0, err + } + + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryProposal), bz) + + if err == nil { + var proposal *types.Proposal + cdc.MustUnmarshalJSON(res, &proposal) + + return proposal, height, nil + } + + // NOTE: !errors.Is(err, types.ErrUnknownProposal) does not work here + if err != nil && !strings.Contains(err.Error(), "proposal not found") { + return nil, 0, err + } + + res, height, err = cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryNextProposalID), nil) + if err != nil { + return nil, 0, err + } + + var nextProposalID uint64 + cdc.MustUnmarshalJSON(res, &nextProposalID) + + if proposalID >= nextProposalID { + return nil, 0, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID) + } + + events := []string{ + fmt.Sprintf("%s.%s='%s'", sdk.EventTypeMessage, sdk.AttributeKeyAction, types.TypeMsgSubmitProposal), + fmt.Sprintf("%s.%s='%s'", types.EventTypeProposalSubmit, types.AttributeKeyProposalID, []byte(fmt.Sprintf("%d", proposalID))), + } + + searchResult, err := utils.QueryTxsByEvents(cliCtx, events, defaultPage, defaultLimit) + if err != nil { + return nil, 0, err + } + + for _, info := range searchResult.Txs { + for _, msg := range info.Tx.GetMsgs() { + if msg.Type() == types.TypeMsgSubmitProposal { + subMsg := msg.(types.MsgSubmitProposal) + + deadline, err := calculateDeadline(cliCtx, cdc, queryRoute, subMsg.CommitteeID, info.Height) + if err != nil { + return nil, 0, err + } + + return &types.Proposal{ + ID: proposalID, + CommitteeID: subMsg.CommitteeID, + PubProposal: subMsg.PubProposal, + Deadline: deadline, + }, height, nil + } + } + } + + return nil, 0, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID) +} + +// calculateDeadline returns the proposal deadline for a committee and block height +func calculateDeadline(cliCtx context.CLIContext, cdc *codec.Codec, queryRoute string, committeeID uint64, blockHeight int64) (time.Time, error) { + var deadline time.Time + + bz, err := cdc.MarshalJSON(types.NewQueryCommitteeParams(committeeID)) + if err != nil { + return deadline, err + } + + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryCommittee), bz) + if err != nil { + return deadline, err + } + + var committee types.Committee + err = cdc.UnmarshalJSON(res, &committee) + if err != nil { + return deadline, err + } + + node, err := cliCtx.GetNode() + if err != nil { + return deadline, err + } + + resultBlock, err := node.Block(&blockHeight) + if err != nil { + return deadline, err + } + + deadline = resultBlock.Block.Header.Time.Add(committee.ProposalDuration) + return deadline, nil +} diff --git a/x/committee/client/common/query_proposer.go b/x/committee/client/common/query_proposer.go deleted file mode 100644 index d6f12a56..00000000 --- a/x/committee/client/common/query_proposer.go +++ /dev/null @@ -1,60 +0,0 @@ -package common - -import ( - "fmt" - - "github.com/cosmos/cosmos-sdk/client/context" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth/client/utils" - - "github.com/kava-labs/kava/x/committee/types" -) - -// Note: QueryProposer is copied in from the gov module - -const ( - defaultPage = 1 - defaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19 -) - -// Proposer contains metadata of a governance proposal used for querying a proposer. -type Proposer struct { - ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"` - Proposer string `json:"proposer" yaml:"proposer"` -} - -// NewProposer returns a new Proposer given id and proposer -func NewProposer(proposalID uint64, proposer string) Proposer { - return Proposer{proposalID, proposer} -} - -func (p Proposer) String() string { - return fmt.Sprintf("Proposal with ID %d was proposed by %s", p.ProposalID, p.Proposer) -} - -// QueryProposer will query for a proposer of a governance proposal by ID. -func QueryProposer(cliCtx context.CLIContext, proposalID uint64) (Proposer, error) { - events := []string{ - fmt.Sprintf("%s.%s='%s'", sdk.EventTypeMessage, sdk.AttributeKeyAction, types.TypeMsgSubmitProposal), - fmt.Sprintf("%s.%s='%s'", types.EventTypeProposalSubmit, types.AttributeKeyProposalID, []byte(fmt.Sprintf("%d", proposalID))), - } - - // NOTE: SearchTxs is used to facilitate the txs query which does not currently - // support configurable pagination. - searchResult, err := utils.QueryTxsByEvents(cliCtx, events, defaultPage, defaultLimit) - if err != nil { - return Proposer{}, err - } - - for _, info := range searchResult.Txs { - for _, msg := range info.Tx.GetMsgs() { - // there should only be a single proposal under the given conditions - if msg.Type() == types.TypeMsgSubmitProposal { - subMsg := msg.(types.MsgSubmitProposal) - return NewProposer(proposalID, subMsg.Proposer.String()), nil - } - } - } - - return Proposer{}, fmt.Errorf("failed to find the proposer for proposalID %d", proposalID) -} diff --git a/x/committee/client/rest/query.go b/x/committee/client/rest/query.go index 5e6b4590..cf182065 100644 --- a/x/committee/client/rest/query.go +++ b/x/committee/client/rest/query.go @@ -148,14 +148,14 @@ func queryProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { if !ok { return } - bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID)) + + proposal, height, err := common.QueryProposalByID(cliCtx, cliCtx.Codec, types.ModuleName, proposalID) if err != nil { - rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - // Query - res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryProposal), bz) + res, err := cliCtx.Codec.MarshalJSON(proposal) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return diff --git a/x/committee/keeper/querier.go b/x/committee/keeper/querier.go index a098854b..d48bcd6b 100644 --- a/x/committee/keeper/querier.go +++ b/x/committee/keeper/querier.go @@ -4,7 +4,6 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/x/committee/types" @@ -29,6 +28,8 @@ func NewQuerier(keeper Keeper) sdk.Querier { return queryVote(ctx, path[1:], req, keeper) case types.QueryTally: return queryTally(ctx, path[1:], req, keeper) + case types.QueryNextProposalID: + return queryNextProposalID(ctx, req, keeper) case types.QueryRawParams: return queryRawParams(ctx, path[1:], req, keeper) @@ -111,6 +112,16 @@ func queryProposal(ctx sdk.Context, path []string, req abci.RequestQuery, keeper return bz, nil } +func queryNextProposalID(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) { + nextProposalID, _ := keeper.GetNextProposalID(ctx) + + bz, err := types.ModuleCdc.MarshalJSON(nextProposalID) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return bz, nil +} + // ------------------------------------------ // Votes // ------------------------------------------ diff --git a/x/committee/keeper/querier_test.go b/x/committee/keeper/querier_test.go index 9c13fda1..3c901e91 100644 --- a/x/committee/keeper/querier_test.go +++ b/x/committee/keeper/querier_test.go @@ -174,6 +174,18 @@ func (suite *QuerierTestSuite) TestQueryProposal() { suite.Equal(suite.testGenesis.Proposals[0], proposal) } +func (suite *QuerierTestSuite) TestQueryNextProposalID() { + bz, err := suite.querier(suite.ctx, []string{types.QueryNextProposalID}, abci.RequestQuery{}) + suite.Require().NoError(err) + suite.Require().NotNil(bz) + + var nextProposalID uint64 + suite.Require().NoError(suite.cdc.UnmarshalJSON(bz, &nextProposalID)) + + expectedID, _ := suite.keeper.GetNextProposalID(suite.ctx) + suite.Require().Equal(expectedID, nextProposalID) +} + func (suite *QuerierTestSuite) TestQueryVotes() { ctx := suite.ctx.WithIsCheckTx(false) // Set up request query diff --git a/x/committee/types/querier.go b/x/committee/types/querier.go index 5512323f..29970036 100644 --- a/x/committee/types/querier.go +++ b/x/committee/types/querier.go @@ -6,14 +6,15 @@ import ( // Query endpoints supported by the Querier const ( - QueryCommittees = "committees" - QueryCommittee = "committee" - QueryProposals = "proposals" - QueryProposal = "proposal" - QueryVotes = "votes" - QueryVote = "vote" - QueryTally = "tally" - QueryRawParams = "raw_params" + QueryCommittees = "committees" + QueryCommittee = "committee" + QueryProposals = "proposals" + QueryProposal = "proposal" + QueryNextProposalID = "next-proposal-id" + QueryVotes = "votes" + QueryVote = "vote" + QueryTally = "tally" + QueryRawParams = "raw_params" ) type QueryCommitteeParams struct {