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  }