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 }