code.vegaprotocol.io/vega@v0.79.0/wallet/api/spam/spam_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  	"testing"
    20  
    21  	vgcrypto "code.vegaprotocol.io/vega/libs/crypto"
    22  	"code.vegaprotocol.io/vega/libs/ptr"
    23  	v1 "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    24  	walletpb "code.vegaprotocol.io/vega/protos/vega/wallet/v1"
    25  	nodetypes "code.vegaprotocol.io/vega/wallet/api/node/types"
    26  	"code.vegaprotocol.io/vega/wallet/api/spam"
    27  
    28  	"github.com/stretchr/testify/require"
    29  )
    30  
    31  var (
    32  	policies = []string{
    33  		"proposals",
    34  		"delegations",
    35  		"announcements",
    36  		"transfers",
    37  		"votes",
    38  		"issueSignatures",
    39  	}
    40  	proposalID = "default-proposal-id"
    41  )
    42  
    43  func TestSpamAwareness(t *testing.T) {
    44  	t.Run("spam policy has been banned", testSpamPolicyBannedUtil)
    45  	t.Run("spam policy will get banned", testSpamPolicyWillGetBanned)
    46  	t.Run("spam policy hits limit", testSpamPolicyHitsLimit)
    47  	t.Run("spam policy new epoch", testSpamPolicyNewEpoch)
    48  	t.Run("spam policy pow ban", testSpamPolicyPoWBan)
    49  	t.Run("spam policy votes separate proposals", testSpamVotesSeparateProposals)
    50  	t.Run("other transaction types not blocked", testOtherTransactionTypesNotBlock)
    51  }
    52  
    53  func testSpamPolicyBannedUtil(t *testing.T) {
    54  	p := spam.NewHandler()
    55  
    56  	pubKey := vgcrypto.RandomHash()
    57  	banned := nodetypes.SpamStatistic{
    58  		BannedUntil:   ptr.From("forever"),
    59  		MaxForEpoch:   100,
    60  		CountForEpoch: 100,
    61  	}
    62  
    63  	for _, f := range policies {
    64  		s, req := getSimplePolicyStats(t, pubKey, f, banned)
    65  		err := p.CheckSubmission(req, s)
    66  		require.Equal(t, err.Error(), "party is banned from submitting transactions of this type until forever")
    67  	}
    68  }
    69  
    70  func testSpamPolicyWillGetBanned(t *testing.T) {
    71  	p := spam.NewHandler()
    72  
    73  	pubKey := vgcrypto.RandomHash()
    74  	willBan := nodetypes.SpamStatistic{
    75  		BannedUntil:   nil,
    76  		MaxForEpoch:   100,
    77  		CountForEpoch: 100,
    78  	}
    79  
    80  	for _, f := range policies {
    81  		s, req := getSimplePolicyStats(t, pubKey, f, willBan)
    82  		err := p.CheckSubmission(req, s)
    83  		require.Equal(t, err.Error(), "party has already submitted the maximum number of transactions of this type per epoch (100)")
    84  	}
    85  }
    86  
    87  func testSpamPolicyHitsLimit(t *testing.T) {
    88  	p := spam.NewHandler()
    89  	pubKey := vgcrypto.RandomHash()
    90  	stat := nodetypes.SpamStatistic{
    91  		BannedUntil:   nil,
    92  		MaxForEpoch:   100,
    93  		CountForEpoch: 99,
    94  	}
    95  
    96  	for _, f := range policies {
    97  		s, req := getSimplePolicyStats(t, pubKey, f, stat)
    98  
    99  		// we are at 99 and this check pushes to 100
   100  		err := p.CheckSubmission(req, s)
   101  		require.NoError(t, err)
   102  
   103  		// so the next one will fail
   104  		err = p.CheckSubmission(req, s)
   105  		require.Error(t, err, spam.ErrPartyWillBeBanned)
   106  	}
   107  }
   108  
   109  func testSpamPolicyNewEpoch(t *testing.T) {
   110  	p := spam.NewHandler()
   111  	pubKey := vgcrypto.RandomHash()
   112  	for i, f := range policies {
   113  		stat := nodetypes.SpamStatistic{
   114  			BannedUntil:   nil,
   115  			MaxForEpoch:   100,
   116  			CountForEpoch: 88,
   117  		}
   118  		s, req := getSimplePolicyStats(t, pubKey, f, stat)
   119  
   120  		// we are at 88 and this check pushes to 100
   121  		err := p.CheckSubmission(req, s)
   122  		require.NoError(t, err)
   123  
   124  		// spam update is more than our count so we trust it
   125  		stat.CountForEpoch = 100
   126  		s, req = getSimplePolicyStats(t, pubKey, f, stat)
   127  		err = p.CheckSubmission(req, s)
   128  		require.Error(t, err, spam.ErrPartyWillBeBanned)
   129  
   130  		// dip into a new epoch so we take the spam stats lower value
   131  		stat.CountForEpoch = 0
   132  		s, req = getSimplePolicyStats(t, pubKey, f, stat)
   133  		s.EpochSeq += (1 + uint64(i))
   134  		err = p.CheckSubmission(req, s)
   135  		require.NoError(t, err)
   136  	}
   137  }
   138  
   139  func testSpamPolicyPoWBan(t *testing.T) {
   140  	p := spam.NewHandler()
   141  	pubKey := vgcrypto.RandomHash()
   142  	for _, f := range policies {
   143  		stat := nodetypes.SpamStatistic{
   144  			BannedUntil:   nil,
   145  			MaxForEpoch:   100,
   146  			CountForEpoch: 88,
   147  		}
   148  		s, req := getSimplePolicyStats(t, pubKey, f, stat)
   149  		s.PoW.BannedUntil = ptr.From("forever")
   150  
   151  		// we are at 88 so find to submit, but banned by PoW
   152  		err := p.CheckSubmission(req, s)
   153  		require.Equal(t, err.Error(), "party is banned from submitting all transactions until forever")
   154  	}
   155  }
   156  
   157  func testSpamVotesSeparateProposals(t *testing.T) {
   158  	p := spam.NewHandler()
   159  	pubKey := vgcrypto.RandomHash()
   160  
   161  	stat := nodetypes.SpamStatistic{
   162  		BannedUntil:   nil,
   163  		MaxForEpoch:   100,
   164  		CountForEpoch: 100,
   165  	}
   166  	s, req := getSimplePolicyStats(t, pubKey, "votes", stat)
   167  
   168  	// we're at our max for the first proposal
   169  	err := p.CheckSubmission(req, s)
   170  	require.Equal(t, err.Error(), "party has already submitted the maximum number of transactions of this type per epoch (100)")
   171  
   172  	// but can still submit against a different proposal
   173  	req = &walletpb.SubmitTransactionRequest{
   174  		PubKey: pubKey,
   175  		Command: &walletpb.SubmitTransactionRequest_VoteSubmission{
   176  			VoteSubmission: &v1.VoteSubmission{
   177  				ProposalId: vgcrypto.RandomHash(),
   178  			},
   179  		},
   180  	}
   181  	err = p.CheckSubmission(req, s)
   182  	require.NoError(t, err)
   183  }
   184  
   185  func testOtherTransactionTypesNotBlock(t *testing.T) {
   186  	p := spam.NewHandler()
   187  	pubKey := vgcrypto.RandomHash()
   188  	until := ptr.From("forever")
   189  
   190  	stat := nodetypes.SpamStatistic{
   191  		BannedUntil: until,
   192  	}
   193  
   194  	// banned on everything except for proof-of-work
   195  	stats := &nodetypes.SpamStatistics{
   196  		PoW:               &nodetypes.PoWStatistics{},
   197  		Proposals:         &stat,
   198  		Delegations:       &stat,
   199  		Transfers:         &stat,
   200  		NodeAnnouncements: &stat,
   201  		IssuesSignatures:  &stat,
   202  		Votes: &nodetypes.VoteSpamStatistics{
   203  			BannedUntil: until,
   204  		},
   205  	}
   206  
   207  	// but can still submit against a different proposal
   208  	req := &walletpb.SubmitTransactionRequest{
   209  		PubKey:  pubKey,
   210  		Command: &walletpb.SubmitTransactionRequest_OrderSubmission{},
   211  	}
   212  	err := p.CheckSubmission(req, stats)
   213  	require.NoError(t, err)
   214  
   215  	// but pow ban still applies
   216  	stats.PoW.BannedUntil = until
   217  	err = p.CheckSubmission(req, stats)
   218  	require.Equal(t, err.Error(), "party is banned from submitting all transactions until forever")
   219  }
   220  
   221  func getSimplePolicyStats(t *testing.T, pubKey, policy string, st nodetypes.SpamStatistic) (*nodetypes.SpamStatistics, *walletpb.SubmitTransactionRequest) {
   222  	t.Helper()
   223  	spam := defaultSpamStats(t)
   224  	req := &walletpb.SubmitTransactionRequest{
   225  		PubKey: pubKey,
   226  	}
   227  
   228  	switch policy {
   229  	case "proposals":
   230  		spam.Proposals = &st
   231  		req.Command = &walletpb.SubmitTransactionRequest_ProposalSubmission{}
   232  	case "delegations":
   233  		spam.Delegations = &st
   234  		req.Command = &walletpb.SubmitTransactionRequest_DelegateSubmission{}
   235  	case "transfers":
   236  		spam.Transfers = &st
   237  		req.Command = &walletpb.SubmitTransactionRequest_Transfer{}
   238  	case "announcements":
   239  		spam.NodeAnnouncements = &st
   240  		req.Command = &walletpb.SubmitTransactionRequest_AnnounceNode{}
   241  	case "issueSignatures":
   242  		spam.IssuesSignatures = &st
   243  		req.Command = &walletpb.SubmitTransactionRequest_IssueSignatures{}
   244  	case "votes":
   245  		spam.Votes.MaxForEpoch = st.MaxForEpoch
   246  		spam.Votes.BannedUntil = st.BannedUntil
   247  		spam.Votes.Proposals = map[string]uint64{proposalID: st.CountForEpoch}
   248  		req.Command = &walletpb.SubmitTransactionRequest_VoteSubmission{
   249  			VoteSubmission: &v1.VoteSubmission{
   250  				ProposalId: proposalID,
   251  			},
   252  		}
   253  	default:
   254  		t.FailNow()
   255  	}
   256  
   257  	return spam, req
   258  }