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  }