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