/
abci.go
326 lines (279 loc) · 10 KB
/
abci.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
package gov
import (
"errors"
"fmt"
"time"
"cosmossdk.io/collections"
"cosmossdk.io/log"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov/keeper"
"github.com/cosmos/cosmos-sdk/x/gov/types"
v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
)
// EndBlocker called every block, process inflation, update validator set.
func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) error {
defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyEndBlocker)
logger := ctx.Logger().With("module", "x/"+types.ModuleName)
// 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](ctx.BlockTime())
err := keeper.InactiveProposalsQueue.Walk(ctx, rng, func(key collections.Pair[time.Time, uint64], _ uint64) (bool, error) {
proposal, err := keeper.Proposals.Get(ctx, 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 = key.K2()
if err := failUnsupportedProposal(logger, ctx, keeper, proposal, err.Error(), false); err != nil {
return false, err
}
if err = keeper.DeleteProposal(ctx, proposal.Id); err != nil {
return false, err
}
return false, nil
}
return false, err
}
if err = keeper.DeleteProposal(ctx, proposal.Id); err != nil {
return false, err
}
params, err := keeper.Params.Get(ctx)
if err != nil {
return false, err
}
if !params.BurnProposalDepositPrevote {
err = keeper.RefundAndDeleteDeposits(ctx, proposal.Id) // refund deposit if proposal got removed without getting 100% of the proposal
} else {
err = keeper.DeleteAndBurnDeposits(ctx, proposal.Id) // burn the deposit if proposal got removed without getting 100% of the proposal
}
if err != nil {
return false, err
}
// called when proposal become inactive
cacheCtx, writeCache := ctx.CacheContext()
err = keeper.Hooks().AfterProposalFailedMinDeposit(cacheCtx, proposal.Id)
if err == nil { // purposely ignoring the error here not to halt the chain if the hook fails
writeCache()
} else {
keeper.Logger(ctx).Error("failed to execute AfterProposalFailedMinDeposit hook", "error", err)
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeInactiveProposal,
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)),
sdk.NewAttribute(types.AttributeKeyProposalResult, types.AttributeValueProposalDropped),
),
)
logger.Info(
"proposal did not meet minimum deposit; deleted",
"proposal", proposal.Id,
"expedited", proposal.Expedited,
"title", proposal.Title,
"min_deposit", sdk.NewCoins(proposal.GetMinDepositFromParams(params)...).String(),
"total_deposit", sdk.NewCoins(proposal.TotalDeposit...).String(),
)
return false, nil
})
if err != nil {
return err
}
// fetch active proposals whose voting periods have ended (are passed the block time)
rng = collections.NewPrefixUntilPairRange[time.Time, uint64](ctx.BlockTime())
err = keeper.ActiveProposalsQueue.Walk(ctx, rng, func(key collections.Pair[time.Time, uint64], _ uint64) (bool, error) {
proposal, err := keeper.Proposals.Get(ctx, 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 = key.K2()
if err := failUnsupportedProposal(logger, ctx, keeper, proposal, err.Error(), true); err != nil {
return false, err
}
if err = keeper.ActiveProposalsQueue.Remove(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id)); err != nil {
return false, err
}
return false, nil
}
return false, err
}
var tagValue, logMsg string
passes, burnDeposits, tallyResults, err := keeper.Tally(ctx, proposal)
if err != nil {
return false, err
}
// If an expedited proposal fails, we do not want to update
// the deposit at this point since the proposal is converted to regular.
// As a result, the deposits are either deleted or refunded in all cases
// EXCEPT when an expedited proposal fails.
if !(proposal.Expedited && !passes) {
if burnDeposits {
err = keeper.DeleteAndBurnDeposits(ctx, proposal.Id)
} else {
err = keeper.RefundAndDeleteDeposits(ctx, proposal.Id)
}
if err != nil {
return false, err
}
}
if err = keeper.ActiveProposalsQueue.Remove(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id)); err != nil {
return false, err
}
switch {
case passes:
var (
idx int
events sdk.Events
msg sdk.Msg
)
// 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.
cacheCtx, writeCache := ctx.CacheContext()
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
}
// execute all messages
for idx, msg = range messages {
handler := keeper.Router().Handler(msg)
var res *sdk.Result
res, err = safeExecuteHandler(cacheCtx, msg, handler)
if err != nil {
break
}
events = append(events, res.GetEvents()...)
}
// `err == nil` when all handlers passed.
// Or else, `idx` and `err` are populated with the msg index and error.
if err == nil {
proposal.Status = v1.StatusPassed
tagValue = types.AttributeValueProposalPassed
logMsg = "passed"
// write state to the underlying multi-store
writeCache()
// propagate the msg events to the current context
ctx.EventManager().EmitEvents(events)
} else {
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)
}
case proposal.Expedited:
// When expedited 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.Expedited = false
params, err := keeper.Params.Get(ctx)
if err != nil {
return false, err
}
endTime := proposal.VotingStartTime.Add(*params.VotingPeriod)
proposal.VotingEndTime = &endTime
err = keeper.ActiveProposalsQueue.Set(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id), proposal.Id)
if err != nil {
return false, err
}
tagValue = types.AttributeValueExpeditedProposalRejected
logMsg = "expedited 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
err = keeper.SetProposal(ctx, proposal)
if err != nil {
return false, err
}
// when proposal become active
cacheCtx, writeCache := ctx.CacheContext()
err = keeper.Hooks().AfterProposalVotingPeriodEnded(cacheCtx, proposal.Id)
if err == nil { // purposely ignoring the error here not to halt the chain if the hook fails
writeCache()
} else {
keeper.Logger(ctx).Error("failed to execute AfterProposalVotingPeriodEnded hook", "error", err)
}
logger.Info(
"proposal tallied",
"proposal", proposal.Id,
"status", proposal.Status.String(),
"expedited", proposal.Expedited,
"title", proposal.Title,
"results", logMsg,
)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeActiveProposal,
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)),
sdk.NewAttribute(types.AttributeKeyProposalResult, tagValue),
sdk.NewAttribute(types.AttributeKeyProposalLog, logMsg),
),
)
return false, nil
})
if err != nil {
return err
}
return nil
}
// executes handle(msg) and recovers from panic.
func safeExecuteHandler(ctx sdk.Context, msg sdk.Msg, handler baseapp.MsgServiceHandler,
) (res *sdk.Result, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("handling x/gov proposal msg [%s] PANICKED: %v", msg, r)
}
}()
res, err = handler(ctx, msg)
return
}
// failUnsupportedProposal fails a proposal that cannot be processed by gov
func failUnsupportedProposal(
logger log.Logger,
ctx sdk.Context,
keeper *keeper.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 := keeper.SetProposal(ctx, proposal); err != nil {
return err
}
if err := keeper.RefundAndDeleteDeposits(ctx, proposal.Id); err != nil {
return err
}
eventType := types.EventTypeInactiveProposal
if active {
eventType = types.EventTypeActiveProposal
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
eventType,
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)),
sdk.NewAttribute(types.AttributeKeyProposalResult, types.AttributeValueProposalFailed),
),
)
logger.Info(
"proposal failed to decode; deleted",
"proposal", proposal.Id,
"expedited", proposal.Expedited,
"title", proposal.Title,
"results", errMsg,
)
return nil
}