code.vegaprotocol.io/vega@v0.79.0/core/spam/simple_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 "sync" 23 24 "code.vegaprotocol.io/vega/core/blockchain/abci" 25 "code.vegaprotocol.io/vega/core/types" 26 "code.vegaprotocol.io/vega/libs/num" 27 "code.vegaprotocol.io/vega/libs/proto" 28 "code.vegaprotocol.io/vega/logging" 29 protoapi "code.vegaprotocol.io/vega/protos/vega/api/v1" 30 ) 31 32 // Simple spam policy supports encforcing of max allowed commands and min required tokens + banning of parties when their reject rate in the block 33 // exceeds x%. 34 type SimpleSpamPolicy struct { 35 log *logging.Logger 36 accounts StakingAccounts 37 policyName string 38 maxAllowedCommands uint64 39 minTokensRequired *num.Uint 40 41 minTokensParamName string 42 maxAllowedParamName string 43 44 partyToCount map[string]uint64 // commands that are already on blockchain 45 blockPartyToCount map[string]uint64 // commands in the current block 46 currentEpochSeq uint64 // current epoch sequence 47 lock sync.RWMutex // global lock to sync calls from multiple tendermint threads 48 insufficientTokensErr error 49 tooManyCommands error 50 } 51 52 // NewSimpleSpamPolicy instantiates the simple spam policy. 53 func NewSimpleSpamPolicy(policyName string, minTokensParamName string, maxAllowedParamName string, log *logging.Logger, accounts StakingAccounts) *SimpleSpamPolicy { 54 return &SimpleSpamPolicy{ 55 log: log, 56 accounts: accounts, 57 policyName: policyName, 58 partyToCount: map[string]uint64{}, 59 blockPartyToCount: map[string]uint64{}, 60 lock: sync.RWMutex{}, 61 minTokensParamName: minTokensParamName, 62 maxAllowedParamName: maxAllowedParamName, 63 minTokensRequired: num.UintZero(), 64 maxAllowedCommands: 1, // default is allow one per epoch 65 insufficientTokensErr: errors.New("party has insufficient associated governance tokens in their staking account to submit " + policyName + " request"), 66 tooManyCommands: errors.New("party has already submitted the maximum number of " + policyName + " requests per epoch"), 67 } 68 } 69 70 func (ssp *SimpleSpamPolicy) Serialise() ([]byte, error) { 71 partyToCount := []*types.PartyCount{} 72 for party, count := range ssp.partyToCount { 73 partyToCount = append(partyToCount, &types.PartyCount{ 74 Party: party, 75 Count: count, 76 }) 77 } 78 79 sort.SliceStable(partyToCount, func(i, j int) bool { return partyToCount[i].Party < partyToCount[j].Party }) 80 81 payload := types.Payload{ 82 Data: &types.PayloadSimpleSpamPolicy{ 83 SimpleSpamPolicy: &types.SimpleSpamPolicy{ 84 PolicyName: ssp.policyName, 85 PartyToCount: partyToCount, 86 CurrentEpochSeq: ssp.currentEpochSeq, 87 }, 88 }, 89 } 90 91 return proto.Marshal(payload.IntoProto()) 92 } 93 94 func (ssp *SimpleSpamPolicy) Deserialise(p *types.Payload) error { 95 pl := p.Data.(*types.PayloadSimpleSpamPolicy).SimpleSpamPolicy 96 97 ssp.partyToCount = map[string]uint64{} 98 for _, ptc := range pl.PartyToCount { 99 ssp.partyToCount[ptc.Party] = ptc.Count 100 } 101 ssp.currentEpochSeq = pl.CurrentEpochSeq 102 103 return nil 104 } 105 106 // UpdateUintParam is called to update Uint net params for the policy 107 // Specifically the min tokens required for executing the command for which the policy is attached. 108 func (ssp *SimpleSpamPolicy) UpdateUintParam(name string, value *num.Uint) error { 109 if name == ssp.minTokensParamName { 110 ssp.minTokensRequired = value.Clone() 111 } else { 112 return errors.New("unknown parameter for simple spam policy") 113 } 114 return nil 115 } 116 117 // UpdateIntParam is called to update int net params for the policy 118 // Specifically the number of commands a party can submit in an epoch. 119 func (ssp *SimpleSpamPolicy) UpdateIntParam(name string, value int64) error { 120 if name == ssp.maxAllowedParamName { 121 ssp.maxAllowedCommands = uint64(value) 122 } else { 123 return errors.New("unknown parameter for simple spam policy") 124 } 125 return nil 126 } 127 128 // Reset is called when the epoch begins to reset policy state. 129 func (ssp *SimpleSpamPolicy) Reset(epoch types.Epoch) { 130 ssp.lock.Lock() 131 defer ssp.lock.Unlock() 132 ssp.currentEpochSeq = epoch.Seq 133 134 // reset counts 135 ssp.partyToCount = map[string]uint64{} 136 ssp.blockPartyToCount = map[string]uint64{} 137 } 138 139 func (ssp *SimpleSpamPolicy) UpdateTx(tx abci.Tx) { 140 ssp.lock.Lock() 141 defer ssp.lock.Unlock() 142 if _, ok := ssp.partyToCount[tx.Party()]; !ok { 143 ssp.partyToCount[tx.Party()] = 0 144 } 145 ssp.partyToCount[tx.Party()]++ 146 } 147 148 // CheckBlockTx is called to verify a transaction from the block before passed to the application layer. 149 func (ssp *SimpleSpamPolicy) CheckBlockTx(tx abci.Tx) error { 150 party := tx.Party() 151 152 ssp.lock.Lock() 153 defer ssp.lock.Unlock() 154 155 // get number of commands preceding the block in this epoch 156 var epochCommands uint64 157 if count, ok := ssp.partyToCount[party]; ok { 158 epochCommands = count 159 } 160 161 // get number of votes so far in current block 162 var blockCommands uint64 163 if count, ok := ssp.blockPartyToCount[party]; ok { 164 blockCommands += count 165 } 166 167 // if too many votes in total - reject 168 if epochCommands+blockCommands >= ssp.maxAllowedCommands { 169 return ssp.tooManyCommands 170 } 171 172 // update block counters 173 if _, ok := ssp.blockPartyToCount[party]; !ok { 174 ssp.blockPartyToCount[party] = 0 175 } 176 ssp.blockPartyToCount[party]++ 177 178 return nil 179 } 180 181 func (ssp *SimpleSpamPolicy) RollbackProposal() { 182 ssp.blockPartyToCount = map[string]uint64{} 183 } 184 185 // PreBlockAccept checks if the commands violates spam rules based on the information we had about the number of existing commands preceding the current block. 186 func (ssp *SimpleSpamPolicy) PreBlockAccept(tx abci.Tx) error { 187 party := tx.Party() 188 189 ssp.lock.RLock() 190 defer ssp.lock.RUnlock() 191 192 // check if the party has enough balance to submit commands 193 balance, err := ssp.accounts.GetAvailableBalance(party) 194 if !ssp.minTokensRequired.IsZero() && (err != nil || balance.LT(ssp.minTokensRequired)) { 195 if ssp.log.GetLevel() <= logging.DebugLevel { 196 ssp.log.Debug("Spam pre: party has insufficient balance for "+ssp.policyName, logging.String("txHash", hex.EncodeToString(tx.Hash())), logging.String("party", party), logging.String("balance", num.UintToString(balance))) 197 } 198 return ssp.insufficientTokensErr 199 } 200 201 // Check we have not exceeded our command limit for this given party in this epoch 202 if commandCount, ok := ssp.partyToCount[party]; ok && commandCount >= ssp.maxAllowedCommands { 203 if ssp.log.GetLevel() <= logging.DebugLevel { 204 ssp.log.Debug("Spam pre: party has already submitted the max amount of commands for "+ssp.policyName, logging.String("txHash", hex.EncodeToString(tx.Hash())), logging.String("party", party), logging.Uint64("count", commandCount), logging.Uint64("maxAllowed", ssp.maxAllowedCommands)) 205 } 206 return ssp.tooManyCommands 207 } 208 209 return nil 210 } 211 212 func (ssp *SimpleSpamPolicy) GetSpamStats(party string) *protoapi.SpamStatistic { 213 ssp.lock.RLock() 214 defer ssp.lock.RUnlock() 215 return &protoapi.SpamStatistic{ 216 CountForEpoch: ssp.partyToCount[party], 217 MaxForEpoch: ssp.maxAllowedCommands, 218 MinTokensRequired: ssp.minTokensRequired.String(), 219 } 220 } 221 222 func (ssp *SimpleSpamPolicy) GetVoteSpamStats(_ string) *protoapi.VoteSpamStatistics { 223 return nil 224 }