-
Notifications
You must be signed in to change notification settings - Fork 22
/
spam.go
161 lines (139 loc) · 5.98 KB
/
spam.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// Copyright (C) 2023 Gobalsky Labs Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package spam
import (
"errors"
"fmt"
"sync"
walletpb "code.vegaprotocol.io/vega/protos/vega/wallet/v1"
nodetypes "code.vegaprotocol.io/vega/wallet/api/node/types"
)
var ErrPartyWillBeBanned = errors.New("submitting this transaction will cause this key to be temporarily banned by the the network")
type Handler struct {
// chainID to the counter for transactions sent.
counters map[string]*txCounter
// chainID -> pubkey -> last known spam statistics
spam map[string]map[string]*nodetypes.SpamStatistics
mu sync.Mutex
}
func NewHandler() *Handler {
return &Handler{
counters: map[string]*txCounter{},
spam: map[string]map[string]*nodetypes.SpamStatistics{},
}
}
func (s *Handler) getSpamStatisticsForChain(chainID string) map[string]*nodetypes.SpamStatistics {
if _, ok := s.spam[chainID]; !ok {
s.spam[chainID] = map[string]*nodetypes.SpamStatistics{}
}
return s.spam[chainID]
}
// checkVote because it has to be a little different...
func (s *Handler) checkVote(propID string, st *nodetypes.VoteSpamStatistics) error {
if st.BannedUntil != nil {
return fmt.Errorf("party is banned from submitting transactions of this type until %s", *st.BannedUntil)
}
v := st.Proposals[propID]
if v == st.MaxForEpoch {
return fmt.Errorf("party has already submitted the maximum number of transactions of this type per epoch (%d)", st.MaxForEpoch)
}
st.Proposals[propID]++
return nil
}
func (s *Handler) checkTxn(st *nodetypes.SpamStatistic) error {
if st.BannedUntil != nil {
return fmt.Errorf("party is banned from submitting transactions of this type until %s", *st.BannedUntil)
}
if st.CountForEpoch == st.MaxForEpoch {
return fmt.Errorf("party has already submitted the maximum number of transactions of this type per epoch (%d)", st.MaxForEpoch)
}
// increment the count by hand because the spam-stats endpoint only updates once a block
// so if we send in multiple transactions between that next update we need to know about
// the past ones
st.CountForEpoch++
return nil
}
func (s *Handler) mergeVotes(st *nodetypes.VoteSpamStatistics, other *nodetypes.VoteSpamStatistics) {
st.BannedUntil = other.BannedUntil
st.MaxForEpoch = other.MaxForEpoch
for pid, cnt := range other.Proposals {
if cnt > st.Proposals[pid] {
st.Proposals[pid] = cnt
}
}
}
// merge will take the spam stats from other and update st only if other's counters are higher.
func (s *Handler) merge(st *nodetypes.SpamStatistic, other *nodetypes.SpamStatistic) {
st.BannedUntil = other.BannedUntil
st.MaxForEpoch = other.MaxForEpoch
// we've pinged the spam endpoint and the count it returns will either
// 1) equal our counts and we're fine
// 2) have a bigger count then ours, meaning something external has submitted for, so we take the bigger count
// 3) its count is smaller than ours meaning we're submitting lots on the same block and so the spam endpoint is behind,
// so we keep what we have
if other.CountForEpoch > st.CountForEpoch {
st.CountForEpoch = other.CountForEpoch
}
}
// CheckSubmission return an error if we are banned from making this type of transaction or if submitting
// the transaction will result in a banning.
func (s *Handler) CheckSubmission(req *walletpb.SubmitTransactionRequest, newStats *nodetypes.SpamStatistics) error {
s.mu.Lock()
defer s.mu.Unlock()
chainStats := s.getSpamStatisticsForChain(newStats.ChainID)
stats, ok := chainStats[req.PubKey]
if !ok {
chainStats[req.PubKey] = newStats
stats = newStats
}
if stats.EpochSeq < newStats.EpochSeq {
// we can reset all the spam statistics now that we're in a new epoch and just take what the spam endpoint tells us
chainStats[req.PubKey] = newStats
stats = newStats
}
if newStats.PoW.BannedUntil != nil {
return fmt.Errorf("party is banned from submitting all transactions until %s", *newStats.PoW.BannedUntil)
}
switch cmd := req.Command.(type) {
case *walletpb.SubmitTransactionRequest_ProposalSubmission:
s.merge(stats.Proposals, newStats.Proposals)
return s.checkTxn(stats.Proposals)
case *walletpb.SubmitTransactionRequest_AnnounceNode:
s.merge(stats.NodeAnnouncements, newStats.NodeAnnouncements)
return s.checkTxn(stats.NodeAnnouncements)
case *walletpb.SubmitTransactionRequest_UndelegateSubmission, *walletpb.SubmitTransactionRequest_DelegateSubmission:
s.merge(stats.Delegations, newStats.Delegations)
return s.checkTxn(stats.Delegations)
case *walletpb.SubmitTransactionRequest_Transfer:
s.merge(stats.Transfers, newStats.Transfers)
return s.checkTxn(stats.Transfers)
case *walletpb.SubmitTransactionRequest_IssueSignatures:
s.merge(stats.IssuesSignatures, newStats.IssuesSignatures)
return s.checkTxn(stats.IssuesSignatures)
case *walletpb.SubmitTransactionRequest_CreateReferralSet:
s.merge(stats.CreateReferralSet, newStats.CreateReferralSet)
return s.checkTxn(stats.CreateReferralSet)
case *walletpb.SubmitTransactionRequest_UpdateReferralSet:
s.merge(stats.UpdateReferralSet, newStats.UpdateReferralSet)
return s.checkTxn(stats.UpdateReferralSet)
case *walletpb.SubmitTransactionRequest_ApplyReferralCode:
s.merge(stats.ApplyReferralCode, newStats.ApplyReferralCode)
return s.checkTxn(stats.ApplyReferralCode)
case *walletpb.SubmitTransactionRequest_VoteSubmission:
s.mergeVotes(stats.Votes, newStats.Votes)
return s.checkVote(cmd.VoteSubmission.ProposalId, stats.Votes)
}
return nil
}