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 }