-
Notifications
You must be signed in to change notification settings - Fork 19
/
spam.go
137 lines (116 loc) · 4.79 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
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_VoteSubmission:
s.mergeVotes(stats.Votes, newStats.Votes)
return s.checkVote(cmd.VoteSubmission.ProposalId, stats.Votes)
}
return nil
}