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  }