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  }