cosmos-sdk/x/gov/keeper/msg_server.go

514 lines
17 KiB
Go

package keeper
import (
"context"
"encoding/json"
"fmt"
"google.golang.org/protobuf/runtime/protoiface"
corecontext "cosmossdk.io/core/context"
"cosmossdk.io/core/event"
"cosmossdk.io/errors"
"cosmossdk.io/math"
govtypes "cosmossdk.io/x/gov/types"
v1 "cosmossdk.io/x/gov/types/v1"
"cosmossdk.io/x/gov/types/v1beta1"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
type msgServer struct {
*Keeper
}
// NewMsgServerImpl returns an implementation of the gov MsgServer interface
// for the provided Keeper.
func NewMsgServerImpl(keeper *Keeper) v1.MsgServer {
return &msgServer{Keeper: keeper}
}
var _ v1.MsgServer = msgServer{}
// SubmitProposal implements the MsgServer.SubmitProposal method.
func (k msgServer) SubmitProposal(ctx context.Context, msg *v1.MsgSubmitProposal) (*v1.MsgSubmitProposalResponse, error) {
proposer, err := k.authKeeper.AddressCodec().StringToBytes(msg.GetProposer())
if err != nil {
return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid proposer address: %s", err)
}
// check that either metadata or Msgs length is non nil.
if len(msg.Messages) == 0 && len(msg.Metadata) == 0 {
return nil, errors.Wrap(govtypes.ErrNoProposalMsgs, "either metadata or Msgs length must be non-nil")
}
// verify that if present, the metadata title and summary equals the proposal title and summary
if len(msg.Metadata) != 0 {
proposalMetadata := govtypes.ProposalMetadata{}
if err := json.Unmarshal([]byte(msg.Metadata), &proposalMetadata); err == nil {
if proposalMetadata.Title != msg.Title {
return nil, errors.Wrapf(govtypes.ErrInvalidProposalContent, "metadata title '%s' must equal proposal title '%s'", proposalMetadata.Title, msg.Title)
}
if proposalMetadata.Summary != msg.Summary {
return nil, errors.Wrapf(govtypes.ErrInvalidProposalContent, "metadata summary '%s' must equal proposal summary '%s'", proposalMetadata.Summary, msg.Summary)
}
}
// if we can't unmarshal the metadata, this means the client didn't use the recommended metadata format
// nothing can be done here, and this is still a valid case, so we ignore the error
}
// This method checks that all message metadata, summary and title
// has the expected length defined in the module configuration.
if err := k.validateProposalLengths(msg.Metadata, msg.Title, msg.Summary); err != nil {
return nil, err
}
proposalMsgs, err := msg.GetMsgs()
if err != nil {
return nil, err
}
params, err := k.Params.Get(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get governance parameters: %w", err)
}
if msg.Expedited { // checking for backward compatibility
msg.ProposalType = v1.ProposalType_PROPOSAL_TYPE_EXPEDITED
}
if err := k.validateInitialDeposit(params, msg.GetInitialDeposit(), msg.ProposalType); err != nil {
return nil, err
}
if err := k.validateDepositDenom(params, msg.GetInitialDeposit()); err != nil {
return nil, err
}
proposal, err := k.Keeper.SubmitProposal(ctx, proposalMsgs, msg.Metadata, msg.Title, msg.Summary, proposer, msg.ProposalType)
if err != nil {
return nil, err
}
bytes, err := proposal.Marshal()
if err != nil {
return nil, err
}
// ref: https://github.com/cosmos/cosmos-sdk/issues/9683
if err := k.GasService.GasMeter(ctx).Consume(
3*k.GasService.GasConfig(ctx).WriteCostPerByte*uint64(len(bytes)),
"submit proposal",
); err != nil {
return nil, err
}
votingStarted, err := k.Keeper.AddDeposit(ctx, proposal.Id, proposer, msg.GetInitialDeposit())
if err != nil {
return nil, err
}
if votingStarted {
if err := k.EventService.EventManager(ctx).EmitKV(
govtypes.EventTypeSubmitProposal,
event.NewAttribute(govtypes.AttributeKeyVotingPeriodStart, fmt.Sprintf("%d", proposal.Id)),
); err != nil {
return nil, errors.Wrapf(err, "failed to emit event: %s", govtypes.EventTypeSubmitProposal)
}
}
return &v1.MsgSubmitProposalResponse{
ProposalId: proposal.Id,
}, nil
}
// SubmitMultipleChoiceProposal implements the MsgServer.SubmitMultipleChoiceProposal method.
func (k msgServer) SubmitMultipleChoiceProposal(ctx context.Context, msg *v1.MsgSubmitMultipleChoiceProposal) (*v1.MsgSubmitMultipleChoiceProposalResponse, error) {
resp, err := k.SubmitProposal(ctx, &v1.MsgSubmitProposal{
InitialDeposit: msg.InitialDeposit,
Proposer: msg.Proposer,
Title: msg.Title,
Summary: msg.Summary,
Metadata: msg.Metadata,
ProposalType: v1.ProposalType_PROPOSAL_TYPE_MULTIPLE_CHOICE,
})
if err != nil {
return nil, err
}
if msg.VoteOptions == nil {
return nil, sdkerrors.ErrInvalidRequest.Wrap("vote options cannot be nil")
}
// check that if a vote option is provided, the previous one is also provided
if (msg.VoteOptions.OptionTwo != "" && msg.VoteOptions.OptionOne == "") ||
(msg.VoteOptions.OptionThree != "" && msg.VoteOptions.OptionTwo == "") ||
(msg.VoteOptions.OptionFour != "" && msg.VoteOptions.OptionThree == "") {
return nil, sdkerrors.ErrInvalidRequest.Wrap("if a vote option is provided, the previous one must also be provided")
}
// check that at least two vote options are provided
if msg.VoteOptions.OptionOne == "" && msg.VoteOptions.OptionTwo == "" {
return nil, sdkerrors.ErrInvalidRequest.Wrap("vote options cannot be empty, two or more options must be provided")
}
if err := k.ProposalVoteOptions.Set(ctx, resp.ProposalId, *msg.VoteOptions); err != nil {
return nil, err
}
return &v1.MsgSubmitMultipleChoiceProposalResponse{
ProposalId: resp.ProposalId,
}, nil
}
// CancelProposal implements the MsgServer.CancelProposal method.
func (k msgServer) CancelProposal(ctx context.Context, msg *v1.MsgCancelProposal) (*v1.MsgCancelProposalResponse, error) {
_, err := k.authKeeper.AddressCodec().StringToBytes(msg.Proposer)
if err != nil {
return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid proposer address: %s", err)
}
if err := k.Keeper.CancelProposal(ctx, msg.ProposalId, msg.Proposer); err != nil {
return nil, err
}
if err := k.EventService.EventManager(ctx).EmitKV(
govtypes.EventTypeCancelProposal,
event.NewAttribute(sdk.AttributeKeySender, msg.Proposer),
event.NewAttribute(govtypes.AttributeKeyProposalID, fmt.Sprint(msg.ProposalId)),
); err != nil {
return nil, errors.Wrapf(err, "failed to emit event: %s", govtypes.EventTypeCancelProposal)
}
return &v1.MsgCancelProposalResponse{
ProposalId: msg.ProposalId,
CanceledTime: k.HeaderService.HeaderInfo(ctx).Time,
CanceledHeight: uint64(k.HeaderService.HeaderInfo(ctx).Height),
}, nil
}
// ExecLegacyContent implements the MsgServer.ExecLegacyContent method.
func (k msgServer) ExecLegacyContent(ctx context.Context, msg *v1.MsgExecLegacyContent) (*v1.MsgExecLegacyContentResponse, error) {
govAcct, err := k.authKeeper.AddressCodec().BytesToString(k.GetGovernanceAccount(ctx).GetAddress())
if err != nil {
return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid governance account address: %s", err)
}
if govAcct != msg.Authority {
return nil, errors.Wrapf(govtypes.ErrInvalidSigner, "expected %s got %s", govAcct, msg.Authority)
}
content, err := v1.LegacyContentFromMessage(msg)
if err != nil {
return nil, errors.Wrapf(govtypes.ErrInvalidProposalContent, "%+v", err)
}
// Ensure that the content has a respective handler
if !k.Keeper.legacyRouter.HasRoute(content.ProposalRoute()) {
return nil, errors.Wrap(govtypes.ErrNoProposalHandlerExists, content.ProposalRoute())
}
handler := k.Keeper.legacyRouter.GetRoute(content.ProposalRoute())
// NOTE: the support of legacy gov proposal in server/v2 is different than for baseapp.
// Legacy proposal in server/v2 can only access services provided by the gov module environment.
if err := handler(context.WithValue(ctx, corecontext.EnvironmentContextKey, k.Environment), content); err != nil {
return nil, errors.Wrapf(govtypes.ErrInvalidProposalContent, "failed to run legacy handler %s, %+v", content.ProposalRoute(), err)
}
return &v1.MsgExecLegacyContentResponse{}, nil
}
// Vote implements the MsgServer.Vote method.
func (k msgServer) Vote(ctx context.Context, msg *v1.MsgVote) (*v1.MsgVoteResponse, error) {
accAddr, err := k.authKeeper.AddressCodec().StringToBytes(msg.Voter)
if err != nil {
return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid voter address: %s", err)
}
if !v1.ValidVoteOption(msg.Option) {
return nil, errors.Wrap(govtypes.ErrInvalidVote, msg.Option.String())
}
if err = k.Keeper.AddVote(ctx, msg.ProposalId, accAddr, v1.NewNonSplitVoteOption(msg.Option), msg.Metadata); err != nil {
return nil, err
}
return &v1.MsgVoteResponse{}, nil
}
// VoteWeighted implements the MsgServer.VoteWeighted method.
func (k msgServer) VoteWeighted(ctx context.Context, msg *v1.MsgVoteWeighted) (*v1.MsgVoteWeightedResponse, error) {
accAddr, accErr := k.authKeeper.AddressCodec().StringToBytes(msg.Voter)
if accErr != nil {
return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid voter address: %s", accErr)
}
if len(msg.Options) == 0 {
return nil, errors.Wrap(sdkerrors.ErrInvalidRequest, v1.WeightedVoteOptions(msg.Options).String())
}
totalWeight := math.LegacyNewDec(0)
usedOptions := make(map[v1.VoteOption]bool)
for _, option := range msg.Options {
if !option.IsValid() {
return nil, errors.Wrap(govtypes.ErrInvalidVote, option.String())
}
weight, err := math.LegacyNewDecFromStr(option.Weight)
if err != nil {
return nil, errors.Wrapf(govtypes.ErrInvalidVote, "invalid weight: %s", err)
}
totalWeight = totalWeight.Add(weight)
if usedOptions[option.Option] {
return nil, errors.Wrap(govtypes.ErrInvalidVote, "duplicated vote option")
}
usedOptions[option.Option] = true
}
if totalWeight.GT(math.LegacyNewDec(1)) {
return nil, errors.Wrap(govtypes.ErrInvalidVote, "total weight overflow 1.00")
}
if totalWeight.LT(math.LegacyNewDec(1)) {
return nil, errors.Wrap(govtypes.ErrInvalidVote, "total weight lower than 1.00")
}
err := k.Keeper.AddVote(ctx, msg.ProposalId, accAddr, msg.Options, msg.Metadata)
if err != nil {
return nil, err
}
return &v1.MsgVoteWeightedResponse{}, nil
}
// Deposit implements the MsgServer.Deposit method.
func (k msgServer) Deposit(ctx context.Context, msg *v1.MsgDeposit) (*v1.MsgDepositResponse, error) {
accAddr, err := k.authKeeper.AddressCodec().StringToBytes(msg.Depositor)
if err != nil {
return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid depositor address: %s", err)
}
if err := validateDeposit(msg.Amount); err != nil {
return nil, err
}
votingStarted, err := k.Keeper.AddDeposit(ctx, msg.ProposalId, accAddr, msg.Amount)
if err != nil {
return nil, err
}
if votingStarted {
if err := k.EventService.EventManager(ctx).EmitKV(
govtypes.EventTypeProposalDeposit,
event.NewAttribute(govtypes.AttributeKeyVotingPeriodStart, fmt.Sprintf("%d", msg.ProposalId)),
); err != nil {
return nil, errors.Wrapf(err, "failed to emit event: %s", govtypes.EventTypeProposalDeposit)
}
}
return &v1.MsgDepositResponse{}, nil
}
// UpdateParams implements the v1.UpdateParams method.
func (k msgServer) UpdateParams(ctx context.Context, msg *v1.MsgUpdateParams) (*v1.MsgUpdateParamsResponse, error) {
if k.authority != msg.Authority {
return nil, errors.Wrapf(govtypes.ErrInvalidSigner, "invalid authority; expected %s, got %s", k.authority, msg.Authority)
}
if err := msg.Params.ValidateBasic(k.authKeeper.AddressCodec()); err != nil {
return nil, err
}
if err := k.Params.Set(ctx, msg.Params); err != nil {
return nil, err
}
return &v1.MsgUpdateParamsResponse{}, nil
}
// UpdateMessageParams implements the v1.MsgServer method
func (k msgServer) UpdateMessageParams(ctx context.Context, msg *v1.MsgUpdateMessageParams) (*v1.MsgUpdateMessageParamsResponse, error) {
if k.authority != msg.Authority {
return nil, errors.Wrapf(govtypes.ErrInvalidSigner, "invalid authority; expected %s, got %s", k.authority, msg.Authority)
}
// delete the message params if the params are empty
if msg.Params == nil || *msg.Params == (v1.MessageBasedParams{}) {
if err := k.MessageBasedParams.Remove(ctx, msg.MsgUrl); err != nil {
return nil, err
}
return &v1.MsgUpdateMessageParamsResponse{}, nil
}
if err := msg.Params.ValidateBasic(); err != nil {
return nil, err
}
// note: we don't need to validate the message URL here, as it is gov gated
// a chain may want to configure proposal messages before having an upgrade
// adding new messages.
if err := k.MessageBasedParams.Set(ctx, msg.MsgUrl, *msg.Params); err != nil {
return nil, err
}
return &v1.MsgUpdateMessageParamsResponse{}, nil
}
// SudoExec implements the v1.MsgServer method
func (k msgServer) SudoExec(ctx context.Context, msg *v1.MsgSudoExec) (*v1.MsgSudoExecResponse, error) {
if msg == nil || msg.Msg == nil {
return nil, errors.Wrap(govtypes.ErrInvalidProposal, "sudo-ed message cannot be nil")
}
if k.authority != msg.Authority {
return nil, errors.Wrapf(govtypes.ErrInvalidSigner, "invalid authority; expected %s, got %s", k.authority, msg.Authority)
}
sudoedMsg, err := msg.GetSudoedMsg()
if err != nil {
return nil, errors.Wrapf(govtypes.ErrInvalidProposal, "invalid sudo-ed message: %s", err)
}
// check if the message implements the HasValidateBasic interface
if m, ok := sudoedMsg.(sdk.HasValidateBasic); ok {
if err := m.ValidateBasic(); err != nil {
return nil, errors.Wrapf(govtypes.ErrInvalidProposal, "invalid sudo-ed message: %s", err)
}
}
var msgResp protoiface.MessageV1
if err := k.BranchService.Execute(ctx, func(ctx context.Context) error {
// TODO add route check here
if err := k.MsgRouterService.CanInvoke(ctx, sdk.MsgTypeURL(sudoedMsg)); err != nil {
return errors.Wrapf(govtypes.ErrInvalidProposal, err.Error())
}
msgResp, err = k.MsgRouterService.InvokeUntyped(ctx, sudoedMsg)
if err != nil {
return errors.Wrapf(err, "failed to execute sudo-ed message; message %v", sudoedMsg)
}
return nil
}); err != nil {
return nil, err
}
// TODO(@julienrbrt): check if events are properly emitted
msgRespBytes, err := k.cdc.MarshalJSON(msgResp)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal sudo-ed message response; message %v", msgResp)
}
return &v1.MsgSudoExecResponse{
Result: msgRespBytes,
}, nil
}
type legacyMsgServer struct {
govAcct string
server v1.MsgServer
}
// NewLegacyMsgServerImpl returns an implementation of the v1beta1 legacy MsgServer interface. It wraps around
// the current MsgServer
func NewLegacyMsgServerImpl(govAcct string, v1Server v1.MsgServer) v1beta1.MsgServer {
return &legacyMsgServer{govAcct: govAcct, server: v1Server}
}
var _ v1beta1.MsgServer = legacyMsgServer{}
func (k legacyMsgServer) SubmitProposal(goCtx context.Context, msg *v1beta1.MsgSubmitProposal) (*v1beta1.MsgSubmitProposalResponse, error) {
content := msg.GetContent()
if content == nil {
return nil, errors.Wrap(govtypes.ErrInvalidProposalContent, "missing content")
}
if !v1beta1.IsValidProposalType(content.ProposalType()) {
return nil, errors.Wrap(govtypes.ErrInvalidProposalType, content.ProposalType())
}
if err := content.ValidateBasic(); err != nil {
return nil, err
}
contentMsg, err := v1.NewLegacyContent(msg.GetContent(), k.govAcct)
if err != nil {
return nil, fmt.Errorf("error converting legacy content into proposal message: %w", err)
}
proposal, err := v1.NewMsgSubmitProposal(
[]sdk.Msg{contentMsg},
msg.InitialDeposit,
msg.Proposer,
"",
msg.GetContent().GetTitle(),
msg.GetContent().GetDescription(),
v1.ProposalType_PROPOSAL_TYPE_STANDARD, // legacy proposals can only be standard
)
if err != nil {
return nil, err
}
resp, err := k.server.SubmitProposal(goCtx, proposal)
if err != nil {
return nil, err
}
return &v1beta1.MsgSubmitProposalResponse{ProposalId: resp.ProposalId}, nil
}
func (k legacyMsgServer) Vote(goCtx context.Context, msg *v1beta1.MsgVote) (*v1beta1.MsgVoteResponse, error) {
_, err := k.server.Vote(goCtx, &v1.MsgVote{
ProposalId: msg.ProposalId,
Voter: msg.Voter,
Option: v1.VoteOption(msg.Option),
})
if err != nil {
return nil, err
}
return &v1beta1.MsgVoteResponse{}, nil
}
func (k legacyMsgServer) VoteWeighted(goCtx context.Context, msg *v1beta1.MsgVoteWeighted) (*v1beta1.MsgVoteWeightedResponse, error) {
opts := make([]*v1.WeightedVoteOption, len(msg.Options))
for idx, opt := range msg.Options {
opts[idx] = &v1.WeightedVoteOption{
Option: v1.VoteOption(opt.Option),
Weight: opt.Weight.String(),
}
}
_, err := k.server.VoteWeighted(goCtx, &v1.MsgVoteWeighted{
ProposalId: msg.ProposalId,
Voter: msg.Voter,
Options: opts,
})
if err != nil {
return nil, err
}
return &v1beta1.MsgVoteWeightedResponse{}, nil
}
func (k legacyMsgServer) Deposit(goCtx context.Context, msg *v1beta1.MsgDeposit) (*v1beta1.MsgDepositResponse, error) {
_, err := k.server.Deposit(goCtx, &v1.MsgDeposit{
ProposalId: msg.ProposalId,
Depositor: msg.Depositor,
Amount: msg.Amount,
})
if err != nil {
return nil, err
}
return &v1beta1.MsgDepositResponse{}, nil
}
// validateDeposit validates the deposit amount, do not use for initial deposit.
func validateDeposit(amount sdk.Coins) error {
if !amount.IsValid() || !amount.IsAllPositive() {
return sdkerrors.ErrInvalidCoins.Wrap(amount.String())
}
return nil
}