code.vegaprotocol.io/vega@v0.79.0/core/spam/vote_spam_policy.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 spam
    17  
    18  import (
    19  	"encoding/hex"
    20  	"errors"
    21  	"sort"
    22  	"strings"
    23  	"sync"
    24  
    25  	"code.vegaprotocol.io/vega/core/blockchain/abci"
    26  	"code.vegaprotocol.io/vega/core/types"
    27  	"code.vegaprotocol.io/vega/libs/num"
    28  	"code.vegaprotocol.io/vega/libs/proto"
    29  	"code.vegaprotocol.io/vega/logging"
    30  	protoapi "code.vegaprotocol.io/vega/protos/vega/api/v1"
    31  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    32  )
    33  
    34  var (
    35  	// ErrInsufficientTokensForVoting is returned when the party has insufficient tokens for voting.
    36  	ErrInsufficientTokensForVoting = errors.New("party has insufficient associated governance tokens in their staking account to submit votes")
    37  	// ErrTooManyVotes is returned when the party has voted already the maximum allowed votes per proposal per epoch.
    38  	ErrTooManyVotes = errors.New("party has already voted the maximum number of times per proposal per epoch")
    39  )
    40  
    41  type VoteSpamPolicy struct {
    42  	log             *logging.Logger
    43  	numVotes        uint64
    44  	minVotingTokens *num.Uint
    45  	accounts        StakingAccounts
    46  
    47  	minTokensParamName  string
    48  	maxAllowedParamName string
    49  	partyToVote         map[string]map[string]uint64 // those are votes that are already on blockchain
    50  	blockPartyToVote    map[string]map[string]uint64 // votes in the current block
    51  	currentEpochSeq     uint64                       // the sequence id of the current epoch
    52  	lock                sync.RWMutex                 // global lock to sync calls from multiple tendermint threads
    53  }
    54  
    55  // NewVoteSpamPolicy instantiates vote spam policy.
    56  func NewVoteSpamPolicy(minTokensParamName string, maxAllowedParamName string, log *logging.Logger, accounts StakingAccounts) *VoteSpamPolicy {
    57  	return &VoteSpamPolicy{
    58  		log:                 log,
    59  		accounts:            accounts,
    60  		minVotingTokens:     num.NewUint(1),
    61  		partyToVote:         map[string]map[string]uint64{},
    62  		blockPartyToVote:    map[string]map[string]uint64{},
    63  		lock:                sync.RWMutex{},
    64  		minTokensParamName:  minTokensParamName,
    65  		maxAllowedParamName: maxAllowedParamName,
    66  	}
    67  }
    68  
    69  func (vsp *VoteSpamPolicy) Serialise() ([]byte, error) {
    70  	partyProposalVoteCount := []*types.PartyProposalVoteCount{}
    71  	for party, proposalToCount := range vsp.partyToVote {
    72  		for proposal, count := range proposalToCount {
    73  			partyProposalVoteCount = append(partyProposalVoteCount, &types.PartyProposalVoteCount{
    74  				Party:    party,
    75  				Proposal: proposal,
    76  				Count:    count,
    77  			})
    78  		}
    79  	}
    80  
    81  	sort.SliceStable(partyProposalVoteCount, func(i, j int) bool {
    82  		switch strings.Compare(partyProposalVoteCount[i].Party, partyProposalVoteCount[j].Party) {
    83  		case -1:
    84  			return true
    85  		case 1:
    86  			return false
    87  		}
    88  		return partyProposalVoteCount[i].Proposal < partyProposalVoteCount[j].Proposal
    89  	})
    90  
    91  	payload := types.Payload{
    92  		Data: &types.PayloadVoteSpamPolicy{
    93  			VoteSpamPolicy: &types.VoteSpamPolicy{
    94  				PartyProposalVoteCount: partyProposalVoteCount,
    95  				CurrentEpochSeq:        vsp.currentEpochSeq,
    96  			},
    97  		},
    98  	}
    99  
   100  	return proto.Marshal(payload.IntoProto())
   101  }
   102  
   103  func (vsp *VoteSpamPolicy) Deserialise(p *types.Payload) error {
   104  	pl := p.Data.(*types.PayloadVoteSpamPolicy).VoteSpamPolicy
   105  	vsp.partyToVote = map[string]map[string]uint64{}
   106  	for _, ptv := range pl.PartyProposalVoteCount {
   107  		if _, ok := vsp.partyToVote[ptv.Party]; !ok {
   108  			vsp.partyToVote[ptv.Party] = map[string]uint64{}
   109  		}
   110  		vsp.partyToVote[ptv.Party][ptv.Proposal] = ptv.Count
   111  	}
   112  	vsp.currentEpochSeq = pl.CurrentEpochSeq
   113  	return nil
   114  }
   115  
   116  // UpdateUintParam is called to update Uint net params for the policy
   117  // Specifically the min tokens required for voting.
   118  func (vsp *VoteSpamPolicy) UpdateUintParam(name string, value *num.Uint) error {
   119  	if name == vsp.minTokensParamName {
   120  		vsp.minVotingTokens = value.Clone()
   121  	} else {
   122  		return errors.New("unknown parameter for vote spam policy")
   123  	}
   124  	return nil
   125  }
   126  
   127  // UpdateIntParam is called to update iint net params for the policy
   128  // Specifically the number of votes to a proposal a party can submit in an epoch.
   129  func (vsp *VoteSpamPolicy) UpdateIntParam(name string, value int64) error {
   130  	if name == vsp.maxAllowedParamName {
   131  		vsp.numVotes = uint64(value)
   132  	} else {
   133  		return errors.New("unknown parameter for vote spam policy")
   134  	}
   135  	return nil
   136  }
   137  
   138  // Reset is called at the beginning of an epoch to reset the settings for the epoch.
   139  func (vsp *VoteSpamPolicy) Reset(epoch types.Epoch) {
   140  	vsp.lock.Lock()
   141  	defer vsp.lock.Unlock()
   142  	// reset the token count factor to 1
   143  	vsp.currentEpochSeq = epoch.Seq
   144  
   145  	// reset vote counts
   146  	vsp.partyToVote = map[string]map[string]uint64{}
   147  
   148  	// reset current block vote counts
   149  	vsp.blockPartyToVote = map[string]map[string]uint64{}
   150  }
   151  
   152  func (vsp *VoteSpamPolicy) UpdateTx(tx abci.Tx) {
   153  	vsp.lock.Lock()
   154  	defer vsp.lock.Unlock()
   155  	if _, ok := vsp.partyToVote[tx.Party()]; !ok {
   156  		vsp.partyToVote[tx.Party()] = map[string]uint64{}
   157  	}
   158  	vote := &commandspb.VoteSubmission{}
   159  	tx.Unmarshal(vote)
   160  	if _, ok := vsp.partyToVote[tx.Party()][vote.ProposalId]; !ok {
   161  		vsp.partyToVote[tx.Party()][vote.ProposalId] = 0
   162  	}
   163  	vsp.partyToVote[tx.Party()][vote.ProposalId] = vsp.partyToVote[tx.Party()][vote.ProposalId] + 1
   164  }
   165  
   166  func (vsp *VoteSpamPolicy) RollbackProposal() {
   167  	vsp.blockPartyToVote = map[string]map[string]uint64{}
   168  }
   169  
   170  func (vsp *VoteSpamPolicy) CheckBlockTx(tx abci.Tx) error {
   171  	party := tx.Party()
   172  
   173  	vsp.lock.Lock()
   174  	defer vsp.lock.Unlock()
   175  
   176  	vote := &commandspb.VoteSubmission{}
   177  	if err := tx.Unmarshal(vote); err != nil {
   178  		return err
   179  	}
   180  
   181  	// get number of votes preceding the block in this epoch
   182  	var epochVotes uint64
   183  	if partyVotes, ok := vsp.partyToVote[party]; ok {
   184  		if voteCount, ok := partyVotes[vote.ProposalId]; ok {
   185  			epochVotes = voteCount
   186  		}
   187  	}
   188  
   189  	// get number of votes so far in current block
   190  	var blockVotes uint64
   191  	if proposals, ok := vsp.blockPartyToVote[party]; ok {
   192  		if votes, ok := proposals[vote.ProposalId]; ok {
   193  			blockVotes += votes
   194  		}
   195  	}
   196  
   197  	// if too many votes in total - reject and update counters
   198  	if epochVotes+blockVotes >= vsp.numVotes {
   199  		return ErrTooManyVotes
   200  	}
   201  
   202  	// update vote counters for party/proposal votes
   203  	if _, ok := vsp.blockPartyToVote[party]; !ok {
   204  		vsp.blockPartyToVote[party] = map[string]uint64{}
   205  	}
   206  	if votes, ok := vsp.blockPartyToVote[party][vote.ProposalId]; !ok {
   207  		vsp.blockPartyToVote[party][vote.ProposalId] = 1
   208  	} else {
   209  		vsp.blockPartyToVote[party][vote.ProposalId] = votes + 1
   210  	}
   211  	return nil
   212  }
   213  
   214  // PreBlockAccept checks if the vote should be rejected as spam or not based on the number of votes in current epoch's preceding blocks and the number of tokens
   215  // held by the party.
   216  // NB: this is done at mempool before adding to block.
   217  func (vsp *VoteSpamPolicy) PreBlockAccept(tx abci.Tx) error {
   218  	party := tx.Party()
   219  
   220  	vsp.lock.RLock()
   221  	defer vsp.lock.RUnlock()
   222  
   223  	// check if the party has enough balance to submit votes
   224  	balance, err := vsp.accounts.GetAvailableBalance(party)
   225  	if err != nil || balance.LT(vsp.minVotingTokens) {
   226  		if vsp.log.GetLevel() <= logging.DebugLevel {
   227  			vsp.log.Debug("Spam pre: party has insufficient balance for voting", logging.String("txHash", hex.EncodeToString(tx.Hash())), logging.String("party", party), logging.String("balance", num.UintToString(balance)))
   228  		}
   229  		return ErrInsufficientTokensForVoting
   230  	}
   231  
   232  	vote := &commandspb.VoteSubmission{}
   233  
   234  	if err := tx.Unmarshal(vote); err != nil {
   235  		return err
   236  	}
   237  
   238  	// Check we have not exceeded our vote limit for this given proposal in this epoch
   239  	if partyVotes, ok := vsp.partyToVote[party]; ok {
   240  		if voteCount, ok := partyVotes[vote.ProposalId]; ok && voteCount >= vsp.numVotes {
   241  			if vsp.log.GetLevel() <= logging.DebugLevel {
   242  				vsp.log.Debug("Spam pre: party has already voted for proposal the max amount of votes", logging.String("txHash", hex.EncodeToString(tx.Hash())), logging.String("party", party), logging.String("proposal", vote.ProposalId), logging.Uint64("voteCount", voteCount), logging.Uint64("maxAllowed", vsp.numVotes))
   243  			}
   244  			return ErrTooManyVotes
   245  		}
   246  	}
   247  
   248  	return nil
   249  }
   250  
   251  func (vsp *VoteSpamPolicy) GetSpamStats(_ string) *protoapi.SpamStatistic {
   252  	return nil
   253  }
   254  
   255  func (vsp *VoteSpamPolicy) GetVoteSpamStats(partyID string) *protoapi.VoteSpamStatistics {
   256  	vsp.lock.RLock()
   257  	defer vsp.lock.RUnlock()
   258  
   259  	partyStats := vsp.partyToVote[partyID]
   260  
   261  	stats := make([]*protoapi.VoteSpamStatistic, 0, len(partyStats))
   262  
   263  	for proposal, votes := range partyStats {
   264  		stats = append(stats, &protoapi.VoteSpamStatistic{
   265  			Proposal:          proposal,
   266  			CountForEpoch:     votes,
   267  			MinTokensRequired: vsp.minVotingTokens.String(),
   268  		})
   269  	}
   270  	return &protoapi.VoteSpamStatistics{
   271  		Statistics:  stats,
   272  		MaxForEpoch: vsp.numVotes,
   273  	}
   274  }