code.vegaprotocol.io/vega@v0.79.0/core/spam/engine_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 "context" 21 "errors" 22 "testing" 23 24 "code.vegaprotocol.io/vega/core/blockchain/abci" 25 "code.vegaprotocol.io/vega/core/spam" 26 "code.vegaprotocol.io/vega/core/txn" 27 "code.vegaprotocol.io/vega/core/types" 28 "code.vegaprotocol.io/vega/libs/num" 29 "code.vegaprotocol.io/vega/libs/proto" 30 "code.vegaprotocol.io/vega/logging" 31 commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1" 32 snapshot "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" 33 34 "github.com/stretchr/testify/require" 35 ) 36 37 func (t *testTx) Command() txn.Command { 38 return t.command 39 } 40 41 func (t *testTx) Unmarshal(cmd interface{}) error { 42 cmd.(*commandspb.VoteSubmission).ProposalId = t.proposal 43 return nil 44 } 45 46 func (t *testTx) PubKey() []byte { 47 return nil 48 } 49 50 func (t *testTx) PubKeyHex() string { 51 return "" 52 } 53 54 func (t *testTx) Party() string { 55 return t.party 56 } 57 58 func (t *testTx) Hash() []byte { 59 return nil 60 } 61 62 func (t *testTx) Signature() []byte { 63 return nil 64 } 65 66 func (t *testTx) Validate() error { 67 return nil 68 } 69 70 func (t *testTx) BlockHeight() uint64 { 71 return 0 72 } 73 74 func (t *testTx) GetCmd() interface{} { 75 return nil 76 } 77 78 func TestEngine(t *testing.T) { 79 t.Run("pre block goes is handled by the appropriate spam policy", testPreBlockAccept) 80 t.Run("post block goes is handled by the appropriate spam policy", testPostBlockAccept) 81 t.Run("end prepare and end process is applied to all policies", testEndOfBlock) 82 t.Run("reset is applied to all policies", testEngineReset) 83 } 84 85 func testEngineReset(t *testing.T) { 86 testEngine := getEngine(t, map[string]*num.Uint{"party1": sufficientPropTokens}) 87 engine := testEngine.engine 88 engine.OnEpochEvent(context.Background(), types.Epoch{Seq: 0}) 89 90 tx1 := &testTx{party: "party1", proposal: "proposal1", command: txn.ProposeCommand} 91 tx2 := &testTx{party: "party1", proposal: "proposal1", command: txn.VoteCommand} 92 93 // pre accept 94 for i := 0; i < 3; i++ { 95 require.NoError(t, engine.PreBlockAccept(tx1)) 96 require.NoError(t, engine.PreBlockAccept(tx2)) 97 } 98 99 // post accept 100 for i := 0; i < 3; i++ { 101 err := engine.CheckBlockTx(tx1) 102 require.NoError(t, err) 103 err = engine.CheckBlockTx(tx2) 104 require.NoError(t, err) 105 } 106 // move to next block, we've voted/proposed everything already so shouldn't be allowed to make more 107 engine.EndPrepareProposal() 108 engine.ProcessProposal([]abci.Tx{tx1, tx1, tx1, tx2, tx2, tx2}) 109 engine.BeginBlock([]abci.Tx{tx1, tx1, tx1, tx2, tx2, tx2}) 110 111 proposalState, _, err := engine.GetState("proposal") 112 require.Nil(t, err) 113 voteState, _, err := engine.GetState((&types.PayloadVoteSpamPolicy{}).Key()) 114 require.Nil(t, err) 115 116 keys := engine.Keys() 117 snap := make(map[string][]byte, len(keys)) 118 for _, k := range keys { 119 data, _, err := engine.GetState(k) 120 require.NoError(t, err) 121 snap[k] = data 122 } 123 124 snapEngine := getEngine(t, map[string]*num.Uint{"party1": sufficientPropTokens}) 125 for _, bytes := range snap { 126 var p snapshot.Payload 127 proto.Unmarshal(bytes, &p) 128 payload := types.PayloadFromProto(&p) 129 snapEngine.engine.LoadState(context.Background(), payload) 130 } 131 132 // restore the epoch we were on 133 snapEngine.engine.OnEpochRestore(context.Background(), types.Epoch{Seq: 0}) 134 135 proposalState2, _, err := snapEngine.engine.GetState("proposal") 136 require.Nil(t, err) 137 require.True(t, bytes.Equal(proposalState, proposalState2)) 138 139 voteState2, _, err := snapEngine.engine.GetState((&types.PayloadVoteSpamPolicy{}).Key()) 140 require.Nil(t, err) 141 require.True(t, bytes.Equal(voteState, voteState2)) 142 143 require.Error(t, errors.New("party has already submitted the maximum number of proposal requests per epoch"), snapEngine.engine.PreBlockAccept(tx1)) 144 require.Equal(t, spam.ErrTooManyVotes, snapEngine.engine.PreBlockAccept(tx2)) 145 146 // Notify an epoch event for the *same* epoch and a reset should not happen 147 snapEngine.engine.OnEpochEvent(context.Background(), types.Epoch{Seq: 0}) 148 proposalStateNoReset, _, err := snapEngine.engine.GetState("proposal") 149 require.Nil(t, err) 150 require.True(t, bytes.Equal(proposalStateNoReset, proposalState2)) 151 152 // move to next epoch 153 snapEngine.engine.OnEpochEvent(context.Background(), types.Epoch{Seq: 1}) 154 155 // expect to be able to submit 3 more votes/proposals successfully 156 for i := 0; i < 3; i++ { 157 require.NoError(t, snapEngine.engine.PreBlockAccept(tx1)) 158 require.NoError(t, snapEngine.engine.PreBlockAccept(tx2)) 159 } 160 161 proposalState3, _, err := snapEngine.engine.GetState("proposal") 162 require.Nil(t, err) 163 require.False(t, bytes.Equal(proposalState3, proposalState2)) 164 165 voteState3, _, err := snapEngine.engine.GetState((&types.PayloadVoteSpamPolicy{}).Key()) 166 require.Nil(t, err) 167 require.False(t, bytes.Equal(voteState3, voteState2)) 168 } 169 170 func testPreBlockAccept(t *testing.T) { 171 testEngine := getEngine(t, map[string]*num.Uint{"party1": sufficientPropTokens}) 172 engine := testEngine.engine 173 engine.OnEpochEvent(context.Background(), types.Epoch{Seq: 0}) 174 175 tx1 := &testTx{party: "party1", proposal: "proposal1", command: txn.ProposeCommand} 176 require.NoError(t, engine.PreBlockAccept(tx1)) 177 178 tx2 := &testTx{party: "party1", proposal: "proposal1", command: txn.VoteCommand} 179 require.NoError(t, engine.PreBlockAccept(tx2)) 180 181 tx1 = &testTx{party: "party2", proposal: "proposal1", command: txn.ProposeCommand} 182 require.Equal(t, errors.New("party has insufficient associated governance tokens in their staking account to submit proposal request"), engine.PreBlockAccept(tx1)) 183 184 tx2 = &testTx{party: "party2", proposal: "proposal1", command: txn.VoteCommand} 185 require.Equal(t, spam.ErrInsufficientTokensForVoting, engine.PreBlockAccept(tx2)) 186 } 187 188 func testPostBlockAccept(t *testing.T) { 189 testEngine := getEngine(t, map[string]*num.Uint{"party1": sufficientPropTokens}) 190 engine := testEngine.engine 191 192 engine.OnEpochEvent(context.Background(), types.Epoch{Seq: 0}) 193 194 tx1 := &testTx{party: "party1", proposal: "proposal1", command: txn.ProposeCommand} 195 tx2 := &testTx{party: "party1", proposal: "proposal1", command: txn.VoteCommand} 196 for i := 0; i < 3; i++ { 197 err := engine.CheckBlockTx(tx1) 198 require.NoError(t, err) 199 200 err = engine.CheckBlockTx(tx2) 201 require.NoError(t, err) 202 } 203 engine.EndPrepareProposal() 204 engine.ProcessProposal([]abci.Tx{tx1, tx1, tx1, tx2, tx2, tx2}) 205 engine.BeginBlock([]abci.Tx{tx1, tx1, tx1, tx2, tx2, tx2}) 206 207 tx1 = &testTx{party: "party1", proposal: "proposal1", command: txn.ProposeCommand} 208 require.Error(t, errors.New("party has already submitted the maximum number of proposal requests per epoch"), engine.CheckBlockTx(tx1)) 209 210 tx2 = &testTx{party: "party1", proposal: "proposal1", command: txn.VoteCommand} 211 require.Error(t, spam.ErrTooManyVotes, engine.CheckBlockTx(tx2)) 212 } 213 214 func testEndOfBlock(t *testing.T) { 215 testEngine := getEngine(t, map[string]*num.Uint{"party1": sufficientPropTokens}) 216 engine := testEngine.engine 217 218 engine.OnEpochEvent(context.Background(), types.Epoch{Seq: 0}) 219 220 tx1 := &testTx{party: "party1", proposal: "proposal1", command: txn.ProposeCommand} 221 tx2 := &testTx{party: "party1", proposal: "proposal1", command: txn.VoteCommand} 222 223 for i := 0; i < 3; i++ { 224 require.NoError(t, engine.CheckBlockTx(tx1)) 225 require.NoError(t, engine.CheckBlockTx(tx2)) 226 } 227 engine.EndPrepareProposal() 228 engine.ProcessProposal([]abci.Tx{tx1, tx1, tx1, tx2, tx2, tx2}) 229 engine.BeginBlock([]abci.Tx{tx1, tx1, tx1, tx2, tx2, tx2}) 230 231 for i := 0; i < 3; i++ { 232 tx1 := &testTx{party: "party1", proposal: "proposal1", command: txn.ProposeCommand} 233 require.Error(t, engine.CheckBlockTx(tx1)) 234 235 tx2 := &testTx{party: "party1", proposal: "proposal1", command: txn.VoteCommand} 236 require.Error(t, engine.CheckBlockTx(tx2)) 237 } 238 // proposal is rejected 239 engine.EndPrepareProposal() 240 241 require.Error(t, errors.New("party has already submitted the maximum number of proposal requests per epoch"), engine.PreBlockAccept(tx1)) 242 require.Equal(t, spam.ErrTooManyVotes, engine.PreBlockAccept(tx2)) 243 } 244 245 type testEngine struct { 246 engine *spam.Engine 247 epochEngine *TestEpochEngine 248 accounts *testAccounts 249 } 250 251 type testAccounts struct { 252 balances map[string]*num.Uint 253 } 254 255 func (t testAccounts) GetAvailableBalance(party string) (*num.Uint, error) { 256 balance, ok := t.balances[party] 257 if !ok { 258 return nil, errors.New("no balance for party") 259 } 260 return balance, nil 261 } 262 263 func getEngine(t *testing.T, balances map[string]*num.Uint) *testEngine { 264 t.Helper() 265 conf := spam.NewDefaultConfig() 266 logger := logging.NewTestLogger() 267 epochEngine := &TestEpochEngine{ 268 callbacks: []func(context.Context, types.Epoch){}, 269 restore: []func(context.Context, types.Epoch){}, 270 } 271 accounts := &testAccounts{balances: balances} 272 273 engine := spam.New(logger, conf, epochEngine, accounts) 274 275 minTokensForVoting, _ := num.DecimalFromString("100000000000000000000") 276 minTokensForProposal, _ := num.DecimalFromString("100000000000000000000000") 277 engine.OnMaxProposalsChanged(context.Background(), 3) 278 engine.OnMaxVotesChanged(context.Background(), 3) 279 engine.OnMinTokensForVotingChanged(context.Background(), minTokensForVoting) 280 engine.OnMinTokensForProposalChanged(context.Background(), minTokensForProposal) 281 282 return &testEngine{ 283 engine: engine, 284 epochEngine: epochEngine, 285 accounts: accounts, 286 } 287 } 288 289 type TestEpochEngine struct { 290 callbacks []func(context.Context, types.Epoch) 291 restore []func(context.Context, types.Epoch) 292 } 293 294 func (e *TestEpochEngine) NotifyOnEpoch(f func(context.Context, types.Epoch), r func(context.Context, types.Epoch)) { 295 e.callbacks = append(e.callbacks, f) 296 e.restore = append(e.restore, r) 297 }