cosmos-sdk/x/gov/keeper/abci.go
2024-09-28 11:20:39 +00:00

338 lines
11 KiB
Go

package keeper
import (
"context"
"errors"
"fmt"
"time"
"google.golang.org/protobuf/runtime/protoiface"
"cosmossdk.io/collections"
"cosmossdk.io/core/event"
"cosmossdk.io/core/router"
"cosmossdk.io/x/gov/types"
v1 "cosmossdk.io/x/gov/types/v1"
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// EndBlocker is called every block.
func (k Keeper) EndBlocker(ctx context.Context) error {
start := telemetry.Now()
defer telemetry.ModuleMeasureSince(types.ModuleName, start, telemetry.MetricKeyEndBlocker)
// delete dead proposals from store and returns theirs deposits.
// A proposal is dead when it's inactive and didn't get enough deposit on time to get into voting phase.
rng := collections.NewPrefixUntilPairRange[time.Time, uint64](k.HeaderService.HeaderInfo(ctx).Time)
iter, err := k.InactiveProposalsQueue.Iterate(ctx, rng)
if err != nil {
return err
}
inactiveProps, err := iter.KeyValues()
if err != nil {
return err
}
params, err := k.Params.Get(ctx)
if err != nil {
return err
}
for _, prop := range inactiveProps {
proposal, err := k.Proposals.Get(ctx, prop.Key.K2())
if err != nil {
// if the proposal has an encoding error, this means it cannot be processed by x/gov
// this could be due to some types missing their registration
// instead of returning an error (i.e, halting the chain), we fail the proposal
if errors.Is(err, collections.ErrEncoding) {
proposal.Id = prop.Key.K2()
if err := failUnsupportedProposal(ctx, k, proposal, err.Error(), false); err != nil {
return err
}
if err = k.DeleteProposal(ctx, proposal.Id); err != nil {
return err
}
continue
}
return err
}
if err = k.DeleteProposal(ctx, proposal.Id); err != nil {
return err
}
if !params.BurnProposalDepositPrevote {
err = k.RefundAndDeleteDeposits(ctx, proposal.Id) // refund deposit if proposal got removed without getting 100% of the proposal
} else {
err = k.DeleteAndBurnDeposits(ctx, proposal.Id) // burn the deposit if proposal got removed without getting 100% of the proposal
}
if err != nil {
return err
}
// called when proposal become inactive
// call hook when proposal become inactive
if err = k.BranchService.Execute(ctx, func(ctx context.Context) error {
return k.Hooks().AfterProposalFailedMinDeposit(ctx, proposal.Id)
}); err != nil {
// purposely ignoring the error here not to halt the chain if the hook fails
k.Logger.Error("failed to execute AfterProposalFailedMinDeposit hook", "error", err)
}
if err := k.EventService.EventManager(ctx).EmitKV(types.EventTypeInactiveProposal,
event.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)),
event.NewAttribute(types.AttributeKeyProposalResult, types.AttributeValueProposalDropped),
); err != nil {
k.Logger.Error("failed to emit event", "error", err)
}
k.Logger.Info(
"proposal did not meet minimum deposit; deleted",
"proposal", proposal.Id,
"proposal_type", proposal.ProposalType,
"title", proposal.Title,
"min_deposit", sdk.NewCoins(proposal.GetMinDepositFromParams(params)...).String(),
"total_deposit", sdk.NewCoins(proposal.TotalDeposit...).String(),
)
}
// fetch active proposals whose voting periods have ended (are passed the block time)
rng = collections.NewPrefixUntilPairRange[time.Time, uint64](k.HeaderService.HeaderInfo(ctx).Time)
iter, err = k.ActiveProposalsQueue.Iterate(ctx, rng)
if err != nil {
return err
}
activeProps, err := iter.KeyValues()
if err != nil {
return err
}
// err = k.ActiveProposalsQueue.Walk(ctx, rng, func(key collections.Pair[time.Time, uint64], _ uint64) (bool, error) {
for _, prop := range activeProps {
proposal, err := k.Proposals.Get(ctx, prop.Key.K2())
if err != nil {
// if the proposal has an encoding error, this means it cannot be processed by x/gov
// this could be due to some types missing their registration
// instead of returning an error (i.e, halting the chain), we fail the proposal
if errors.Is(err, collections.ErrEncoding) {
proposal.Id = prop.Key.K2()
if err := failUnsupportedProposal(ctx, k, proposal, err.Error(), true); err != nil {
return err
}
if err = k.ActiveProposalsQueue.Remove(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id)); err != nil {
return err
}
continue
}
return err
}
var tagValue, logMsg string
passes, burnDeposits, tallyResults, err := k.Tally(ctx, proposal)
if err != nil {
return err
}
// Deposits are always burned if tally said so, regardless of the proposal type.
// If a proposal passes, deposits are always refunded, regardless of the proposal type.
// If a proposal fails, and isn't spammy, deposits are refunded, unless the proposal is expedited or optimistic.
// An expedited or optimistic proposal that fails and isn't spammy is converted to a regular proposal.
if burnDeposits {
err = k.DeleteAndBurnDeposits(ctx, proposal.Id)
} else if passes || !(proposal.ProposalType == v1.ProposalType_PROPOSAL_TYPE_EXPEDITED || proposal.ProposalType == v1.ProposalType_PROPOSAL_TYPE_OPTIMISTIC) {
err = k.RefundAndDeleteDeposits(ctx, proposal.Id)
}
if err != nil {
// in case of an error, log it and emit an event
// we do not want to halt the chain if the refund/burn fails
// as it could happen due to a governance mistake (governance has let a proposal pass that sends gov funds that were from proposal deposits)
k.Logger.Error("failed to refund or burn deposits", "error", err)
if err := k.EventService.EventManager(ctx).EmitKV(types.EventTypeProposalDeposit,
event.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)),
event.NewAttribute(types.AttributeKeyProposalDepositError, "failed to refund or burn deposits"),
event.NewAttribute("error", err.Error()),
); err != nil {
k.Logger.Error("failed to emit event", "error", err)
}
}
if err = k.ActiveProposalsQueue.Remove(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id)); err != nil {
return err
}
switch {
case passes:
var (
idx int
msg sdk.Msg
)
messages, err := proposal.GetMsgs()
if err != nil {
proposal.Status = v1.StatusFailed
proposal.FailedReason = err.Error()
tagValue = types.AttributeValueProposalFailed
logMsg = fmt.Sprintf("passed proposal (%v) failed to execute; msgs: %s", proposal, err)
break
}
// attempt to execute all messages within the passed proposal
// Messages may mutate state thus we use a cached context. If one of
// the handlers fails, no state mutation is written and the error
// message is logged.
_, err = k.BranchService.ExecuteWithGasLimit(ctx, params.ProposalExecutionGas, func(ctx context.Context) error {
// execute all messages
for idx, msg = range messages {
if _, err := safeExecuteHandler(ctx, msg, k.MsgRouterService); err != nil {
return err
}
}
proposal.Status = v1.StatusPassed
tagValue = types.AttributeValueProposalPassed
logMsg = "passed"
return nil
})
if err != nil {
// `idx` and `err` are populated with the msg index and error.
proposal.Status = v1.StatusFailed
proposal.FailedReason = err.Error()
tagValue = types.AttributeValueProposalFailed
logMsg = fmt.Sprintf("passed, but msg %d (%s) failed on execution: %s", idx, sdk.MsgTypeURL(msg), err)
break // We do not anything with the error. Returning an error halts the chain, and proposal struct is already updated.
}
case !burnDeposits && (proposal.ProposalType == v1.ProposalType_PROPOSAL_TYPE_EXPEDITED ||
proposal.ProposalType == v1.ProposalType_PROPOSAL_TYPE_OPTIMISTIC):
// When a non spammy expedited/optimistic proposal fails, it is converted
// to a regular proposal. As a result, the voting period is extended, and,
// once the regular voting period expires again, the tally is repeated
// according to the regular proposal rules.
proposal.ProposalType = v1.ProposalType_PROPOSAL_TYPE_STANDARD
endTime := proposal.VotingStartTime.Add(*params.VotingPeriod)
proposal.VotingEndTime = &endTime
err = k.ActiveProposalsQueue.Set(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id), proposal.Id)
if err != nil {
return err
}
if proposal.ProposalType == v1.ProposalType_PROPOSAL_TYPE_EXPEDITED {
tagValue = types.AttributeValueExpeditedProposalRejected
logMsg = "expedited proposal converted to regular"
} else {
tagValue = types.AttributeValueOptimisticProposalRejected
logMsg = "optimistic proposal converted to regular"
}
default:
proposal.Status = v1.StatusRejected
proposal.FailedReason = "proposal did not get enough votes to pass"
tagValue = types.AttributeValueProposalRejected
logMsg = "rejected"
}
proposal.FinalTallyResult = &tallyResults
if err = k.Proposals.Set(ctx, proposal.Id, proposal); err != nil {
return err
}
// call hook when proposal become active
if err := k.BranchService.Execute(ctx, func(ctx context.Context) error {
return k.Hooks().AfterProposalVotingPeriodEnded(ctx, proposal.Id)
}); err != nil {
// purposely ignoring the error here not to halt the chain if the hook fails
k.Logger.Error("failed to execute AfterProposalVotingPeriodEnded hook", "error", err)
}
k.Logger.Info(
"proposal tallied",
"proposal", proposal.Id,
"proposal_type", proposal.ProposalType,
"status", proposal.Status.String(),
"title", proposal.Title,
"results", logMsg,
)
if err := k.EventService.EventManager(ctx).EmitKV(types.EventTypeActiveProposal,
event.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)),
event.NewAttribute(types.AttributeKeyProposalResult, tagValue),
event.NewAttribute(types.AttributeKeyProposalLog, logMsg),
); err != nil {
k.Logger.Error("failed to emit event", "error", err)
}
}
return nil
}
// executes route(msg) and recovers from panic.
func safeExecuteHandler(ctx context.Context, msg sdk.Msg, router router.Service) (res protoiface.MessageV1, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("handling x/gov proposal msg [%s] PANICKED: %v", msg, r)
}
}()
res, err = router.Invoke(ctx, msg)
return
}
// failUnsupportedProposal fails a proposal that cannot be processed by gov
func failUnsupportedProposal(
ctx context.Context,
k Keeper,
proposal v1.Proposal,
errMsg string,
active bool,
) error {
proposal.Status = v1.StatusFailed
proposal.FailedReason = fmt.Sprintf("proposal failed because it cannot be processed by gov: %s", errMsg)
proposal.Messages = nil // clear out the messages
if err := k.Proposals.Set(ctx, proposal.Id, proposal); err != nil {
return err
}
if err := k.RefundAndDeleteDeposits(ctx, proposal.Id); err != nil {
return err
}
eventType := types.EventTypeInactiveProposal
if active {
eventType = types.EventTypeActiveProposal
}
if err := k.EventService.EventManager(ctx).EmitKV(eventType,
event.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)),
event.NewAttribute(types.AttributeKeyProposalResult, types.AttributeValueProposalFailed),
); err != nil {
k.Logger.Error("failed to emit event", "error", err)
}
k.Logger.Info(
"proposal failed to decode; deleted",
"proposal", proposal.Id,
"proposal_type", proposal.ProposalType,
"title", proposal.Title,
"results", errMsg,
)
return nil
}