-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Expand file tree
/
Copy pathabci.go
More file actions
343 lines (291 loc) · 11.6 KB
/
Copy pathabci.go
File metadata and controls
343 lines (291 loc) · 11.6 KB
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
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
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/log"
"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)
logger := k.Logger()
// 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.environment.HeaderService.GetHeaderInfo(ctx).Time)
iter, err := k.InactiveProposalsQueue.Iterate(ctx, rng)
if err != nil {
return err
}
inactiveProps, err := iter.KeyValues()
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(logger, 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
}
params, err := k.Params.Get(ctx)
if 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.environment.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
logger.Error("failed to execute AfterProposalFailedMinDeposit hook", "error", err)
}
if err := k.environment.EventService.EventManager(ctx).EmitKV(types.EventTypeInactiveProposal,
event.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)),
event.NewAttribute(types.AttributeKeyProposalResult, types.AttributeValueProposalDropped),
); err != nil {
logger.Error("failed to emit event", "error", err)
}
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.environment.HeaderService.GetHeaderInfo(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(logger, 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.environment.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.
if err := k.environment.BranchService.Execute(ctx, func(ctx context.Context) error {
// execute all messages
for idx, msg = range messages {
if _, err := safeExecuteHandler(ctx, msg, k.environment.RouterService.MessageRouterService()); 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)
return err
}
}
proposal.Status = v1.StatusPassed
tagValue = types.AttributeValueProposalPassed
logMsg = "passed"
return nil
}); err != nil {
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
proposal.Expedited = false // can be removed as never read but kept for state coherence
params, err := k.Params.Get(ctx)
if err != nil {
return err
}
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.environment.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
logger.Error("failed to execute AfterProposalVotingPeriodEnded hook", "error", err)
}
logger.Info(
"proposal tallied",
"proposal", proposal.Id,
"proposal_type", proposal.ProposalType,
"status", proposal.Status.String(),
"title", proposal.Title,
"results", logMsg,
)
if err := k.environment.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 {
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.Router) (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.InvokeUntyped(ctx, msg)
return
}
// failUnsupportedProposal fails a proposal that cannot be processed by gov
func failUnsupportedProposal(
logger log.Logger,
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.environment.EventService.EventManager(ctx).EmitKV(eventType,
event.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)),
event.NewAttribute(types.AttributeKeyProposalResult, types.AttributeValueProposalFailed),
); err != nil {
logger.Error("failed to emit event", "error", err)
}
logger.Info(
"proposal failed to decode; deleted",
"proposal", proposal.Id,
"proposal_type", proposal.ProposalType,
"title", proposal.Title,
"results", errMsg,
)
return nil
}