github.com/koko1123/flow-go-1@v0.29.6/network/p2p/cache/node_blocklist_wrapper_test.go (about)

     1  package cache_test
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"github.com/dgraph-io/badger/v3"
     8  	"github.com/libp2p/go-libp2p/core/peer"
     9  	"github.com/stretchr/testify/mock"
    10  	"github.com/stretchr/testify/require"
    11  	"github.com/stretchr/testify/suite"
    12  	"go.uber.org/atomic"
    13  
    14  	"github.com/koko1123/flow-go-1/model/flow"
    15  	"github.com/koko1123/flow-go-1/model/flow/filter"
    16  	mocks "github.com/koko1123/flow-go-1/module/mock"
    17  	"github.com/koko1123/flow-go-1/network/p2p"
    18  	"github.com/koko1123/flow-go-1/network/p2p/cache"
    19  	"github.com/koko1123/flow-go-1/utils/unittest"
    20  )
    21  
    22  type NodeBlocklistWrapperTestSuite struct {
    23  	suite.Suite
    24  	DB       *badger.DB
    25  	provider *mocks.IdentityProvider
    26  
    27  	wrapper *cache.NodeBlocklistWrapper
    28  }
    29  
    30  func (s *NodeBlocklistWrapperTestSuite) SetupTest() {
    31  	s.DB, _ = unittest.TempBadgerDB(s.T())
    32  	s.provider = new(mocks.IdentityProvider)
    33  
    34  	var err error
    35  	s.wrapper, err = cache.NewNodeBlocklistWrapper(s.provider, s.DB)
    36  	require.NoError(s.T(), err)
    37  }
    38  
    39  func TestNodeBlocklistWrapperTestSuite(t *testing.T) {
    40  	suite.Run(t, new(NodeBlocklistWrapperTestSuite))
    41  }
    42  
    43  // TestHonestNode verifies:
    44  // For nodes _not_ on the blocklist, the `cache.NodeBlocklistWrapper` should forward
    45  // the identities from the wrapped `IdentityProvider` without modification.
    46  func (s *NodeBlocklistWrapperTestSuite) TestHonestNode() {
    47  	s.Run("ByNodeID", func() {
    48  		identity := unittest.IdentityFixture()
    49  		s.provider.On("ByNodeID", identity.NodeID).Return(identity, true)
    50  
    51  		i, found := s.wrapper.ByNodeID(identity.NodeID)
    52  		require.True(s.T(), found)
    53  		require.Equal(s.T(), i, identity)
    54  	})
    55  	s.Run("ByPeerID", func() {
    56  		identity := unittest.IdentityFixture()
    57  		peerID := (peer.ID)("some_peer_ID")
    58  		s.provider.On("ByPeerID", peerID).Return(identity, true)
    59  
    60  		i, found := s.wrapper.ByPeerID(peerID)
    61  		require.True(s.T(), found)
    62  		require.Equal(s.T(), i, identity)
    63  	})
    64  	s.Run("Identities", func() {
    65  		identities := unittest.IdentityListFixture(11)
    66  		f := filter.In(identities[3:4])
    67  		expectedFilteredIdentities := identities.Filter(f)
    68  		s.provider.On("Identities", mock.Anything).Return(
    69  			func(filter flow.IdentityFilter) flow.IdentityList {
    70  				return identities.Filter(filter)
    71  			},
    72  			nil,
    73  		)
    74  		require.Equal(s.T(), expectedFilteredIdentities, s.wrapper.Identities(f))
    75  	})
    76  }
    77  
    78  // TestBlacklistedNode tests proper handling of identities _on_ the blocklist:
    79  //   - For any identity `i` with `i.NodeID ∈ blocklist`, the returned identity
    80  //     should have `i.Ejected` set to `true` (irrespective of the `Ejected`
    81  //     flag's initial returned by the wrapped `IdentityProvider`).
    82  //   - The wrapper should _copy_ the identity and _not_ write into the wrapped
    83  //     IdentityProvider's memory.
    84  //   - For `IdentityProvider.ByNodeID` and `IdentityProvider.ByPeerID`:
    85  //     whether or not the wrapper modifies the `Ejected` flag should depend only
    86  //     in the NodeID of the returned identity, irrespective of the second return
    87  //     value (boolean).
    88  //     While returning (non-nil identity, false) is not a defined return value,
    89  //     we expect the wrapper to nevertheless handle this case to increase its
    90  //     generality.
    91  func (s *NodeBlocklistWrapperTestSuite) TestBlacklistedNode() {
    92  	blocklist := unittest.IdentityListFixture(11)
    93  	err := s.wrapper.Update(blocklist.NodeIDs())
    94  	require.NoError(s.T(), err)
    95  
    96  	index := atomic.NewInt32(0)
    97  	for _, b := range []bool{true, false} {
    98  		expectedfound := b
    99  
   100  		s.Run(fmt.Sprintf("IdentityProvider.ByNodeID returning (<non-nil identity>, %v)", expectedfound), func() {
   101  			originalIdentity := blocklist[index.Inc()]
   102  			s.provider.On("ByNodeID", originalIdentity.NodeID).Return(originalIdentity, expectedfound)
   103  
   104  			var expectedIdentity flow.Identity = *originalIdentity // expected Identity is a copy of the original
   105  			expectedIdentity.Ejected = true                        // with the `Ejected` flag set to true
   106  
   107  			i, found := s.wrapper.ByNodeID(originalIdentity.NodeID)
   108  			require.Equal(s.T(), expectedfound, found)
   109  			require.Equal(s.T(), &expectedIdentity, i)
   110  
   111  			// check that originalIdentity returned by wrapped `IdentityProvider` is _not_ modified
   112  			require.False(s.T(), originalIdentity.Ejected)
   113  		})
   114  
   115  		s.Run(fmt.Sprintf("IdentityProvider.ByPeerID returning (<non-nil identity>, %v)", expectedfound), func() {
   116  			originalIdentity := blocklist[index.Inc()]
   117  			peerID := (peer.ID)(originalIdentity.NodeID.String())
   118  			s.provider.On("ByPeerID", peerID).Return(originalIdentity, expectedfound)
   119  
   120  			var expectedIdentity flow.Identity = *originalIdentity // expected Identity is a copy of the original
   121  			expectedIdentity.Ejected = true                        // with the `Ejected` flag set to true
   122  
   123  			i, found := s.wrapper.ByPeerID(peerID)
   124  			require.Equal(s.T(), expectedfound, found)
   125  			require.Equal(s.T(), &expectedIdentity, i)
   126  
   127  			// check that originalIdentity returned by `IdentityProvider` is _not_ modified by wrapper
   128  			require.False(s.T(), originalIdentity.Ejected)
   129  		})
   130  	}
   131  
   132  	s.Run("Identities", func() {
   133  		blocklistLookup := blocklist.Lookup()
   134  		honestIdentities := unittest.IdentityListFixture(8)
   135  		combinedIdentities := honestIdentities.Union(blocklist)
   136  		combinedIdentities = combinedIdentities.DeterministicShuffle(1234)
   137  		numIdentities := len(combinedIdentities)
   138  
   139  		s.provider.On("Identities", mock.Anything).Return(combinedIdentities)
   140  
   141  		noFilter := filter.Not(filter.In(nil))
   142  		identities := s.wrapper.Identities(noFilter)
   143  
   144  		require.Equal(s.T(), numIdentities, len(identities)) // expected number resulting identities have the
   145  		for _, i := range identities {
   146  			_, isBlocked := blocklistLookup[i.NodeID]
   147  			require.Equal(s.T(), isBlocked, i.Ejected)
   148  		}
   149  
   150  		// check that original `combinedIdentities` returned by `IdentityProvider` are _not_ modified by wrapper
   151  		require.Equal(s.T(), numIdentities, len(combinedIdentities)) // length of list should not be modified by wrapper
   152  		for _, i := range combinedIdentities {
   153  			require.False(s.T(), i.Ejected) // Ejected flag should still have the original value (false here)
   154  		}
   155  	})
   156  
   157  	// this tests the edge case where the  Identities func is invoked with the p2p.NotEjectedFilter. Block listed
   158  	// nodes are expected to be filtered from the identity list returned after setting the ejected field.
   159  	s.Run("Identities(p2p.NotEjectedFilter) should not return block listed nodes", func() {
   160  		blocklistLookup := blocklist.Lookup()
   161  		honestIdentities := unittest.IdentityListFixture(8)
   162  		combinedIdentities := honestIdentities.Union(blocklist)
   163  		combinedIdentities = combinedIdentities.DeterministicShuffle(1234)
   164  		numIdentities := len(combinedIdentities)
   165  
   166  		s.provider.On("Identities", mock.Anything).Return(combinedIdentities)
   167  
   168  		identities := s.wrapper.Identities(p2p.NotEjectedFilter)
   169  
   170  		require.Equal(s.T(), len(honestIdentities), len(identities)) // expected only honest nodes to be returned
   171  		for _, i := range identities {
   172  			_, isBlocked := blocklistLookup[i.NodeID]
   173  			require.False(s.T(), isBlocked)
   174  			require.False(s.T(), i.Ejected)
   175  		}
   176  
   177  		// check that original `combinedIdentities` returned by `IdentityProvider` are _not_ modified by wrapper
   178  		require.Equal(s.T(), numIdentities, len(combinedIdentities)) // length of list should not be modified by wrapper
   179  		for _, i := range combinedIdentities {
   180  			require.False(s.T(), i.Ejected) // Ejected flag should still have the original value (false here)
   181  		}
   182  	})
   183  }
   184  
   185  // TestUnknownNode verifies that the wrapper forwards nil identities
   186  // irrespective of the boolean return values.
   187  func (s *NodeBlocklistWrapperTestSuite) TestUnknownNode() {
   188  	for _, b := range []bool{true, false} {
   189  		s.Run(fmt.Sprintf("IdentityProvider.ByNodeID returning (nil, %v)", b), func() {
   190  			id := unittest.IdentifierFixture()
   191  			s.provider.On("ByNodeID", id).Return(nil, b)
   192  
   193  			identity, found := s.wrapper.ByNodeID(id)
   194  			require.Equal(s.T(), b, found)
   195  			require.Nil(s.T(), identity)
   196  		})
   197  
   198  		s.Run(fmt.Sprintf("IdentityProvider.ByPeerID returning (nil, %v)", b), func() {
   199  			peerID := (peer.ID)(unittest.IdentifierFixture().String())
   200  			s.provider.On("ByPeerID", peerID).Return(nil, b)
   201  
   202  			identity, found := s.wrapper.ByPeerID(peerID)
   203  			require.Equal(s.T(), b, found)
   204  			require.Nil(s.T(), identity)
   205  		})
   206  	}
   207  }
   208  
   209  // TestBlocklistAddRemove checks that adding and subsequently removing a node from the blocklist
   210  // it in combination a no-op. We test two scenarious
   211  //   - Node whose original `Identity` has `Ejected = false`:
   212  //     After adding the node to the blocklist and then removing it again, the `Ejected` should be false.
   213  //   - Node whose original `Identity` has `Ejected = true`:
   214  //     After adding the node to the blocklist and then removing it again, the `Ejected` should be still be true.
   215  func (s *NodeBlocklistWrapperTestSuite) TestBlocklistAddRemove() {
   216  	for _, originalEjected := range []bool{true, false} {
   217  		s.Run(fmt.Sprintf("Add & remove node with Ejected = %v", originalEjected), func() {
   218  			originalIdentity := unittest.IdentityFixture()
   219  			originalIdentity.Ejected = originalEjected
   220  			peerID := (peer.ID)(originalIdentity.NodeID.String())
   221  			s.provider.On("ByNodeID", originalIdentity.NodeID).Return(originalIdentity, true)
   222  			s.provider.On("ByPeerID", peerID).Return(originalIdentity, true)
   223  
   224  			// step 1: before putting node on blocklist,
   225  			// an Identity with `Ejected` equal to the original value should be returned
   226  			i, found := s.wrapper.ByNodeID(originalIdentity.NodeID)
   227  			require.True(s.T(), found)
   228  			require.Equal(s.T(), originalEjected, i.Ejected)
   229  
   230  			i, found = s.wrapper.ByPeerID(peerID)
   231  			require.True(s.T(), found)
   232  			require.Equal(s.T(), originalEjected, i.Ejected)
   233  
   234  			// step 2: _after_ putting node on blocklist,
   235  			// an Identity with `Ejected` equal to `true` should be returned
   236  			err := s.wrapper.Update(flow.IdentifierList{originalIdentity.NodeID})
   237  			require.NoError(s.T(), err)
   238  
   239  			i, found = s.wrapper.ByNodeID(originalIdentity.NodeID)
   240  			require.True(s.T(), found)
   241  			require.True(s.T(), i.Ejected)
   242  
   243  			i, found = s.wrapper.ByPeerID(peerID)
   244  			require.True(s.T(), found)
   245  			require.True(s.T(), i.Ejected)
   246  
   247  			// step 3: after removing the node from the blocklist,
   248  			// an Identity with `Ejected` equal to the original value should be returned
   249  			err = s.wrapper.Update(flow.IdentifierList{})
   250  			require.NoError(s.T(), err)
   251  
   252  			i, found = s.wrapper.ByNodeID(originalIdentity.NodeID)
   253  			require.True(s.T(), found)
   254  			require.Equal(s.T(), originalEjected, i.Ejected)
   255  
   256  			i, found = s.wrapper.ByPeerID(peerID)
   257  			require.True(s.T(), found)
   258  			require.Equal(s.T(), originalEjected, i.Ejected)
   259  		})
   260  	}
   261  }
   262  
   263  // TestUpdate tests updating, clearing and retrieving the blocklist.
   264  // This test verifies that the wrapper updates _its own internal state_ correctly.
   265  // Note:
   266  // conceptually, the blocklist is a set, i.e. not order dependent.
   267  // The wrapper internally converts the list to a set and vice versa. Therefore
   268  // the order is not preserved by `GetBlocklist`. Consequently, we compare
   269  // map-based representations here.
   270  func (s *NodeBlocklistWrapperTestSuite) TestUpdate() {
   271  	blocklist1 := unittest.IdentifierListFixture(8)
   272  	blocklist2 := unittest.IdentifierListFixture(11)
   273  	blocklist3 := unittest.IdentifierListFixture(5)
   274  
   275  	err := s.wrapper.Update(blocklist1)
   276  	require.NoError(s.T(), err)
   277  	require.Equal(s.T(), blocklist1.Lookup(), s.wrapper.GetBlocklist().Lookup())
   278  
   279  	err = s.wrapper.Update(blocklist2)
   280  	require.NoError(s.T(), err)
   281  	require.Equal(s.T(), blocklist2.Lookup(), s.wrapper.GetBlocklist().Lookup())
   282  
   283  	err = s.wrapper.ClearBlocklist()
   284  	require.NoError(s.T(), err)
   285  	require.Empty(s.T(), s.wrapper.GetBlocklist())
   286  
   287  	err = s.wrapper.Update(blocklist3)
   288  	require.NoError(s.T(), err)
   289  	require.Equal(s.T(), blocklist3.Lookup(), s.wrapper.GetBlocklist().Lookup())
   290  }
   291  
   292  // TestDataBasePersist verifies database interactions of the wrapper with the data base.
   293  // This test verifies that the blocklist updates are persisted across restarts.
   294  // To decouple this test from the lower-level data base design, we proceed as follows:
   295  //   - We do data-base operation through the exported methods from `NodeBlocklistWrapper`
   296  //   - Then, we create a new `NodeBlocklistWrapper` backed by the same data base. Since it is a
   297  //     new wrapper, it must read its state from the data base. Hence, if the new wrapper returns
   298  //     the correct data, we have strong evidence that data-base interactions are correct.
   299  //
   300  // Note: The wrapper internally converts the list to a set and vice versa. Therefore
   301  // the order is not preserved by `GetBlocklist`. Consequently, we compare
   302  // map-based representations here.
   303  func (s *NodeBlocklistWrapperTestSuite) TestDataBasePersist() {
   304  	blocklist := unittest.IdentifierListFixture(8)
   305  	blocklist2 := unittest.IdentifierListFixture(8)
   306  
   307  	s.Run("Get blocklist from empty database", func() {
   308  		require.Empty(s.T(), s.wrapper.GetBlocklist())
   309  	})
   310  
   311  	s.Run("Clear blocklist on empty database", func() {
   312  		err := s.wrapper.ClearBlocklist() // No-op as data base does not contain any block list
   313  		require.NoError(s.T(), err)
   314  		require.Empty(s.T(), s.wrapper.GetBlocklist())
   315  
   316  		// newly created wrapper should read `blocklist` from data base during initialization
   317  		w, err := cache.NewNodeBlocklistWrapper(s.provider, s.DB)
   318  		require.NoError(s.T(), err)
   319  		require.Empty(s.T(), w.GetBlocklist())
   320  	})
   321  
   322  	s.Run("Update blocklist and init new wrapper from database", func() {
   323  		err := s.wrapper.Update(blocklist)
   324  		require.NoError(s.T(), err)
   325  
   326  		// newly created wrapper should read `blocklist` from data base during initialization
   327  		w, err := cache.NewNodeBlocklistWrapper(s.provider, s.DB)
   328  		require.NoError(s.T(), err)
   329  		require.Equal(s.T(), blocklist.Lookup(), w.GetBlocklist().Lookup())
   330  	})
   331  
   332  	s.Run("Update and overwrite blocklist and then init new wrapper from database", func() {
   333  		err := s.wrapper.Update(blocklist)
   334  		require.NoError(s.T(), err)
   335  
   336  		err = s.wrapper.Update(blocklist2)
   337  		require.NoError(s.T(), err)
   338  
   339  		// newly created wrapper should read initial state from data base
   340  		w, err := cache.NewNodeBlocklistWrapper(s.provider, s.DB)
   341  		require.NoError(s.T(), err)
   342  		require.Equal(s.T(), blocklist2.Lookup(), w.GetBlocklist().Lookup())
   343  	})
   344  
   345  	s.Run("Update & clear & update and then init new wrapper from database", func() {
   346  		// set blocklist ->
   347  		// newly created wrapper should now read this list from data base during initialization
   348  		err := s.wrapper.Update(blocklist)
   349  		require.NoError(s.T(), err)
   350  
   351  		w0, err := cache.NewNodeBlocklistWrapper(s.provider, s.DB)
   352  		require.NoError(s.T(), err)
   353  		require.Equal(s.T(), blocklist.Lookup(), w0.GetBlocklist().Lookup())
   354  
   355  		// clear blocklist ->
   356  		// newly created wrapper should now read empty blocklist from data base during initialization
   357  		err = s.wrapper.ClearBlocklist()
   358  		require.NoError(s.T(), err)
   359  
   360  		w1, err := cache.NewNodeBlocklistWrapper(s.provider, s.DB)
   361  		require.NoError(s.T(), err)
   362  		require.Empty(s.T(), w1.GetBlocklist())
   363  
   364  		// set blocklist2 ->
   365  		// newly created wrapper should now read this list from data base during initialization
   366  		err = s.wrapper.Update(blocklist2)
   367  		require.NoError(s.T(), err)
   368  
   369  		w2, err := cache.NewNodeBlocklistWrapper(s.provider, s.DB)
   370  		require.NoError(s.T(), err)
   371  		require.Equal(s.T(), blocklist2.Lookup(), w2.GetBlocklist().Lookup())
   372  	})
   373  }