code.vegaprotocol.io/vega@v0.79.0/core/governance/engine_batch.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package governance 17 18 import ( 19 "context" 20 "fmt" 21 "sort" 22 23 "code.vegaprotocol.io/vega/core/events" 24 "code.vegaprotocol.io/vega/core/types" 25 vgerrors "code.vegaprotocol.io/vega/libs/errors" 26 "code.vegaprotocol.io/vega/libs/num" 27 "code.vegaprotocol.io/vega/logging" 28 29 "golang.org/x/exp/maps" 30 ) 31 32 func (e *Engine) SubmitBatchProposal( 33 ctx context.Context, 34 bpsub types.BatchProposalSubmission, 35 batchID, party string, 36 ) ([]*ToSubmit, error) { 37 if _, ok := e.getBatchProposal(batchID); ok { 38 return nil, ErrProposalIsDuplicate // state is not allowed to change externally 39 } 40 41 timeNow := e.timeService.GetTimeNow().UnixNano() 42 43 bp := &types.BatchProposal{ 44 ID: batchID, 45 Timestamp: timeNow, 46 ClosingTimestamp: bpsub.Terms.ClosingTimestamp, 47 Party: party, 48 State: types.ProposalStateOpen, 49 Reference: bpsub.Reference, 50 Rationale: bpsub.Rationale, 51 Proposals: make([]*types.Proposal, 0, len(bpsub.Terms.Changes)), 52 } 53 54 var proposalsEvents []events.Event //nolint:prealloc 55 defer func() { 56 e.broker.Send(events.NewProposalEventFromProto(ctx, bp.ToProto())) 57 58 if len(proposalsEvents) > 0 { 59 e.broker.SendBatch(proposalsEvents) 60 } 61 }() 62 63 proposalParamsPerProposalTermType := map[types.ProposalTermsType]*types.ProposalParameters{} 64 65 for _, change := range bpsub.Terms.Changes { 66 p := &types.Proposal{ 67 ID: change.ID, 68 BatchID: &batchID, 69 Timestamp: timeNow, 70 Party: party, 71 State: bp.State, 72 Reference: bp.Reference, 73 Rationale: bp.Rationale, 74 Terms: &types.ProposalTerms{ 75 ClosingTimestamp: bp.ClosingTimestamp, 76 EnactmentTimestamp: change.EnactmentTime, 77 ValidationTimestamp: change.ValidationTime, 78 Change: change.Change, 79 }, 80 } 81 82 params, err := e.getProposalParams(change.Change) 83 if err != nil { 84 bp.RejectWithErr(types.ProposalErrorUnknownType, err) 85 return nil, err 86 } 87 88 proposalParamsPerProposalTermType[change.Change.GetTermType()] = params 89 90 bp.SetProposalParams(params.Clone()) 91 bp.Proposals = append(bp.Proposals, p) 92 } 93 94 var toSubmits []*ToSubmit //nolint:prealloc 95 errs := vgerrors.NewCumulatedErrors() 96 97 for _, p := range bp.Proposals { 98 perTypeParams := proposalParamsPerProposalTermType[p.Terms.Change.GetTermType()] 99 params := bp.ProposalParameters.Clone() 100 101 submit, err := e.validateProposalFromBatch(ctx, p, params, *perTypeParams) 102 if err != nil { 103 errs.Add(err) 104 continue 105 } 106 107 toSubmits = append(toSubmits, submit) 108 } 109 110 for _, p := range bp.Proposals { 111 if !p.IsRejected() && errs.HasAny() { 112 p.Reject(types.ProposalErrorProposalInBatchRejected) 113 } 114 115 proposalsEvents = append(proposalsEvents, events.NewProposalEvent(ctx, *p)) 116 } 117 118 if errs.HasAny() { 119 bp.State = types.ProposalStateRejected 120 bp.Reason = types.ProposalErrorProposalInBatchRejected 121 122 return nil, errs 123 } 124 125 if e.isTwoStepsBatchProposal(bp) { 126 // set all proposals as WaitForNodeVote then 127 bp.WaitForNodeVote() 128 // reset events here as we will need to send another updated one instead 129 proposalsEvents = []events.Event{} 130 for _, p := range bp.Proposals { 131 proposalsEvents = append(proposalsEvents, events.NewProposalEvent(ctx, *p)) 132 } 133 134 if err := e.startTwoStepsBatchProposal(ctx, bp); err != nil { 135 bp.RejectWithErr(types.ProposalErrorNodeValidationFailed, err) 136 proposalsEvents = []events.Event{} 137 for _, p := range bp.Proposals { 138 proposalsEvents = append(proposalsEvents, events.NewProposalEvent(ctx, *p)) 139 } 140 if e.log.IsDebug() { 141 e.log.Debug("Proposal rejected", 142 logging.String("batch-proposal-id", bp.ID)) 143 } 144 return nil, err 145 } 146 } else { 147 e.startBatchProposal(bp) 148 } 149 150 return toSubmits, nil 151 } 152 153 func (e *Engine) isTwoStepsBatchProposal(p *types.BatchProposal) bool { 154 return e.nodeProposalValidation.IsNodeValidationRequiredBatch(p) 155 } 156 157 func (e *Engine) RejectBatchProposal( 158 ctx context.Context, proposalID string, r types.ProposalError, errorDetails error, 159 ) error { 160 bp, ok := e.getBatchProposal(proposalID) 161 if !ok { 162 return ErrProposalDoesNotExist 163 } 164 165 bp.RejectWithErr(r, errorDetails) 166 167 evts := make([]events.Event, 0, len(bp.Proposals)) 168 for _, proposal := range bp.Proposals { 169 e.rejectProposal(ctx, proposal, r, errorDetails) 170 evts = append(evts, events.NewProposalEvent(ctx, *proposal)) 171 } 172 173 e.broker.Send(events.NewProposalEventFromProto(ctx, bp.ToProto())) 174 e.broker.SendBatch(evts) 175 return nil 176 } 177 178 func (e *Engine) evaluateBatchProposals( 179 ctx context.Context, now int64, 180 ) (voteClosed []*VoteClosed, addToActiveProposals []*proposal) { 181 batchIDs := maps.Keys(e.activeBatchProposals) 182 sort.Strings(batchIDs) 183 184 for _, batchID := range batchIDs { 185 batchProposal := e.activeBatchProposals[batchID] 186 187 var batchHasRejectedProposal bool 188 var batchHasDeclinedProposal bool 189 var closedProposals []*proposal 190 for _, propType := range batchProposal.Proposals { 191 proposal := &proposal{ 192 Proposal: propType, 193 yes: batchProposal.yes, 194 no: batchProposal.no, 195 invalidVotes: map[string]*types.Vote{}, 196 } 197 198 // check if the market for successor proposals still exists, if not, reject the proposal 199 // in case a single proposal is rejected we can reject the whole batch 200 if nm := proposal.Terms.GetNewMarket(); nm != nil && nm.Successor() != nil { 201 if _, err := e.markets.GetMarketState(proposal.ID); err != nil { 202 proposal.RejectWithErr(types.ProposalErrorInvalidSuccessorMarket, 203 ErrParentMarketSucceededByCompeting) 204 batchHasRejectedProposal = true 205 break 206 } 207 } 208 209 // do not check parent market, the market was either rejected when the parent was succeeded 210 // or, if the parent market state is gone (ie succession window has expired), the proposal simply 211 // loses its parent market reference 212 if proposal.ShouldClose(now) { 213 proposal.Close(e.accs, e.markets) 214 if proposal.IsPassed() { 215 e.log.Debug("Proposal passed", 216 logging.ProposalID(proposal.ID), 217 logging.ProposalBatchID(batchID), 218 ) 219 } else if proposal.IsDeclined() { 220 e.log.Debug("Proposal declined", 221 logging.ProposalID(proposal.ID), 222 logging.String("details", proposal.ErrorDetails), 223 logging.String("reason", proposal.Reason.String()), 224 logging.ProposalBatchID(batchID), 225 ) 226 batchHasDeclinedProposal = true 227 } 228 229 closedProposals = append(closedProposals, proposal) 230 voteClosed = append(voteClosed, e.preVoteClosedProposal(proposal)) 231 } 232 } 233 234 if batchHasRejectedProposal { 235 batchProposal.State = types.ProposalStateRejected 236 batchProposal.Reason = types.ProposalErrorProposalInBatchRejected 237 238 proposalsEvents := make([]events.Event, 0, len(batchProposal.Proposals)) 239 for _, proposal := range batchProposal.Proposals { 240 if proposal.IsPassed() { 241 proposal.Reject(types.ProposalErrorProposalInBatchRejected) 242 } 243 244 proposalsEvents = append(proposalsEvents, events.NewProposalEvent(ctx, *proposal)) 245 } 246 247 e.broker.Send(events.NewProposalEventFromProto(ctx, batchProposal.ToProto())) 248 e.broker.SendBatch(proposalsEvents) 249 250 delete(e.activeBatchProposals, batchProposal.ID) 251 continue 252 } 253 254 if len(closedProposals) < 1 { 255 continue 256 } 257 258 // all the proposal in the batch should close at the same time so this should never happen 259 if len(closedProposals) != len(batchProposal.Proposals) { 260 e.log.Panic("Failed to close all proposals in batch proposal", 261 logging.ProposalBatchID(batchID), 262 ) 263 } 264 265 proposalEvents := make([]events.Event, 0, len(closedProposals)) 266 for _, proposal := range closedProposals { 267 if proposal.IsPassed() && batchHasDeclinedProposal { 268 proposal.Decline(types.ProposalErrorProposalInBatchDeclined) 269 } else if proposal.IsPassed() { 270 addToActiveProposals = append(addToActiveProposals, proposal) 271 } 272 273 proposalEvents = append(proposalEvents, events.NewProposalEvent(ctx, *proposal.Proposal)) 274 proposalEvents = append(proposalEvents, newUpdatedProposalEvents(ctx, proposal)...) 275 } 276 277 batchProposal.State = types.ProposalStatePassed 278 if batchHasDeclinedProposal { 279 batchProposal.State = types.ProposalStateDeclined 280 batchProposal.Reason = types.ProposalErrorProposalInBatchDeclined 281 } 282 283 e.broker.Send(events.NewProposalEventFromProto(ctx, batchProposal.ToProto())) 284 e.broker.SendBatch(proposalEvents) 285 delete(e.activeBatchProposals, batchProposal.ID) 286 } 287 288 return 289 } 290 291 func (e *Engine) getBatchProposal(id string) (*batchProposal, bool) { 292 bp, ok := e.activeBatchProposals[id] 293 if ok { 294 return bp, ok 295 } 296 297 nbp, ok := e.nodeProposalValidation.getBatchProposal(id) 298 if !ok { 299 return nil, false 300 } 301 302 return nbp.batchProposal, ok 303 } 304 305 func (e *Engine) validateProposalFromBatch( 306 ctx context.Context, 307 p *types.Proposal, 308 batchParams, perTypeParams types.ProposalParameters, 309 ) (*ToSubmit, error) { 310 batchParams.MaxEnact = perTypeParams.MaxEnact 311 batchParams.MinEnact = perTypeParams.MinEnact 312 313 if proposalErr, err := e.validateOpenProposal(p, &batchParams); err != nil { 314 p.RejectWithErr(proposalErr, err) 315 316 if e.log.IsDebug() { 317 e.log.Debug("Batch proposal rejected", 318 logging.String("proposal-id", p.ID), 319 logging.String("proposal details", p.String()), 320 logging.Error(err), 321 ) 322 } 323 324 return nil, err 325 } 326 327 submit, err := e.intoToSubmit(ctx, p, &enactmentTime{current: p.Terms.EnactmentTimestamp}, false) 328 if err != nil { 329 if e.log.IsDebug() { 330 e.log.Debug("Batch proposal rejected", 331 logging.String("proposal-id", p.ID), 332 logging.String("proposal details", p.String()), 333 logging.Error(err), 334 ) 335 } 336 return nil, err 337 } 338 339 return submit, nil 340 } 341 342 func (e *Engine) startBatchProposal(p *types.BatchProposal) { 343 e.activeBatchProposals[p.ID] = &batchProposal{ 344 BatchProposal: p, 345 yes: map[string]*types.Vote{}, 346 no: map[string]*types.Vote{}, 347 invalidVotes: map[string]*types.Vote{}, 348 } 349 } 350 351 func (e *Engine) addBatchVote(ctx context.Context, batchProposal *batchProposal, cmd types.VoteSubmission, party string) error { 352 validationErrs := vgerrors.NewCumulatedErrors() 353 354 perMarketELS := map[string]num.Decimal{} 355 for _, proposal := range batchProposal.Proposals { 356 if err := e.canVote(proposal, batchProposal.ProposalParameters, party); err != nil { 357 validationErrs.Add(fmt.Errorf("proposal term %q has failed with: %w", proposal.Terms.Change.GetTermType(), err)) 358 continue 359 } 360 361 if proposal.IsMarketUpdate() { 362 marketID := proposal.MarketUpdate().MarketID 363 els, _ := e.markets.GetEquityLikeShareForMarketAndParty(marketID, party) 364 365 perMarketELS[marketID] = els 366 } 367 } 368 369 if validationErrs.HasAny() { 370 e.log.Debug("invalid vote submission", 371 logging.PartyID(party), 372 logging.String("vote", cmd.String()), 373 logging.Error(validationErrs), 374 ) 375 return validationErrs 376 } 377 378 vote := types.Vote{ 379 PartyID: party, 380 ProposalID: cmd.ProposalID, 381 Value: cmd.Value, 382 Timestamp: e.timeService.GetTimeNow().UnixNano(), 383 TotalGovernanceTokenBalance: getTokensBalance(e.accs, party), 384 TotalGovernanceTokenWeight: num.DecimalZero(), 385 TotalEquityLikeShareWeight: num.DecimalZero(), 386 PerMarketEquityLikeShareWeight: perMarketELS, 387 } 388 389 if err := batchProposal.AddVote(vote); err != nil { 390 return fmt.Errorf("couldn't cast the vote: %w", err) 391 } 392 393 if e.log.IsDebug() { 394 e.log.Debug("vote submission accepted", 395 logging.PartyID(party), 396 logging.String("vote", cmd.String()), 397 ) 398 } 399 e.broker.Send(events.NewVoteEvent(ctx, vote)) 400 401 return nil 402 }