code.vegaprotocol.io/vega@v0.79.0/wallet/api/spam/spam.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  	"errors"
    20  	"fmt"
    21  	"sync"
    22  
    23  	walletpb "code.vegaprotocol.io/vega/protos/vega/wallet/v1"
    24  	nodetypes "code.vegaprotocol.io/vega/wallet/api/node/types"
    25  )
    26  
    27  var ErrPartyWillBeBanned = errors.New("submitting this transaction will cause this key to be temporarily banned by the the network")
    28  
    29  type Handler struct {
    30  	// chainID to the counter for transactions sent.
    31  	counters map[string]*txCounter
    32  
    33  	// chainID -> pubkey -> last known spam statistics
    34  	spam map[string]map[string]*nodetypes.SpamStatistics
    35  
    36  	mu sync.Mutex
    37  }
    38  
    39  func NewHandler() *Handler {
    40  	return &Handler{
    41  		counters: map[string]*txCounter{},
    42  		spam:     map[string]map[string]*nodetypes.SpamStatistics{},
    43  	}
    44  }
    45  
    46  func (s *Handler) getSpamStatisticsForChain(chainID string) map[string]*nodetypes.SpamStatistics {
    47  	if _, ok := s.spam[chainID]; !ok {
    48  		s.spam[chainID] = map[string]*nodetypes.SpamStatistics{}
    49  	}
    50  	return s.spam[chainID]
    51  }
    52  
    53  // checkVote because it has to be a little different...
    54  func (s *Handler) checkVote(propID string, st *nodetypes.VoteSpamStatistics) error {
    55  	if st.BannedUntil != nil {
    56  		return fmt.Errorf("party is banned from submitting transactions of this type until %s", *st.BannedUntil)
    57  	}
    58  	v := st.Proposals[propID]
    59  	if v == st.MaxForEpoch {
    60  		return fmt.Errorf("party has already submitted the maximum number of transactions of this type per epoch (%d)", st.MaxForEpoch)
    61  	}
    62  	st.Proposals[propID]++
    63  	return nil
    64  }
    65  
    66  func (s *Handler) checkTxn(st *nodetypes.SpamStatistic) error {
    67  	if st.BannedUntil != nil {
    68  		return fmt.Errorf("party is banned from submitting transactions of this type until %s", *st.BannedUntil)
    69  	}
    70  
    71  	if st.CountForEpoch == st.MaxForEpoch {
    72  		return fmt.Errorf("party has already submitted the maximum number of transactions of this type per epoch (%d)", st.MaxForEpoch)
    73  	}
    74  
    75  	// increment the count by hand because the spam-stats endpoint only updates once a block
    76  	// so if we send in multiple transactions between that next update we need to know about
    77  	// the past ones
    78  	st.CountForEpoch++
    79  	return nil
    80  }
    81  
    82  func (s *Handler) mergeVotes(st *nodetypes.VoteSpamStatistics, other *nodetypes.VoteSpamStatistics) {
    83  	st.BannedUntil = other.BannedUntil
    84  	st.MaxForEpoch = other.MaxForEpoch
    85  	for pid, cnt := range other.Proposals {
    86  		if cnt > st.Proposals[pid] {
    87  			st.Proposals[pid] = cnt
    88  		}
    89  	}
    90  }
    91  
    92  // merge will take the spam stats from other and update st only if other's counters are higher.
    93  func (s *Handler) merge(st *nodetypes.SpamStatistic, other *nodetypes.SpamStatistic) {
    94  	st.BannedUntil = other.BannedUntil
    95  	st.MaxForEpoch = other.MaxForEpoch
    96  
    97  	// we've pinged the spam endpoint and the count it returns will either
    98  	// 1) equal our counts and we're fine
    99  	// 2) have a bigger count then ours, meaning something external has submitted for, so we take the bigger count
   100  	// 3) its count is smaller than ours meaning we're submitting lots on the same block and so the spam endpoint is behind,
   101  	//    so we keep what we have
   102  	if other.CountForEpoch > st.CountForEpoch {
   103  		st.CountForEpoch = other.CountForEpoch
   104  	}
   105  }
   106  
   107  // CheckSubmission return an error if we are banned from making this type of transaction or if submitting
   108  // the transaction will result in a banning.
   109  func (s *Handler) CheckSubmission(req *walletpb.SubmitTransactionRequest, newStats *nodetypes.SpamStatistics) error {
   110  	s.mu.Lock()
   111  	defer s.mu.Unlock()
   112  	chainStats := s.getSpamStatisticsForChain(newStats.ChainID)
   113  
   114  	stats, ok := chainStats[req.PubKey]
   115  	if !ok {
   116  		chainStats[req.PubKey] = newStats
   117  		stats = newStats
   118  	}
   119  
   120  	if stats.EpochSeq < newStats.EpochSeq {
   121  		// we can reset all the spam statistics now that we're in a new epoch and just take what the spam endpoint tells us
   122  		chainStats[req.PubKey] = newStats
   123  		stats = newStats
   124  	}
   125  
   126  	if newStats.PoW.BannedUntil != nil {
   127  		return fmt.Errorf("party is banned from submitting all transactions until %s", *newStats.PoW.BannedUntil)
   128  	}
   129  
   130  	switch cmd := req.Command.(type) {
   131  	case *walletpb.SubmitTransactionRequest_ProposalSubmission:
   132  		s.merge(stats.Proposals, newStats.Proposals)
   133  		return s.checkTxn(stats.Proposals)
   134  	case *walletpb.SubmitTransactionRequest_AnnounceNode:
   135  		s.merge(stats.NodeAnnouncements, newStats.NodeAnnouncements)
   136  		return s.checkTxn(stats.NodeAnnouncements)
   137  	case *walletpb.SubmitTransactionRequest_UndelegateSubmission, *walletpb.SubmitTransactionRequest_DelegateSubmission:
   138  		s.merge(stats.Delegations, newStats.Delegations)
   139  		return s.checkTxn(stats.Delegations)
   140  	case *walletpb.SubmitTransactionRequest_Transfer:
   141  		s.merge(stats.Transfers, newStats.Transfers)
   142  		return s.checkTxn(stats.Transfers)
   143  	case *walletpb.SubmitTransactionRequest_IssueSignatures:
   144  		s.merge(stats.IssuesSignatures, newStats.IssuesSignatures)
   145  		return s.checkTxn(stats.IssuesSignatures)
   146  	case *walletpb.SubmitTransactionRequest_CreateReferralSet:
   147  		s.merge(stats.CreateReferralSet, newStats.CreateReferralSet)
   148  		return s.checkTxn(stats.CreateReferralSet)
   149  	case *walletpb.SubmitTransactionRequest_UpdateReferralSet:
   150  		s.merge(stats.UpdateReferralSet, newStats.UpdateReferralSet)
   151  		return s.checkTxn(stats.UpdateReferralSet)
   152  	case *walletpb.SubmitTransactionRequest_ApplyReferralCode:
   153  		s.merge(stats.ApplyReferralCode, newStats.ApplyReferralCode)
   154  		return s.checkTxn(stats.ApplyReferralCode)
   155  	case *walletpb.SubmitTransactionRequest_VoteSubmission:
   156  		s.mergeVotes(stats.Votes, newStats.Votes)
   157  		return s.checkVote(cmd.VoteSubmission.ProposalId, stats.Votes)
   158  	}
   159  
   160  	return nil
   161  }