code.vegaprotocol.io/vega@v0.79.0/core/spam/vote_spam_policy_test.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_test 17 18 import ( 19 "bytes" 20 "strconv" 21 "testing" 22 23 "code.vegaprotocol.io/vega/core/netparams" 24 "code.vegaprotocol.io/vega/core/spam" 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/libs/proto" 29 "code.vegaprotocol.io/vega/logging" 30 snapshot "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" 31 32 "github.com/stretchr/testify/require" 33 ) 34 35 type testTx struct { 36 party string 37 proposal string 38 command txn.Command 39 } 40 41 func (*testTx) GetLength() int { return 0 } 42 func (*testTx) GetPoWNonce() uint64 { return 0 } 43 func (*testTx) GetNonce() uint64 { return 0 } 44 func (*testTx) GetPoWTID() string { return "" } 45 func (*testTx) GetVersion() uint32 { return 2 } 46 47 var sufficientTokensForVoting, _ = num.UintFromString("100000000000000000000", 10) 48 49 func TestVotingSpamProtection(t *testing.T) { 50 t.Run("Pre reject vote from party with insufficient balance at the beginning of the epoch", testPreRejectInsufficientBalance) 51 t.Run("Pre reject vote from party that already had more than 3 votes for the epoch", testPreRejectTooManyVotesPerProposal) 52 t.Run("Pre accept vote success", testPreAccept) 53 t.Run("Post accept vote success", testPostAccept) 54 t.Run("Vote counts from the block carried over to next block", testCountersUpdated) 55 t.Run("On epoch start voting counters are reset", testReset) 56 t.Run("On end of block, block voting counters are reset and take a snapshot roundtrip", testVoteEndBlockReset) 57 } 58 59 func getVotingSpamPolicy(accounts map[string]*num.Uint) *spam.VoteSpamPolicy { 60 logger := logging.NewTestLogger() 61 testAccounts := testAccounts{balances: accounts} 62 policy := spam.NewVoteSpamPolicy(netparams.SpamProtectionMinTokensForVoting, netparams.SpamProtectionMaxVotes, logger, testAccounts) 63 minTokensForVoting, _ := num.UintFromString("100000000000000000000", 10) 64 policy.UpdateUintParam(netparams.SpamProtectionMinTokensForVoting, minTokensForVoting) 65 policy.UpdateIntParam(netparams.SpamProtectionMaxVotes, 3) 66 return policy 67 } 68 69 // reject vote requests when the voter doesn't have sufficient balance at the beginning of the epoch. 70 func testPreRejectInsufficientBalance(t *testing.T) { 71 policy := getVotingSpamPolicy(map[string]*num.Uint{"party1": num.NewUint(50)}) 72 policy.Reset(types.Epoch{Seq: 0}) 73 tx := &testTx{party: "party1", proposal: "proposal1"} 74 require.Equal(t, spam.ErrInsufficientTokensForVoting, policy.PreBlockAccept(tx)) 75 } 76 77 func testPreRejectTooManyVotesPerProposal(t *testing.T) { 78 policy := getVotingSpamPolicy(map[string]*num.Uint{"party1": sufficientTokensForVoting}) 79 // epoch 0 block 0 80 policy.Reset(types.Epoch{Seq: 0}) 81 82 // vote 5 times for each proposal all pre accepted, 3 for each post accepted 83 for i := 0; i < 2; i++ { 84 tx := &testTx{party: "party1", proposal: "proposal" + strconv.Itoa(i+1)} 85 // pre accepted 86 for i := 0; i < 5; i++ { 87 require.NoError(t, policy.PreBlockAccept(tx)) 88 } 89 90 // prepare block check 91 for i := 0; i < 5; i++ { 92 if i < 3 { 93 require.NoError(t, policy.CheckBlockTx(tx)) 94 } else { 95 require.Error(t, policy.CheckBlockTx(tx)) 96 } 97 } 98 99 policy.RollbackProposal() 100 101 for i := 0; i < 3; i++ { 102 policy.UpdateTx(tx) 103 } 104 } 105 106 // try to submit 107 for i := 0; i < 2; i++ { 108 tx := &testTx{party: "party1", proposal: "proposal" + strconv.Itoa(i+1)} 109 require.Equal(t, spam.ErrTooManyVotes.Error(), policy.PreBlockAccept(tx).Error()) 110 } 111 112 tx := &testTx{party: "party1", proposal: "proposal3"} 113 // pre accepted 114 require.NoError(t, policy.PreBlockAccept(tx)) 115 116 // advance to next epoch to reset limits 117 policy.Reset(types.Epoch{Seq: 0}) 118 for i := 0; i < 3; i++ { 119 tx := &testTx{party: "party1", proposal: "proposal" + strconv.Itoa(i+1)} 120 require.NoError(t, policy.PreBlockAccept(tx)) 121 } 122 } 123 124 func testPreAccept(t *testing.T) { 125 policy := getVotingSpamPolicy(map[string]*num.Uint{"party1": sufficientTokensForVoting}) 126 // epoch 0 block 0 127 policy.Reset(types.Epoch{Seq: 0}) 128 129 // vote 5 times for each proposal all pre accepted, 3 for each post accepted 130 for i := 0; i < 2; i++ { 131 tx := &testTx{party: "party1", proposal: "proposal" + strconv.Itoa(i+1)} 132 // pre accepted 133 for i := 0; i < 5; i++ { 134 require.Nil(t, policy.PreBlockAccept(tx)) 135 } 136 } 137 } 138 139 func testPostAccept(t *testing.T) { 140 policy := getVotingSpamPolicy(map[string]*num.Uint{"party1": sufficientTokensForVoting}) 141 policy.Reset(types.Epoch{Seq: 0}) 142 143 policy.Reset(types.Epoch{Seq: 0}) 144 145 tx1 := &testTx{party: "party1", proposal: "proposal1"} 146 tx2 := &testTx{party: "party1", proposal: "proposal1"} 147 tx3 := &testTx{party: "party1", proposal: "proposal1"} 148 tx4 := &testTx{party: "party1", proposal: "proposal1"} 149 150 require.NoError(t, policy.CheckBlockTx(tx1)) 151 require.NoError(t, policy.CheckBlockTx(tx2)) 152 require.NoError(t, policy.CheckBlockTx(tx3)) 153 require.Error(t, policy.CheckBlockTx(tx4)) 154 155 policy.RollbackProposal() 156 157 // as the state has nothing, expect pre block accept of all 4 txs 158 require.NoError(t, policy.PreBlockAccept(tx1)) 159 require.NoError(t, policy.PreBlockAccept(tx2)) 160 require.NoError(t, policy.PreBlockAccept(tx3)) 161 require.NoError(t, policy.PreBlockAccept(tx4)) 162 163 // now a block is made with the first 3 txs 164 require.NoError(t, policy.CheckBlockTx(tx1)) 165 require.NoError(t, policy.CheckBlockTx(tx2)) 166 require.NoError(t, policy.CheckBlockTx(tx3)) 167 168 // and the block is confirmed 169 policy.RollbackProposal() 170 policy.UpdateTx(tx1) 171 policy.UpdateTx(tx2) 172 policy.UpdateTx(tx3) 173 174 stats := policy.GetVoteSpamStats(tx1.party).GetStatistics()[0] 175 require.Equal(t, uint64(3), stats.CountForEpoch) 176 177 // now that there's been 3 proposals already, the 4th should be pre-rejected 178 require.Error(t, policy.PreBlockAccept(tx4)) 179 180 // start a new epoch to reset counters 181 policy.Reset(types.Epoch{Seq: 0}) 182 183 // check that the new proposal is pre-block accepted 184 require.NoError(t, policy.PreBlockAccept(tx4)) 185 186 require.Equal(t, 0, len(policy.GetVoteSpamStats(tx1.party).GetStatistics())) 187 } 188 189 func testCountersUpdated(t *testing.T) { 190 policy := getVotingSpamPolicy(map[string]*num.Uint{"party1": sufficientTokensForVoting}) 191 policy.Reset(types.Epoch{Seq: 0}) 192 193 for i := 0; i < 2; i++ { 194 tx := &testTx{party: "party1", proposal: "proposal" + strconv.Itoa(i+1)} 195 // post accepted 196 for i := 0; i < 2; i++ { 197 require.NoError(t, policy.CheckBlockTx(tx)) 198 } 199 } 200 policy.RollbackProposal() 201 for i := 0; i < 2; i++ { 202 tx := &testTx{party: "party1", proposal: "proposal" + strconv.Itoa(i+1)} 203 // post accepted 204 for i := 0; i < 2; i++ { 205 policy.UpdateTx(tx) 206 } 207 } 208 209 for i := 0; i < 2; i++ { 210 tx := &testTx{party: "party1", proposal: "proposal" + strconv.Itoa(i+1)} 211 // pre accepted 212 for i := 0; i < 2; i++ { 213 require.NoError(t, policy.PreBlockAccept(tx)) 214 } 215 } 216 for i := 0; i < 2; i++ { 217 tx := &testTx{party: "party1", proposal: "proposal" + strconv.Itoa(i+1)} 218 // post accepted 219 require.NoError(t, policy.CheckBlockTx(tx)) 220 221 // post rejected 222 require.Error(t, spam.ErrTooManyVotes, policy.CheckBlockTx(tx)) 223 } 224 } 225 226 func testReset(t *testing.T) { 227 // set state 228 policy := getVotingSpamPolicy(map[string]*num.Uint{"party1": sufficientTokensForVoting}) 229 policy.Reset(types.Epoch{Seq: 0}) 230 for i := 0; i < 2; i++ { 231 tx := &testTx{party: "party1", proposal: "proposal" + strconv.Itoa(i+1)} 232 // pre accepted 233 for i := 0; i < 6; i++ { 234 require.NoError(t, policy.PreBlockAccept(tx)) 235 } 236 } 237 238 for i := 0; i < 2; i++ { 239 tx := &testTx{party: "party1", proposal: "proposal" + strconv.Itoa(i+1)} 240 // post accepted 241 for i := 0; i < 3; i++ { 242 require.NoError(t, policy.CheckBlockTx(tx)) 243 } 244 245 for i := 0; i < 3; i++ { 246 require.Equal(t, spam.ErrTooManyVotes, policy.CheckBlockTx(tx)) 247 } 248 } 249 policy.RollbackProposal() 250 for i := 0; i < 2; i++ { 251 tx := &testTx{party: "party1", proposal: "proposal" + strconv.Itoa(i+1)} 252 // post accepted 253 for i := 0; i < 3; i++ { 254 policy.UpdateTx(tx) 255 } 256 } 257 258 tx := &testTx{party: "party1", proposal: "proposal1"} 259 require.Error(t, policy.PreBlockAccept(tx)) 260 261 policy.Reset(types.Epoch{Seq: 1}) 262 require.NoError(t, policy.PreBlockAccept(tx)) 263 } 264 265 func testVoteEndBlockReset(t *testing.T) { 266 // set state 267 policy := getVotingSpamPolicy(map[string]*num.Uint{"party1": sufficientTokensForVoting}) 268 policy.Reset(types.Epoch{Seq: 0}) 269 270 // in each block we vote once 271 var i uint64 272 for ; i < 3; i++ { 273 tx := &testTx{party: "party1", proposal: "proposal1"} 274 require.NoError(t, policy.PreBlockAccept(tx)) 275 require.NoError(t, policy.CheckBlockTx(tx)) 276 policy.RollbackProposal() 277 policy.UpdateTx(tx) 278 } 279 280 tx := &testTx{party: "party1", proposal: "proposal1"} 281 require.Error(t, spam.ErrTooManyVotes, policy.PreBlockAccept(tx)) 282 283 bytes1, err := policy.Serialise() 284 require.Nil(t, err) 285 var votePayload snapshot.Payload 286 proto.Unmarshal(bytes1, &votePayload) 287 payload := types.PayloadFromProto(&votePayload) 288 policy.Deserialise(payload) 289 bytes2, err := policy.Serialise() 290 require.Nil(t, err) 291 require.True(t, bytes.Equal(bytes1, bytes2)) 292 293 tx2 := &testTx{party: "party1", proposal: "proposal2"} 294 require.NoError(t, policy.CheckBlockTx(tx2)) 295 policy.RollbackProposal() 296 297 // verify that changes made during prepare proposal are properly rolled back and not affecting the state 298 bytes3, err := policy.Serialise() 299 require.NoError(t, err) 300 require.True(t, bytes.Equal(bytes3, bytes2)) 301 302 // now the block has been processed, verify that the state has changed 303 policy.UpdateTx(tx2) 304 bytes4, err := policy.Serialise() 305 require.NoError(t, err) 306 require.False(t, bytes.Equal(bytes4, bytes3)) 307 }