code.vegaprotocol.io/vega@v0.79.0/core/spam/engine.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  	"context"
    20  	"encoding/hex"
    21  	"sync"
    22  
    23  	"code.vegaprotocol.io/vega/core/blockchain/abci"
    24  	"code.vegaprotocol.io/vega/core/netparams"
    25  	"code.vegaprotocol.io/vega/core/txn"
    26  	"code.vegaprotocol.io/vega/core/types"
    27  	"code.vegaprotocol.io/vega/libs/num"
    28  	"code.vegaprotocol.io/vega/logging"
    29  	protoapi "code.vegaprotocol.io/vega/protos/vega/api/v1"
    30  )
    31  
    32  type StakingAccounts interface {
    33  	GetAvailableBalance(party string) (*num.Uint, error)
    34  }
    35  
    36  type EpochEngine interface {
    37  	NotifyOnEpoch(f func(context.Context, types.Epoch), r func(context.Context, types.Epoch))
    38  }
    39  
    40  type Engine struct {
    41  	log        *logging.Logger
    42  	config     Config
    43  	accounting StakingAccounts
    44  	cfgMu      sync.Mutex
    45  
    46  	transactionTypeToPolicy map[txn.Command]Policy
    47  	currentEpoch            *types.Epoch
    48  	policyNameToPolicy      map[string]Policy
    49  	hashKeys                []string
    50  	noSpamProtection        bool // flag that disables chesk for the spam policies, that is useful for the nullchain
    51  }
    52  
    53  type Policy interface {
    54  	Reset(epoch types.Epoch)
    55  	UpdateTx(tx abci.Tx)
    56  	RollbackProposal()
    57  	CheckBlockTx(abci.Tx) error
    58  	PreBlockAccept(tx abci.Tx) error
    59  	UpdateUintParam(name string, value *num.Uint) error
    60  	UpdateIntParam(name string, value int64) error
    61  	Serialise() ([]byte, error)
    62  	Deserialise(payload *types.Payload) error
    63  	GetSpamStats(partyID string) *protoapi.SpamStatistic
    64  	GetVoteSpamStats(partyID string) *protoapi.VoteSpamStatistics
    65  }
    66  
    67  // ReloadConf updates the internal configuration of the spam engine.
    68  func (e *Engine) ReloadConf(cfg Config) {
    69  	e.log.Info("reloading configuration")
    70  	if e.log.GetLevel() != cfg.Level.Get() {
    71  		e.log.Info("updating log level",
    72  			logging.String("old", e.log.GetLevel().String()),
    73  			logging.String("new", cfg.Level.String()),
    74  		)
    75  		e.log.SetLevel(cfg.Level.Get())
    76  	}
    77  
    78  	e.cfgMu.Lock()
    79  	e.config = cfg
    80  	e.cfgMu.Unlock()
    81  }
    82  
    83  // New instantiates a new spam engine.
    84  func New(log *logging.Logger, config Config, epochEngine EpochEngine, accounting StakingAccounts) *Engine {
    85  	log = log.Named(namedLogger)
    86  	log.SetLevel(config.Level.Get())
    87  	e := &Engine{
    88  		config:                  config,
    89  		log:                     log,
    90  		accounting:              accounting,
    91  		transactionTypeToPolicy: map[txn.Command]Policy{},
    92  	}
    93  
    94  	// simple policies
    95  	proposalPolicy := NewSimpleSpamPolicy("proposal", netparams.SpamProtectionMinTokensForProposal, netparams.SpamProtectionMaxProposals, log, accounting)
    96  	valJoinPolicy := NewSimpleSpamPolicy("validatorJoin", netparams.StakingAndDelegationRewardMinimumValidatorStake, "", log, accounting)
    97  	delegationPolicy := NewSimpleSpamPolicy("delegation", netparams.SpamProtectionMinTokensForDelegation, netparams.SpamProtectionMaxDelegations, log, accounting)
    98  	transferPolicy := NewSimpleSpamPolicy("transfer", "", netparams.TransferMaxCommandsPerEpoch, log, accounting)
    99  	issuesSignaturesPolicy := NewSimpleSpamPolicy("issueSignature", netparams.SpamProtectionMinMultisigUpdates, netparams.SpamProtectionMaxMultisigUpdates, log, accounting)
   100  
   101  	createReferralSetPolicy := NewSimpleSpamPolicy("createReferralSet", netparams.ReferralProgramMinStakedVegaTokens, netparams.SpamProtectionMaxCreateReferralSet, log, accounting)
   102  	updateReferralSetPolicy := NewSimpleSpamPolicy("updateReferralSet", netparams.ReferralProgramMinStakedVegaTokens, netparams.SpamProtectionMaxUpdateReferralSet, log, accounting)
   103  	applyReferralCodePolicy := NewSimpleSpamPolicy("applyReferralCode", "", netparams.SpamProtectionMaxApplyReferralCode, log, accounting)
   104  	updatePartyProfilePolicy := NewSimpleSpamPolicy("updatePartyProfile", "", netparams.SpamProtectionMaxUpdatePartyProfile, log, accounting)
   105  
   106  	// complex policies
   107  	votePolicy := NewVoteSpamPolicy(netparams.SpamProtectionMinTokensForVoting, netparams.SpamProtectionMaxVotes, log, accounting)
   108  
   109  	voteKey := (&types.PayloadVoteSpamPolicy{}).Key()
   110  	e.policyNameToPolicy = map[string]Policy{
   111  		proposalPolicy.policyName:           proposalPolicy,
   112  		valJoinPolicy.policyName:            valJoinPolicy,
   113  		delegationPolicy.policyName:         delegationPolicy,
   114  		transferPolicy.policyName:           transferPolicy,
   115  		issuesSignaturesPolicy.policyName:   issuesSignaturesPolicy,
   116  		voteKey:                             votePolicy,
   117  		createReferralSetPolicy.policyName:  createReferralSetPolicy,
   118  		updateReferralSetPolicy.policyName:  updateReferralSetPolicy,
   119  		applyReferralCodePolicy.policyName:  applyReferralCodePolicy,
   120  		updatePartyProfilePolicy.policyName: updatePartyProfilePolicy,
   121  	}
   122  	e.hashKeys = []string{
   123  		proposalPolicy.policyName,
   124  		valJoinPolicy.policyName,
   125  		delegationPolicy.policyName,
   126  		transferPolicy.policyName,
   127  		issuesSignaturesPolicy.policyName,
   128  		createReferralSetPolicy.policyName,
   129  		updateReferralSetPolicy.policyName,
   130  		applyReferralCodePolicy.policyName,
   131  		updatePartyProfilePolicy.policyName,
   132  		voteKey,
   133  	}
   134  
   135  	e.transactionTypeToPolicy[txn.ProposeCommand] = proposalPolicy
   136  	e.transactionTypeToPolicy[txn.BatchProposeCommand] = proposalPolicy
   137  	e.transactionTypeToPolicy[txn.AnnounceNodeCommand] = valJoinPolicy
   138  	e.transactionTypeToPolicy[txn.DelegateCommand] = delegationPolicy
   139  	e.transactionTypeToPolicy[txn.UndelegateCommand] = delegationPolicy
   140  	e.transactionTypeToPolicy[txn.TransferFundsCommand] = transferPolicy
   141  	e.transactionTypeToPolicy[txn.CancelTransferFundsCommand] = transferPolicy
   142  	e.transactionTypeToPolicy[txn.IssueSignatures] = issuesSignaturesPolicy
   143  	e.transactionTypeToPolicy[txn.VoteCommand] = votePolicy
   144  	e.transactionTypeToPolicy[txn.CreateReferralSetCommand] = createReferralSetPolicy
   145  	e.transactionTypeToPolicy[txn.UpdateReferralSetCommand] = updateReferralSetPolicy
   146  	e.transactionTypeToPolicy[txn.ApplyReferralCodeCommand] = applyReferralCodePolicy
   147  	e.transactionTypeToPolicy[txn.UpdatePartyProfileCommand] = updatePartyProfilePolicy
   148  
   149  	// register for epoch end notifications
   150  	epochEngine.NotifyOnEpoch(e.OnEpochEvent, e.OnEpochRestore)
   151  	e.log.Info("Spam protection started")
   152  
   153  	return e
   154  }
   155  
   156  func (e *Engine) DisableSpamProtection() {
   157  	e.log.Infof("Disabling spam protection for the Spam Engine")
   158  	e.noSpamProtection = true
   159  }
   160  
   161  // OnCreateReferralSet is called when the net param for max create referral set per epoch has changed.
   162  func (e *Engine) OnMaxCreateReferralSet(ctx context.Context, max int64) error {
   163  	return e.transactionTypeToPolicy[txn.CreateReferralSetCommand].UpdateIntParam(netparams.SpamProtectionMaxCreateReferralSet, max)
   164  }
   165  
   166  // OnMaxPartyProfileUpdate is called when the net param for max update party profile per epoch has changed.
   167  func (e *Engine) OnMaxPartyProfile(ctx context.Context, max int64) error {
   168  	return e.transactionTypeToPolicy[txn.UpdatePartyProfileCommand].UpdateIntParam(netparams.SpamProtectionMaxUpdatePartyProfile, max)
   169  }
   170  
   171  // OnMaxUpdateReferralSet is called when the net param for max update referral set per epoch has changed.
   172  func (e *Engine) OnMaxUpdateReferralSet(ctx context.Context, max int64) error {
   173  	return e.transactionTypeToPolicy[txn.UpdateReferralSetCommand].UpdateIntParam(netparams.SpamProtectionMaxUpdateReferralSet, max)
   174  }
   175  
   176  // OnMaxApplyReferralCode is called when the net param for max update referral set per epoch has changed.
   177  func (e *Engine) OnMaxApplyReferralCode(ctx context.Context, max int64) error {
   178  	return e.transactionTypeToPolicy[txn.ApplyReferralCodeCommand].UpdateIntParam(netparams.SpamProtectionMaxApplyReferralCode, max)
   179  }
   180  
   181  // OnMinTokensForReferral is called when the net param for min staked tokens requirement for referral set create/update has changed.
   182  func (e *Engine) OnMinTokensForReferral(ctx context.Context, minTokens *num.Uint) error {
   183  	err := e.transactionTypeToPolicy[txn.CreateReferralSetCommand].UpdateUintParam(netparams.ReferralProgramMinStakedVegaTokens, minTokens)
   184  	if err != nil {
   185  		return err
   186  	}
   187  	return e.transactionTypeToPolicy[txn.UpdateReferralSetCommand].UpdateUintParam(netparams.ReferralProgramMinStakedVegaTokens, minTokens)
   188  }
   189  
   190  // OnMaxDelegationsChanged is called when the net param for max delegations per epoch has changed.
   191  func (e *Engine) OnMaxDelegationsChanged(ctx context.Context, maxDelegations int64) error {
   192  	return e.transactionTypeToPolicy[txn.DelegateCommand].UpdateIntParam(netparams.SpamProtectionMaxDelegations, maxDelegations)
   193  }
   194  
   195  // OnMinTokensForDelegationChanged is called when the net param for min tokens requirement for voting has changed.
   196  func (e *Engine) OnMinTokensForDelegationChanged(ctx context.Context, minTokens num.Decimal) error {
   197  	minTokensFoDelegation, _ := num.UintFromDecimal(minTokens)
   198  	return e.transactionTypeToPolicy[txn.DelegateCommand].UpdateUintParam(netparams.SpamProtectionMinTokensForDelegation, minTokensFoDelegation)
   199  }
   200  
   201  // OnMaxVotesChanged is called when the net param for max votes per epoch has changed.
   202  func (e *Engine) OnMaxVotesChanged(ctx context.Context, maxVotes int64) error {
   203  	return e.transactionTypeToPolicy[txn.VoteCommand].UpdateIntParam(netparams.SpamProtectionMaxVotes, maxVotes)
   204  }
   205  
   206  // OnMinTokensForVotingChanged is called when the net param for min tokens requirement for voting has changed.
   207  func (e *Engine) OnMinTokensForVotingChanged(ctx context.Context, minTokens num.Decimal) error {
   208  	minTokensForVoting, _ := num.UintFromDecimal(minTokens)
   209  	return e.transactionTypeToPolicy[txn.VoteCommand].UpdateUintParam(netparams.SpamProtectionMinTokensForVoting, minTokensForVoting)
   210  }
   211  
   212  // OnMaxProposalsChanged is called when the net param for max proposals per epoch has changed.
   213  func (e *Engine) OnMaxProposalsChanged(ctx context.Context, maxProposals int64) error {
   214  	return e.transactionTypeToPolicy[txn.ProposeCommand].UpdateIntParam(netparams.SpamProtectionMaxProposals, maxProposals)
   215  }
   216  
   217  // OnMinTokensForProposalChanged is called when the net param for min tokens requirement for submitting a proposal has changed.
   218  func (e *Engine) OnMinTokensForProposalChanged(ctx context.Context, minTokens num.Decimal) error {
   219  	minTokensForProposal, _ := num.UintFromDecimal(minTokens)
   220  	return e.transactionTypeToPolicy[txn.ProposeCommand].UpdateUintParam(netparams.SpamProtectionMinTokensForProposal, minTokensForProposal)
   221  }
   222  
   223  // OnMaxTransfersChanged is called when the net param for max transfers per epoch changes.
   224  func (e *Engine) OnMaxTransfersChanged(_ context.Context, maxTransfers int64) error {
   225  	return e.transactionTypeToPolicy[txn.TransferFundsCommand].UpdateIntParam(netparams.TransferMaxCommandsPerEpoch, maxTransfers)
   226  }
   227  
   228  // OnMinValidatorTokensChanged is called when the net param for min tokens for joining validator changes.
   229  func (e *Engine) OnMinValidatorTokensChanged(_ context.Context, minTokens num.Decimal) error {
   230  	minTokensForJoiningValidator, _ := num.UintFromDecimal(minTokens)
   231  	return e.transactionTypeToPolicy[txn.AnnounceNodeCommand].UpdateUintParam(netparams.StakingAndDelegationRewardMinimumValidatorStake, minTokensForJoiningValidator)
   232  }
   233  
   234  // OnMinTokensForProposalChanged is called when the net param for min tokens requirement for submitting a proposal has changed.
   235  func (e *Engine) OnMinTokensForMultisigUpdatesChanged(_ context.Context, minTokens num.Decimal) error {
   236  	minTokensForMultisigUpdates, _ := num.UintFromDecimal(minTokens)
   237  	return e.transactionTypeToPolicy[txn.IssueSignatures].UpdateUintParam(netparams.SpamProtectionMinMultisigUpdates, minTokensForMultisigUpdates)
   238  }
   239  
   240  // OnMinTokensForProposalChanged is called when the net param for min tokens requirement for submitting a proposal has changed.
   241  func (e *Engine) OnMaxMultisigUpdatesChanged(_ context.Context, maxUpdates int64) error {
   242  	return e.transactionTypeToPolicy[txn.IssueSignatures].UpdateIntParam(netparams.SpamProtectionMaxMultisigUpdates, maxUpdates)
   243  }
   244  
   245  // OnEpochEvent is a callback for epoch events.
   246  func (e *Engine) OnEpochEvent(ctx context.Context, epoch types.Epoch) {
   247  	e.log.Info("Spam protection OnEpochEvent called", logging.Uint64("epoch", epoch.Seq))
   248  
   249  	if e.noSpamProtection {
   250  		e.log.Info("Spam protection OnEpochEvent disabled", logging.Uint64("epoch", epoch.Seq))
   251  		return
   252  	}
   253  
   254  	if e.currentEpoch == nil || e.currentEpoch.Seq != epoch.Seq {
   255  		if e.log.GetLevel() <= logging.DebugLevel {
   256  			e.log.Debug("Spam protection new epoch started", logging.Uint64("epochSeq", epoch.Seq))
   257  		}
   258  		e.currentEpoch = &epoch
   259  
   260  		for _, policy := range e.transactionTypeToPolicy {
   261  			policy.Reset(epoch)
   262  		}
   263  	}
   264  }
   265  
   266  func (e *Engine) BeginBlock(txs []abci.Tx) {
   267  	for _, tx := range txs {
   268  		if _, ok := e.transactionTypeToPolicy[tx.Command()]; !ok {
   269  			continue
   270  		}
   271  		e.transactionTypeToPolicy[tx.Command()].UpdateTx(tx)
   272  	}
   273  }
   274  
   275  func (e *Engine) EndPrepareProposal() {
   276  	for _, policy := range e.transactionTypeToPolicy {
   277  		policy.RollbackProposal()
   278  	}
   279  }
   280  
   281  // PreBlockAccept is called from onCheckTx before a tx is added to mempool
   282  // returns false is rejected by spam engine with a corresponding error.
   283  func (e *Engine) PreBlockAccept(tx abci.Tx) error {
   284  	command := tx.Command()
   285  	if _, ok := e.transactionTypeToPolicy[command]; !ok {
   286  		return nil
   287  	}
   288  	if e.log.GetLevel() <= logging.DebugLevel {
   289  		e.log.Debug("Spam protection PreBlockAccept called for policy", logging.String("txHash", hex.EncodeToString(tx.Hash())), logging.String("command", command.String()))
   290  	}
   291  	if e.noSpamProtection {
   292  		e.log.Debug("Spam protection PreBlockAccept disabled for policy", logging.String("txHash", hex.EncodeToString(tx.Hash())), logging.String("command", command.String()))
   293  		return nil
   294  	}
   295  	return e.transactionTypeToPolicy[command].PreBlockAccept(tx)
   296  }
   297  
   298  func (e *Engine) ProcessProposal(txs []abci.Tx) bool {
   299  	success := true
   300  	for _, tx := range txs {
   301  		command := tx.Command()
   302  		if _, ok := e.transactionTypeToPolicy[command]; !ok {
   303  			continue
   304  		}
   305  		if e.noSpamProtection {
   306  			e.log.Debug("Spam protection PreBlockAccept disabled for policy", logging.String("txHash", hex.EncodeToString(tx.Hash())), logging.String("command", command.String()))
   307  			continue
   308  		}
   309  
   310  		if err := e.transactionTypeToPolicy[command].CheckBlockTx(tx); err != nil {
   311  			success = false
   312  		}
   313  	}
   314  	for _, p := range e.transactionTypeToPolicy {
   315  		p.RollbackProposal()
   316  	}
   317  	return success
   318  }
   319  
   320  // PostBlockAccept is called from onDeliverTx before the block is processed
   321  // returns false is rejected by spam engine with a corresponding error.
   322  func (e *Engine) CheckBlockTx(tx abci.Tx) error {
   323  	command := tx.Command()
   324  	if _, ok := e.transactionTypeToPolicy[command]; !ok {
   325  		return nil
   326  	}
   327  	if e.log.GetLevel() <= logging.DebugLevel {
   328  		e.log.Debug("Spam protection PostBlockAccept called for policy", logging.String("txHash", hex.EncodeToString(tx.Hash())), logging.String("command", command.String()))
   329  	}
   330  
   331  	if e.noSpamProtection {
   332  		e.log.Debug("Spam protection PreBlockAccept disabled for policy", logging.String("txHash", hex.EncodeToString(tx.Hash())), logging.String("command", command.String()))
   333  		return nil
   334  	}
   335  	return e.transactionTypeToPolicy[command].CheckBlockTx(tx)
   336  }
   337  
   338  func (e *Engine) GetSpamStatistics(partyID string) *protoapi.SpamStatistics {
   339  	stats := &protoapi.SpamStatistics{}
   340  
   341  	for txType, policy := range e.transactionTypeToPolicy {
   342  		switch txType {
   343  		case txn.ProposeCommand, txn.BatchProposeCommand:
   344  			stats.Proposals = policy.GetSpamStats(partyID)
   345  		case txn.DelegateCommand:
   346  			stats.Delegations = policy.GetSpamStats(partyID)
   347  		case txn.TransferFundsCommand:
   348  			stats.Transfers = policy.GetSpamStats(partyID)
   349  		case txn.AnnounceNodeCommand:
   350  			stats.NodeAnnouncements = policy.GetSpamStats(partyID)
   351  		case txn.IssueSignatures:
   352  			stats.IssueSignatures = policy.GetSpamStats(partyID)
   353  		case txn.VoteCommand:
   354  			stats.Votes = policy.GetVoteSpamStats(partyID)
   355  		case txn.CreateReferralSetCommand:
   356  			stats.CreateReferralSet = policy.GetSpamStats(partyID)
   357  		case txn.UpdateReferralSetCommand:
   358  			stats.UpdateReferralSet = policy.GetSpamStats(partyID)
   359  		case txn.ApplyReferralCodeCommand:
   360  			stats.ApplyReferralCode = policy.GetSpamStats(partyID)
   361  		default:
   362  			continue
   363  		}
   364  	}
   365  
   366  	return stats
   367  }